fast_xlsx 0.7.0 → 0.8.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: eddcb1a0c063b94222c0a83e15b7a855a83b26674390ee87f59f1938d7007c9f
4
- data.tar.gz: 5aa9b9a7b6dbfb70bb53364b16fa31bb2c745534aefc9f0631367ed6cf658043
3
+ metadata.gz: 5115d5bf58c96ec3a87ab9168e954b77d2360cac3314c049efd8bed45feb634a
4
+ data.tar.gz: 6ecf4642347e56626a0508acd2784cd6773f4be9ab52f3c4ede1da48031e57e1
5
5
  SHA512:
6
- metadata.gz: d854850a2dd9d9b82d8bbbe4568f41336b3e4a89b977ddfcffb8e08a083305222111dded66d33e8f999dfbfae4d1da319e16f40835e83962bb7ac6081686b333
7
- data.tar.gz: 973794fd6d14e139f8173e0726f0938f61022f5b503d5b761b4abaf089b9e0dd3debec9083248859f9380a64fd6ead89ec2efc8af64296338960b6caed1269df
6
+ metadata.gz: d95432cd98346cc8339c20f71c14839325e2236f43b05180960125764eb4dbff1fe434b1acf00919425b8be613e00b8fc318e548f5e4359450f9119f12cab205
7
+ data.tar.gz: 2e2563be474cd5613a713f680c433a8e35186423d367004b4c02afa02905859cc139fc1efd762da66b04d748f3b1c3404def56bedb3b5c86cc0c8c3ecc4f2041
@@ -0,0 +1,7 @@
1
+ # cargo-mutants settings (see .github/workflows/mutants.yml).
2
+ exclude_re = [
3
+ # Equivalent mutant: sending every format through FastXlsx::Format._coerce,
4
+ # not just Hashes, changes nothing but speed (_coerce returns a Format or
5
+ # nil as it is), so no test can catch it.
6
+ 'replace match guard RHash::from_value\(\*?f\)\.is_some\(\) with true in Worksheet::write_any',
7
+ ]
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0] - 2026-10-02
4
+
5
+ ### Changed
6
+
7
+ - The API is frozen for 1.0: 0.8.x releases only fix bugs. The README states the deprecation policy.
8
+
9
+ ### Added
10
+
11
+ - `write` and `merge_range` also take the format as `format:`, like `append` (positional still works).
12
+
3
13
  ## [0.7.0] - 2026-10-02
4
14
 
5
15
  ### Added
data/README.md CHANGED
@@ -5,9 +5,85 @@
5
5
 
