162 lines
6.2 KiB
Rust
162 lines
6.2 KiB
Rust
use anyhow::{Context, Result};
|
|
use calamine::{open_workbook, Data, DataType, Reader, Xlsx};
|
|
use chrono::{Duration, NaiveDate};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Student {
|
|
pub id: Option<String>,
|
|
pub year: Option<String>,
|
|
pub grade: Option<String>,
|
|
pub class: Option<String>,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AttendanceRecord {
|
|
pub student: Student,
|
|
// Date, Teacher Name, and attendance count (1.0 for present)
|
|
pub attendance: Vec<(NaiveDate, Option<String>, f64)>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ClassData {
|
|
pub class_name: String,
|
|
pub records: Vec<AttendanceRecord>,
|
|
}
|
|
|
|
pub fn read_attendance_file<P: AsRef<Path>>(path: P) -> Result<Vec<ClassData>> {
|
|
let mut workbook: Xlsx<_> = open_workbook(path).context("Failed to open Excel file")?;
|
|
let mut classes = Vec::new();
|
|
|
|
for sheet_name in workbook.sheet_names().to_owned() {
|
|
if let Ok(range) = workbook.worksheet_range(&sheet_name) {
|
|
let mut records = Vec::new();
|
|
let mut dates = Vec::new();
|
|
let mut date_col_indices = Vec::new();
|
|
|
|
// Find header row (row 2, index 1)
|
|
if range.height() < 2 {
|
|
continue;
|
|
}
|
|
|
|
// Parse headers to find date columns
|
|
// Assuming row 2 (index 1) contains headers and dates
|
|
let header_row = range.rows().nth(1).unwrap();
|
|
|
|
for (col_idx, cell) in header_row.iter().enumerate() {
|
|
// Dates in Excel are often floats or ints (serial dates)
|
|
// Check if the cell value looks like a date (e.g., > 40000)
|
|
if let Some(float_val) = cell.as_f64() {
|
|
if float_val > 40000.0 && float_val < 50000.0 {
|
|
// Convert serial date to NaiveDate
|
|
// Excel base date is usually 1899-12-30
|
|
let base_date = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap();
|
|
let days = float_val as i64;
|
|
if let Some(date) = base_date.checked_add_signed(Duration::days(days)) {
|
|
dates.push(date);
|
|
date_col_indices.push(col_idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if dates.is_empty() {
|
|
// Skip sheets without dates
|
|
continue;
|
|
}
|
|
|
|
// Find Teacher Row
|
|
// Scan all rows to find "授课教师确认签名"
|
|
let mut teacher_names = vec![None; dates.len()];
|
|
for row in range.rows() {
|
|
if let Some(first_cell) = row.first() {
|
|
if let Data::String(s) = first_cell {
|
|
if s.contains("授课教师确认签名") {
|
|
// Found teacher row
|
|
for (i, &col_idx) in date_col_indices.iter().enumerate() {
|
|
if col_idx < row.len() {
|
|
if let Data::String(name) = &row[col_idx] {
|
|
let trimmed = name.trim();
|
|
if !trimmed.is_empty() {
|
|
teacher_names[i] = Some(trimmed.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iterate over data rows (starting from row 4, index 3)
|
|
// Row 1: Title, Row 2: Headers, Row 3: Weekdays, Row 4: Data
|
|
for row in range.rows().skip(3) {
|
|
// Check if row has a name (Column E, index 4)
|
|
// If name is empty, skip
|
|
let name_cell = &row[4];
|
|
let name = match name_cell {
|
|
Data::String(s) => s.trim().to_string(),
|
|
_ => continue, // Skip if no name
|
|
};
|
|
|
|
if name.is_empty() || name == "姓名" || name == "到班人数" || name.contains("授课教师") {
|
|
continue;
|
|
}
|
|
|
|
let id = row[0].to_string();
|
|
let year = row[1].to_string();
|
|
let grade = row[2].to_string();
|
|
let class_info = row[3].to_string();
|
|
|
|
let student = Student {
|
|
id: if id.is_empty() { None } else { Some(id) },
|
|
year: if year.is_empty() { None } else { Some(year) },
|
|
grade: if grade.is_empty() { None } else { Some(grade) },
|
|
class: if class_info.is_empty() { None } else { Some(class_info) },
|
|
name,
|
|
};
|
|
|
|
let mut attendance_entries = Vec::new();
|
|
|
|
for (i, &col_idx) in date_col_indices.iter().enumerate() {
|
|
if col_idx < row.len() {
|
|
let cell = &row[col_idx];
|
|
// Check for "1" or numeric 1
|
|
let is_present = match cell {
|
|
Data::Int(v) => *v == 1,
|
|
Data::Float(v) => (*v - 1.0).abs() < 1e-6,
|
|
Data::String(s) => s.trim() == "1",
|
|
_ => false,
|
|
};
|
|
|
|
if is_present {
|
|
if i < dates.len() {
|
|
attendance_entries.push((dates[i], teacher_names[i].clone(), 1.0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !attendance_entries.is_empty() {
|
|
records.push(AttendanceRecord {
|
|
student,
|
|
attendance: attendance_entries,
|
|
});
|
|
}
|
|
}
|
|
|
|
if !records.is_empty() {
|
|
classes.push(ClassData {
|
|
class_name: sheet_name.clone(),
|
|
records,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(classes)
|
|
}
|