Files
count_attendance/src-tauri/src/excel_reader.rs

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)
}