6
6
  Fast `.xlsx` writer for Ruby, built on [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) via [magnus](https://github.com/matsadler/magnus).
7
7
 
8
- > **Status: early.** The API may still change before 1.0.
8
+ > **Status: release candidate.** The API is frozen as of 0.8: 0.8.x releases only fix bugs, and 1.0 follows once it has been in real use without needing changes.
9
9
 
10
- ## Usage
10
+ **Roadmap:** [1.0.0](https://github.com/7a6163/fast_xlsx/milestone/1) (release plan) · [1.x](https://github.com/7a6163/fast_xlsx/milestone/2) (planned features)
11
+
12
+ **Contents:** [Installation](#installation) · [Getting started](#getting-started) · [Guide](#guide) · [Performance](#performance) · [Support and versioning](#support-and-versioning) · [Development](#development)
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bundle add fast_xlsx
18
+ ```
19
+
20
+ Requires CRuby 3.3 or later. Precompiled gems cover Linux, macOS and Windows; see [Support and versioning](#support-and-versioning).
21
+
22
+ Coming from fast_excel? See [Migrating from fast_excel](docs/MIGRATING_FROM_FAST_EXCEL.md).
23
+
24
+ ## Getting started
25
+
26
+ A sales report, step by step: a header, one row per sale, a totals row, then sized columns, a frozen header and filter buttons.
27
+
28
+ ```ruby
29
+ require "date"
30
+ require "fast_xlsx"
31
+
32
+ # Sample data; in an app it comes from your database.
33
+ Sale = Struct.new(:region, :product, :units, :price, :sold_on)
34
+ sales = [
35
+ Sale.new("North", "Widget", 120, 9.5, Date.new(2026, 7, 3)),
36
+ Sale.new("North", "Gadget", 45, 24.0, Date.new(2026, 7, 9)),
37
+ Sale.new("South", "Widget", 80, 9.5, Date.new(2026, 7, 14)),
38
+ Sale.new("South", "Gizmo", 12, 120.0, Date.new(2026, 7, 21))
39
+ ]
40
+
41
+ # 1. A workbook with one worksheet.
42
+ wb = FastXlsx::Workbook.new
43
+ ws = wb.add_worksheet("Sales")
44
+
45
+ # 2. A header row, bold on a light blue fill.
46
+ header = FastXlsx::Format.new(bold: true, bg_color: "#DDEBF7", border_bottom: :thin)
47
+ ws.append(["Region", "Product", "Units", "Price", "Sold on", "Revenue"], format: header)
48
+
49
+ # 3. One row per sale. Dates show as yyyy-mm-dd on their own; a formula is a
50
+ # FastXlsx::Formula. In formulas rows count from 1, so the first sale is row 2.
51
+ money = FastXlsx::Format.new(num_format: "#,##0.00")
52
+ sales.each.with_index(2) do |sale, row|
53
+ ws.append([sale.region, sale.product, sale.units, sale.price, sale.sold_on,
54
+ FastXlsx::Formula.new("C#{row}*D#{row}")],
55
+ format: [nil, nil, nil, money, nil, money])
56
+ end
57
+
58
+ # 4. A totals row: the header's look, with the money format added.
59
+ last = sales.size + 1
60
+ ws.append(["Total", nil, FastXlsx::Formula.new("SUM(C2:C#{last})"), nil, nil,
61
+ FastXlsx::Formula.new("SUM(F2:F#{last})")],
62
+ format: [header, header, header, header, header, header.merge(num_format: "#,##0.00")])
63
+
64
+ # 5. Easier to read: columns sized to fit (set by hand where autofit can't tell), the header kept in view while
65
+ # scrolling, and filter buttons on the header (rows and columns count from 0).
66
+ ws.autofit
67
+ ws.column_width(3..5, 12) # money, dates, formulas: autofit measures raw values
68
+ ws.freeze_panes("A2")
69
+ ws.autofilter("A1:F#{last}")
70
+
71
+ # 6. Save it. (In Rails: send_data wb.to_xlsx, filename: "sales.xlsx", ...)
72
+ wb.save("sales.xlsx")
73
+ ```
74
+
75
+ - **Rows and columns count from 0** in method calls (`write(0, 0, ...)` is A1); Excel references like `"A2"` and formulas count from 1 as in Excel.
76
+ - **Formulas** are calculated by Excel when it opens the file.
77
+ - **Large exports** (tens of thousands of rows and up): use `FastXlsx::Workbook.new(memory: :constant)`. Rows go to disk as they are added, so memory stays flat; fill each worksheet from top to bottom. See [Memory modes](#memory-modes).
78
+ - **Rails**: build the workbook in the controller and `send_data wb.to_xlsx`; see [Rails](#rails).
79
+
80
+ The [Guide](#guide) below covers every feature; the [API docs](https://www.rubydoc.info/gems/fast_xlsx) list every method.
81
+
82
+ ## Guide
83
+
84
+ [Writing cells](#writing-cells) · [Rails](#rails) · [Memory modes](#memory-modes) · [Formats](#formats) · [Columns and filters](#columns-and-filters) · [Sheet view and printing](#sheet-view-and-printing) · [Cell ranges](#cell-ranges) · [Layout](#layout) · [Outline groups](#outline-groups) · [Defined names](#defined-names) · [Protection](#protection) · [Conditional formats](#conditional-formats) · [Data validation](#data-validation) · [Comments](#comments) · [Images](#images) · [Tables](#tables) · [Charts](#charts) · [Errors](#errors)
85
+
86
+ ### Writing cells
11
87
 
12
88
  ```ruby
13
89
  require "fast_xlsx"
@@ -19,6 +95,7 @@ ws << ["id", "name", "created_at"] # append a row
19
95
  ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
20
96
  ws.write(0, 5, 42) # write a single cell (row, col, value)
21
97
  ws.write("F1", 42) # or by its Excel reference
98
+ ws.write("F1", 42, { bold: true }) # a format positionally, or as format: like append
22
99
  ws["F1"] = 42 # the same (ws[0, 5] = 42 too)
23
100
 
24
101
  wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
@@ -367,17 +444,12 @@ BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb b
367
444
  BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb fast_xlsx:low unique
368
445
  ```
369
446
 
370
- ## Installation
371
-
372
- ```bash
373
- bundle add fast_xlsx
374
- ```
375
-
376
- ### Support and versioning
447
+ ## Support and versioning
377
448
 
378
449
  - **Ruby:** CRuby 3.3 and later. A Ruby version is dropped only after it reaches its end of life, and only in a minor release. JRuby and TruffleRuby are not supported (this is a native extension).
379
450
  - **Precompiled gems:** Linux (x86_64 and aarch64, glibc and musl; ARM musl), macOS (arm64 and x86_64) and Windows (x64). Before each release the Linux x86_64, macOS arm64 and Windows gems are installed and loaded on Ruby 3.3, 3.4 and 4.0. Other platforms build from source and need a Rust toolchain.
380
451
  - **Versions:** from 1.0, [Semantic Versioning](https://semver.org): no breaking API change before 2.0. Until then a minor release (0.x.0) can change behaviour; the [changelog](CHANGELOG.md) lists every change under "Changed".
452
+ - **Deprecations:** from 1.0, a method or option to be removed first prints a warning (with `warn`, so on by default) for at least one minor release, with the CHANGELOG naming its replacement; it goes only in the next major version.
381
453
  - **Output:** `.xlsx` files that open in Excel 2007 and later, LibreOffice and Google Sheets. Updating rust_xlsxwriter can change the bytes of the file but not what it contains.
382
454
 
383
455
  ## Development
data/bench/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fast_xlsx (0.7.0)
4
+ fast_xlsx (0.8.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.7.0)
80
+ fast_xlsx (0.8.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
@@ -0,0 +1,123 @@
1
+ # Migrating from fast_excel
2
+
3
+ fast_xlsx started as a rewrite of [fast_excel](https://github.com/Paxa/fast_excel) on [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter), and keeps its spirit: append rows fast, with little memory. Most code moves over with a few renames. This guide lists them, then the differences in behaviour to check.
4
+
5
+ **Why switch**
6
+
7
+ - 2.8–3.3× faster on the [README benchmark](../README.md#speed), and `memory: :constant` adds about 1 MB however large the file.
8
+ - Precompiled gems for Linux, macOS and Windows: no C compiler or FFI at install.
9
+ - Dates show as dates without a format, every option is checked (a typo raises instead of being ignored), and saving doesn't block other threads.
10
+ - Conditional formats, data validation, tables, charts, images, comments, protection and printing options as Ruby methods.
11
+
12
+ **Before you start**
13
+
14
+ - fast_xlsx needs CRuby 3.3 or later (fast_excel supports 2.7+).
15
+ - Rows and columns are 0-based in both, and `<<` appends in both.
16
+
17
+ ## A report, before and after
18
+
19
+ ```ruby
20
+ # fast_excel
21
+ workbook = FastExcel.open(constant_memory: true)
22
+ worksheet = workbook.add_worksheet("Orders")
23
+ bold = workbook.bold_format
24
+ worksheet.append_row(["Order", "Total", "Placed at"], bold)
25
+ money = workbook.number_format("#,##0.00")
26
+ orders.each { |o| worksheet.append_row([o.number, o.total, o.created_at], [nil, money, nil]) }
27
+ worksheet.set_column_width(0, 20)
28
+ send_data workbook.read_string, filename: "orders.xlsx"
29
+ ```
30
+
31
+ ```ruby
32
+ # fast_xlsx
33
+ workbook = FastXlsx::Workbook.new(memory: :constant)
34
+ worksheet = workbook.add_worksheet("Orders")
35
+ worksheet.append(["Order", "Total", "Placed at"], format: { bold: true })
36
+ money = FastXlsx::Format.new(num_format: "#,##0.00")
37
+ orders.each { |o| worksheet.append([o.number, o.total, o.created_at], format: [nil, money, nil]) }
38
+ worksheet.column_width(0, 20)
39
+ send_data workbook.to_xlsx, filename: "orders.xlsx"
40
+ ```
41
+
42
+ `created_at` now shows as `yyyy-mm-dd hh:mm:ss` without a format; with fast_excel it was a number unless you passed a date format.
43
+
44
+ ## Method by method
45
+
46
+ ### Workbook
47
+
48
+ | fast_excel | fast_xlsx |
49
+ |---|---|
50
+ | `FastExcel.open` | `FastXlsx::Workbook.new` |
51
+ | `FastExcel.open(constant_memory: true)` | `FastXlsx::Workbook.new(memory: :constant)` (or `:low`, which keeps Excel's shared strings) |
52
+ | `FastExcel.open("report.xlsx")` … `workbook.close` | `FastXlsx::Workbook.new` … `workbook.save("report.xlsx")` |
53
+ | `workbook.read_string` | `workbook.to_xlsx` |
54
+ | `workbook.remove_tmp_folder` | not needed: no temp file is created |
55
+ | `workbook.add_worksheet(name)` | `workbook.add_worksheet(name)` |
56
+ | `workbook.get_worksheet_by_name(name)` | `workbook.worksheet(name)` |
57
+ | `workbook.add_format(bold: true)` | `FastXlsx::Format.new(bold: true)`, or pass `{ bold: true }` where a format goes |
58
+ | `workbook.bold_format` | `FastXlsx::Format.new(bold: true)` |
59
+ | `workbook.number_format("0.00")` | `FastXlsx::Format.new(num_format: "0.00")` |
60
+
61
+ ### Writing
62
+
63
+ | fast_excel | fast_xlsx |
64
+ |---|---|
65
+ | `worksheet << values` / `append_row(values, format)` | `worksheet << values` / `append(values, format: format)` |
66
+ | `write_value(row, col, value, format)` | `write(row, col, value, format)`, or `write("B2", value, format)`, or `worksheet["B2"] = value` |
67
+ | `write_number`, `write_string`, `write_datetime`, `write_formula`, `write_url`, `write_boolean` | `write`: the type comes from the value |
68
+ | `write_row(row, values, formats)` | `values.each_with_index { \|v, col\| worksheet.write(row, col, v, format) }`, or `append` when it is the next row |
69
+ | `FastExcel::Formula.new("SUM(A1:A9)")` | `FastXlsx::Formula.new("SUM(A1:A9)")` |
70
+ | `FastExcel::URL.new(url)` | `FastXlsx::URL.new(url)`, and `URL.new(url, text: "shown")` |
71
+ | `FastExcel.date_num(time, offset)` with a date format | `time` as it is: dates are converted and formatted for you |
72
+ | `worksheet.last_row_number` | `worksheet.next_row - 1` |
73
+
74
+ ### Columns, rows and filters
75
+
76
+ | fast_excel | fast_xlsx |
77
+ |---|---|
78
+ | `set_column_width(col, width)` | `column_width(col, width)` |
79
+ | `set_columns_width(first, last, width)` | `column_width(first..last, width)` |
80
+ | `set_column(first, last, width, format)` | `column_width(first..last, width)` and `column_format(first..last, format)` |
81
+ | `set_row(row, height, format)` | `row_height(row, height)` and `row_format(row, format)` |
82
+ | `worksheet.auto_width = true` (before writing) | `worksheet.autofit` (after writing; see below) |
83
+ | `enable_filters!(end_col: 3)` | `autofilter(0, 0, worksheet.next_row - 1, 3)`, or `autofilter("A1:D100")` |
84
+ | `freeze_panes`, `merge_range` and other libxlsxwriter functions | Ruby methods with the same idea: `freeze_panes("A2")`, `merge_range("A1:D1", "Title")`; see the [Guide](../README.md#guide) |
85
+
86
+ ### Format options
87
+
88
+ | fast_excel | fast_xlsx |
89
+ |---|---|
90
+ | `bold:`, `italic:`, `text_wrap:`, `shrink:`, `font_size:`, `font_name:`, `num_format:`, `rotation:`, `indent:`, `bg_color:` | the same |
91
+ | `font_family:` | `font_name:` |
92
+ | `font_strikeout: true` | `strikeout: true` |
93
+ | `underline: :underline_single` (`_double`, `_single_accounting`, `_double_accounting`) | `underline: true` or `:single` (`:double`, `:single_accounting`, `:double_accounting`) |
94
+ | `font_script: :font_subscript` / `:font_superscript` | `font_script: :subscript` / `:superscript` |
95
+ | `font_color: :orange`, `"#FF0000"`, `0xFF0000` | `"#FF0000"` or `0xFF0000`: color names aren't supported, use their hex value |
96
+ | `border: :border_thin` | `border: :thin` (also `:medium`, `:thick`, `:dashed`, `:dotted`, `:double`, `:hair`) |
97
+ | `left: :medium`, `top:`, `right:`, `bottom:` | `border_left: :medium`, `border_top:`, `border_right:`, `border_bottom:` |
98
+ | `left_color:`, `top_color:` … per side | `border_color:`, one color for every side |
99
+ | `align: { h: :align_center, v: :align_vertical_center }` | `align: :center, valign: :center` |
100
+
101
+ ## Differences in behaviour
102
+
103
+ - **Dates have a format.** fast_excel wrote `Time` and `Date` as plain numbers unless you passed a date format. fast_xlsx shows them as `yyyy-mm-dd` (`Date`) or `yyyy-mm-dd hh:mm:ss` (`Time`, `DateTime`), and adds that to a format without a `num_format` (a bold row stays bold). Pass your own `num_format` to keep a different one. Both use the value's own UTC offset, so times don't shift. Dates before 1900-03-01 now show correctly; fast_excel's showed one day late, because Excel counts a 1900-02-29 that never existed.
104
+ - **Errors raise.** Unknown or misspelled options, invalid colors and values Excel can't hold (a string over 32,767 characters, a row past 1,048,576) raise `ArgumentError`, `RangeError` or `FastXlsx::Error`; see [Errors](../README.md#errors). In `:constant` mode, writing to a row already on disk raises `FastXlsx::Error` (fast_excel raised `ArgumentError`).
105
+ - **Autofit runs after writing.** fast_excel measured text as you wrote it (strings only). `autofit` measures what is in memory when you call it, so in `:constant` / `:low` mode it only sees the last rows: set widths with `column_width` there. It measures raw values, so give formatted numbers, dates and formulas a width by hand.
106
+ - **No temp files.** fast_excel always wrote to a file, a temporary one without a filename. fast_xlsx builds the workbook in memory (`:standard`) or in its own temp files (`:constant` / `:low`) and gives you the result with `to_xlsx` or `save`.
107
+ - **Formats are values.** A `FastXlsx::Format` is frozen and created on its own, not from a workbook, so one format can serve several workbooks. Use `format.merge(italic: true)` for a variant.
108
+
109
+ ## Not available
110
+
111
+ - **The libxlsxwriter functions under `Libxlsxwriter.*`.** fast_xlsx has no raw binding; the [Guide](../README.md#guide) and the [API docs](https://www.rubydoc.info/gems/fast_xlsx) cover what is exposed, and the [roadmap](https://github.com/7a6163/fast_xlsx/milestone/2) what is planned.
112
+ - **`FastExcel.open(default_format: ...)`.** Use `column_format` / `row_format`, or pass the format when writing.
113
+ - **Some format options:** color names, per-side border colors, `font_outline`, `font_shadow`, `text_justlast`, and alignments other than left, center and right (horizontal) or top, center and bottom (vertical).
114
+ - **Reading or editing existing files**: neither library does this.
115
+
116
+ ## Checklist
117
+
118
+ 1. Replace `gem "fast_excel"` with `gem "fast_xlsx"`, and check you are on Ruby 3.3+.
119
+ 2. Swap `FastExcel.open` / `read_string` / `close` for `FastXlsx::Workbook.new` / `to_xlsx` / `save`.
120
+ 3. Rename the methods and format options from the tables above; run your code: a missed rename raises `NoMethodError` or `ArgumentError` rather than being ignored.
121
+ 4. Drop `FastExcel.date_num` and date formats you only added to make dates readable.
122
+ 5. Replace `auto_width = true` with `autofit` after writing (or `column_width` in `:constant` mode).
123
+ 6. Open one generated file in Excel and compare.
@@ -0,0 +1,44 @@
1
+ require "date"
2
+ require "fast_xlsx"
3
+
4
+ # Sample data; in an app it comes from your database.
5
+ Sale = Struct.new(:region, :product, :units, :price, :sold_on)
6
+ sales = [
7
+ Sale.new("North", "Widget", 120, 9.5, Date.new(2026, 7, 3)),
8
+ Sale.new("North", "Gadget", 45, 24.0, Date.new(2026, 7, 9)),
9
+ Sale.new("South", "Widget", 80, 9.5, Date.new(2026, 7, 14)),
10
+ Sale.new("South", "Gizmo", 12, 120.0, Date.new(2026, 7, 21))
11
+ ]
12
+
13
+ # 1. A workbook with one worksheet.
14
+ wb = FastXlsx::Workbook.new
15
+ ws = wb.add_worksheet("Sales")
16
+
17
+ # 2. A header row, bold on a light blue fill.
18
+ header = FastXlsx::Format.new(bold: true, bg_color: "#DDEBF7", border_bottom: :thin)
19
+ ws.append(["Region", "Product", "Units", "Price", "Sold on", "Revenue"], format: header)
20
+
21
+ # 3. One row per sale. Dates show as yyyy-mm-dd on their own; a formula is a
22
+ # FastXlsx::Formula. In formulas rows count from 1, so the first sale is row 2.
23
+ money = FastXlsx::Format.new(num_format: "#,##0.00")
24
+ sales.each.with_index(2) do |sale, row|
25
+ ws.append([sale.region, sale.product, sale.units, sale.price, sale.sold_on,
26
+ FastXlsx::Formula.new("C#{row}*D#{row}")],
27
+ format: [nil, nil, nil, money, nil, money])
28
+ end
29
+
30
+ # 4. A totals row: the header's look, with the money format added.
31
+ last = sales.size + 1
32
+ ws.append(["Total", nil, FastXlsx::Formula.new("SUM(C2:C#{last})"), nil, nil,
33
+ FastXlsx::Formula.new("SUM(F2:F#{last})")],
34
+ format: [header, header, header, header, header, header.merge(num_format: "#,##0.00")])
35
+
36
+ # 5. Easier to read: columns sized to fit (set by hand where autofit can't tell), the header kept in view while
37
+ # scrolling, and filter buttons on the header (rows and columns count from 0).
38
+ ws.autofit
39
+ ws.column_width(3..5, 12) # money, dates, formulas: autofit measures raw values
40
+ ws.freeze_panes("A2")
41
+ ws.autofilter("A1:F#{last}")
42
+
43
+ # 6. Save it. (In Rails: send_data wb.to_xlsx, filename: "sales.xlsx", ...)
44
+ wb.save("sales.xlsx")
@@ -52,6 +52,27 @@ fn xerr(e: XlsxError) -> Error {
52
52
 
53
53
  // Excel's limits that rust_xlsxwriter doesn't check: a negative width or
54
54
  // height hides the column or row, a larger one is capped or invalid.
55
+ const BOTH_FORMATS: &str = "pass the format either positionally or as format:, not both";
56
+
57
+ // The format from write's keywords ({ format: f }); others are rejected.
58
+ fn format_keyword(ruby: &Ruby, keywords: Value) -> Result<Value, Error> {
59
+ let keywords = RHash::try_convert(keywords)?;
60
+ let format = ruby.to_symbol("format");
61
+ keywords.foreach(|key: Value, _: Value| {
62
+ if key.eql(format)? {
63
+ Ok(ForEach::Continue)
64
+ } else {
65
+ Err(Error::new(
66
+ ruby.exception_arg_error(),
67
+ format!("unknown keyword: {}", key.inspect()),
68
+ ))
69
+ }
70
+ })?;
71
+ Ok(keywords
72
+ .get(format)
73
+ .unwrap_or_else(|| ruby.qnil().as_value()))
74
+ }
75
+
55
76
  // A size that must be a finite number above 0 (NaN or a negative one would
56
77
  // be written into the file as is).
57
78
  fn check_positive(ruby: &Ruby, what: &str, size: f64) -> Result<f64, Error> {
@@ -1171,15 +1192,25 @@ impl Worksheet {
1171
1192
  // here rather than in a Ruby wrapper, since it runs once per cell; the
1172
1193
  // rest ("B2", or a wrong argument count) goes to Ruby's _write_ref.
1173
1194
  fn write_any(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
1195
+ // A real format: keyword arrives as a trailing Hash; Ruby says whether
1196
+ // it was one, so a Hash written as the value isn't mistaken for it.
1197
+ let keyword_given = unsafe { rb_sys::rb_keyword_given_p() } != 0;
1198
+ let (args, keyword) = match (keyword_given, args.split_last()) {
1199
+ (true, Some((last, rest))) => (rest, Some(format_keyword(ruby, *last)?)),
1200
+ _ => (args, None),
1201
+ };
1174
1202
  let index = |i: usize| args.get(i).and_then(|v| Integer::from_value(*v));
1175
1203
  if let (3 | 4, Some(row), Some(col)) = (args.len(), index(0), index(1)) {
1204
+ if keyword.is_some() && args.len() == 4 {
1205
+ return Err(Error::new(ruby.exception_arg_error(), BOTH_FORMATS));
1206
+ }
1176
1207
  // A Hash of options becomes a (cached) Format, as in Ruby.
1177
- let format_arg = match args.get(3) {
1178
- Some(f) if RHash::from_value(*f).is_some() => Some(
1208
+ let format_arg = match keyword.or_else(|| args.get(3).copied()) {
1209
+ Some(f) if RHash::from_value(f).is_some() => Some(
1179
1210
  ruby.get_inner(&FORMAT)
1180
- .funcall::<_, _, Value>("_coerce", (*f,))?,
1211
+ .funcall::<_, _, Value>("_coerce", (f,))?,
1181
1212
  ),
1182
- f => f.copied(),
1213
+ f => f,
1183
1214
  };
1184
1215
  let format = match format_arg {
1185
1216
  Some(f) => Option::<&Format>::try_convert(f)?,
@@ -1195,7 +1226,14 @@ impl Worksheet {
1195
1226
  )?;
1196
1227
  return Ok(rb_self);
1197
1228
  }
1198
- rb_self.funcall("_write_ref", args)
1229
+ rb_self.funcall(
1230
+ "_write_ref",
1231
+ (
1232
+ ruby.ary_from_iter(args.iter().copied()),
1233
+ keyword.is_some(),
1234
+ keyword,
1235
+ ),
1236
+ )
1199
1237
  }
1200
1238
 
1201
1239
  fn write(
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FastXlsx
4
4
  # The gem version.
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
6
6
  end
data/lib/fast_xlsx.rb CHANGED
@@ -186,10 +186,13 @@ module FastXlsx
186
186
  # Writes one cell.
187
187
  # @overload write(row, col, value, format = nil)
188
188
  # @overload write(ref, value, format = nil)
189
+ # @overload write(row, col, value, format: nil)
190
+ # @overload write(ref, value, format: nil)
189
191
  # @param value [Numeric, String, Time, Date, DateTime, true, false, nil,
190
192
  # Formula, URL, RichString, #to_s] dates get yyyy-mm-dd (hh:mm:ss) unless
191
193
  # the format has a num_format
192
- # @param format [Format, Hash, nil] a Hash of options works too
194
+ # @param format [Format, Hash, nil] positional or format: (a little slower:
195
+ # Ruby builds a Hash for the keyword); a Hash of options works too
193
196
  # @return [self]
194
197
  #
195
198
  # @!method <<(values)
@@ -252,11 +255,15 @@ module FastXlsx
252
255
  # Hides the gridlines on screen (see {#page_setup} for printing).
253
256
  # @return [self]
254
257
  class Worksheet
258
+ NO_FORMAT = Object.new.freeze # format: not given
259
+ private_constant :NO_FORMAT
260
+
255
261
  # write(row, col, value, format = nil) or write("B2", value, format = nil)
256
262
  # is native: the (row, col) form runs once per cell, so it skips a Ruby
257
263
  # wrapper. Other forms come here.
258
- def _write_ref(*args)
264
+ def _write_ref(args, keyword_given, keyword_format)
259
265
  row, col, (value, format) = CellRange.cell(args, 1..2)
266
+ format = Format.check_both(args.size > (args.first.is_a?(String) ? 2 : 3), keyword_given, format, keyword_format)
260
267
  _write(row, col, value, Format._coerce(format))
261
268
  self
262
269
  end
@@ -311,10 +318,12 @@ module FastXlsx
311
318
 
312
319
  # Merges the range and writes value (any cell type) into its first cell.
313
320
  # @overload merge_range(*range, value, format = nil)
321
+ # @overload merge_range(*range, value, format: nil)
314
322
  # @return [self]
315
323
  # @raise [FastXlsx::Error] when it overlaps an earlier merge
316
- def merge_range(*args)
317
- range, (value, format) = CellRange.split(args, 1..2)
324
+ def merge_range(*args, format: NO_FORMAT)
325
+ range, (value, positional) = CellRange.split(args, 1..2)
326
+ format = Format.check_both(!positional.nil?, !NO_FORMAT.equal?(format), positional, format)
318
327
  _merge_range(*range, value, Format._coerce(format))
319
328
  end
320
329
 
@@ -726,6 +735,15 @@ module FastXlsx
726
735
  Format.new(**to_h, **other.to_h, **)
727
736
  end
728
737
 
738
+ # The format given positionally or as format:, raising if both were.
739
+ # @api private
740
+ def self.check_both(positional_given, keyword_given, positional, keyword)
741
+ return positional unless keyword_given
742
+ raise ArgumentError, "pass the format either positionally or as format:, not both" if positional_given
743
+
744
+ keyword
745
+ end
746
+
729
747
  # Distinct Hashes kept by {._coerce}; past this, the oldest is dropped.
730
748
  CACHE_SIZE = 1_024
731
749
 
data/sig/fast_xlsx.rbs CHANGED
@@ -67,7 +67,9 @@ module FastXlsx
67
67
  def []=: (String cell, cell value) -> cell
68
68
  | (Integer row, Integer col, cell value) -> cell
69
69
  def write: (Integer row, Integer col, cell value, ?format? format) -> self
70
+ | (Integer row, Integer col, cell value, format: format?) -> self
70
71
  | (String cell, cell value, ?format? format) -> self
72
+ | (String cell, cell value, format: format?) -> self
71
73
  def append: (Array[cell] values, ?format: format? | Array[format?]) -> self
72
74
  def <<: (Array[cell] row) -> self
73
75
  def concat: (Array[Array[cell]] rows) -> self
@@ -120,6 +122,7 @@ module FastXlsx
120
122
  def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?format? format) -> self
121
123
  | (String range, cell value, ?format? format) -> self
122
124
  | (indexes rows, indexes cols, cell value, ?format? format) -> self
125
+ | (*cell_range | cell args, format: format?) -> self
123
126
  def conditional_format: (*cell_range range,
124
127
  type: :cell | :text | :formula | :data_bar | :color_scale,
125
128
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac
@@ -32,6 +32,7 @@ extensions:
32
32
  - ext/fast_xlsx/extconf.rb
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".cargo/mutants.toml"
35
36
  - ".yardopts"
36
37
  - CHANGELOG.md
37
38
  - Cargo.lock
@@ -44,6 +45,8 @@ files:
44
45
  - bench/compare.rb
45
46
  - bench/memory.rb
46
47
  - bench/write.rb
48
+ - docs/MIGRATING_FROM_FAST_EXCEL.md
49
+ - examples/getting_started.rb
47
50
  - examples/showcase.rb
48
51
  - ext/fast_xlsx/Cargo.toml
49
52
  - ext/fast_xlsx/build.rs