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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14314a7af34dfb1bfd00bac2f8fde41c4fa03447020dfef48571055bdac3151d
4
- data.tar.gz: 915ca1cd79de28ca34023af4d884eb9c6728bd86abd29aa19bd62ca08e3914fe
3
+ metadata.gz: '045446748368a71ae4010c17da8b09d0a23ac2c3e9bdb0d63bf5ed54bf04221c'
4
+ data.tar.gz: 213cafbae07330020d41e179fc8473a92e5eab5da4cc595fb60e62f3764fa21e
5
5
  SHA512:
6
- metadata.gz: 346ed04a149c9f6201fdb84af8b5b1a5a1008a17ac1df8faaa26af8d500dc2e13ecd8eb55705638a99a27501d7ca757f08a50825c7faac4a4a2593fb556a94e5
7
- data.tar.gz: 712d24c13ab60499f91795cdbdd4d3c251e41c6b30b60dc8c9e3d25aaa671da98710d128dd673c859ad2ec336c817d69da6a46ea5e9f7a8015025444db59bcef
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` | number (Excel serial date, local time) |
254
- | `Date`, `DateTime` | number (Excel serial date, own offset) |
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`) | **69 ms** | 1.0x | 7 |
277
- | **fast_xlsx** (`memory: :low`) | **78 ms** | 1.1x | 7 |
278
- | **fast_xlsx** | **80 ms** | 1.2x | 10 |
279
- | [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 171 ms | 2.5x | 561,728 |
280
- | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 194 ms | 2.8x | 20,079 |
281
- | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 228 ms | 3.3x | 320,076 |
282
- | [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 583 ms | 8.4x | 1,483,899 |
283
- | [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 691 ms | 10.0x | 745,122 |
284
- | [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2650 ms | 38.3x | 8,700,448 |
285
-
286
- All outputs are 681–750 KB.
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.3.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.3.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
@@ -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::Number(excel_time(v)?)
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
- CellValue::Number(excel_date(v)?)
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 = Self::cell_format(&tables, row, *col, format.as_deref());
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
- Self::cell_format(&tables, row, col, format.map(|f| &*f.0)),
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 = Self::cell_format(&tables, first_row, first_col, format.map(|f| &*f.0));
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastXlsx
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac