fast_xlsx 0.6.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: c78ccbcfe82884370ef3e64e4613812a91c521f479495184bd99df1cb7f1d36c
4
- data.tar.gz: 86702f59f3c33c490ef3879e0178a7bcc9650fdba612bd1cd17ba24662b0392d
3
+ metadata.gz: 5115d5bf58c96ec3a87ab9168e954b77d2360cac3314c049efd8bed45feb634a
4
+ data.tar.gz: 6ecf4642347e56626a0508acd2784cd6773f4be9ab52f3c4ede1da48031e57e1
5
5
  SHA512:
6
- metadata.gz: 629352aa7bf14685eef2d940f78b23e142f66b946b6d8f2eb36db0bd004574aefb2e04fd29fe2fc42e9684ac5d125725cfea5384d7a439b8f5d361411f68149c
7
- data.tar.gz: f8755a00230b81f405339f9d123344bf827c3010d80f159c0b5408e2fbcc984d3fd94758fd65e7fa5562d0e8b23825cb0d0b791bf97057db013259afd49c3c96
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,33 @@
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
+
13
+ ## [0.7.0] - 2026-10-02
14
+
15
+ ### Added
16
+
17
+ - A Hash of options works wherever a format goes (`write(0, 0, "x", { bold: true })`, `append(row, format: { bold: true })`, table columns, conditional formats, rich strings); equal Hashes share one format.
18
+ - `ws["B2"] = value` and `ws[row, col] = value` write a cell.
19
+ - `Format#to_h` (its options) and `Format#merge` (a new format with more options, from keywords, a Hash or another Format).
20
+ - Formats with the same options are `==`; `eql?` and `hash` follow `Hash#eql?` (so `10` and `10.0` differ there), letting formats serve as Hash keys. Options may have String keys (e.g. parsed from JSON or YAML); the same option as a String and a Symbol raises `ArgumentError`.
21
+ - `Workbook.new` and `Workbook#add_worksheet` yield the new object to a block.
22
+
23
+ ### Changed
24
+
25
+ - A `Format` is frozen, and keeps frozen copies of its option values: one is shared wherever the same options are used.
26
+
27
+ ### Fixed
28
+
29
+ - `Workbook.new { |wb| ... }` ignored its block.
30
+
3
31
  ## [0.6.0] - 2026-10-02
4
32
 
5
33
  ### 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,11 +95,37 @@ 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
99
+ ws["F1"] = 42 # the same (ws[0, 5] = 42 too)
22
100
 
23
101
  wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
24
102
  wb.save("report.xlsx") # or wb.to_xlsx => binary String
25
103
  ```
26
104
 
105
+ `Workbook.new` and `add_worksheet` also take a block, which gets the new object:
106
+
107
+ ```ruby
108
+ wb = FastXlsx::Workbook.new do |wb|
109
+ wb.add_worksheet("Users") do |ws|
110
+ ws.append(%w[id name email], format: { bold: true })
111
+ User.find_each { |u| ws << u.attributes.values_at("id", "name", "email") } # a Hash's values in column order
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Rails
117
+
118
+ ```ruby
119
+ def export
120
+ wb = FastXlsx::Workbook.new(memory: :constant) # large exports: memory stays flat
121
+ ws = wb.add_worksheet("Orders")
122
+ ws.append(["Order", "Customer", "Total", "Placed at"], format: { bold: true })
123
+ Order.includes(:customer).find_each { |o| ws << [o.number, o.customer.name, o.total, o.created_at] }
124
+ send_data wb.to_xlsx, filename: "orders.xlsx",
125
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
126
+ end
127
+ ```
128
+
27
129
  ### Memory modes
28
130
 
29
131
  By default every cell stays in memory until the file is saved. For large exports, two modes write each finished row to a temp file instead:
@@ -80,6 +182,15 @@ ws.write(1, 2, Date.today, date) # format one cell
80
182
  | `locked` | `false` keeps the cell editable on a protected sheet (default `true`) |
81
183
  | `hidden` | `true` hides the cell's formula on a protected sheet |
82
184
 
185
+ A Hash of options works wherever a format goes (equal Hashes share one format), and a format can be extended:
186
+
187
+ ```ruby
188
+ ws.append(["Total", 1_234], format: { bold: true })
189
+ title = header.merge(font_size: 16) # header's options plus font_size; header.to_h lists them
190
+ ```
191
+
192
+ In a loop over many cells, create the `Format` once: building one from a Hash on every call is slower.
193
+
83
194
  Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
