From 642fa49340f0774148681213881b22aab386970d Mon Sep 17 00:00:00 2001 From: macro Date: Sat, 25 Apr 2026 23:58:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0Excel=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E6=97=B6=E9=97=B4=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成chrono库支持日期时间解析和格式转换 - 实现Excel序列号与日期时间的双向转换功能 - 添加多种日期时间格式的自动识别和解析 - 在查询功能中支持日期格式的正确显示 - 在插入和更新功能中支持日期时间值的智能写入 - 自动设置Excel单元格的日期格式代码 - 保持原有样式保留的核心功能不变 --- dexcel/src/main.rs | 200 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 154 insertions(+), 46 deletions(-) diff --git a/dexcel/src/main.rs b/dexcel/src/main.rs index 71f3de0..08f2f97 100644 --- a/dexcel/src/main.rs +++ b/dexcel/src/main.rs @@ -3,6 +3,72 @@ use std::path::PathBuf; use umya_spreadsheet::*; use std::fs; use std::io::Write; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +// --- 日期时间辅助逻辑 (新增) --- + +/// 尝试将字符串解析为日期时间,并返回对应的 Excel 格式代码 +fn try_parse_datetime(s: &str) -> Option<(NaiveDateTime, &'static str)> { + const FORMATS: &[(&str, &str)] = &[ + ("%Y-%m-%d %H:%M:%S", "yyyy-mm-dd hh:mm:ss"), + ("%Y/%m/%d %H:%M:%S", "yyyy/mm/dd hh:mm:ss"), + ("%Y-%m-%d", "yyyy-mm-dd"), + ("%Y/%m/%d", "yyyy/mm/dd"), + ("%H:%M:%S", "hh:mm:ss"), + ("%H:%M", "hh:mm"), + ]; + + for (fmt_str, excel_fmt) in FORMATS { + // 1. 尝试解析为完整 DateTime + if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt_str) { + return Some((dt, excel_fmt)); + } + // 2. 尝试解析为 Date (默认时间 00:00:00) + if let Ok(d) = NaiveDate::parse_from_str(s, fmt_str) { + let dt = d.and_hms_opt(0, 0, 0).unwrap(); + return Some((dt, excel_fmt)); + } + // 3. 尝试解析为 Time (默认日期 1899-12-30,Excel 纪元) + if let Ok(t) = NaiveTime::parse_from_str(s, fmt_str) { + let dt = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap().and_time(t); + return Some((dt, excel_fmt)); + } + } + None +} + +/// 将 Chrono 时间转换为 Excel 序列号 (f64) +fn datetime_to_excel_serial(dt: &NaiveDateTime) -> f64 { + // Excel 的 Windows 纪元是 1899-12-30 (由于历史 Bug) + let excel_epoch = NaiveDate::from_ymd_opt(1899, 12, 30) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let duration = dt.signed_duration_since(excel_epoch); + let days = duration.num_days() as f64; + let secs = (duration.num_seconds() % 86400).abs() as f64; + + days + (secs / 86400.0) +} + +/// 将 Excel 序列号转换回 Chrono 时间 +fn excel_serial_to_datetime(serial: f64) -> Option { + let excel_epoch = NaiveDate::from_ymd_opt(1899, 12, 30) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let days = serial.trunc() as i64; + let fraction = serial.fract().abs(); + let secs = (fraction * 86400.0).round() as i64; + + excel_epoch + .checked_add_signed(chrono::Duration::days(days))? + .checked_add_signed(chrono::Duration::seconds(secs)) +} + +// --- 错误处理 (原有) --- #[derive(Debug, thiserror::Error)] enum DexcelError { @@ -16,6 +82,8 @@ enum DexcelError { type Result = std::result::Result; +// --- CLI 定义 (原有,未改动) --- + #[derive(Parser)] #[command(name = "dexcel")] #[command(about = "Excel 读写工具 (样式保留版)", long_about = None)] @@ -108,16 +176,15 @@ enum Commands { }, } -// 安全保存辅助:利用 umya 直接保存,但为了安全,依然先备份 +// --- 核心工具函数 (原有 + 修改 get_cell_value) --- + +// 安全保存辅助 fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> { - // 1. 备份原文件 if target.exists() { let backup = target.with_extension("xlsx.bak"); let _ = fs::remove_file(&backup); fs::copy(target, &backup)?; } - - // 2. 直接写入 (umya 内部处理了 zip 结构,保留所有内容) writer::xlsx::write(book, target).map_err(|e| DexcelError::Umya(e.to_string()))?; Ok(()) } @@ -148,6 +215,49 @@ fn resolve_row(input: i32, max: usize) -> Result { } } +// --- 修改版:获取单元格值 (支持日期格式化显示) --- +fn get_cell_value(sheet: &Worksheet, row: usize, col: usize) -> String { + // 坐标是 (col, row) 1-based + let coord = (col as u32 + 1, row as u32 + 1); + + if let Some(c) = sheet.get_cell(coord) { + let value = c.get_value(); + + // 尝试判断是否为日期格式的数字 + if let Some(serial) = c.get_value_number() { + if let Some(num_fmt) = c.get_style().get_number_format() { + let fmt_code = num_fmt.get_format_code(); + + // 简单 heuristic:如果格式码包含日期关键字,尝试转换 + let is_date_like = fmt_code.contains("yyyy") || fmt_code.contains("mm") || fmt_code.contains("dd") || + fmt_code.contains("hh") || fmt_code.contains("ss"); + + if is_date_like { + if let Some(dt) = excel_serial_to_datetime(serial) { + // 根据 Excel 格式的类型返回不同的 Rust 格式 + if fmt_code.contains("yyyy") && fmt_code.contains("hh") { + return dt.format("%Y-%m-%d %H:%M:%S").to_string(); + } else if fmt_code.contains("yyyy") { + return dt.format("%Y-%m-%d").to_string(); + } else if fmt_code.contains("hh") && fmt_code.contains("ss") { + return dt.format("%H:%M:%S").to_string(); + } else if fmt_code.contains("hh") { + return dt.format("%H:%M").to_string(); + } + } + } + } + } + + // 默认返回 + value.to_string() + } else { + String::new() + } +} + +// --- Main 入口 (原有,未改动) --- + fn main() -> Result<()> { let cli = Cli::parse(); let result = match &cli.command { @@ -205,28 +315,19 @@ fn main() -> Result<()> { Ok(()) } -// --- 具体实现 --- - -fn get_cell_value(sheet: &Worksheet, row: usize, col: usize) -> String { - // umya 的坐标是 (col, row) 且从 1 开始 - if let Some(c) = sheet.get_cell((col as u32 + 1, row as u32 + 1)) { - c.get_value().to_string() - } else { - String::new() - } -} +// --- 具体实现 (修改了 Insert 和 Update 的写入逻辑) --- fn query_excel(book: &Spreadsheet, count: i32, start: Option, last: bool, sheet_name: &Option) -> Result<()> { let sheet = get_sheet(book, sheet_name)?; - let (h, _) = sheet.get_highest_column_and_row(); - let total_rows = h as usize; + let (_, max_row) = sheet.get_highest_column_and_row(); // 注意:umya 返回 (col, row) + let total_rows = max_row as usize; if total_rows == 0 { println!("表格为空"); return Ok(()); } - // 计算范围逻辑 (保持与之前一致) + // 计算范围逻辑 let (start_row, end_row) = if last { (total_rows - 1, total_rows) } else if let Some(s) = start { @@ -282,7 +383,7 @@ fn count_excel(book: &Spreadsheet, sheet_name: &Option) -> Result<()> { Ok(()) } -// 核心:插入行 (umya 支持 insert_row,会自动下移并保留样式) +// --- 修改版:插入行 (支持日期写入) --- fn insert_excel( book: &mut Spreadsheet, value: &str, @@ -297,7 +398,7 @@ fn insert_excel( let insert_idx_1based = match row { Some(r) => { if r < 0 { - (max_row as i32 + r + 2).max(1) as u32 // 负数逻辑微调 + (max_row as i32 + r + 2).max(1) as u32 } else { r.max(1) as u32 } @@ -305,24 +406,35 @@ fn insert_excel( None => max_row + 1, }; - // 关键操作:如果不是追加到最末尾,调用库的插入方法 - // 这会自动将下方的行下移,并且复制样式! if insert_idx_1based <= max_row { sheet.insert_new_row(&insert_idx_1based, &1); } - // 写入数据 + // 写入数据 (逻辑增强) let vals: Vec<&str> = value.split(split).collect(); for (i, val) in vals.iter().enumerate() { let col = (i + 1) as u32; - sheet.get_cell_mut((col, insert_idx_1based)).set_value(val.trim()); + let cell = sheet.get_cell_mut((col, insert_idx_1based)); + let val_trimmed = val.trim(); + + if !val_trimmed.is_empty() { + // 新增逻辑:尝试解析日期 + if let Some((dt, excel_fmt)) = try_parse_datetime(val_trimmed) { + let serial = datetime_to_excel_serial(&dt); + cell.set_value_number(serial); + // 关键:设置单元格格式,这样 Excel 才会把它当日期看 + cell.get_style_mut().get_number_format_mut().set_format_code(excel_fmt); + } else { + cell.set_value(val_trimmed); + } + } } println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based); Ok(()) } -// 核心:更新数据 (只改值,不动格式) +// --- 修改版:更新行 (支持日期写入) --- fn update_excel( book: &mut Spreadsheet, value: &str, @@ -341,11 +453,18 @@ fn update_excel( for (i, val) in vals.iter().enumerate() { let c = start_col + i as u32; - // 获取已存在的单元格对象 (它包含样式) let cell_obj = sheet.get_cell_mut((c, target_row)); - // 仅设置值,不触碰格式属性 - if !val.trim().is_empty() { - cell_obj.set_value(val.trim()); + let val_trimmed = val.trim(); + + if !val_trimmed.is_empty() { + // 新增逻辑:尝试解析日期 + if let Some((dt, excel_fmt)) = try_parse_datetime(val_trimmed) { + let serial = datetime_to_excel_serial(&dt); + cell_obj.set_value_number(serial); + cell_obj.get_style_mut().get_number_format_mut().set_format_code(excel_fmt); + } else { + cell_obj.set_value(val_trimmed); + } } } @@ -360,9 +479,7 @@ fn delete_excel( ) -> Result<()> { let sheet = get_sheet_mut(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); - let _total = max_row as usize; - // 解析范围 (转为 1-based) let (start_del, end_del) = match range { [n] if n > &0 => (1, *n as u32), [n] if n < &0 => { @@ -376,7 +493,6 @@ fn delete_excel( _ => return Err(DexcelError::Operation("参数错误".into())), }; - // 确认 eprintln!("⚠ 即将删除第 {} 至 {} 行", start_del, end_del); eprint!("确认? (y/N): "); std::io::stdout().flush()?; @@ -387,9 +503,6 @@ fn delete_excel( return Ok(()); } - // 调用 umya 的删除方法 (会自动上移下方内容) - // 注意:umya 似乎没有直接的 range delete,我们循环删除或者从后往前删 - // 最稳妥的是从后往前删 let num_to_del = end_del - start_del + 1; sheet.remove_row(&start_del, &num_to_del); @@ -410,7 +523,6 @@ fn style_excel( let (_, max_row) = sheet.get_highest_column_and_row(); let (max_col, _) = sheet.get_highest_column_and_row(); - // 确定目标范围 let target_rows: Vec = if rows.is_empty() { (1..=max_row).collect() } else { @@ -423,7 +535,6 @@ fn style_excel( cols.to_vec() }; - // 应用样式 for &r in &target_rows { if let Some(h) = row_height { let row = sheet.get_row_dimension_mut(&r); @@ -439,17 +550,14 @@ fn style_excel( } if wrap_text { - // 需要对每个单元格设置 - for r in target_rows { + use umya_spreadsheet::structs::HorizontalAlignmentValues; + for r in &target_rows { for c in &target_cols { - let cell = sheet.get_cell_mut((*c, r)); - let alignment = cell.get_style().get_alignment(); - if let Some(align) = alignment { - let mut new_alignment = align.clone(); - new_alignment.set_horizontal(HorizontalAlignmentValues::Left); - new_alignment.set_wrap_text(true); - cell.get_style_mut().set_alignment(new_alignment); - } + let cell = sheet.get_cell_mut((*c, *r)); + let mut align = cell.get_style().get_alignment().cloned().unwrap_or_default(); + align.set_horizontal(HorizontalAlignmentValues::Left); + align.set_wrap_text(true); + cell.get_style_mut().set_alignment(align); } } }