第一次提交

This commit is contained in:
2025-12-02 17:38:59 +08:00
parent 56f87ecd25
commit e09f1f38e6
40 changed files with 6647 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
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 and attendance count (1.0 for present)
pub attendance: Vec<(NaiveDate, 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;
}
// 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 == "姓名" {
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], 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)
}