xlsxturbo 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/CHANGELOG.md +13 -2
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/Cargo.lock +19 -19
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/Cargo.toml +1 -1
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/PKG-INFO +1 -1
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/ROADMAP.md +5 -2
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/pyproject.toml +1 -1
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/src/lib.rs +497 -7
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/.github/workflows/ci.yml +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/.github/workflows/release.yml +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/.gitignore +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/LICENSE +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/README.md +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/benchmark.py +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/benchmark_parallel.py +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/python/xlsxturbo/__init__.py +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/python/xlsxturbo/__init__.pyi +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/src/main.rs +0 -0
- {xlsxturbo-0.6.0 → xlsxturbo-0.7.0}/tests/test_v060_features.py +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2025-12-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Column formatting with wildcards** - `column_formats` parameter for styling columns by pattern
|
|
12
|
+
- Wildcard patterns: `prefix*`, `*suffix`, `*contains*`, or exact match
|
|
13
|
+
- Format options: `bg_color`, `font_color`, `num_format`, `bold`, `italic`, `underline`, `border`
|
|
14
|
+
- Example: `column_formats={'mcpt_*': {'bg_color': '#D6EAF8', 'num_format': '0.00000', 'border': True}}`
|
|
15
|
+
- Available in both `df_to_xlsx()` and `dfs_to_xlsx()`
|
|
16
|
+
- Per-sheet column formats via options dict in `dfs_to_xlsx()`
|
|
17
|
+
|
|
8
18
|
## [0.6.0] - 2025-12-08
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -111,10 +121,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
111
121
|
- Support for custom sheet names
|
|
112
122
|
- Verbose mode for progress reporting
|
|
113
123
|
|
|
124
|
+
[0.7.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.7.0
|
|
125
|
+
[0.6.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.6.0
|
|
126
|
+
[0.5.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.5.0
|
|
114
127
|
[0.4.1]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.4.1
|
|
115
128
|
[0.4.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.4.0
|
|
116
129
|
[0.3.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.3.0
|
|
117
130
|
[0.2.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.2.0
|
|
118
131
|
[0.1.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.1.0
|
|
119
|
-
[0.5.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.5.0
|
|
120
|
-
[0.6.0]: https://github.com/tstone-1/xlsxturbo/releases/tag/v0.6.0
|
|
@@ -90,15 +90,15 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
|
|
90
90
|
|
|
91
91
|
[[package]]
|
|
92
92
|
name = "bumpalo"
|
|
93
|
-
version = "3.19.
|
|
93
|
+
version = "3.19.1"
|
|
94
94
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
-
checksum = "
|
|
95
|
+
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
|
96
96
|
|
|
97
97
|
[[package]]
|
|
98
98
|
name = "cc"
|
|
99
|
-
version = "1.2.
|
|
99
|
+
version = "1.2.51"
|
|
100
100
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
101
|
-
checksum = "
|
|
101
|
+
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
|
102
102
|
dependencies = [
|
|
103
103
|
"find-msvc-tools",
|
|
104
104
|
"shlex",
|
|
@@ -282,9 +282,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
|
|
282
282
|
|
|
283
283
|
[[package]]
|
|
284
284
|
name = "find-msvc-tools"
|
|
285
|
-
version = "0.1.
|
|
285
|
+
version = "0.1.6"
|
|
286
286
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
287
|
-
checksum = "
|
|
287
|
+
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
|
288
288
|
|
|
289
289
|
[[package]]
|
|
290
290
|
name = "flate2"
|
|
@@ -371,9 +371,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|
|
371
371
|
|
|
372
372
|
[[package]]
|
|
373
373
|
name = "itoa"
|
|
374
|
-
version = "1.0.
|
|
374
|
+
version = "1.0.17"
|
|
375
375
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
376
|
-
checksum = "
|
|
376
|
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
|
377
377
|
|
|
378
378
|
[[package]]
|
|
379
379
|
name = "js-sys"
|
|
@@ -451,15 +451,15 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|
|
451
451
|
|
|
452
452
|
[[package]]
|
|
453
453
|
name = "portable-atomic"
|
|
454
|
-
version = "1.
|
|
454
|
+
version = "1.13.0"
|
|
455
455
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
456
|
-
checksum = "
|
|
456
|
+
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
|
457
457
|
|
|
458
458
|
[[package]]
|
|
459
459
|
name = "proc-macro2"
|
|
460
|
-
version = "1.0.
|
|
460
|
+
version = "1.0.104"
|
|
461
461
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
462
|
-
checksum = "
|
|
462
|
+
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
|
463
463
|
dependencies = [
|
|
464
464
|
"unicode-ident",
|
|
465
465
|
]
|
|
@@ -574,9 +574,9 @@ dependencies = [
|
|
|
574
574
|
|
|
575
575
|
[[package]]
|
|
576
576
|
name = "rustix"
|
|
577
|
-
version = "1.1.
|
|
577
|
+
version = "1.1.3"
|
|
578
578
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
579
|
-
checksum = "
|
|
579
|
+
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
|
580
580
|
dependencies = [
|
|
581
581
|
"bitflags",
|
|
582
582
|
"errno",
|
|
@@ -593,9 +593,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
|
593
593
|
|
|
594
594
|
[[package]]
|
|
595
595
|
name = "ryu"
|
|
596
|
-
version = "1.0.
|
|
596
|
+
version = "1.0.22"
|
|
597
597
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
598
|
-
checksum = "
|
|
598
|
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
|
599
599
|
|
|
600
600
|
[[package]]
|
|
601
601
|
name = "serde_core"
|
|
@@ -654,9 +654,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
|
654
654
|
|
|
655
655
|
[[package]]
|
|
656
656
|
name = "tempfile"
|
|
657
|
-
version = "3.
|
|
657
|
+
version = "3.24.0"
|
|
658
658
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
659
|
-
checksum = "
|
|
659
|
+
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
|
660
660
|
dependencies = [
|
|
661
661
|
"fastrand",
|
|
662
662
|
"getrandom",
|
|
@@ -833,7 +833,7 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
|
|
833
833
|
|
|
834
834
|
[[package]]
|
|
835
835
|
name = "xlsxturbo"
|
|
836
|
-
version = "0.
|
|
836
|
+
version = "0.7.0"
|
|
837
837
|
dependencies = [
|
|
838
838
|
"chrono",
|
|
839
839
|
"clap",
|
|
@@ -20,8 +20,10 @@ Features that would enable more migrations from pandas/polars write_excel.
|
|
|
20
20
|
Power user features for more control over output.
|
|
21
21
|
|
|
22
22
|
- [x] **Multi-core support** - Parallel CSV parsing with rayon (~7% speedup for large files)
|
|
23
|
-
- [
|
|
24
|
-
-
|
|
23
|
+
- [x] **Column formatting with wildcards** - `column_formats` with pattern matching (v0.7.0)
|
|
24
|
+
- Supports: `prefix*`, `*suffix`, `*contains*`, exact match
|
|
25
|
+
- Format options: bg_color, font_color, num_format, bold, italic, underline, border
|
|
26
|
+
- [ ] **Row-level cell formatting** - Conditional styling based on cell values
|
|
25
27
|
- [ ] **Merged cells** - Merge cell ranges for headers/documentation sheets
|
|
26
28
|
- [ ] **Conditional formatting** - Color scales, data bars, icon sets
|
|
27
29
|
- [x] **Table styles** - Create Excel tables with auto-filters and 61 built-in styles (v0.3.0)
|
|
@@ -38,6 +40,7 @@ Niche features for specific use cases.
|
|
|
38
40
|
|
|
39
41
|
## Completed
|
|
40
42
|
|
|
43
|
+
- [x] Column formatting with wildcards via `column_formats` parameter (v0.7.0)
|
|
41
44
|
- [x] Global column width cap with `column_widths={'_all': value}` (v0.6.0)
|
|
42
45
|
- [x] Table name parameter with `table_name` (v0.6.0)
|
|
43
46
|
- [x] Header styling with `header_format` (v0.6.0)
|
|
@@ -61,6 +61,7 @@ struct SheetConfig {
|
|
|
61
61
|
table_name: Option<String>,
|
|
62
62
|
header_format: Option<HashMap<String, PyObject>>,
|
|
63
63
|
row_heights: Option<HashMap<u32, f64>>,
|
|
64
|
+
column_formats: Option<HashMap<String, HashMap<String, PyObject>>>, // Pattern -> format dict
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/// Extract sheet info from a Python tuple (supports both 2-tuple and 3-tuple formats)
|
|
@@ -150,6 +151,26 @@ fn extract_sheet_info<'py>(
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
}
|
|
154
|
+
if let Ok(val) = opts.get_item("column_formats") {
|
|
155
|
+
if !val.is_none() {
|
|
156
|
+
if let Ok(outer_dict) = val.downcast::<pyo3::types::PyDict>() {
|
|
157
|
+
let mut col_fmts: HashMap<String, HashMap<String, PyObject>> = HashMap::new();
|
|
158
|
+
for (pattern, fmt_dict) in outer_dict.iter() {
|
|
159
|
+
let pattern_str: String = pattern.extract()?;
|
|
160
|
+
if let Ok(inner_dict) = fmt_dict.downcast::<pyo3::types::PyDict>() {
|
|
161
|
+
let mut fmt: HashMap<String, PyObject> = HashMap::new();
|
|
162
|
+
for (k, v) in inner_dict.iter() {
|
|
163
|
+
fmt.insert(k.extract()?, v.unbind());
|
|
164
|
+
}
|
|
165
|
+
col_fmts.insert(pattern_str, fmt);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if !col_fmts.is_empty() {
|
|
169
|
+
config.column_formats = Some(col_fmts);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
153
174
|
|
|
154
175
|
config
|
|
155
176
|
} else {
|
|
@@ -312,6 +333,24 @@ fn extract_header_format(
|
|
|
312
333
|
Ok(fmt)
|
|
313
334
|
}
|
|
314
335
|
|
|
336
|
+
/// Extract column_formats from Python dict (pattern -> format dict)
|
|
337
|
+
fn extract_column_formats(
|
|
338
|
+
py_dict: &Bound<'_, pyo3::types::PyDict>,
|
|
339
|
+
) -> PyResult<HashMap<String, HashMap<String, PyObject>>> {
|
|
340
|
+
let mut col_fmts: HashMap<String, HashMap<String, PyObject>> = HashMap::new();
|
|
341
|
+
for (pattern, fmt_dict) in py_dict.iter() {
|
|
342
|
+
let pattern_str: String = pattern.extract()?;
|
|
343
|
+
if let Ok(inner_dict) = fmt_dict.downcast::<pyo3::types::PyDict>() {
|
|
344
|
+
let mut fmt: HashMap<String, PyObject> = HashMap::new();
|
|
345
|
+
for (k, v) in inner_dict.iter() {
|
|
346
|
+
fmt.insert(k.extract()?, v.unbind());
|
|
347
|
+
}
|
|
348
|
+
col_fmts.insert(pattern_str, fmt);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
Ok(col_fmts)
|
|
352
|
+
}
|
|
353
|
+
|
|
315
354
|
/// Sanitize table name for Excel (alphanumeric + underscore, must start with letter/underscore)
|
|
316
355
|
fn sanitize_table_name(name: &str) -> String {
|
|
317
356
|
let mut sanitized: String = name
|
|
@@ -413,6 +452,126 @@ fn parse_header_format(
|
|
|
413
452
|
Ok(format)
|
|
414
453
|
}
|
|
415
454
|
|
|
455
|
+
/// Check if a column name matches a wildcard pattern.
|
|
456
|
+
/// Supports: "prefix*", "*suffix", "*contains*", or exact match
|
|
457
|
+
fn matches_pattern(column_name: &str, pattern: &str) -> bool {
|
|
458
|
+
let starts_with_star = pattern.starts_with('*');
|
|
459
|
+
let ends_with_star = pattern.ends_with('*');
|
|
460
|
+
|
|
461
|
+
match (starts_with_star, ends_with_star) {
|
|
462
|
+
(true, true) => {
|
|
463
|
+
// *contains* - match substring
|
|
464
|
+
let inner = &pattern[1..pattern.len() - 1];
|
|
465
|
+
column_name.contains(inner)
|
|
466
|
+
}
|
|
467
|
+
(true, false) => {
|
|
468
|
+
// *suffix - match ending
|
|
469
|
+
let suffix = &pattern[1..];
|
|
470
|
+
column_name.ends_with(suffix)
|
|
471
|
+
}
|
|
472
|
+
(false, true) => {
|
|
473
|
+
// prefix* - match beginning
|
|
474
|
+
let prefix = &pattern[..pattern.len() - 1];
|
|
475
|
+
column_name.starts_with(prefix)
|
|
476
|
+
}
|
|
477
|
+
(false, false) => {
|
|
478
|
+
// Exact match
|
|
479
|
+
column_name == pattern
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// Parse column format dictionary into rust_xlsxwriter Format
|
|
485
|
+
/// Similar to parse_header_format but also supports num_format
|
|
486
|
+
fn parse_column_format(
|
|
487
|
+
py: Python<'_>,
|
|
488
|
+
fmt_dict: &HashMap<String, PyObject>,
|
|
489
|
+
) -> Result<Format, String> {
|
|
490
|
+
let mut format = Format::new();
|
|
491
|
+
|
|
492
|
+
if let Some(bold_obj) = fmt_dict.get("bold") {
|
|
493
|
+
let bold: bool = bold_obj.bind(py).extract().unwrap_or(false);
|
|
494
|
+
if bold {
|
|
495
|
+
format = format.set_bold();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if let Some(italic_obj) = fmt_dict.get("italic") {
|
|
500
|
+
let italic: bool = italic_obj.bind(py).extract().unwrap_or(false);
|
|
501
|
+
if italic {
|
|
502
|
+
format = format.set_italic();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if let Some(bg_obj) = fmt_dict.get("bg_color") {
|
|
507
|
+
if let Ok(color_str) = bg_obj.bind(py).extract::<String>() {
|
|
508
|
+
let color = parse_color(&color_str)?;
|
|
509
|
+
format = format.set_background_color(color);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if let Some(font_obj) = fmt_dict.get("font_color") {
|
|
514
|
+
if let Ok(color_str) = font_obj.bind(py).extract::<String>() {
|
|
515
|
+
let color = parse_color(&color_str)?;
|
|
516
|
+
format = format.set_font_color(color);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if let Some(size_obj) = fmt_dict.get("font_size") {
|
|
521
|
+
if let Ok(size) = size_obj.bind(py).extract::<f64>() {
|
|
522
|
+
format = format.set_font_size(size);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if let Some(underline_obj) = fmt_dict.get("underline") {
|
|
527
|
+
let underline: bool = underline_obj.bind(py).extract().unwrap_or(false);
|
|
528
|
+
if underline {
|
|
529
|
+
format = format.set_underline(rust_xlsxwriter::FormatUnderline::Single);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Support num_format for number formatting (e.g., "0.00000", "#,##0")
|
|
534
|
+
if let Some(num_fmt_obj) = fmt_dict.get("num_format") {
|
|
535
|
+
if let Ok(num_fmt_str) = num_fmt_obj.bind(py).extract::<String>() {
|
|
536
|
+
format = format.set_num_format(&num_fmt_str);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Support border (adds thin border around cell)
|
|
541
|
+
if let Some(border_obj) = fmt_dict.get("border") {
|
|
542
|
+
let border: bool = border_obj.bind(py).extract().unwrap_or(false);
|
|
543
|
+
if border {
|
|
544
|
+
format = format.set_border(rust_xlsxwriter::FormatBorder::Thin);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
Ok(format)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// Build a vector of column formats, one for each column.
|
|
552
|
+
/// Returns None for columns with no matching pattern.
|
|
553
|
+
fn build_column_formats(
|
|
554
|
+
py: Python<'_>,
|
|
555
|
+
columns: &[String],
|
|
556
|
+
column_formats: &HashMap<String, HashMap<String, PyObject>>,
|
|
557
|
+
) -> Result<Vec<Option<Format>>, String> {
|
|
558
|
+
let mut formats = Vec::with_capacity(columns.len());
|
|
559
|
+
|
|
560
|
+
for col_name in columns {
|
|
561
|
+
// Find the first matching pattern
|
|
562
|
+
let mut matched_format: Option<Format> = None;
|
|
563
|
+
for (pattern, fmt_dict) in column_formats {
|
|
564
|
+
if matches_pattern(col_name, pattern) {
|
|
565
|
+
matched_format = Some(parse_column_format(py, fmt_dict)?);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
formats.push(matched_format);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
Ok(formats)
|
|
573
|
+
}
|
|
574
|
+
|
|
416
575
|
/// Parse a string value and detect its type
|
|
417
576
|
fn parse_value(value: &str) -> CellValue {
|
|
418
577
|
let trimmed = value.trim();
|
|
@@ -842,6 +1001,250 @@ fn write_py_value(
|
|
|
842
1001
|
Ok(())
|
|
843
1002
|
}
|
|
844
1003
|
|
|
1004
|
+
/// Write a Python value to the worksheet with optional column format
|
|
1005
|
+
fn write_py_value_with_format(
|
|
1006
|
+
worksheet: &mut Worksheet,
|
|
1007
|
+
row: u32,
|
|
1008
|
+
col: u16,
|
|
1009
|
+
value: &Bound<'_, PyAny>,
|
|
1010
|
+
date_format: &Format,
|
|
1011
|
+
datetime_format: &Format,
|
|
1012
|
+
column_format: Option<&Format>,
|
|
1013
|
+
) -> Result<(), String> {
|
|
1014
|
+
// Check for None first
|
|
1015
|
+
if value.is_none() {
|
|
1016
|
+
if let Some(fmt) = column_format {
|
|
1017
|
+
worksheet
|
|
1018
|
+
.write_string_with_format(row, col, "", fmt)
|
|
1019
|
+
.map_err(|e| e.to_string())?;
|
|
1020
|
+
} else {
|
|
1021
|
+
worksheet
|
|
1022
|
+
.write_string(row, col, "")
|
|
1023
|
+
.map_err(|e| e.to_string())?;
|
|
1024
|
+
}
|
|
1025
|
+
return Ok(());
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Check for pandas NA/NaT
|
|
1029
|
+
let type_name = value
|
|
1030
|
+
.get_type()
|
|
1031
|
+
.name()
|
|
1032
|
+
.map_err(|e| e.to_string())?
|
|
1033
|
+
.to_string();
|
|
1034
|
+
if type_name == "NAType" || type_name == "NaTType" {
|
|
1035
|
+
if let Some(fmt) = column_format {
|
|
1036
|
+
worksheet
|
|
1037
|
+
.write_string_with_format(row, col, "", fmt)
|
|
1038
|
+
.map_err(|e| e.to_string())?;
|
|
1039
|
+
} else {
|
|
1040
|
+
worksheet
|
|
1041
|
+
.write_string(row, col, "")
|
|
1042
|
+
.map_err(|e| e.to_string())?;
|
|
1043
|
+
}
|
|
1044
|
+
return Ok(());
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Try boolean first (before int, since bool is subclass of int in Python)
|
|
1048
|
+
if let Ok(b) = value.downcast::<PyBool>() {
|
|
1049
|
+
worksheet
|
|
1050
|
+
.write_boolean(row, col, b.is_true())
|
|
1051
|
+
.map_err(|e| e.to_string())?;
|
|
1052
|
+
return Ok(());
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Try datetime (before date, since datetime is subclass of date)
|
|
1056
|
+
if type_name == "datetime" || type_name == "Timestamp" {
|
|
1057
|
+
let year: i32 = value
|
|
1058
|
+
.getattr("year")
|
|
1059
|
+
.ok()
|
|
1060
|
+
.and_then(|v| v.extract().ok())
|
|
1061
|
+
.unwrap_or(1900);
|
|
1062
|
+
let month: u32 = value
|
|
1063
|
+
.getattr("month")
|
|
1064
|
+
.ok()
|
|
1065
|
+
.and_then(|v| v.extract().ok())
|
|
1066
|
+
.unwrap_or(1);
|
|
1067
|
+
let day: u32 = value
|
|
1068
|
+
.getattr("day")
|
|
1069
|
+
.ok()
|
|
1070
|
+
.and_then(|v| v.extract().ok())
|
|
1071
|
+
.unwrap_or(1);
|
|
1072
|
+
let hour: u32 = value
|
|
1073
|
+
.getattr("hour")
|
|
1074
|
+
.ok()
|
|
1075
|
+
.and_then(|v| v.extract().ok())
|
|
1076
|
+
.unwrap_or(0);
|
|
1077
|
+
let minute: u32 = value
|
|
1078
|
+
.getattr("minute")
|
|
1079
|
+
.ok()
|
|
1080
|
+
.and_then(|v| v.extract().ok())
|
|
1081
|
+
.unwrap_or(0);
|
|
1082
|
+
let second: u32 = value
|
|
1083
|
+
.getattr("second")
|
|
1084
|
+
.ok()
|
|
1085
|
+
.and_then(|v| v.extract().ok())
|
|
1086
|
+
.unwrap_or(0);
|
|
1087
|
+
|
|
1088
|
+
if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
|
1089
|
+
if let Some(time) = chrono::NaiveTime::from_hms_opt(hour, minute, second) {
|
|
1090
|
+
let dt = chrono::NaiveDateTime::new(date, time);
|
|
1091
|
+
let excel_dt = naive_datetime_to_excel(dt);
|
|
1092
|
+
// For datetime, use column format if provided, otherwise datetime_format
|
|
1093
|
+
let fmt = column_format.unwrap_or(datetime_format);
|
|
1094
|
+
worksheet
|
|
1095
|
+
.write_number_with_format(row, col, excel_dt, fmt)
|
|
1096
|
+
.map_err(|e| e.to_string())?;
|
|
1097
|
+
return Ok(());
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Try date
|
|
1103
|
+
if type_name == "date" {
|
|
1104
|
+
let year: i32 = value
|
|
1105
|
+
.getattr("year")
|
|
1106
|
+
.ok()
|
|
1107
|
+
.and_then(|v| v.extract().ok())
|
|
1108
|
+
.unwrap_or(1900);
|
|
1109
|
+
let month: u32 = value
|
|
1110
|
+
.getattr("month")
|
|
1111
|
+
.ok()
|
|
1112
|
+
.and_then(|v| v.extract().ok())
|
|
1113
|
+
.unwrap_or(1);
|
|
1114
|
+
let day: u32 = value
|
|
1115
|
+
.getattr("day")
|
|
1116
|
+
.ok()
|
|
1117
|
+
.and_then(|v| v.extract().ok())
|
|
1118
|
+
.unwrap_or(1);
|
|
1119
|
+
|
|
1120
|
+
if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
|
1121
|
+
let excel_date = naive_date_to_excel(date);
|
|
1122
|
+
// For date, use column format if provided, otherwise date_format
|
|
1123
|
+
let fmt = column_format.unwrap_or(date_format);
|
|
1124
|
+
worksheet
|
|
1125
|
+
.write_number_with_format(row, col, excel_date, fmt)
|
|
1126
|
+
.map_err(|e| e.to_string())?;
|
|
1127
|
+
return Ok(());
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Try integer
|
|
1132
|
+
if let Ok(i) = value.downcast::<PyInt>() {
|
|
1133
|
+
if let Ok(val) = i.extract::<i64>() {
|
|
1134
|
+
if let Some(fmt) = column_format {
|
|
1135
|
+
worksheet
|
|
1136
|
+
.write_number_with_format(row, col, val as f64, fmt)
|
|
1137
|
+
.map_err(|e| e.to_string())?;
|
|
1138
|
+
} else {
|
|
1139
|
+
worksheet
|
|
1140
|
+
.write_number(row, col, val as f64)
|
|
1141
|
+
.map_err(|e| e.to_string())?;
|
|
1142
|
+
}
|
|
1143
|
+
return Ok(());
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Try float
|
|
1148
|
+
if let Ok(f) = value.downcast::<PyFloat>() {
|
|
1149
|
+
if let Ok(val) = f.extract::<f64>() {
|
|
1150
|
+
if val.is_nan() || val.is_infinite() {
|
|
1151
|
+
if let Some(fmt) = column_format {
|
|
1152
|
+
worksheet
|
|
1153
|
+
.write_string_with_format(row, col, "", fmt)
|
|
1154
|
+
.map_err(|e| e.to_string())?;
|
|
1155
|
+
} else {
|
|
1156
|
+
worksheet
|
|
1157
|
+
.write_string(row, col, "")
|
|
1158
|
+
.map_err(|e| e.to_string())?;
|
|
1159
|
+
}
|
|
1160
|
+
} else if let Some(fmt) = column_format {
|
|
1161
|
+
worksheet
|
|
1162
|
+
.write_number_with_format(row, col, val, fmt)
|
|
1163
|
+
.map_err(|e| e.to_string())?;
|
|
1164
|
+
} else {
|
|
1165
|
+
worksheet
|
|
1166
|
+
.write_number(row, col, val)
|
|
1167
|
+
.map_err(|e| e.to_string())?;
|
|
1168
|
+
}
|
|
1169
|
+
return Ok(());
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Try to extract as f64 (covers numpy types)
|
|
1174
|
+
if let Ok(val) = value.extract::<f64>() {
|
|
1175
|
+
if val.is_nan() || val.is_infinite() {
|
|
1176
|
+
if let Some(fmt) = column_format {
|
|
1177
|
+
worksheet
|
|
1178
|
+
.write_string_with_format(row, col, "", fmt)
|
|
1179
|
+
.map_err(|e| e.to_string())?;
|
|
1180
|
+
} else {
|
|
1181
|
+
worksheet
|
|
1182
|
+
.write_string(row, col, "")
|
|
1183
|
+
.map_err(|e| e.to_string())?;
|
|
1184
|
+
}
|
|
1185
|
+
} else if let Some(fmt) = column_format {
|
|
1186
|
+
worksheet
|
|
1187
|
+
.write_number_with_format(row, col, val, fmt)
|
|
1188
|
+
.map_err(|e| e.to_string())?;
|
|
1189
|
+
} else {
|
|
1190
|
+
worksheet
|
|
1191
|
+
.write_number(row, col, val)
|
|
1192
|
+
.map_err(|e| e.to_string())?;
|
|
1193
|
+
}
|
|
1194
|
+
return Ok(());
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Try to extract as i64 (covers numpy int types)
|
|
1198
|
+
if let Ok(val) = value.extract::<i64>() {
|
|
1199
|
+
if let Some(fmt) = column_format {
|
|
1200
|
+
worksheet
|
|
1201
|
+
.write_number_with_format(row, col, val as f64, fmt)
|
|
1202
|
+
.map_err(|e| e.to_string())?;
|
|
1203
|
+
} else {
|
|
1204
|
+
worksheet
|
|
1205
|
+
.write_number(row, col, val as f64)
|
|
1206
|
+
.map_err(|e| e.to_string())?;
|
|
1207
|
+
}
|
|
1208
|
+
return Ok(());
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Try to extract as bool
|
|
1212
|
+
if let Ok(val) = value.extract::<bool>() {
|
|
1213
|
+
worksheet
|
|
1214
|
+
.write_boolean(row, col, val)
|
|
1215
|
+
.map_err(|e| e.to_string())?;
|
|
1216
|
+
return Ok(());
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Try string
|
|
1220
|
+
if let Ok(s) = value.downcast::<PyString>() {
|
|
1221
|
+
if let Some(fmt) = column_format {
|
|
1222
|
+
worksheet
|
|
1223
|
+
.write_string_with_format(row, col, s.to_string(), fmt)
|
|
1224
|
+
.map_err(|e| e.to_string())?;
|
|
1225
|
+
} else {
|
|
1226
|
+
worksheet
|
|
1227
|
+
.write_string(row, col, s.to_string())
|
|
1228
|
+
.map_err(|e| e.to_string())?;
|
|
1229
|
+
}
|
|
1230
|
+
return Ok(());
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Fallback: convert to string
|
|
1234
|
+
let s = value.str().map_err(|e| e.to_string())?.to_string();
|
|
1235
|
+
if let Some(fmt) = column_format {
|
|
1236
|
+
worksheet
|
|
1237
|
+
.write_string_with_format(row, col, &s, fmt)
|
|
1238
|
+
.map_err(|e| e.to_string())?;
|
|
1239
|
+
} else {
|
|
1240
|
+
worksheet
|
|
1241
|
+
.write_string(row, col, &s)
|
|
1242
|
+
.map_err(|e| e.to_string())?;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
Ok(())
|
|
1246
|
+
}
|
|
1247
|
+
|
|
845
1248
|
/// Convert a DataFrame (pandas or polars) to XLSX format
|
|
846
1249
|
#[allow(clippy::too_many_arguments)]
|
|
847
1250
|
fn convert_dataframe_to_xlsx(
|
|
@@ -858,6 +1261,7 @@ fn convert_dataframe_to_xlsx(
|
|
|
858
1261
|
header_format: Option<&HashMap<String, PyObject>>,
|
|
859
1262
|
row_heights: Option<&HashMap<u32, f64>>,
|
|
860
1263
|
constant_memory: bool,
|
|
1264
|
+
column_formats: Option<&HashMap<String, HashMap<String, PyObject>>>,
|
|
861
1265
|
) -> Result<(u32, u16), String> {
|
|
862
1266
|
// Create workbook and worksheet
|
|
863
1267
|
let mut workbook = Workbook::new();
|
|
@@ -900,6 +1304,13 @@ fn convert_dataframe_to_xlsx(
|
|
|
900
1304
|
|
|
901
1305
|
let col_count = columns.len() as u16;
|
|
902
1306
|
|
|
1307
|
+
// Build column formats if provided
|
|
1308
|
+
let col_formats: Vec<Option<Format>> = if let Some(cf) = column_formats {
|
|
1309
|
+
build_column_formats(py, &columns, cf)?
|
|
1310
|
+
} else {
|
|
1311
|
+
vec![None; columns.len()]
|
|
1312
|
+
};
|
|
1313
|
+
|
|
903
1314
|
// Write header if requested (and not using table, since table handles headers)
|
|
904
1315
|
if include_header && table_style.is_none() {
|
|
905
1316
|
for (col_idx, col_name) in columns.iter().enumerate() {
|
|
@@ -962,13 +1373,14 @@ fn convert_dataframe_to_xlsx(
|
|
|
962
1373
|
.map_err(|e: PyErr| e.to_string())?;
|
|
963
1374
|
|
|
964
1375
|
for (col_idx, value) in row_tuple.iter().enumerate() {
|
|
965
|
-
|
|
1376
|
+
write_py_value_with_format(
|
|
966
1377
|
worksheet,
|
|
967
1378
|
row_idx,
|
|
968
1379
|
col_idx as u16,
|
|
969
1380
|
value,
|
|
970
1381
|
&date_format,
|
|
971
1382
|
&datetime_format,
|
|
1383
|
+
col_formats.get(col_idx).and_then(|f| f.as_ref()),
|
|
972
1384
|
)?;
|
|
973
1385
|
}
|
|
974
1386
|
row_idx += 1;
|
|
@@ -987,13 +1399,14 @@ fn convert_dataframe_to_xlsx(
|
|
|
987
1399
|
.get_item(col_idx)
|
|
988
1400
|
.map_err(|e| format!("Failed to get value at ({}, {}): {}", i, col_idx, e))?;
|
|
989
1401
|
|
|
990
|
-
|
|
1402
|
+
write_py_value_with_format(
|
|
991
1403
|
worksheet,
|
|
992
1404
|
row_idx,
|
|
993
1405
|
col_idx as u16,
|
|
994
1406
|
&value,
|
|
995
1407
|
&date_format,
|
|
996
1408
|
&datetime_format,
|
|
1409
|
+
col_formats.get(col_idx).and_then(|f| f.as_ref()),
|
|
997
1410
|
)?;
|
|
998
1411
|
}
|
|
999
1412
|
row_idx += 1;
|
|
@@ -1134,6 +1547,10 @@ fn csv_to_xlsx(
|
|
|
1134
1547
|
/// constant_memory: Use constant memory mode for large files (default: False).
|
|
1135
1548
|
/// Reduces memory usage but disables table_style, freeze_panes,
|
|
1136
1549
|
/// row_heights, and autofit features.
|
|
1550
|
+
/// column_formats: Dict mapping column name patterns to format dicts (default: None)
|
|
1551
|
+
/// Supports wildcards: "prefix*", "*suffix", "*contains*", or exact match.
|
|
1552
|
+
/// Format options: bg_color, font_color, num_format, bold, italic, underline.
|
|
1553
|
+
/// Example: {"mcpt_*": {"bg_color": "#D6EAF8", "num_format": "0.00000"}}
|
|
1137
1554
|
///
|
|
1138
1555
|
/// Returns:
|
|
1139
1556
|
/// Tuple of (rows, columns) written to the Excel file
|
|
@@ -1154,7 +1571,7 @@ fn csv_to_xlsx(
|
|
|
1154
1571
|
/// >>> # For very large files, use constant_memory mode:
|
|
1155
1572
|
/// >>> xlsxturbo.df_to_xlsx(large_df, "big.xlsx", constant_memory=True)
|
|
1156
1573
|
#[pyfunction]
|
|
1157
|
-
#[pyo3(signature = (df, output_path, sheet_name = "Sheet1", header = true, autofit = false, table_style = None, freeze_panes = false, column_widths = None, table_name = None, header_format = None, row_heights = None, constant_memory = false))]
|
|
1574
|
+
#[pyo3(signature = (df, output_path, sheet_name = "Sheet1", header = true, autofit = false, table_style = None, freeze_panes = false, column_widths = None, table_name = None, header_format = None, row_heights = None, constant_memory = false, column_formats = None))]
|
|
1158
1575
|
#[allow(clippy::too_many_arguments)]
|
|
1159
1576
|
fn df_to_xlsx<'py>(
|
|
1160
1577
|
py: Python<'py>,
|
|
@@ -1170,6 +1587,7 @@ fn df_to_xlsx<'py>(
|
|
|
1170
1587
|
header_format: Option<&Bound<'py, PyAny>>,
|
|
1171
1588
|
row_heights: Option<HashMap<u32, f64>>,
|
|
1172
1589
|
constant_memory: bool,
|
|
1590
|
+
column_formats: Option<&Bound<'py, PyAny>>,
|
|
1173
1591
|
) -> PyResult<(u32, u16)> {
|
|
1174
1592
|
// Extract column_widths if provided
|
|
1175
1593
|
let extracted_column_widths = if let Some(cw) = column_widths {
|
|
@@ -1193,6 +1611,17 @@ fn df_to_xlsx<'py>(
|
|
|
1193
1611
|
None
|
|
1194
1612
|
};
|
|
1195
1613
|
|
|
1614
|
+
// Extract column_formats if provided
|
|
1615
|
+
let extracted_column_formats = if let Some(cf) = column_formats {
|
|
1616
|
+
if let Ok(dict) = cf.downcast::<pyo3::types::PyDict>() {
|
|
1617
|
+
Some(extract_column_formats(dict)?)
|
|
1618
|
+
} else {
|
|
1619
|
+
None
|
|
1620
|
+
}
|
|
1621
|
+
} else {
|
|
1622
|
+
None
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1196
1625
|
convert_dataframe_to_xlsx(
|
|
1197
1626
|
py,
|
|
1198
1627
|
df,
|
|
@@ -1207,6 +1636,7 @@ fn df_to_xlsx<'py>(
|
|
|
1207
1636
|
extracted_header_format.as_ref(),
|
|
1208
1637
|
row_heights.as_ref(),
|
|
1209
1638
|
constant_memory,
|
|
1639
|
+
extracted_column_formats.as_ref(),
|
|
1210
1640
|
)
|
|
1211
1641
|
.map_err(pyo3::exceptions::PyValueError::new_err)
|
|
1212
1642
|
}
|
|
@@ -1228,7 +1658,7 @@ fn version() -> &'static str {
|
|
|
1228
1658
|
/// - (DataFrame, sheet_name) - uses global defaults
|
|
1229
1659
|
/// - (DataFrame, sheet_name, options_dict) - per-sheet overrides
|
|
1230
1660
|
/// Options dict keys: header, autofit, table_style, freeze_panes,
|
|
1231
|
-
/// column_widths, row_heights, table_name, header_format
|
|
1661
|
+
/// column_widths, row_heights, table_name, header_format, column_formats
|
|
1232
1662
|
/// output_path: Path for the output XLSX file
|
|
1233
1663
|
/// header: Include column names as header row (default: True)
|
|
1234
1664
|
/// autofit: Automatically adjust column widths to fit content (default: False)
|
|
@@ -1243,6 +1673,10 @@ fn version() -> &'static str {
|
|
|
1243
1673
|
/// Example: {"bold": True, "bg_color": "#4F81BD", "font_color": "white"}
|
|
1244
1674
|
/// row_heights: Dict mapping row index (0-based) to height in points (default: None)
|
|
1245
1675
|
/// constant_memory: Use constant memory mode for large files (default: False).
|
|
1676
|
+
/// column_formats: Dict mapping column name patterns to format dicts (default: None)
|
|
1677
|
+
/// Supports wildcards: "prefix*", "*suffix", "*contains*", or exact match.
|
|
1678
|
+
/// Format options: bg_color, font_color, num_format, bold, italic, underline.
|
|
1679
|
+
/// Example: {"mcpt_*": {"bg_color": "#D6EAF8", "num_format": "0.00000"}}
|
|
1246
1680
|
///
|
|
1247
1681
|
/// Returns:
|
|
1248
1682
|
/// List of (rows, columns) tuples for each sheet
|
|
@@ -1265,7 +1699,7 @@ fn version() -> &'static str {
|
|
|
1265
1699
|
/// ... (df2, "Instructions", {"header": False})
|
|
1266
1700
|
/// ... ], "report.xlsx", autofit=True)
|
|
1267
1701
|
#[pyfunction]
|
|
1268
|
-
#[pyo3(signature = (sheets, output_path, header = true, autofit = false, table_style = None, freeze_panes = false, column_widths = None, table_name = None, header_format = None, row_heights = None, constant_memory = false))]
|
|
1702
|
+
#[pyo3(signature = (sheets, output_path, header = true, autofit = false, table_style = None, freeze_panes = false, column_widths = None, table_name = None, header_format = None, row_heights = None, constant_memory = false, column_formats = None))]
|
|
1269
1703
|
#[allow(clippy::too_many_arguments)]
|
|
1270
1704
|
fn dfs_to_xlsx<'py>(
|
|
1271
1705
|
py: Python<'py>,
|
|
@@ -1280,6 +1714,7 @@ fn dfs_to_xlsx<'py>(
|
|
|
1280
1714
|
header_format: Option<&Bound<'py, PyAny>>,
|
|
1281
1715
|
row_heights: Option<HashMap<u32, f64>>,
|
|
1282
1716
|
constant_memory: bool,
|
|
1717
|
+
column_formats: Option<&Bound<'py, PyAny>>,
|
|
1283
1718
|
) -> PyResult<Vec<(u32, u16)>> {
|
|
1284
1719
|
let mut workbook = Workbook::new();
|
|
1285
1720
|
let mut stats = Vec::new();
|
|
@@ -1306,6 +1741,17 @@ fn dfs_to_xlsx<'py>(
|
|
|
1306
1741
|
None
|
|
1307
1742
|
};
|
|
1308
1743
|
|
|
1744
|
+
// Extract global column_formats if provided
|
|
1745
|
+
let extracted_column_formats = if let Some(cf) = column_formats {
|
|
1746
|
+
if let Ok(dict) = cf.downcast::<pyo3::types::PyDict>() {
|
|
1747
|
+
Some(extract_column_formats(dict)?)
|
|
1748
|
+
} else {
|
|
1749
|
+
None
|
|
1750
|
+
}
|
|
1751
|
+
} else {
|
|
1752
|
+
None
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1309
1755
|
// Create formats
|
|
1310
1756
|
let date_format = Format::new().set_num_format("yyyy-mm-dd");
|
|
1311
1757
|
let datetime_format = Format::new().set_num_format("yyyy-mm-dd hh:mm:ss");
|
|
@@ -1346,6 +1792,12 @@ fn dfs_to_xlsx<'py>(
|
|
|
1346
1792
|
global_header_fmt.clone()
|
|
1347
1793
|
};
|
|
1348
1794
|
|
|
1795
|
+
// Get effective column formats (per-sheet or global)
|
|
1796
|
+
let effective_column_formats = sheet_config
|
|
1797
|
+
.column_formats
|
|
1798
|
+
.as_ref()
|
|
1799
|
+
.or(extracted_column_formats.as_ref());
|
|
1800
|
+
|
|
1349
1801
|
let worksheet = if constant_memory {
|
|
1350
1802
|
workbook.add_worksheet_with_constant_memory()
|
|
1351
1803
|
} else {
|
|
@@ -1386,6 +1838,14 @@ fn dfs_to_xlsx<'py>(
|
|
|
1386
1838
|
|
|
1387
1839
|
let col_count = columns.len() as u16;
|
|
1388
1840
|
|
|
1841
|
+
// Build column formats if provided
|
|
1842
|
+
let col_formats: Vec<Option<Format>> = if let Some(cf) = effective_column_formats {
|
|
1843
|
+
build_column_formats(py, &columns, cf)
|
|
1844
|
+
.map_err(pyo3::exceptions::PyValueError::new_err)?
|
|
1845
|
+
} else {
|
|
1846
|
+
vec![None; columns.len()]
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1389
1849
|
// Write header if requested
|
|
1390
1850
|
if effective_header {
|
|
1391
1851
|
for (col_idx, col_name) in columns.iter().enumerate() {
|
|
@@ -1440,13 +1900,14 @@ fn dfs_to_xlsx<'py>(
|
|
|
1440
1900
|
.map_err(|e: PyErr| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
|
|
1441
1901
|
|
|
1442
1902
|
for (col_idx, value) in row_tuple.iter().enumerate() {
|
|
1443
|
-
|
|
1903
|
+
write_py_value_with_format(
|
|
1444
1904
|
worksheet,
|
|
1445
1905
|
row_idx,
|
|
1446
1906
|
col_idx as u16,
|
|
1447
1907
|
value,
|
|
1448
1908
|
&date_format,
|
|
1449
1909
|
&datetime_format,
|
|
1910
|
+
col_formats.get(col_idx).and_then(|f| f.as_ref()),
|
|
1450
1911
|
)
|
|
1451
1912
|
.map_err(pyo3::exceptions::PyValueError::new_err)?;
|
|
1452
1913
|
}
|
|
@@ -1472,13 +1933,14 @@ fn dfs_to_xlsx<'py>(
|
|
|
1472
1933
|
))
|
|
1473
1934
|
})?;
|
|
1474
1935
|
|
|
1475
|
-
|
|
1936
|
+
write_py_value_with_format(
|
|
1476
1937
|
worksheet,
|
|
1477
1938
|
row_idx,
|
|
1478
1939
|
col_idx as u16,
|
|
1479
1940
|
&value,
|
|
1480
1941
|
&date_format,
|
|
1481
1942
|
&datetime_format,
|
|
1943
|
+
col_formats.get(col_idx).and_then(|f| f.as_ref()),
|
|
1482
1944
|
)
|
|
1483
1945
|
.map_err(pyo3::exceptions::PyValueError::new_err)?;
|
|
1484
1946
|
}
|
|
@@ -1647,4 +2109,32 @@ mod tests {
|
|
|
1647
2109
|
fn test_parse_string() {
|
|
1648
2110
|
assert!(matches!(parse_value("hello"), CellValue::String(_)));
|
|
1649
2111
|
}
|
|
2112
|
+
|
|
2113
|
+
#[test]
|
|
2114
|
+
fn test_matches_pattern_exact() {
|
|
2115
|
+
assert!(matches_pattern("column_name", "column_name"));
|
|
2116
|
+
assert!(!matches_pattern("column_name", "other"));
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
#[test]
|
|
2120
|
+
fn test_matches_pattern_prefix() {
|
|
2121
|
+
assert!(matches_pattern("mcpt_weight", "mcpt_*"));
|
|
2122
|
+
assert!(matches_pattern("mcpt_", "mcpt_*"));
|
|
2123
|
+
assert!(!matches_pattern("cc_weight", "mcpt_*"));
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
#[test]
|
|
2127
|
+
fn test_matches_pattern_suffix() {
|
|
2128
|
+
assert!(matches_pattern("col_weight", "*_weight"));
|
|
2129
|
+
assert!(matches_pattern("_weight", "*_weight"));
|
|
2130
|
+
assert!(!matches_pattern("col_height", "*_weight"));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
#[test]
|
|
2134
|
+
fn test_matches_pattern_contains() {
|
|
2135
|
+
assert!(matches_pattern("leadframe_difference", "*difference*"));
|
|
2136
|
+
assert!(matches_pattern("difference", "*difference*"));
|
|
2137
|
+
assert!(matches_pattern("my_difference_col", "*difference*"));
|
|
2138
|
+
assert!(!matches_pattern("other_column", "*difference*"));
|
|
2139
|
+
}
|
|
1650
2140
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|