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 +4 -4
- data/.cargo/mutants.toml +7 -0
- data/CHANGELOG.md +10 -0
- data/README.md +81 -9
- data/bench/Gemfile.lock +2 -2
- data/docs/MIGRATING_FROM_FAST_EXCEL.md +123 -0
- data/examples/getting_started.rb +44 -0
- data/ext/fast_xlsx/src/lib.rs +43 -5
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +22 -4
- data/sig/fast_xlsx.rbs +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5115d5bf58c96ec3a87ab9168e954b77d2360cac3314c049efd8bed45feb634a
|
|
4
|
+
data.tar.gz: 6ecf4642347e56626a0508acd2784cd6773f4be9ab52f3c4ede1da48031e57e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d95432cd98346cc8339c20f71c14839325e2236f43b05180960125764eb4dbff1fe434b1acf00919425b8be613e00b8fc318e548f5e4359450f9119f12cab205
|
|
7
|
+
data.tar.gz: 2e2563be474cd5613a713f680c433a8e35186423d367004b4c02afa02905859cc139fc1efd762da66b04d748f3b1c3404def56bedb3b5c86cc0c8c3ecc4f2041
|
data/.cargo/mutants.toml
ADDED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
##
|
|
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.
|
|
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.
|
|
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")
|
data/ext/fast_xlsx/src/lib.rs
CHANGED
|
@@ -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(
|
|
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", (
|
|
1211
|
+
.funcall::<_, _, Value>("_coerce", (f,))?,
|
|
1181
1212
|
),
|
|
1182
|
-
f => f
|
|
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(
|
|
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(
|
data/lib/fast_xlsx/version.rb
CHANGED
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]
|
|
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(
|
|
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,
|
|
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.
|
|
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
|