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, pub year: Option, pub grade: Option, pub class: Option, 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, f64)>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassData { pub class_name: String, pub records: Vec, } pub fn read_attendance_file>(path: P) -> Result> { 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) }