feat(core): 添加Excel日期时间处理功能

- 集成chrono库支持日期时间解析和格式转换
- 实现Excel序列号与日期时间的双向转换功能
- 添加多种日期时间格式的自动识别和解析
- 在查询功能中支持日期格式的正确显示
- 在插入和更新功能中支持日期时间值的智能写入
- 自动设置Excel单元格的日期格式代码
- 保持原有样式保留的核心功能不变
This commit is contained in:
macro
2026-04-25 23:58:23 +08:00
parent 436475174d
commit 642fa49340
+154 -46
View File
@@ -3,6 +3,72 @@ use std::path::PathBuf;
use umya_spreadsheet::*; use umya_spreadsheet::*;
use std::fs; use std::fs;
use std::io::Write; 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-30Excel 纪元)
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<NaiveDateTime> {
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)] #[derive(Debug, thiserror::Error)]
enum DexcelError { enum DexcelError {
@@ -16,6 +82,8 @@ enum DexcelError {
type Result<T> = std::result::Result<T, DexcelError>; type Result<T> = std::result::Result<T, DexcelError>;
// --- CLI 定义 (原有,未改动) ---
#[derive(Parser)] #[derive(Parser)]
#[command(name = "dexcel")] #[command(name = "dexcel")]
#[command(about = "Excel 读写工具 (样式保留版)", long_about = None)] #[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<()> { fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> {
// 1. 备份原文件
if target.exists() { if target.exists() {
let backup = target.with_extension("xlsx.bak"); let backup = target.with_extension("xlsx.bak");
let _ = fs::remove_file(&backup); let _ = fs::remove_file(&backup);
fs::copy(target, &backup)?; fs::copy(target, &backup)?;
} }
// 2. 直接写入 (umya 内部处理了 zip 结构,保留所有内容)
writer::xlsx::write(book, target).map_err(|e| DexcelError::Umya(e.to_string()))?; writer::xlsx::write(book, target).map_err(|e| DexcelError::Umya(e.to_string()))?;
Ok(()) Ok(())
} }
@@ -148,6 +215,49 @@ fn resolve_row(input: i32, max: usize) -> Result<usize> {
} }
} }
// --- 修改版:获取单元格值 (支持日期格式化显示) ---
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<()> { fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let result = match &cli.command { let result = match &cli.command {
@@ -205,28 +315,19 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
// --- 具体实现 --- // --- 具体实现 (修改了 Insert 和 Update 的写入逻辑) ---
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()
}
}
fn query_excel(book: &Spreadsheet, count: i32, start: Option<i32>, last: bool, sheet_name: &Option<String>) -> Result<()> { fn query_excel(book: &Spreadsheet, count: i32, start: Option<i32>, last: bool, sheet_name: &Option<String>) -> Result<()> {
let sheet = get_sheet(book, sheet_name)?; let sheet = get_sheet(book, sheet_name)?;
let (h, _) = sheet.get_highest_column_and_row(); let (_, max_row) = sheet.get_highest_column_and_row(); // 注意:umya 返回 (col, row)
let total_rows = h as usize; let total_rows = max_row as usize;
if total_rows == 0 { if total_rows == 0 {
println!("表格为空"); println!("表格为空");
return Ok(()); return Ok(());
} }
// 计算范围逻辑 (保持与之前一致) // 计算范围逻辑
let (start_row, end_row) = if last { let (start_row, end_row) = if last {
(total_rows - 1, total_rows) (total_rows - 1, total_rows)
} else if let Some(s) = start { } else if let Some(s) = start {
@@ -282,7 +383,7 @@ fn count_excel(book: &Spreadsheet, sheet_name: &Option<String>) -> Result<()> {
Ok(()) Ok(())
} }
// 核心:插入行 (umya 支持 insert_row,会自动下移并保留样式) // --- 修改版:插入行 (支持日期写入) ---
fn insert_excel( fn insert_excel(
book: &mut Spreadsheet, book: &mut Spreadsheet,
value: &str, value: &str,
@@ -297,7 +398,7 @@ fn insert_excel(
let insert_idx_1based = match row { let insert_idx_1based = match row {
Some(r) => { Some(r) => {
if r < 0 { if r < 0 {
(max_row as i32 + r + 2).max(1) as u32 // 负数逻辑微调 (max_row as i32 + r + 2).max(1) as u32
} else { } else {
r.max(1) as u32 r.max(1) as u32
} }
@@ -305,24 +406,35 @@ fn insert_excel(
None => max_row + 1, None => max_row + 1,
}; };
// 关键操作:如果不是追加到最末尾,调用库的插入方法
// 这会自动将下方的行下移,并且复制样式!
if insert_idx_1based <= max_row { if insert_idx_1based <= max_row {
sheet.insert_new_row(&insert_idx_1based, &1); sheet.insert_new_row(&insert_idx_1based, &1);
} }
// 写入数据 // 写入数据 (逻辑增强)
let vals: Vec<&str> = value.split(split).collect(); let vals: Vec<&str> = value.split(split).collect();
for (i, val) in vals.iter().enumerate() { for (i, val) in vals.iter().enumerate() {
let col = (i + 1) as u32; 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); println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based);
Ok(()) Ok(())
} }
// 核心:更新数据 (只改值,不动格式) // --- 修改版:更新行 (支持日期写入) ---
fn update_excel( fn update_excel(
book: &mut Spreadsheet, book: &mut Spreadsheet,
value: &str, value: &str,
@@ -341,11 +453,18 @@ fn update_excel(
for (i, val) in vals.iter().enumerate() { for (i, val) in vals.iter().enumerate() {
let c = start_col + i as u32; let c = start_col + i as u32;
// 获取已存在的单元格对象 (它包含样式)
let cell_obj = sheet.get_cell_mut((c, target_row)); let cell_obj = sheet.get_cell_mut((c, target_row));
// 仅设置值,不触碰格式属性 let val_trimmed = val.trim();
if !val.trim().is_empty() {
cell_obj.set_value(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<()> { ) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?; let sheet = get_sheet_mut(book, sheet_name)?;
let (_, max_row) = sheet.get_highest_column_and_row(); let (_, max_row) = sheet.get_highest_column_and_row();
let _total = max_row as usize;
// 解析范围 (转为 1-based)
let (start_del, end_del) = match range { let (start_del, end_del) = match range {
[n] if n > &0 => (1, *n as u32), [n] if n > &0 => (1, *n as u32),
[n] if n < &0 => { [n] if n < &0 => {
@@ -376,7 +493,6 @@ fn delete_excel(
_ => return Err(DexcelError::Operation("参数错误".into())), _ => return Err(DexcelError::Operation("参数错误".into())),
}; };
// 确认
eprintln!("⚠ 即将删除第 {}{}", start_del, end_del); eprintln!("⚠ 即将删除第 {}{}", start_del, end_del);
eprint!("确认? (y/N): "); eprint!("确认? (y/N): ");
std::io::stdout().flush()?; std::io::stdout().flush()?;
@@ -387,9 +503,6 @@ fn delete_excel(
return Ok(()); return Ok(());
} }
// 调用 umya 的删除方法 (会自动上移下方内容)
// 注意:umya 似乎没有直接的 range delete,我们循环删除或者从后往前删
// 最稳妥的是从后往前删
let num_to_del = end_del - start_del + 1; let num_to_del = end_del - start_del + 1;
sheet.remove_row(&start_del, &num_to_del); 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_row) = sheet.get_highest_column_and_row();
let (max_col, _) = sheet.get_highest_column_and_row(); let (max_col, _) = sheet.get_highest_column_and_row();
// 确定目标范围
let target_rows: Vec<u32> = if rows.is_empty() { let target_rows: Vec<u32> = if rows.is_empty() {
(1..=max_row).collect() (1..=max_row).collect()
} else { } else {
@@ -423,7 +535,6 @@ fn style_excel(
cols.to_vec() cols.to_vec()
}; };
// 应用样式
for &r in &target_rows { for &r in &target_rows {
if let Some(h) = row_height { if let Some(h) = row_height {
let row = sheet.get_row_dimension_mut(&r); let row = sheet.get_row_dimension_mut(&r);
@@ -439,17 +550,14 @@ fn style_excel(
} }
if wrap_text { if wrap_text {
// 需要对每个单元格设置 use umya_spreadsheet::structs::HorizontalAlignmentValues;
for r in target_rows { for r in &target_rows {
for c in &target_cols { for c in &target_cols {
let cell = sheet.get_cell_mut((*c, r)); let cell = sheet.get_cell_mut((*c, *r));
let alignment = cell.get_style().get_alignment(); let mut align = cell.get_style().get_alignment().cloned().unwrap_or_default();
if let Some(align) = alignment { align.set_horizontal(HorizontalAlignmentValues::Left);
let mut new_alignment = align.clone(); align.set_wrap_text(true);
new_alignment.set_horizontal(HorizontalAlignmentValues::Left); cell.get_style_mut().set_alignment(align);
new_alignment.set_wrap_text(true);
cell.get_style_mut().set_alignment(new_alignment);
}
} }
} }
} }