Files
tools/dexcel/src/main.rs
T
macro 1abac4e28d feat(core): 添加日期时间解析功能
- 新增 try_parse_datetime 函数用于解析多种日期时间格式
- 实现 datetime_to_excel_serial 函数将 Chrono 时间转换为 Excel 序列号
- 添加 excel_serial_to_datetime 函数将 Excel 序列号转回 Chrono 时间
- 在 insert_excel 函数中添加日期解析和格式化写入功能
- 在 update_excel 函数中集成日期时间处理逻辑
- 修改 get_cell_value 函数支持日期格式化显示
- 优化日期相关错误处理和边界情况检查
2026-04-26 00:41:21 +08:00

601 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use clap::{Parser, Subcommand};
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-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)]
enum DexcelError {
#[error("IO错误: {0}")]
Io(#[from] std::io::Error),
#[error("Excel错误: {0}")]
Umya(String),
#[error("操作错误: {0}")]
Operation(String),
}
type Result<T> = std::result::Result<T, DexcelError>;
// --- CLI 定义 (原有,未改动) ---
#[derive(Parser)]
#[command(name = "dexcel")]
#[command(about = "Excel 读写工具 (样式保留版)", long_about = None)]
struct Cli {
/// Excel 文件路径
#[arg(required_if_eq("command", "query"))]
#[arg(required_if_eq("command", "count"))]
#[arg(required_if_eq("command", "insert"))]
#[arg(required_if_eq("command", "update"))]
#[arg(required_if_eq("command", "delete"))]
#[arg(required_if_eq("command", "style"))]
file: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 查询数据 (只读)
Query {
#[arg(default_value_t = 0, allow_hyphen_values = true)]
count: i32,
#[arg(long, allow_hyphen_values = true)]
start: Option<i32>,
#[arg(long)]
last: bool,
#[arg(long)]
sheet: Option<String>,
},
/// 统计行数
Count {
#[arg(long)]
sheet: Option<String>,
},
/// 创建新文件
New {
name: String,
#[arg(long)]
sheet: Option<String>,
},
/// 插入行 (保留样式)
Insert {
/// 要插入的值 (用 | 分隔)
value: String,
/// 插入位置 (空则追加)
#[arg(long, allow_hyphen_values = true)]
row: Option<i32>,
#[arg(long)]
sheet: Option<String>,
#[arg(long, default_value = "|")]
split: String,
},
/// 更新数据 (保留样式)
Update {
/// 新值
value: String,
/// 行号
#[arg(long, allow_hyphen_values = true)]
row: i32,
/// 起始列号 (默认1)
#[arg(long)]
cell: Option<u32>,
#[arg(long)]
sheet: Option<String>,
#[arg(long, default_value = "|")]
split: String,
},
/// 删除行
Delete {
/// 起始行号(支持负数)
#[arg(allow_hyphen_values = true)]
start: i32,
/// 删除的行数(默认1
#[arg(long, default_value_t = 1)]
count: u32,
/// 跳过确认
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
sheet: Option<String>,
},
/// 设置样式
Style {
#[arg(long, allow_hyphen_values = true)]
rows: Vec<i32>,
#[arg(long)]
cols: Vec<u32>,
#[arg(long)]
row_height: Option<f64>,
#[arg(long)]
col_width: Option<f64>,
#[arg(long)]
wrap_text: bool,
#[arg(long)]
sheet: Option<String>,
},
}
// --- 核心工具函数 (原有 + 修改 get_cell_value) ---
// 安全保存辅助
fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> {
let backup = if target.exists() {
let backup = target.with_extension("xlsx.bak");
let _ = fs::remove_file(&backup);
fs::copy(target, &backup)?;
Some(backup)
} else {
None
};
// 尝试写入文件
match writer::xlsx::write(book, target) {
Ok(_) => {
// 写入成功,删除备份文件
if let Some(bak) = backup {
let _ = fs::remove_file(bak);
}
Ok(())
}
Err(e) => {
// 写入失败,恢复备份
if let Some(bak) = &backup {
if bak.exists() {
let _ = fs::copy(bak, target);
}
}
Err(DexcelError::Umya(e.to_string()))
}
}
}
fn get_sheet_mut<'a>(book: &'a mut Spreadsheet, name: &Option<String>) -> Result<&'a mut Worksheet> {
match name {
Some(n) => book.get_sheet_by_name_mut(n).ok_or_else(|| DexcelError::Operation(format!("找不到Sheet: {}", n))),
None => Ok(book.get_sheet_mut(&0).ok_or_else(|| DexcelError::Operation("找不到Sheet".to_string()))?),
}
}
fn get_sheet<'a>(book: &'a Spreadsheet, name: &Option<String>) -> Result<&'a Worksheet> {
match name {
Some(n) => book.get_sheet_by_name(n).ok_or_else(|| DexcelError::Operation(format!("找不到Sheet: {}", n))),
None => Ok(book.get_sheet(&0).ok_or_else(|| DexcelError::Operation("找不到Sheet".to_string()))?),
}
}
// 解析行号 (1-based -> 0-based)
fn resolve_row(input: i32, max: usize) -> Result<usize> {
let max_i = max as i32;
let idx = if input < 0 { max_i + input } else { input - 1 };
if idx < 0 || idx >= max_i {
Err(DexcelError::Operation(format!("行号 {} 超出范围", input)))
} else {
Ok(idx as 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 {
Commands::Query { count, start, last, sheet } => {
let file = cli.file.as_ref().unwrap();
let book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
query_excel(&book, *count, *start, *last, sheet)
}
Commands::Count { sheet } => {
let file = cli.file.as_ref().unwrap();
let book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
count_excel(&book, sheet)
}
Commands::New { name, sheet } => {
let path = if name.ends_with(".xlsx") { PathBuf::from(name) } else { PathBuf::from(format!("{}.xlsx", name)) };
let mut book = new_file();
if let Some(s) = sheet {
book.get_sheet_mut(&0).unwrap().set_name(s);
}
writer::xlsx::write(&book, &path).map_err(|e| DexcelError::Umya(e.to_string()))?;
println!("✓ 已创建: {}", path.display());
Ok(())
}
Commands::Insert { value, row, sheet, split } => {
let file = cli.file.as_ref().unwrap();
let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
insert_excel(&mut book, value, *row, sheet, split)?;
safe_save_umya(&mut book, file)
}
Commands::Update { value, row, cell, sheet, split } => {
let file = cli.file.as_ref().unwrap();
let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
update_excel(&mut book, value, *row, *cell, sheet, split)?;
safe_save_umya(&mut book, file)
}
Commands::Delete { start, count, yes, sheet } => {
let file = cli.file.as_ref().unwrap();
let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
delete_excel(&mut book, *start, *count, *yes, sheet)?;
safe_save_umya(&mut book, file)
}
Commands::Style { rows, cols, row_height, col_width, wrap_text, sheet } => {
let file = cli.file.as_ref().unwrap();
let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?;
style_excel(&mut book, rows, cols, *row_height, *col_width, *wrap_text, sheet)?;
safe_save_umya(&mut book, file)
}
};
if let Err(e) = result {
eprintln!("❌ 错误: {}", e);
std::process::exit(1);
}
Ok(())
}
// --- 具体实现 (修改了 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 (_, 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 {
let actual_start = if s < 0 {
let pos = total_rows as i32 + s;
if pos < 0 { 0 } else { pos as usize }
} else {
(s as usize).saturating_sub(1)
};
if count > 0 {
(actual_start, (actual_start + count as usize).min(total_rows))
} else if count < 0 {
(actual_start, (actual_start + (-count) as usize).min(total_rows))
} else {
let max = 20;
(actual_start, (actual_start + max).min(total_rows))
}
} else if count > 0 {
(0, count as usize)
} else if count < 0 {
let abs = (-count) as usize;
let start = if abs >= total_rows { 0 } else { total_rows - abs };
(start, total_rows)
} else {
let max = 20;
((total_rows - max).max(0), total_rows)
};
let (max_col, _) = sheet.get_highest_column_and_row();
use tabled::builder::Builder;
let mut builder = Builder::default();
for r in start_row..end_row.min(total_rows) {
let row: Vec<String> = (0..max_col as usize)
.map(|c| get_cell_value(sheet, r, c))
.collect();
builder.push_record(row);
}
let mut table = builder.build();
table.with(tabled::settings::Style::modern());
println!("{}", table);
Ok(())
}
fn count_excel(book: &Spreadsheet, sheet_name: &Option<String>) -> Result<()> {
let sheet = get_sheet(book, sheet_name)?;
let (_, row) = sheet.get_highest_column_and_row();
println!("{}", row);
Ok(())
}
// --- 修改版:插入行 (支持日期写入) ---
fn insert_excel(
book: &mut Spreadsheet,
value: &str,
row: Option<i32>,
sheet_name: &Option<String>,
split: &str,
) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?;
let (_, max_row) = sheet.get_highest_column_and_row();
// 确定插入位置 (umya 是 1-based)
let insert_idx_1based = match row {
Some(r) => {
if r < 0 {
(max_row as i32 + r + 2).max(1) as u32
} else {
r.max(1) as u32
}
}
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;
let cell = sheet.get_cell_mut((col, insert_idx_1based));
let val_trimmed = val.trim();
if !val_trimmed.is_empty() {
// 1. 尝试解析日期
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 {
// 2. 写入文本
cell.set_value(val_trimmed);
}
}
}
println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based);
Ok(())
}
// --- 修改版:更新行 (支持日期写入) ---
fn update_excel(
book: &mut Spreadsheet,
value: &str,
row: i32,
cell: Option<u32>,
sheet_name: &Option<String>,
split: &str,
) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?;
let (_, max_row) = sheet.get_highest_column_and_row();
let target_row = resolve_row(row, max_row as usize)? as u32 + 1; // 转为 1-based
let start_col = cell.unwrap_or(1);
let vals: Vec<&str> = value.split(split).collect();
for (i, val) in vals.iter().enumerate() {
let c = start_col + i as u32;
let cell_obj = sheet.get_cell_mut((c, target_row));
let val_trimmed = val.trim();
if !val_trimmed.is_empty() {
// 1. 尝试解析日期
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 {
// 2. 写入文本
cell_obj.set_value(val_trimmed);
}
}
}
println!("✓ 已更新第 {} 行 (样式未改变)", row);
Ok(())
}
fn delete_excel(
book: &mut Spreadsheet,
start: i32,
count: u32,
yes: bool,
sheet_name: &Option<String>,
) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?;
let (_, max_row) = sheet.get_highest_column_and_row();
// 解析起始行 (转为 1-based)
let start_del = if start < 0 {
// 负数:从后往前数
let pos = (max_row as i32 + start + 1).max(1) as u32;
pos
} else {
// 正数:直接使用
start.max(1) as u32
};
let end_del = (start_del + count - 1).min(max_row);
// 确认
if !yes {
eprintln!("⚠ 即将删除第 {}{} 行(共 {} 行)", start_del, end_del, count);
eprint!("确认? (y/N): ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("操作已取消");
return Ok(());
}
}
// 调用 umya 的删除方法
sheet.remove_row(&start_del, &count);
println!("✓ 已删除第 {}{} 行(共 {} 行)", start_del, end_del, count);
Ok(())
}
fn style_excel(
book: &mut Spreadsheet,
rows: &[i32],
cols: &[u32],
row_height: Option<f64>,
col_width: Option<f64>,
wrap_text: bool,
sheet_name: &Option<String>,
) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?;
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 {
rows.iter().filter_map(|&r| resolve_row(r, max_row as usize).ok()).map(|x| x as u32 + 1).collect()
};
let target_cols: Vec<u32> = if cols.is_empty() {
(1..=max_col).collect()
} else {
cols.to_vec()
};
for &r in &target_rows {
if let Some(h) = row_height {
let row = sheet.get_row_dimension_mut(&r);
row.set_height(h);
}
}
for &c in &target_cols {
if let Some(w) = col_width {
let col = sheet.get_column_dimension_by_number_mut(&c);
col.set_width(w);
}
}
if wrap_text {
use umya_spreadsheet::structs::HorizontalAlignmentValues;
for r in &target_rows {
for c in &target_cols {
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);
}
}
}
println!("✓ 样式应用成功");
Ok(())
}