84
195
 
85
196
  Dates and times are shown as `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss`. A format without a `num_format` (yours, a table column's or `column_format`'s) keeps its look and gets that date format added, so `append(row, format: bold)` gives bold dates; a format with a `num_format` is used as is.
@@ -333,17 +444,12 @@ BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb b
333
444
  BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb fast_xlsx:low unique
334
445
  ```
335
446
 
336
- ## Installation
337
-
338
- ```bash
339
- bundle add fast_xlsx
340
- ```
341
-
342
- ### Support and versioning
447
+ ## Support and versioning
343
448
 
344
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).
345
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.
346
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.
347
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.
348
454
 
349
455
  ## Development
data/bench/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fast_xlsx (0.6.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.6.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")
@@ -33,6 +33,7 @@ static ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Err
33
33
  static FORMULA: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Formula"));
34
34
  static URL: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "URL"));
35
35
  static RICH_STRING: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "RichString"));
36
+ static FORMAT: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Format"));
36
37
 
37
38
  // A row or column outside the sheet is a RangeError, like a negative one
38
39
  // (which fails converting to an unsigned number before reaching the writer).
@@ -51,6 +52,27 @@ fn xerr(e: XlsxError) -> Error {
51
52
 
52
53
  // Excel's limits that rust_xlsxwriter doesn't check: a negative width or
53
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
+
54
76
  // A size that must be a finite number above 0 (NaN or a negative one would
55
77
  // be written into the file as is).
56
78
  fn check_positive(ruby: &Ruby, what: &str, size: f64) -> Result<f64, Error> {
@@ -1170,10 +1192,28 @@ impl Worksheet {
1170
1192
  // here rather than in a Ruby wrapper, since it runs once per cell; the
1171
1193
  // rest ("B2", or a wrong argument count) goes to Ruby's _write_ref.
1172
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
+ };
1173
1202
  let index = |i: usize| args.get(i).and_then(|v| Integer::from_value(*v));
1174
1203
  if let (3 | 4, Some(row), Some(col)) = (args.len(), index(0), index(1)) {
1175
- let format = match args.get(3) {
1176
- Some(f) => Option::<&Format>::try_convert(*f)?,
1204
+ if keyword.is_some() && args.len() == 4 {
1205
+ return Err(Error::new(ruby.exception_arg_error(), BOTH_FORMATS));
1206
+ }
1207
+ // A Hash of options becomes a (cached) Format, as in Ruby.
1208
+ let format_arg = match keyword.or_else(|| args.get(3).copied()) {
1209
+ Some(f) if RHash::from_value(f).is_some() => Some(
1210
+ ruby.get_inner(&FORMAT)
1211
+ .funcall::<_, _, Value>("_coerce", (f,))?,
1212
+ ),
1213
+ f => f,
1214
+ };
1215
+ let format = match format_arg {
1216
+ Some(f) => Option::<&Format>::try_convert(f)?,
1177
1217
  None => None,
1178
1218
  };
1179
1219
  Self::write(
@@ -1186,7 +1226,14 @@ impl Worksheet {
1186
1226
  )?;
1187
1227
  return Ok(rb_self);
1188
1228
  }
1189
- 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
+ )
1190
1237
  }
1191
1238
 
1192
1239
  fn write(
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FastXlsx
4
4
  # The gem version.
5
- VERSION = "0.6.0"
5
+ VERSION = "0.8.0"
6
6
  end
data/lib/fast_xlsx.rb CHANGED
@@ -74,7 +74,9 @@ module FastXlsx
74
74
  def initialize(*parts)
75
75
  raise ArgumentError, "RichString needs at least one segment" if parts.empty?
76
76
 
77
- @segments = parts.map { |part| part.is_a?(Array) ? [part[0].to_s, part[1]] : [part.to_s, nil] }.freeze
77
+ @segments = parts.map do |part|
78
+ part.is_a?(Array) ? [part[0].to_s, Format._coerce(part[1])] : [part.to_s, nil]
79
+ end.freeze
78
80
  end
79
81
  end
80
82
 
@@ -111,21 +113,33 @@ module FastXlsx
111
113
  # must be filled top to bottom: :constant stores strings inline (memory
112
114
  # stays flat), :low keeps Excel's shared string table (memory grows with
113
115
  # the number of unique strings).
116
+ # @yieldparam workbook [Workbook] when a block is given
117
+ # @return [Workbook]
114
118
  # @raise [ArgumentError] for an unknown mode
119
+ # @example
120
+ # wb = FastXlsx::Workbook.new do |wb|
121
+ # wb.add_worksheet("Report") { |ws| ws << %w[id name] }
122
+ # end
115
123
  def self.new(memory: :standard)
116
124
  unless MEMORY_MODES.include?(memory)
117
125
  raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
118
126
  end
119
127
 
120
- _new(memory == :constant, memory == :low)
128
+ workbook = _new(memory == :constant, memory == :low)
129
+ yield workbook if block_given?
130
+ workbook
121
131
  end
122
132
 
123
133
  # @param name [String, nil] nil takes the first free "SheetN"
134
+ # @yieldparam worksheet [Worksheet] when a block is given
124
135
  # @return [Worksheet]
125
136
  # @raise [FastXlsx::Error] for an invalid name or one already used
126
137
  # (names ignore case)
127
138
  def add_worksheet(name = nil)
128
- _add_worksheet(name).tap { |ws| worksheets << ws }
139
+ worksheet = _add_worksheet(name)
140
+ worksheets << worksheet
141
+ yield worksheet if block_given?
142
+ worksheet
129
143
  end
130
144
 
131
145
  # The same Worksheet objects add_worksheet returned, so their append
@@ -172,10 +186,13 @@ module FastXlsx
172
186
  # Writes one cell.
173
187
  # @overload write(row, col, value, format = nil)
174
188
  # @overload write(ref, value, format = nil)
189
+ # @overload write(row, col, value, format: nil)
190
+ # @overload write(ref, value, format: nil)
175
191
  # @param value [Numeric, String, Time, Date, DateTime, true, false, nil,
176
192
  # Formula, URL, RichString, #to_s] dates get yyyy-mm-dd (hh:mm:ss) unless
177
193
  # the format has a num_format
178
- # @param format [Format, nil]
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
179
196
  # @return [self]
180
197
  #
181
198
  # @!method <<(values)
@@ -238,23 +255,37 @@ module FastXlsx
238
255
  # Hides the gridlines on screen (see {#page_setup} for printing).
239
256
  # @return [self]
240
257
  class Worksheet
258
+ NO_FORMAT = Object.new.freeze # format: not given
259
+ private_constant :NO_FORMAT
260
+
241
261
  # write(row, col, value, format = nil) or write("B2", value, format = nil)
242
262
  # is native: the (row, col) form runs once per cell, so it skips a Ruby
243
263
  # wrapper. Other forms come here.
244
- def _write_ref(*args)
264
+ def _write_ref(args, keyword_given, keyword_format)
245
265
  row, col, (value, format) = CellRange.cell(args, 1..2)
246
- _write(row, col, value, format)
266
+ format = Format.check_both(args.size > (args.first.is_a?(String) ? 2 : 3), keyword_given, format, keyword_format)
267
+ _write(row, col, value, Format._coerce(format))
247
268
  self
248
269
  end
249
270
  private :_write_ref
250
271
 
272
+ # Writes one cell: ws["B2"] = 42 or ws[1, 1] = 42. Use {#write} to give
273
+ # it a format.
274
+ # @overload []=(ref, value)
275
+ # @overload []=(row, col, value)
276
+ def []=(*cell, value)
277
+ row, col = CellRange.cell(cell)
278
+ write(row, col, value)
279
+ value
280
+ end
281
+
251
282
  # Appends a row after the last row written.
252
283
  # @param values [Array] cell values, as for {#write}
253
- # @param format [Format, Array<Format, nil>, nil] one for every cell, or
284
+ # @param format [Format, Hash, Array<Format, Hash, nil>, nil] one for every cell, or
254
285
  # one per cell
255
286
  # @return [self]
256
287
  def append(values, format: nil)
257
- _append(values, format)
288
+ _append(values, format.is_a?(Array) ? format.map { |f| Format._coerce(f) } : Format._coerce(format))
258
289
  end
259
290
 
260
291
  # @param columns [Integer, Range<Integer>]
@@ -287,11 +318,13 @@ module FastXlsx
287
318
 
288
319
  # Merges the range and writes value (any cell type) into its first cell.
289
320
  # @overload merge_range(*range, value, format = nil)
321
+ # @overload merge_range(*range, value, format: nil)
290
322
  # @return [self]
291
323
  # @raise [FastXlsx::Error] when it overlaps an earlier merge
292
- def merge_range(*args)
293
- range, (value, format) = CellRange.split(args, 1..2)
294
- _merge_range(*range, value, format)
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)
327
+ _merge_range(*range, value, Format._coerce(format))
295
328
  end
296
329
 
297
330
  # Highlights cells in the range by rule; see the README for each type's
@@ -300,11 +333,12 @@ module FastXlsx
300
333
  # @param type [Symbol] :cell, :text, :formula, :data_bar or :color_scale
301
334
  # @option options [Symbol] :criteria e.g. :>, :between, :contains
302
335
  # @option options [Numeric, String, Array] :value
303
- # @option options [Format] :format
336
+ # @option options [Format, Hash] :format
304
337
  # @option options [Integer] :colors 2 or 3 (:color_scale)
305
338
  # @return [self]
306
- def conditional_format(*range, type:, **)
307
- _conditional_format(*CellRange.split(range).first, { type: type, ** })
339
+ def conditional_format(*range, type:, **options)
340
+ options[:format] = Format._coerce(options[:format]) if options.key?(:format)
341
+ _conditional_format(*CellRange.split(range).first, { type: type, **options })
308
342
  end
309
343
 
310
344
  # Restricts what can be entered in the range; see the README.
@@ -364,8 +398,13 @@ module FastXlsx
364
398
  # @option options [String] :name
365
399
  # @option options [Boolean] :total_row, :banded_rows, :autofilter
366
400
  # @return [self]
367
- def add_table(*range, **)
368
- _add_table(*CellRange.split(range).first, { ** })
401
+ def add_table(*range, **options)
402
+ if options[:columns].is_a?(Array)
403
+ options[:columns] = options[:columns].map do |column|
404
+ column.is_a?(Hash) && column.key?(:format) ? column.merge(format: Format._coerce(column[:format])) : column
405
+ end
406
+ end
407
+ _add_table(*CellRange.split(range).first, options)
369
408
  end
370
409
 
371
410
  # Printed page header, using Excel codes such as "&CPage &P of &N".
@@ -506,20 +545,20 @@ module FastXlsx
506
545
  # may combine them or show the row's. Each row is kept (about 1 KB) until
507
546
  # saving, so style a whole sheet with {#column_format}.
508
547
  # @param rows [Integer, Range<Integer>]
509
- # @param format [Format]
548
+ # @param format [Format, Hash]
510
549
  # @return [self]
511
550
  # @raise [FastXlsx::Error] in :constant / :low mode, for rows already on disk
512
551
  def row_format(rows, format)
513
- _row_format(*CellRange.bounds(rows), format)
552
+ _row_format(*CellRange.bounds(rows), Format._coerce(format))
514
553
  self
515
554
  end
516
555
 
517
556
  # Default format for cells in these columns that are written without one.
518
557
  # @param columns [Integer, Range<Integer>]
519
- # @param format [Format]
558
+ # @param format [Format, Hash]
520
559
  # @return [self]
521
560
  def column_format(columns, format)
522
- _column_format(*CellRange.bounds(columns), format)
561
+ _column_format(*CellRange.bounds(columns), Format._coerce(format))
523
562
  self
524
563
  end
525
564
  end
@@ -633,9 +672,97 @@ module FastXlsx
633
672
  # border_color:, locked:, hidden:
634
673
  # @raise [ArgumentError] for an unknown option or an invalid value
635
674
  def self.new(**options)
675
+ options = _options(options)
636
676
  # Apply border: first so border_left: etc. override it whatever the order.
637
- options = { border: options[:border], **options.except(:border) } if options.key?(:border)
638
- _new(options)
677
+ ordered = options.key?(:border) ? { border: options[:border], **options.except(:border) } : options
678
+ format = _new(ordered)
679
+ format.instance_variable_set(:@options, options)
680
+ format.freeze
681
+ end
682
+
683
+ # Options with Symbol keys (String ones come from JSON or YAML) and frozen
684
+ # copies of the values, so the format can't change behind the file's back.
685
+ # @api private
686
+ def self._options(given)
687
+ options = given.to_h { |key, value| [key.to_sym, _frozen(value)] }.freeze
688
+ return options if options.size == given.size
689
+
690
+ twice = given.keys.map(&:to_sym).tally.select { |_, count| count > 1 }.keys
691
+ raise ArgumentError, "format option given twice (as a String and a Symbol): #{twice.join(", ")}"
692
+ end
693
+
694
+ # @api private
695
+ def self._frozen(value)
696
+ value.frozen? ? value : value.dup.freeze
697
+ end
698
+
699
+ # The options this format was created with.
700
+ # @return [Hash{Symbol => Object}] frozen
701
+ def to_h
702
+ @options
703
+ end
704
+
705
+ # Formats with the same options are equal; 10 and 10.0 count as the same.
706
+ # @return [Boolean]
707
+ def ==(other)
708
+ other.is_a?(Format) && to_h == other.to_h
709
+ end
710
+
711
+ # Equal as Hash keys: like Hash#eql?, 10 and 10.0 differ, matching {#hash}.
712
+ # @return [Boolean]
713
+ def eql?(other)
714
+ other.is_a?(Format) && to_h.eql?(other.to_h)
715
+ end
716
+
717
+ # @return [Integer]
718
+ def hash
719
+ [Format, to_h].hash
720
+ end
721
+
722
+ # A new format with these options added to (or replacing) this one's.
723
+ # @param other [Format, Hash, nil] options to add, like Hash#merge
724
+ # @example
725
+ # title = header.merge(font_size: 16)
726
+ # bold_money = bold.merge(money)
727
+ # @return [Format]
728
+ # @raise [TypeError] when other has no #to_h
729
+ def merge(other = nil, **)
730
+ unless other.respond_to?(:to_h)
731
+ raise TypeError,
732
+ "can't merge #{other.class} into a Format: use a Hash or a Format"
733
+ end
734
+
735
+ Format.new(**to_h, **other.to_h, **)
736
+ end
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
+
747
+ # Distinct Hashes kept by {._coerce}; past this, the oldest is dropped.
748
+ CACHE_SIZE = 1_024
749
+
750
+ # A Format for a Hash of options; equal Hashes share one, so a Hash written
751
+ # with every row costs about as much as a Format made once. Other values
752
+ # are returned as they are. An invalid Hash raises and isn't kept.
753
+ # @api private
754
+ def self._coerce(format)
755
+ return format unless format.is_a?(Hash)
756
+
757
+ # ponytail: evicts the oldest entry, not the least used one, since
758
+ # moving each hit costs every write; a Hash still in use is rebuilt at
759
+ # worst once per CACHE_SIZE new Hashes.
760
+ cache = (@cache ||= {})
761
+ cache.fetch(format) do
762
+ coerced = new(**format)
763
+ cache.shift if cache.size >= CACHE_SIZE
764
+ cache[format.dup.freeze] = coerced
765
+ end
639
766
  end
640
767
  end
641
768
  end
data/sig/fast_xlsx.rbs CHANGED
@@ -8,11 +8,14 @@ module FastXlsx
8
8
  def read: () -> String
9
9
  end
10
10
 
11
+ # A Format, or a Hash of its options.
12
+ type format = Format | Hash[Symbol | String, untyped]
13
+
11
14
  type cell = Numeric | String | Time | Date | Formula | URL | RichString | bool | nil | _ToS
12
15
 
13
16
  class RichString
14
17
  attr_reader segments: Array[[String, Format?]]
15
- def initialize: (*(_ToS | [_ToS, Format?]) parts) -> void
18
+ def initialize: (*(_ToS | [_ToS, format?]) parts) -> void
16
19
  end
17
20
 
18
21
  class Formula
@@ -28,8 +31,8 @@ module FastXlsx
28
31
 
29
32
  class Workbook
30
33
  MEMORY_MODES: Array[Symbol]
31
- def self.new: (?memory: :standard | :constant | :low) -> Workbook
32
- def add_worksheet: (?String? name) -> Worksheet
34
+ def self.new: (?memory: :standard | :constant | :low) ?{ (Workbook) -> void } -> Workbook
35
+ def add_worksheet: (?String? name) ?{ (Worksheet) -> void } -> Worksheet
33
36
  def worksheets: () -> Array[Worksheet]
34
37
  def worksheet: (String name) -> Worksheet?
35
38
  def properties: (?title: String, ?subject: String, ?author: String, ?manager: String,
@@ -53,18 +56,27 @@ module FastXlsx
53
56
  ?rotation: Integer, ?indent: Integer,
54
57
  ?border: border, ?border_left: border, ?border_right: border,
55
58
  ?border_top: border, ?border_bottom: border, ?border_color: color) -> Format
59
+ def to_h: () -> Hash[Symbol, untyped]
60
+ def merge: (?format? other, **untyped options) -> Format
61
+ def ==: (untyped other) -> bool
62
+ def eql?: (untyped other) -> bool
63
+ def hash: () -> Integer
56
64
  end
57
65
 
58
66
  class Worksheet
59
- def write: (Integer row, Integer col, cell value, ?Format? format) -> self
60
- | (String cell, cell value, ?Format? format) -> self
61
- def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
67
+ def []=: (String cell, cell value) -> cell
68
+ | (Integer row, Integer col, cell value) -> cell
69
+ def write: (Integer row, Integer col, cell value, ?format? format) -> self
70
+ | (Integer row, Integer col, cell value, format: format?) -> self
71
+ | (String cell, cell value, ?format? format) -> self
72
+ | (String cell, cell value, format: format?) -> self
73
+ def append: (Array[cell] values, ?format: format? | Array[format?]) -> self
62
74
  def <<: (Array[cell] row) -> self
63
75
  def concat: (Array[Array[cell]] rows) -> self
64
76
  def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
65
77
  def hide_rows: (Integer | Range[Integer] rows) -> self
66
78
  def hide_columns: (Integer | Range[Integer] columns) -> self
67
- def row_format: (Integer | Range[Integer] rows, Format format) -> self
79
+ def row_format: (Integer | Range[Integer] rows, format format) -> self
68
80
  def default_row_height: (Numeric height) -> self
69
81
  def selection: (*cell_range range) -> self
70
82
  def top_left_cell: (Integer row, Integer col) -> self
@@ -74,7 +86,7 @@ module FastXlsx
74
86
  | :inconsistent_column_formula
75
87
  def ignore_error: (*cell_range | ignorable_error args) -> self
76
88
  def unprotect_range: (*cell_range range, ?name: String?, ?password: String?) -> self
77
- def column_format: (Integer | Range[Integer] columns, Format format) -> self
89
+ def column_format: (Integer | Range[Integer] columns, format format) -> self
78
90
  def autofit: () -> self
79
91
  # A cell range: four 0-based numbers, an Excel reference ("A1:D10"), or
80
92
  # rows and columns as Integers or Ranges. Methods with options take it as
@@ -107,13 +119,14 @@ module FastXlsx
107
119
  def group_rows: (Integer | Range[Integer] rows, ?collapsed: bool) -> self
108
120
  def group_columns: (Integer | Range[Integer] columns, ?collapsed: bool) -> self
109
121
  def protect: (?password: String?, ?allow: protect_action | Array[protect_action] | nil) -> self
110
- def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
111
- | (String range, cell value, ?Format? format) -> self
112
- | (indexes rows, indexes cols, cell value, ?Format? format) -> self
122
+ def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?format? format) -> self
123
+ | (String range, cell value, ?format? format) -> self
124
+ | (indexes rows, indexes cols, cell value, ?format? format) -> self
125
+ | (*cell_range | cell args, format: format?) -> self
113
126
  def conditional_format: (*cell_range range,
114
127
  type: :cell | :text | :formula | :data_bar | :color_scale,
115
128
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
116
- ?format: Format, ?colors: 2 | 3) -> self
129
+ ?format: format, ?colors: 2 | 3) -> self
117
130
  def data_validation: (*cell_range range,
118
131
  type: :list | :whole_number | :decimal | :text_length,
119
132
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
@@ -133,7 +146,7 @@ module FastXlsx
133
146
  | (String cell, type: Symbol, series: Array[chart_series],
134
147
  ?title: String, ?x_axis: String, ?y_axis: String,
135
148
  ?width: Integer, ?height: Integer) -> self
136
- type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
149
+ type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: format }
137
150
 
138
151
  def add_table: (*cell_range range,
139
152
  ?columns: Array[table_column], ?style: Symbol, ?name: 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.6.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