feat(core): 添加Excel日期时间处理功能
- 集成chrono库支持日期时间解析和格式转换 - 实现Excel序列号与日期时间的双向转换功能 - 添加多种日期时间格式的自动识别和解析 - 在查询功能中支持日期格式的正确显示 - 在插入和更新功能中支持日期时间值的智能写入 - 自动设置Excel单元格的日期格式代码 - 保持原有样式保留的核心功能不变
This commit is contained in:
+154
-46
@@ -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<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)]
|
||||
enum DexcelError {
|
||||
@@ -16,6 +82,8 @@ enum DexcelError {
|
||||
|
||||
type Result<T> = std::result::Result<T, DexcelError>;
|
||||
|
||||
// --- 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<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<()> {
|
||||
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<i32>, last: bool, sheet_name: &Option<String>) -> 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<String>) -> 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<u32> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user