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 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-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)]
|
#[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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user