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 +4 -4
- data/.cargo/mutants.toml +7 -0
- data/CHANGELOG.md +28 -0
- data/README.md +115 -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 +50 -3
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +149 -22
- data/sig/fast_xlsx.rbs +26 -13
- 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,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:
|
|
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,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
|
-
##
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
1176
|
-
|
|
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(
|
|
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(
|
data/lib/fast_xlsx/version.rb
CHANGED
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
|
|
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)
|
|
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(
|
|
264
|
+
def _write_ref(args, keyword_given, keyword_format)
|
|
245
265
|
row, col, (value, format) = CellRange.cell(args, 1..2)
|
|
246
|
-
|
|
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,
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
_new(
|
|
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,
|
|
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
|
|
60
|
-
|
|
61
|
-
def
|
|
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,
|
|
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,
|
|
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, ?
|
|
111
|
-
| (String range, cell value, ?
|
|
112
|
-
| (indexes rows, indexes cols, cell value, ?
|
|
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:
|
|
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:
|
|
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.
|
|
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
|