fast_xlsx 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +15 -13
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/src/lib.rs +53 -7
- data/lib/fast_xlsx/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '045446748368a71ae4010c17da8b09d0a23ac2c3e9bdb0d63bf5ed54bf04221c'
|
|
4
|
+
data.tar.gz: 213cafbae07330020d41e179fc8473a92e5eab5da4cc595fb60e62f3764fa21e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dce10715e570fffa0c5ff69899065c07a3f34f6beb8a9597c095a39f271991d11f05239e62135d853063759a016518fca9f61113e0ae8991c97663a62b4066ff
|
|
7
|
+
data.tar.gz: d87800324af3edab7719ce665cb836d213456102833e04351cfdec508ef063e4c4b16aa80e298ab764262b6a4c6c97e8b2844b48b97639a3e005572369ecf2ce
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Dates and times written without a format now get `yyyy-mm-dd` (`Date`) or `yyyy-mm-dd hh:mm:ss` (`Time`, `DateTime`) instead of showing as serial numbers. A cell's own format, its table column's and `column_format` still win. Applying the format makes writing date-heavy data about 15% slower, the same as passing a date format yourself.
|
|
8
|
+
|
|
3
9
|
## [0.3.0] - 2026-10-01
|
|
4
10
|
|
|
5
11
|
### Changed
|
data/README.md
CHANGED
|
@@ -82,6 +82,8 @@ ws.write(1, 2, Date.today, date) # format one cell
|
|
|
82
82
|
|
|
83
83
|
Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
|
|
84
84
|
|
|
85
|
+
Dates and times written without a format get `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss`; a table column's format or `column_format` wins over that. A format you pass replaces it, so give date cells one with a `num_format` (e.g. `Format.new(bold: true, num_format: "yyyy-mm-dd")`), or Excel shows the serial number.
|
|
86
|
+
|
|
85
87
|
### Columns and filters
|
|
86
88
|
|
|
87
89
|
```ruby
|
|
@@ -250,8 +252,8 @@ Values are mapped by type:
|
|
|
250
252
|
|---|---|
|
|
251
253
|
| `Integer`, `Float`, any `Numeric` | number |
|
|
252
254
|
| `String` | string |
|
|
253
|
-
| `Time` |
|
|
254
|
-
| `Date
|
|
255
|
+
| `Time`, `DateTime` | date and time (`yyyy-mm-dd hh:mm:ss` unless formatted), in its own offset |
|
|
256
|
+
| `Date` | date (`yyyy-mm-dd` unless formatted) |
|
|
255
257
|
| `FastXlsx::Formula.new("SUM(A1:A9)")` | formula |
|
|
256
258
|
| `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
|
|
257
259
|
| `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
|
|
@@ -273,17 +275,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
|
|
|
273
275
|
|
|
274
276
|
| Library | Time | vs fastest | Ruby objects allocated |
|
|
275
277
|
|---|---:|---:|---:|
|
|
276
|
-
| **fast_xlsx** (`memory: :constant`) | **
|
|
277
|
-
| **fast_xlsx** (`memory: :low`) | **
|
|
278
|
-
| **fast_xlsx** | **
|
|
279
|
-
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 |
|
|
280
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) |
|
|
281
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 |
|
|
282
|
-
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 |
|
|
283
|
-
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 |
|
|
284
|
-
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 |
|
|
285
|
-
|
|
286
|
-
All outputs are
|
|
278
|
+
| **fast_xlsx** (`memory: :constant`) | **77 ms** | 1.0x | 7 |
|
|
279
|
+
| **fast_xlsx** (`memory: :low`) | **86 ms** | 1.1x | 7 |
|
|
280
|
+
| **fast_xlsx** | **89 ms** | 1.1x | 10 |
|
|
281
|
+
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 183 ms | 2.4x | 561,728 |
|
|
282
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 200 ms | 2.6x | 20,079 |
|
|
283
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 238 ms | 3.1x | 320,076 |
|
|
284
|
+
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 608 ms | 7.8x | 1,483,899 |
|
|
285
|
+
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 689 ms | 8.9x | 745,122 |
|
|
286
|
+
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2755 ms | 35.6x | 8,700,448 |
|
|
287
|
+
|
|
288
|
+
All outputs are 689–750 KB.
|
|
287
289
|
|
|
288
290
|
### Memory
|
|
289
291
|
|
data/bench/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
fast_xlsx (0.
|
|
4
|
+
fast_xlsx (0.4.0)
|
|
5
5
|
rb_sys (~> 0.9.130)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -77,7 +77,7 @@ DEPENDENCIES
|
|
|
77
77
|
CHECKSUMS
|
|
78
78
|
caxlsx (4.5.0) sha256=e3d98d859f148df05d5462086b5079b523f29c1766b569535f1d68629ce743ff
|
|
79
79
|
fast_excel (0.5.0) sha256=59c418bdcf586a6030798d4e3a9f575742badaceaf7bb90f38eee5feb5bdc478
|
|
80
|
-
fast_xlsx (0.
|
|
80
|
+
fast_xlsx (0.4.0)
|
|
81
81
|
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
|
82
82
|
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
|
83
83
|
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
data/ext/fast_xlsx/src/lib.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::cell::{Cell, RefCell};
|
|
2
2
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
3
|
+
use std::sync::LazyLock;
|
|
3
4
|
use std::sync::{Arc, Mutex};
|
|
4
5
|
|
|
5
6
|
use magnus::{
|
|
@@ -112,6 +113,8 @@ struct Worksheet {
|
|
|
112
113
|
table_formats: RefCell<Vec<TableColumnFormat>>,
|
|
113
114
|
// Merged ranges as (first_row, first_col, last_row, last_col).
|
|
114
115
|
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
116
|
+
// Column ranges given a format with column_format.
|
|
117
|
+
formatted_columns: RefCell<Vec<(u16, u16)>>,
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
struct TableColumnFormat {
|
|
@@ -179,6 +182,7 @@ impl Workbook {
|
|
|
179
182
|
flushes_rows: self.constant_memory || self.low_memory,
|
|
180
183
|
table_formats: RefCell::new(Vec::new()),
|
|
181
184
|
merges: RefCell::new(Vec::new()),
|
|
185
|
+
formatted_columns: RefCell::new(Vec::new()),
|
|
182
186
|
})
|
|
183
187
|
}
|
|
184
188
|
|
|
@@ -297,6 +301,8 @@ enum CellValue {
|
|
|
297
301
|
Empty,
|
|
298
302
|
Text(String),
|
|
299
303
|
Number(f64),
|
|
304
|
+
// An Excel serial date; true when it has a time of day (Time, DateTime).
|
|
305
|
+
Date(f64, bool),
|
|
300
306
|
Bool(bool),
|
|
301
307
|
Formula(String),
|
|
302
308
|
Url(String, Option<String>),
|
|
@@ -315,7 +321,7 @@ impl CellValue {
|
|
|
315
321
|
} else if v.is_kind_of(ruby.class_numeric()) {
|
|
316
322
|
CellValue::Number(f64::try_convert(v)?)
|
|
317
323
|
} else if v.is_kind_of(ruby.class_time()) {
|
|
318
|
-
CellValue::
|
|
324
|
+
CellValue::Date(excel_time(v)?, true)
|
|
319
325
|
} else if v.is_kind_of(ruby.class_true_class()) || v.is_kind_of(ruby.class_false_class()) {
|
|
320
326
|
CellValue::Bool(v.to_bool())
|
|
321
327
|
} else if v.is_kind_of(ruby.get_inner(&FORMULA)) {
|
|
@@ -333,7 +339,8 @@ impl CellValue {
|
|
|
333
339
|
})?;
|
|
334
340
|
CellValue::Rich(parts)
|
|
335
341
|
} else if v.respond_to("jd", false)? {
|
|
336
|
-
|
|
342
|
+
// Date has no #hour; DateTime does.
|
|
343
|
+
CellValue::Date(excel_date(v)?, v.respond_to("hour", false)?)
|
|
337
344
|
} else {
|
|
338
345
|
CellValue::Text(v.funcall("to_s", ())?)
|
|
339
346
|
};
|
|
@@ -341,6 +348,20 @@ impl CellValue {
|
|
|
341
348
|
Ok(value)
|
|
342
349
|
}
|
|
343
350
|
|
|
351
|
+
// The format for a value written without one: without a number format
|
|
352
|
+
// Excel shows a date as its serial number.
|
|
353
|
+
fn default_format(&self) -> Option<&'static rust_xlsxwriter::Format> {
|
|
354
|
+
static DATE: LazyLock<rust_xlsxwriter::Format> =
|
|
355
|
+
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd"));
|
|
356
|
+
static DATE_TIME: LazyLock<rust_xlsxwriter::Format> =
|
|
357
|
+
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd hh:mm:ss"));
|
|
358
|
+
match self {
|
|
359
|
+
CellValue::Date(_, false) => Some(&DATE),
|
|
360
|
+
CellValue::Date(_, true) => Some(&DATE_TIME),
|
|
361
|
+
_ => None,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
344
365
|
// Rejects what the writer would, so a row or merge fails before anything
|
|
345
366
|
// is written. Numbers, booleans and formulas only fail on a bad row/column.
|
|
346
367
|
fn check(&self) -> Result<(), Error> {
|
|
@@ -371,7 +392,7 @@ impl CellValue {
|
|
|
371
392
|
match self {
|
|
372
393
|
CellValue::Empty => Ok(()),
|
|
373
394
|
CellValue::Text(s) => emit(ws, row, col, s.as_str(), format),
|
|
374
|
-
CellValue::Number(n) => emit(ws, row, col, *n, format),
|
|
395
|
+
CellValue::Number(n) | CellValue::Date(n, _) => emit(ws, row, col, *n, format),
|
|
375
396
|
CellValue::Bool(b) => emit(ws, row, col, *b, format),
|
|
376
397
|
CellValue::Formula(f) => emit(
|
|
377
398
|
ws,
|
|
@@ -950,6 +971,28 @@ impl Worksheet {
|
|
|
950
971
|
})
|
|
951
972
|
}
|
|
952
973
|
|
|
974
|
+
// The format a value is written with: its own, else its table column's,
|
|
975
|
+
// else its type's default (dates) unless column_format covers the column,
|
|
976
|
+
// whose format rust_xlsxwriter applies to cells written without one.
|
|
977
|
+
fn format_for<'a>(
|
|
978
|
+
&self,
|
|
979
|
+
tables: &'a [TableColumnFormat],
|
|
980
|
+
row: u32,
|
|
981
|
+
col: u16,
|
|
982
|
+
own: Option<&'a rust_xlsxwriter::Format>,
|
|
983
|
+
value: &CellValue,
|
|
984
|
+
) -> Option<&'a rust_xlsxwriter::Format> {
|
|
985
|
+
Self::cell_format(tables, row, col, own).or_else(|| {
|
|
986
|
+
value.default_format().filter(|_| {
|
|
987
|
+
!self
|
|
988
|
+
.formatted_columns
|
|
989
|
+
.borrow()
|
|
990
|
+
.iter()
|
|
991
|
+
.any(|&(first, last)| (first..=last).contains(&col))
|
|
992
|
+
})
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
|
|
953
996
|
fn write_row(
|
|
954
997
|
&self,
|
|
955
998
|
ruby: &Ruby,
|
|
@@ -976,7 +1019,7 @@ impl Worksheet {
|
|
|
976
1019
|
// Values were checked when converted, so only a bad row number
|
|
977
1020
|
// fails here, and it fails on the first cell.
|
|
978
1021
|
for (col, value, format) in &values {
|
|
979
|
-
let format =
|
|
1022
|
+
let format = self.format_for(&tables, row, *col, format.as_deref(), value);
|
|
980
1023
|
value.write(ws, row, *col, format)?;
|
|
981
1024
|
}
|
|
982
1025
|
Ok(())
|
|
@@ -1025,7 +1068,7 @@ impl Worksheet {
|
|
|
1025
1068
|
ws,
|
|
1026
1069
|
row,
|
|
1027
1070
|
col,
|
|
1028
|
-
|
|
1071
|
+
rb_self.format_for(&tables, row, col, format.map(|f| &*f.0), &value),
|
|
1029
1072
|
)
|
|
1030
1073
|
})?;
|
|
1031
1074
|
rb_self.advance(row);
|
|
@@ -1070,7 +1113,9 @@ impl Worksheet {
|
|
|
1070
1113
|
ws.set_column_range_format(first, last, &format.0)
|
|
1071
1114
|
.map(|_| ())
|
|
1072
1115
|
.map_err(xerr)
|
|
1073
|
-
})
|
|
1116
|
+
})?;
|
|
1117
|
+
self.formatted_columns.borrow_mut().push((first, last));
|
|
1118
|
+
Ok(())
|
|
1074
1119
|
}
|
|
1075
1120
|
|
|
1076
1121
|
fn autofilter(
|
|
@@ -1193,7 +1238,8 @@ impl Worksheet {
|
|
|
1193
1238
|
}
|
|
1194
1239
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1195
1240
|
let tables = rb_self.table_formats.borrow();
|
|
1196
|
-
let format =
|
|
1241
|
+
let format =
|
|
1242
|
+
rb_self.format_for(&tables, first_row, first_col, format.map(|f| &*f.0), &value);
|
|
1197
1243
|
let default = rust_xlsxwriter::Format::new();
|
|
1198
1244
|
rb_self.with_ws(|ws| {
|
|
1199
1245
|
// The value was checked when converted, so once the range is
|
data/lib/fast_xlsx/version.rb
CHANGED