Files
tools/dexcel/src/main.rs
T

588 lines
19 KiB
Rust
Raw Normal View History

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(num_args = 1..=2, allow_hyphen_values = true, value_delimiter = ' ')]
range: Vec<i32>,
#[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 { range, 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, range, 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() {
// 新增逻辑:尝试解析日期
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,
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() {
// 新增逻辑:尝试解析日期
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);
}
}
}
println!("✓ 已更新第 {} 行 (样式未改变)", row);
Ok(())
}
fn delete_excel(
book: &mut Spreadsheet,
range: &[i32],
sheet_name: &Option<String>,
) -> Result<()> {
let sheet = get_sheet_mut(book, sheet_name)?;
let (_, max_row) = sheet.get_highest_column_and_row();
let (start_del, end_del) = match range {
[n] if n > &0 => (1, *n as u32),
[n] if n < &0 => {
let count = (-n) as u32;
(max_row - count + 1, max_row)
},
[a, b] => {
if a <= &0 || b < a { return Err(DexcelError::Operation("范围无效".into())); }
(*a as u32, *b as u32)
}
_ => return Err(DexcelError::Operation("参数错误".into())),
};
eprintln!("⚠ 即将删除第 {}{}", start_del, end_del);
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(());
}
let num_to_del = end_del - start_del + 1;
sheet.remove_row(&start_del, &num_to_del);
println!("✓ 删除完成");
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(())
}