fast_xlsx 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Cargo.lock +1 -48
- data/Cargo.toml +4 -0
- data/README.md +43 -18
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/Cargo.toml +3 -1
- data/ext/fast_xlsx/src/lib.rs +322 -16
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +57 -10
- data/sig/fast_xlsx.rbs +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '045446748368a71ae4010c17da8b09d0a23ac2c3e9bdb0d63bf5ed54bf04221c'
|
|
4
|
+
data.tar.gz: 213cafbae07330020d41e179fc8473a92e5eab5da4cc595fb60e62f3764fa21e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dce10715e570fffa0c5ff69899065c07a3f34f6beb8a9597c095a39f271991d11f05239e62135d853063759a016518fca9f61113e0ae8991c97663a62b4066ff
|
|
7
|
+
data.tar.gz: d87800324af3edab7719ce665cb836d213456102833e04351cfdec508ef063e4c4b16aa80e298ab764262b6a4c6c97e8b2844b48b97639a3e005572369ecf2ce
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Dates and times written without a format now get `yyyy-mm-dd` (`Date`) or `yyyy-mm-dd hh:mm:ss` (`Time`, `DateTime`) instead of showing as serial numbers. A cell's own format, its table column's and `column_format` still win. Applying the format makes writing date-heavy data about 15% slower, the same as passing a date format yourself.
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `to_xlsx` and `save` release Ruby's global lock while building and compressing the file, so other threads (e.g. in Puma or Sidekiq) keep running. An interrupt (Ctrl-C, `Timeout`, `Thread#raise`) takes effect once the save finishes.
|
|
14
|
+
- Saving is 20-30% faster: files are compressed with zlib-rs instead of C zlib (which also drops the `libz-sys` dependency). Adding rows is about 15% faster from building with link-time optimisation.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `write`, `write_comment`, `insert_image`, `insert_chart` and `freeze_panes` also take a cell reference such as `"B2"`. `write` is now native, which makes it about 15% faster.
|
|
19
|
+
- `Worksheet#zoom`, `#tab_color`, `#hide_gridlines`, `#activate`, `#hide`, and `#page_setup` (orientation, paper size, fit to pages, rows/columns repeated on every page, print area, printed gridlines).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- A `merge_range` that overlaps an earlier merge raises before writing anything; before, it also blanked the earlier merge's value.
|
|
24
|
+
|
|
3
25
|
## [0.2.0] - 2026-10-01
|
|
4
26
|
|
|
5
27
|
### Added
|
data/Cargo.lock
CHANGED
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
"quote",
|
|
26
26
|
"regex",
|
|
27
27
|
"rustc-hash",
|
|
28
|
-
"shlex
|
|
28
|
+
"shlex",
|
|
29
29
|
"syn",
|
|
30
30
|
]
|
|
31
31
|
|
|
@@ -41,16 +41,6 @@ version = "3.20.3"
|
|
|
41
41
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
42
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
43
43
|
|
|
44
|
-
[[package]]
|
|
45
|
-
name = "cc"
|
|
46
|
-
version = "1.5.1"
|
|
47
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
48
|
-
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
|
|
49
|
-
dependencies = [
|
|
50
|
-
"find-msvc-tools",
|
|
51
|
-
"shlex 2.0.1",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
44
|
[[package]]
|
|
55
45
|
name = "cexpr"
|
|
56
46
|
version = "0.6.0"
|
|
@@ -124,20 +114,12 @@ version = "2.5.0"
|
|
|
124
114
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
125
115
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
126
116
|
|
|
127
|
-
[[package]]
|
|
128
|
-
name = "find-msvc-tools"
|
|
129
|
-
version = "0.1.14"
|
|
130
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
131
|
-
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
|
132
|
-
|
|
133
117
|
[[package]]
|
|
134
118
|
name = "flate2"
|
|
135
119
|
version = "1.1.10"
|
|
136
120
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
137
121
|
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
|
138
122
|
dependencies = [
|
|
139
|
-
"crc32fast",
|
|
140
|
-
"libz-sys",
|
|
141
123
|
"zlib-rs",
|
|
142
124
|
]
|
|
143
125
|
|
|
@@ -205,17 +187,6 @@ dependencies = [
|
|
|
205
187
|
"windows-link",
|
|
206
188
|
]
|
|
207
189
|
|
|
208
|
-
[[package]]
|
|
209
|
-
name = "libz-sys"
|
|
210
|
-
version = "1.1.29"
|
|
211
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
-
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
|
|
213
|
-
dependencies = [
|
|
214
|
-
"cc",
|
|
215
|
-
"pkg-config",
|
|
216
|
-
"vcpkg",
|
|
217
|
-
]
|
|
218
|
-
|
|
219
190
|
[[package]]
|
|
220
191
|
name = "linux-raw-sys"
|
|
221
192
|
version = "0.12.1"
|
|
@@ -279,12 +250,6 @@ version = "1.21.4"
|
|
|
279
250
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
280
251
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
281
252
|
|
|
282
|
-
[[package]]
|
|
283
|
-
name = "pkg-config"
|
|
284
|
-
version = "0.3.34"
|
|
285
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
-
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
|
287
|
-
|
|
288
253
|
[[package]]
|
|
289
254
|
name = "proc-macro2"
|
|
290
255
|
version = "1.0.107"
|
|
@@ -415,12 +380,6 @@ version = "1.3.0"
|
|
|
415
380
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
416
381
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
417
382
|
|
|
418
|
-
[[package]]
|
|
419
|
-
name = "shlex"
|
|
420
|
-
version = "2.0.1"
|
|
421
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
422
|
-
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
423
|
-
|
|
424
383
|
[[package]]
|
|
425
384
|
name = "simd-adler32"
|
|
426
385
|
version = "0.3.10"
|
|
@@ -463,12 +422,6 @@ version = "1.0.26"
|
|
|
463
422
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
464
423
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
|
465
424
|
|
|
466
|
-
[[package]]
|
|
467
|
-
name = "vcpkg"
|
|
468
|
-
version = "0.2.15"
|
|
469
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
470
|
-
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|
471
|
-
|
|
472
425
|
[[package]]
|
|
473
426
|
name = "windows-link"
|
|
474
427
|
version = "0.2.1"
|
data/Cargo.toml
CHANGED
|
@@ -11,3 +11,7 @@ resolver = "2"
|
|
|
11
11
|
# harder to debug if something goes wrong. It's recommended to keep debug
|
|
12
12
|
# symbols in the release build so that you can debug the final binary if needed.
|
|
13
13
|
debug = true
|
|
14
|
+
# One codegen unit and link-time optimisation across crates: adding rows is
|
|
15
|
+
# about 15% faster, at the cost of a slower gem build.
|
|
16
|
+
lto = "fat"
|
|
17
|
+
codegen-units = 1
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ ws = wb.add_worksheet("Report") # later: wb.worksheet("Report")
|
|
|
18
18
|
ws << ["id", "name", "created_at"] # append a row
|
|
19
19
|
ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
|
|
20
20
|
ws.write(0, 5, 42) # write a single cell (row, col, value)
|
|
21
|
+
ws.write("F1", 42) # or by its Excel reference
|
|
21
22
|
|
|
22
23
|
wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
|
|
23
24
|
wb.save("report.xlsx") # or wb.to_xlsx => binary String
|
|
@@ -81,6 +82,8 @@ ws.write(1, 2, Date.today, date) # format one cell
|
|
|
81
82
|
|
|
82
83
|
Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
|
|
83
84
|
|
|
85
|
+
Dates and times written without a format get `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss`; a table column's format or `column_format` wins over that. A format you pass replaces it, so give date cells one with a `num_format` (e.g. `Format.new(bold: true, num_format: "yyyy-mm-dd")`), or Excel shows the serial number.
|
|
86
|
+
|
|
84
87
|
### Columns and filters
|
|
85
88
|
|
|
86
89
|
```ruby
|
|
@@ -91,6 +94,22 @@ ws.autofit # size other columns to the data written so fa
|
|
|
91
94
|
ws.autofilter("A1:D101") # filter buttons on A1:D101
|
|
92
95
|
```
|
|
93
96
|
|
|
97
|
+
### Sheet view and printing
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
ws.zoom(150) # 10..400 percent
|
|
101
|
+
ws.tab_color("#C00000")
|
|
102
|
+
ws.hide_gridlines
|
|
103
|
+
ws.activate # Excel opens on this sheet (un-hides it if hidden)
|
|
104
|
+
other.hide # the sheet Excel opens on can't be hidden: activate another one first
|
|
105
|
+
|
|
106
|
+
ws.page_setup(landscape: true, paper: :a4, # or :letter, :legal, :tabloid, :a3, :a5, Excel's paper number, 0 = printer default
|
|
107
|
+
fit_width: 1, # 1 page wide, as many pages tall as needed
|
|
108
|
+
repeat_rows: 0, # print the header row on every page (an index or a Range)
|
|
109
|
+
print_area: "A1:D100", # any cell range style
|
|
110
|
+
gridlines: true) # print the gridlines
|
|
111
|
+
```
|
|
112
|
+
|
|
94
113
|
### Cell ranges
|
|
95
114
|
|
|
96
115
|
`autofilter`, `merge_range`, `conditional_format`, `data_validation` and `add_table` take a range in any of these styles:
|
|
@@ -102,6 +121,8 @@ ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
|
|
|
102
121
|
ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
|
|
103
122
|
```
|
|
104
123
|
|
|
124
|
+
Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_chart`, `freeze_panes`) take `(row, col)` or a reference like `"B2"`.
|
|
125
|
+
|
|
105
126
|
`autofit` only sees rows still in memory, so in `:constant` / `:low` memory mode it ignores rows already written to disk; set widths with `column_width` instead.
|
|
106
127
|
|
|
107
128
|
### Layout
|
|
@@ -231,8 +252,8 @@ Values are mapped by type:
|
|
|
231
252
|
|---|---|
|
|
232
253
|
| `Integer`, `Float`, any `Numeric` | number |
|
|
233
254
|
| `String` | string |
|
|
234
|
-
| `Time` |
|
|
235
|
-
| `Date
|
|
255
|
+
| `Time`, `DateTime` | date and time (`yyyy-mm-dd hh:mm:ss` unless formatted), in its own offset |
|
|
256
|
+
| `Date` | date (`yyyy-mm-dd` unless formatted) |
|
|
236
257
|
| `FastXlsx::Formula.new("SUM(A1:A9)")` | formula |
|
|
237
258
|
| `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
|
|
238
259
|
| `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
|
|
@@ -254,17 +275,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
|
|
|
254
275
|
|
|
255
276
|
| Library | Time | vs fastest | Ruby objects allocated |
|
|
256
277
|
|---|---:|---:|---:|
|
|
257
|
-
| **fast_xlsx** (`memory: :constant`) | **
|
|
258
|
-
| **fast_xlsx** (`memory: :low`) | **
|
|
259
|
-
| **fast_xlsx** | **
|
|
260
|
-
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 |
|
|
261
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) |
|
|
262
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 |
|
|
263
|
-
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 |
|
|
264
|
-
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 |
|
|
265
|
-
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 |
|
|
266
|
-
|
|
267
|
-
All outputs are
|
|
278
|
+
| **fast_xlsx** (`memory: :constant`) | **77 ms** | 1.0x | 7 |
|
|
279
|
+
| **fast_xlsx** (`memory: :low`) | **86 ms** | 1.1x | 7 |
|
|
280
|
+
| **fast_xlsx** | **89 ms** | 1.1x | 10 |
|
|
281
|
+
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 183 ms | 2.4x | 561,728 |
|
|
282
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 200 ms | 2.6x | 20,079 |
|
|
283
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 238 ms | 3.1x | 320,076 |
|
|
284
|
+
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 608 ms | 7.8x | 1,483,899 |
|
|
285
|
+
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 689 ms | 8.9x | 745,122 |
|
|
286
|
+
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2755 ms | 35.6x | 8,700,448 |
|
|
287
|
+
|
|
288
|
+
All outputs are 689–750 KB.
|
|
268
289
|
|
|
269
290
|
### Memory
|
|
270
291
|
|
|
@@ -272,14 +293,18 @@ All outputs are 702–750 KB.
|
|
|
272
293
|
|
|
273
294
|
| Library | Unique strings | Repeated strings |
|
|
274
295
|
|---|---:|---:|
|
|
275
|
-
| **fast_xlsx** (`memory: :constant`) | **+
|
|
276
|
-
| **fast_xlsx** (`memory: :low`) | +
|
|
277
|
-
| **fast_xlsx** | +
|
|
278
|
-
| fast_excel 0.5 (constant_memory) | +10 MB | +
|
|
279
|
-
| fast_excel 0.5 | +
|
|
296
|
+
| **fast_xlsx** (`memory: :constant`) | **+1 MB** | **+1 MB** |
|
|
297
|
+
| **fast_xlsx** (`memory: :low`) | +61 MB | **+1 MB** |
|
|
298
|
+
| **fast_xlsx** | +271 MB | +217 MB |
|
|
299
|
+
| fast_excel 0.5 (constant_memory) | +10 MB | +9 MB |
|
|
300
|
+
| fast_excel 0.5 | +182 MB | +151 MB |
|
|
280
301
|
|
|
281
302
|
The `:standard` mode uses more memory than fast_excel's: when saving, rust_xlsxwriter assembles each worksheet's XML in memory (so several worksheets can be built in parallel) instead of streaming it from a temp file. Use `memory: :constant` or `memory: :low` for large exports.
|
|
282
303
|
|
|
304
|
+
### Threads
|
|
305
|
+
|
|
306
|
+
Most of the time goes into saving (building the XML and compressing it). `to_xlsx` and `save` do that without holding Ruby's global lock, so in a threaded server (Puma, Sidekiq) other threads keep running while a large export is saved. An interrupt (Ctrl-C, `Timeout`, `Thread#raise`) takes effect once the save finishes.
|
|
307
|
+
|
|
283
308
|
### Reproduce
|
|
284
309
|
|
|
285
310
|
```bash
|
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.4.0)
|
|
5
5
|
rb_sys (~> 0.9.130)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -77,7 +77,7 @@ DEPENDENCIES
|
|
|
77
77
|
CHECKSUMS
|
|
78
78
|
caxlsx (4.5.0) sha256=e3d98d859f148df05d5462086b5079b523f29c1766b569535f1d68629ce743ff
|
|
79
79
|
fast_excel (0.5.0) sha256=59c418bdcf586a6030798d4e3a9f575742badaceaf7bb90f38eee5feb5bdc478
|
|
80
|
-
fast_xlsx (0.
|
|
80
|
+
fast_xlsx (0.4.0)
|
|
81
81
|
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
|
82
82
|
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
|
83
83
|
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
data/ext/fast_xlsx/Cargo.toml
CHANGED
|
@@ -12,7 +12,9 @@ crate-type = ["cdylib"]
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
magnus = "0.9"
|
|
14
14
|
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
15
|
-
|
|
15
|
+
# No "zlib" feature: without it zip compresses with zlib-rs, which saves
|
|
16
|
+
# 20-30% faster than the system zlib (and needs no C library).
|
|
17
|
+
rust_xlsxwriter = { version = "0.99.1", features = ["constant_memory"] }
|
|
16
18
|
|
|
17
19
|
[build-dependencies]
|
|
18
20
|
rb-sys-env = "0.2.2"
|
data/ext/fast_xlsx/src/lib.rs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
use std::cell::{Cell, RefCell};
|
|
2
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
3
|
+
use std::sync::LazyLock;
|
|
2
4
|
use std::sync::{Arc, Mutex};
|
|
3
5
|
|
|
4
6
|
use magnus::{
|
|
@@ -39,9 +41,56 @@ fn xerr(e: XlsxError) -> Error {
|
|
|
39
41
|
|
|
40
42
|
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
41
43
|
|
|
44
|
+
// Runs `f` without Ruby's global lock, so other Ruby threads run meanwhile.
|
|
45
|
+
// `f` must not touch any Ruby object, and must release the workbook mutex
|
|
46
|
+
// before returning: a Ruby thread waiting on that mutex holds the lock this
|
|
47
|
+
// thread then needs back.
|
|
48
|
+
//
|
|
49
|
+
// Uses the "2" variant: rb_thread_call_without_gvl raises pending interrupts
|
|
50
|
+
// (Thread#raise, Timeout, Ctrl-C) by longjmp-ing over these Rust frames,
|
|
51
|
+
// which skips their destructors. This one never raises; if an interrupt is
|
|
52
|
+
// already pending it returns without calling `f`, which then runs with the
|
|
53
|
+
// lock held. Either way Ruby raises the interrupt after the method returns.
|
|
54
|
+
// Without an unblocking function, an interrupt waits for `f` to finish.
|
|
55
|
+
fn without_gvl<R>(f: impl FnOnce() -> R) -> R {
|
|
56
|
+
unsafe extern "C" fn call<F: FnOnce() -> R, R>(
|
|
57
|
+
data: *mut std::ffi::c_void,
|
|
58
|
+
) -> *mut std::ffi::c_void {
|
|
59
|
+
let (f, result) = &mut *(data as *mut (Option<F>, Option<std::thread::Result<R>>));
|
|
60
|
+
// A panic must not unwind into Ruby's C code; it is resumed below.
|
|
61
|
+
*result = Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|
|
62
|
+
f.take().unwrap(),
|
|
63
|
+
)));
|
|
64
|
+
std::ptr::null_mut()
|
|
65
|
+
}
|
|
66
|
+
fn run<F: FnOnce() -> R, R>(f: F) -> R {
|
|
67
|
+
let mut data: (Option<F>, Option<std::thread::Result<R>>) = (Some(f), None);
|
|
68
|
+
unsafe {
|
|
69
|
+
rb_sys::rb_thread_call_without_gvl2(
|
|
70
|
+
Some(call::<F, R>),
|
|
71
|
+
&mut data as *mut _ as *mut std::ffi::c_void,
|
|
72
|
+
None,
|
|
73
|
+
std::ptr::null_mut(),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
let Some(result) = data.1 else {
|
|
77
|
+
// An interrupt was pending, so `call` never ran.
|
|
78
|
+
return (data.0.take().unwrap())();
|
|
79
|
+
};
|
|
80
|
+
match result {
|
|
81
|
+
Ok(result) => result,
|
|
82
|
+
Err(panic) => std::panic::resume_unwind(panic),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
run(f)
|
|
86
|
+
}
|
|
87
|
+
|
|
42
88
|
#[magnus::wrap(class = "FastXlsx::Workbook", free_immediately)]
|
|
43
89
|
struct Workbook {
|
|
44
90
|
inner: Shared,
|
|
91
|
+
// Index of the sheet Excel opens on (0 unless one is activated), shared
|
|
92
|
+
// with the worksheets: it can't be hidden.
|
|
93
|
+
active: Arc<AtomicUsize>,
|
|
45
94
|
constant_memory: bool,
|
|
46
95
|
low_memory: bool,
|
|
47
96
|
}
|
|
@@ -50,6 +99,7 @@ struct Workbook {
|
|
|
50
99
|
struct Worksheet {
|
|
51
100
|
wb: Shared,
|
|
52
101
|
index: usize,
|
|
102
|
+
active: Arc<AtomicUsize>,
|
|
53
103
|
// Where << / append write next.
|
|
54
104
|
next_row: Cell<u32>,
|
|
55
105
|
// Highest row with cells written. In :constant / :low mode rows above it
|
|
@@ -61,6 +111,10 @@ struct Worksheet {
|
|
|
61
111
|
// after add_table (rust_xlsxwriter only formats cells that already exist).
|
|
62
112
|
// Owned copies, so they don't depend on the Ruby Format objects living on.
|
|
63
113
|
table_formats: RefCell<Vec<TableColumnFormat>>,
|
|
114
|
+
// Merged ranges as (first_row, first_col, last_row, last_col).
|
|
115
|
+
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
116
|
+
// Column ranges given a format with column_format.
|
|
117
|
+
formatted_columns: RefCell<Vec<(u16, u16)>>,
|
|
64
118
|
}
|
|
65
119
|
|
|
66
120
|
struct TableColumnFormat {
|
|
@@ -73,6 +127,7 @@ impl Workbook {
|
|
|
73
127
|
fn new(constant_memory: bool, low_memory: bool) -> Self {
|
|
74
128
|
Workbook {
|
|
75
129
|
inner: Arc::new(Mutex::new(rust_xlsxwriter::Workbook::new())),
|
|
130
|
+
active: Arc::new(AtomicUsize::new(0)),
|
|
76
131
|
constant_memory,
|
|
77
132
|
low_memory,
|
|
78
133
|
}
|
|
@@ -121,25 +176,27 @@ impl Workbook {
|
|
|
121
176
|
Ok(Worksheet {
|
|
122
177
|
wb: self.inner.clone(),
|
|
123
178
|
index: wb.worksheets().len() - 1,
|
|
179
|
+
active: self.active.clone(),
|
|
124
180
|
next_row: Cell::new(0),
|
|
125
181
|
last_written_row: Cell::new(0),
|
|
126
182
|
flushes_rows: self.constant_memory || self.low_memory,
|
|
127
183
|
table_formats: RefCell::new(Vec::new()),
|
|
184
|
+
merges: RefCell::new(Vec::new()),
|
|
185
|
+
formatted_columns: RefCell::new(Vec::new()),
|
|
128
186
|
})
|
|
129
187
|
}
|
|
130
188
|
|
|
189
|
+
// Saving (XML and compression) can take a while, so it runs without
|
|
190
|
+
// Ruby's global lock. The mutex guard is dropped inside the closure.
|
|
131
191
|
fn to_xlsx(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
.lock()
|
|
135
|
-
.unwrap()
|
|
136
|
-
.save_to_buffer()
|
|
137
|
-
.map_err(xerr)?;
|
|
192
|
+
let inner = &rb_self.inner;
|
|
193
|
+
let buf = without_gvl(|| inner.lock().unwrap().save_to_buffer()).map_err(xerr)?;
|
|
138
194
|
Ok(ruby.str_from_slice(&buf))
|
|
139
195
|
}
|
|
140
196
|
|
|
141
197
|
fn save(&self, path: String) -> Result<(), Error> {
|
|
142
|
-
self.inner
|
|
198
|
+
let inner = &self.inner;
|
|
199
|
+
without_gvl(|| inner.lock().unwrap().save(path).map(|_| ())).map_err(xerr)
|
|
143
200
|
}
|
|
144
201
|
|
|
145
202
|
// "Name" for the whole workbook, "Sheet1!Name" for one sheet. Duplicate
|
|
@@ -229,8 +286,9 @@ fn emit<T: IntoExcelData>(
|
|
|
229
286
|
|
|
230
287
|
// Excel's limit on the text in a cell.
|
|
231
288
|
const MAX_CHARS: usize = 32_767;
|
|
232
|
-
// Excel's column
|
|
289
|
+
// Excel's column and row counts.
|
|
233
290
|
const MAX_COLS: usize = 16_384;
|
|
291
|
+
const MAX_ROWS: u32 = 1_048_576;
|
|
234
292
|
|
|
235
293
|
// A cell value converted from Ruby. Converting may run Ruby code (to_s, jd,
|
|
236
294
|
// url, ...), so it happens before the workbook lock is taken: Ruby code that
|
|
@@ -243,6 +301,8 @@ enum CellValue {
|
|
|
243
301
|
Empty,
|
|
244
302
|
Text(String),
|
|
245
303
|
Number(f64),
|
|
304
|
+
// An Excel serial date; true when it has a time of day (Time, DateTime).
|
|
305
|
+
Date(f64, bool),
|
|
246
306
|
Bool(bool),
|
|
247
307
|
Formula(String),
|
|
248
308
|
Url(String, Option<String>),
|
|
@@ -261,7 +321,7 @@ impl CellValue {
|
|
|
261
321
|
} else if v.is_kind_of(ruby.class_numeric()) {
|
|
262
322
|
CellValue::Number(f64::try_convert(v)?)
|
|
263
323
|
} else if v.is_kind_of(ruby.class_time()) {
|
|
264
|
-
CellValue::
|
|
324
|
+
CellValue::Date(excel_time(v)?, true)
|
|
265
325
|
} else if v.is_kind_of(ruby.class_true_class()) || v.is_kind_of(ruby.class_false_class()) {
|
|
266
326
|
CellValue::Bool(v.to_bool())
|
|
267
327
|
} else if v.is_kind_of(ruby.get_inner(&FORMULA)) {
|
|
@@ -279,7 +339,8 @@ impl CellValue {
|
|
|
279
339
|
})?;
|
|
280
340
|
CellValue::Rich(parts)
|
|
281
341
|
} else if v.respond_to("jd", false)? {
|
|
282
|
-
|
|
342
|
+
// Date has no #hour; DateTime does.
|
|
343
|
+
CellValue::Date(excel_date(v)?, v.respond_to("hour", false)?)
|
|
283
344
|
} else {
|
|
284
345
|
CellValue::Text(v.funcall("to_s", ())?)
|
|
285
346
|
};
|
|
@@ -287,6 +348,20 @@ impl CellValue {
|
|
|
287
348
|
Ok(value)
|
|
288
349
|
}
|
|
289
350
|
|
|
351
|
+
// The format for a value written without one: without a number format
|
|
352
|
+
// Excel shows a date as its serial number.
|
|
353
|
+
fn default_format(&self) -> Option<&'static rust_xlsxwriter::Format> {
|
|
354
|
+
static DATE: LazyLock<rust_xlsxwriter::Format> =
|
|
355
|
+
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd"));
|
|
356
|
+
static DATE_TIME: LazyLock<rust_xlsxwriter::Format> =
|
|
357
|
+
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd hh:mm:ss"));
|
|
358
|
+
match self {
|
|
359
|
+
CellValue::Date(_, false) => Some(&DATE),
|
|
360
|
+
CellValue::Date(_, true) => Some(&DATE_TIME),
|
|
361
|
+
_ => None,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
290
365
|
// Rejects what the writer would, so a row or merge fails before anything
|
|
291
366
|
// is written. Numbers, booleans and formulas only fail on a bad row/column.
|
|
292
367
|
fn check(&self) -> Result<(), Error> {
|
|
@@ -317,7 +392,7 @@ impl CellValue {
|
|
|
317
392
|
match self {
|
|
318
393
|
CellValue::Empty => Ok(()),
|
|
319
394
|
CellValue::Text(s) => emit(ws, row, col, s.as_str(), format),
|
|
320
|
-
CellValue::Number(n) => emit(ws, row, col, *n, format),
|
|
395
|
+
CellValue::Number(n) | CellValue::Date(n, _) => emit(ws, row, col, *n, format),
|
|
321
396
|
CellValue::Bool(b) => emit(ws, row, col, *b, format),
|
|
322
397
|
CellValue::Formula(f) => emit(
|
|
323
398
|
ws,
|
|
@@ -686,6 +761,16 @@ const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
|
|
|
686
761
|
("edit_objects", |o| &mut o.edit_objects),
|
|
687
762
|
];
|
|
688
763
|
|
|
764
|
+
// Excel's paper size codes; page_setup also takes the number itself.
|
|
765
|
+
const PAPER_SIZES: &[(&str, u8)] = &[
|
|
766
|
+
("letter", 1),
|
|
767
|
+
("tabloid", 3),
|
|
768
|
+
("legal", 5),
|
|
769
|
+
("a3", 8),
|
|
770
|
+
("a4", 9),
|
|
771
|
+
("a5", 11),
|
|
772
|
+
];
|
|
773
|
+
|
|
689
774
|
const BORDERS: &[(&str, FormatBorder)] = &[
|
|
690
775
|
("thin", FormatBorder::Thin),
|
|
691
776
|
("medium", FormatBorder::Medium),
|
|
@@ -886,6 +971,28 @@ impl Worksheet {
|
|
|
886
971
|
})
|
|
887
972
|
}
|
|
888
973
|
|
|
974
|
+
// The format a value is written with: its own, else its table column's,
|
|
975
|
+
// else its type's default (dates) unless column_format covers the column,
|
|
976
|
+
// whose format rust_xlsxwriter applies to cells written without one.
|
|
977
|
+
fn format_for<'a>(
|
|
978
|
+
&self,
|
|
979
|
+
tables: &'a [TableColumnFormat],
|
|
980
|
+
row: u32,
|
|
981
|
+
col: u16,
|
|
982
|
+
own: Option<&'a rust_xlsxwriter::Format>,
|
|
983
|
+
value: &CellValue,
|
|
984
|
+
) -> Option<&'a rust_xlsxwriter::Format> {
|
|
985
|
+
Self::cell_format(tables, row, col, own).or_else(|| {
|
|
986
|
+
value.default_format().filter(|_| {
|
|
987
|
+
!self
|
|
988
|
+
.formatted_columns
|
|
989
|
+
.borrow()
|
|
990
|
+
.iter()
|
|
991
|
+
.any(|&(first, last)| (first..=last).contains(&col))
|
|
992
|
+
})
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
|
|
889
996
|
fn write_row(
|
|
890
997
|
&self,
|
|
891
998
|
ruby: &Ruby,
|
|
@@ -912,7 +1019,7 @@ impl Worksheet {
|
|
|
912
1019
|
// Values were checked when converted, so only a bad row number
|
|
913
1020
|
// fails here, and it fails on the first cell.
|
|
914
1021
|
for (col, value, format) in &values {
|
|
915
|
-
let format =
|
|
1022
|
+
let format = self.format_for(&tables, row, *col, format.as_deref(), value);
|
|
916
1023
|
value.write(ws, row, *col, format)?;
|
|
917
1024
|
}
|
|
918
1025
|
Ok(())
|
|
@@ -922,6 +1029,29 @@ impl Worksheet {
|
|
|
922
1029
|
Ok(())
|
|
923
1030
|
}
|
|
924
1031
|
|
|
1032
|
+
// Worksheet#write. The (row, col, value, format = nil) form is handled
|
|
1033
|
+
// here rather than in a Ruby wrapper, since it runs once per cell; the
|
|
1034
|
+
// rest ("B2", or a wrong argument count) goes to Ruby's _write_ref.
|
|
1035
|
+
fn write_any(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
|
|
1036
|
+
let index = |i: usize| args.get(i).and_then(|v| Integer::from_value(*v));
|
|
1037
|
+
if let (3 | 4, Some(row), Some(col)) = (args.len(), index(0), index(1)) {
|
|
1038
|
+
let format = match args.get(3) {
|
|
1039
|
+
Some(f) => Option::<&Format>::try_convert(*f)?,
|
|
1040
|
+
None => None,
|
|
1041
|
+
};
|
|
1042
|
+
Self::write(
|
|
1043
|
+
ruby,
|
|
1044
|
+
&rb_self,
|
|
1045
|
+
row.to_u32()?,
|
|
1046
|
+
col.to_u16()?,
|
|
1047
|
+
args[2],
|
|
1048
|
+
format,
|
|
1049
|
+
)?;
|
|
1050
|
+
return Ok(rb_self);
|
|
1051
|
+
}
|
|
1052
|
+
rb_self.funcall("_write_ref", args)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
925
1055
|
fn write(
|
|
926
1056
|
ruby: &Ruby,
|
|
927
1057
|
rb_self: &Self,
|
|
@@ -938,7 +1068,7 @@ impl Worksheet {
|
|
|
938
1068
|
ws,
|
|
939
1069
|
row,
|
|
940
1070
|
col,
|
|
941
|
-
|
|
1071
|
+
rb_self.format_for(&tables, row, col, format.map(|f| &*f.0), &value),
|
|
942
1072
|
)
|
|
943
1073
|
})?;
|
|
944
1074
|
rb_self.advance(row);
|
|
@@ -983,7 +1113,9 @@ impl Worksheet {
|
|
|
983
1113
|
ws.set_column_range_format(first, last, &format.0)
|
|
984
1114
|
.map(|_| ())
|
|
985
1115
|
.map_err(xerr)
|
|
986
|
-
})
|
|
1116
|
+
})?;
|
|
1117
|
+
self.formatted_columns.borrow_mut().push((first, last));
|
|
1118
|
+
Ok(())
|
|
987
1119
|
}
|
|
988
1120
|
|
|
989
1121
|
fn autofilter(
|
|
@@ -1075,9 +1207,39 @@ impl Worksheet {
|
|
|
1075
1207
|
format: Option<&Format>,
|
|
1076
1208
|
) -> Result<Obj<Self>, Error> {
|
|
1077
1209
|
rb_self.check_not_flushed(ruby, first_row)?;
|
|
1210
|
+
// rust_xlsxwriter blanks the range before it notices an overlap, which
|
|
1211
|
+
// wipes the earlier merge's value, so check first.
|
|
1212
|
+
// A range rust_xlsxwriter rejects anyway (reversed, too big) is left
|
|
1213
|
+
// to it, so its error names the real problem.
|
|
1214
|
+
// ponytail: linear scan; sheets have few merges.
|
|
1215
|
+
let range = (first_row, first_col, last_row, last_col);
|
|
1216
|
+
let valid = first_row <= last_row
|
|
1217
|
+
&& first_col <= last_col
|
|
1218
|
+
&& last_row < MAX_ROWS
|
|
1219
|
+
&& usize::from(last_col) < MAX_COLS;
|
|
1220
|
+
let overlap = rb_self
|
|
1221
|
+
.merges
|
|
1222
|
+
.borrow()
|
|
1223
|
+
.iter()
|
|
1224
|
+
.copied()
|
|
1225
|
+
.find(|&(fr, fc, lr, lc)| {
|
|
1226
|
+
first_row <= lr && fr <= last_row && first_col <= lc && fc <= last_col
|
|
1227
|
+
})
|
|
1228
|
+
.filter(|_| valid);
|
|
1229
|
+
if let Some((fr, fc, lr, lc)) = overlap {
|
|
1230
|
+
return Err(Error::new(
|
|
1231
|
+
ruby.get_inner(&ERROR),
|
|
1232
|
+
format!(
|
|
1233
|
+
"merge range {} overlaps the earlier merge {}",
|
|
1234
|
+
rust_xlsxwriter::utility::cell_range(first_row, first_col, last_row, last_col),
|
|
1235
|
+
rust_xlsxwriter::utility::cell_range(fr, fc, lr, lc),
|
|
1236
|
+
),
|
|
1237
|
+
));
|
|
1238
|
+
}
|
|
1078
1239
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1079
1240
|
let tables = rb_self.table_formats.borrow();
|
|
1080
|
-
let format =
|
|
1241
|
+
let format =
|
|
1242
|
+
rb_self.format_for(&tables, first_row, first_col, format.map(|f| &*f.0), &value);
|
|
1081
1243
|
let default = rust_xlsxwriter::Format::new();
|
|
1082
1244
|
rb_self.with_ws(|ws| {
|
|
1083
1245
|
// The value was checked when converted, so once the range is
|
|
@@ -1093,6 +1255,7 @@ impl Worksheet {
|
|
|
1093
1255
|
.map_err(xerr)?;
|
|
1094
1256
|
value.write(ws, first_row, first_col, format)
|
|
1095
1257
|
})?;
|
|
1258
|
+
rb_self.merges.borrow_mut().push(range);
|
|
1096
1259
|
rb_self.advance(last_row);
|
|
1097
1260
|
rb_self.note_written(first_row);
|
|
1098
1261
|
Ok(rb_self)
|
|
@@ -1490,6 +1653,142 @@ impl Worksheet {
|
|
|
1490
1653
|
Ok(rb_self)
|
|
1491
1654
|
}
|
|
1492
1655
|
|
|
1656
|
+
fn activate(rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1657
|
+
let mut wb = rb_self.wb.lock().unwrap();
|
|
1658
|
+
// rust_xlsxwriter leaves sheets activated earlier selected, which
|
|
1659
|
+
// groups them in Excel (edits then go to all of them).
|
|
1660
|
+
for (i, ws) in wb.worksheets_mut().iter_mut().enumerate() {
|
|
1661
|
+
ws.set_active(i == rb_self.index);
|
|
1662
|
+
ws.set_selected(i == rb_self.index);
|
|
1663
|
+
}
|
|
1664
|
+
rb_self.active.store(rb_self.index, Ordering::Relaxed);
|
|
1665
|
+
Ok(rb_self)
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
fn hide(ruby: &Ruby, rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1669
|
+
// rust_xlsxwriter would quietly unhide it when saving.
|
|
1670
|
+
if rb_self.active.load(Ordering::Relaxed) == rb_self.index {
|
|
1671
|
+
return Err(Error::new(
|
|
1672
|
+
ruby.get_inner(&ERROR),
|
|
1673
|
+
"can't hide the sheet Excel opens on (the first one unless another is activated): activate another sheet first",
|
|
1674
|
+
));
|
|
1675
|
+
}
|
|
1676
|
+
rb_self.with_ws(|ws| {
|
|
1677
|
+
ws.set_hidden(true);
|
|
1678
|
+
Ok(())
|
|
1679
|
+
})?;
|
|
1680
|
+
Ok(rb_self)
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent: Value) -> Result<Obj<Self>, Error> {
|
|
1684
|
+
// Integers only: converting would quietly truncate 150.9.
|
|
1685
|
+
let percent = Integer::from_value(percent)
|
|
1686
|
+
.ok_or_else(|| {
|
|
1687
|
+
Error::new(
|
|
1688
|
+
ruby.exception_type_error(),
|
|
1689
|
+
format!("zoom must be an Integer, got {}", percent.inspect()),
|
|
1690
|
+
)
|
|
1691
|
+
})?
|
|
1692
|
+
.to_i64()?;
|
|
1693
|
+
// rust_xlsxwriter only prints a warning for these.
|
|
1694
|
+
if !(10..=400).contains(&percent) {
|
|
1695
|
+
return Err(Error::new(
|
|
1696
|
+
ruby.exception_arg_error(),
|
|
1697
|
+
format!("invalid zoom {percent}: use 10..400"),
|
|
1698
|
+
));
|
|
1699
|
+
}
|
|
1700
|
+
rb_self.with_ws(|ws| {
|
|
1701
|
+
ws.set_zoom(percent as u16);
|
|
1702
|
+
Ok(())
|
|
1703
|
+
})?;
|
|
1704
|
+
Ok(rb_self)
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
fn tab_color(ruby: &Ruby, rb_self: Obj<Self>, value: Value) -> Result<Obj<Self>, Error> {
|
|
1708
|
+
let rgb = color(ruby, value)?;
|
|
1709
|
+
rb_self.with_ws(|ws| {
|
|
1710
|
+
ws.set_tab_color(rgb);
|
|
1711
|
+
Ok(())
|
|
1712
|
+
})?;
|
|
1713
|
+
Ok(rb_self)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
fn hide_gridlines(rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1717
|
+
rb_self.with_ws(|ws| {
|
|
1718
|
+
ws.set_screen_gridlines(false);
|
|
1719
|
+
Ok(())
|
|
1720
|
+
})?;
|
|
1721
|
+
Ok(rb_self)
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Ranges arrive as [first, last] / [first_row, first_col, last_row,
|
|
1725
|
+
// last_col], already parsed by lib/fast_xlsx.rb.
|
|
1726
|
+
fn page_setup(ruby: &Ruby, rb_self: &Self, options: RHash) -> Result<(), Error> {
|
|
1727
|
+
check_keys(
|
|
1728
|
+
ruby,
|
|
1729
|
+
options,
|
|
1730
|
+
&[
|
|
1731
|
+
"landscape",
|
|
1732
|
+
"paper",
|
|
1733
|
+
"fit_width",
|
|
1734
|
+
"fit_height",
|
|
1735
|
+
"repeat_rows",
|
|
1736
|
+
"repeat_columns",
|
|
1737
|
+
"print_area",
|
|
1738
|
+
"gridlines",
|
|
1739
|
+
],
|
|
1740
|
+
"page_setup",
|
|
1741
|
+
)?;
|
|
1742
|
+
let landscape = opt::<Value>(ruby, options, "landscape")?.map(|v| v.to_bool());
|
|
1743
|
+
let paper = match opt::<Value>(ruby, options, "paper")? {
|
|
1744
|
+
None => None,
|
|
1745
|
+
Some(v) if Integer::from_value(v).is_some() => Some(
|
|
1746
|
+
Integer::from_value(v)
|
|
1747
|
+
.and_then(|n| n.to_u8().ok())
|
|
1748
|
+
.ok_or_else(|| {
|
|
1749
|
+
Error::new(
|
|
1750
|
+
ruby.exception_arg_error(),
|
|
1751
|
+
format!("invalid paper {}: use a symbol or Excel's paper number (0 for the printer's default)", v.inspect()),
|
|
1752
|
+
)
|
|
1753
|
+
})?,
|
|
1754
|
+
),
|
|
1755
|
+
Some(v) => Some(choice(ruby, "paper", v, PAPER_SIZES)?),
|
|
1756
|
+
};
|
|
1757
|
+
let fit_width = opt::<u16>(ruby, options, "fit_width")?;
|
|
1758
|
+
let fit_height = opt::<u16>(ruby, options, "fit_height")?;
|
|
1759
|
+
let repeat_rows = opt::<(u32, u32)>(ruby, options, "repeat_rows")?;
|
|
1760
|
+
let repeat_columns = opt::<(u16, u16)>(ruby, options, "repeat_columns")?;
|
|
1761
|
+
let print_area = opt::<(u32, u16, u32, u16)>(ruby, options, "print_area")?;
|
|
1762
|
+
let gridlines = opt::<Value>(ruby, options, "gridlines")?.map(|v| v.to_bool());
|
|
1763
|
+
rb_self.with_ws(|ws| {
|
|
1764
|
+
match landscape {
|
|
1765
|
+
Some(true) => ws.set_landscape(),
|
|
1766
|
+
Some(false) => ws.set_portrait(),
|
|
1767
|
+
None => ws,
|
|
1768
|
+
};
|
|
1769
|
+
if let Some(paper) = paper {
|
|
1770
|
+
ws.set_paper_size(paper);
|
|
1771
|
+
}
|
|
1772
|
+
// 0 means as many pages as the content needs.
|
|
1773
|
+
if fit_width.unwrap_or(0) > 0 || fit_height.unwrap_or(0) > 0 {
|
|
1774
|
+
ws.set_print_fit_to_pages(fit_width.unwrap_or(0), fit_height.unwrap_or(0));
|
|
1775
|
+
}
|
|
1776
|
+
if let Some((first, last)) = repeat_rows {
|
|
1777
|
+
ws.set_repeat_rows(first, last).map_err(xerr)?;
|
|
1778
|
+
}
|
|
1779
|
+
if let Some((first, last)) = repeat_columns {
|
|
1780
|
+
ws.set_repeat_columns(first, last).map_err(xerr)?;
|
|
1781
|
+
}
|
|
1782
|
+
if let Some((fr, fc, lr, lc)) = print_area {
|
|
1783
|
+
ws.set_print_area(fr, fc, lr, lc).map_err(xerr)?;
|
|
1784
|
+
}
|
|
1785
|
+
if let Some(gridlines) = gridlines {
|
|
1786
|
+
ws.set_print_gridlines(gridlines);
|
|
1787
|
+
}
|
|
1788
|
+
Ok(())
|
|
1789
|
+
})
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1493
1792
|
fn group_rows(
|
|
1494
1793
|
ruby: &Ruby,
|
|
1495
1794
|
rb_self: Obj<Self>,
|
|
@@ -1578,6 +1877,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1578
1877
|
|
|
1579
1878
|
let ws = module.define_class("Worksheet", ruby.class_object())?;
|
|
1580
1879
|
ws.define_method("_write", method!(Worksheet::write, 4))?;
|
|
1880
|
+
ws.define_method("write", method!(Worksheet::write_any, -1))?;
|
|
1581
1881
|
ws.define_method("_append", method!(Worksheet::append, 2))?;
|
|
1582
1882
|
ws.define_method("_column_width", method!(Worksheet::set_column_width, 3))?;
|
|
1583
1883
|
ws.define_method("_column_format", method!(Worksheet::set_column_format, 3))?;
|
|
@@ -1593,7 +1893,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1593
1893
|
method!(Worksheet::conditional_format, 5),
|
|
1594
1894
|
)?;
|
|
1595
1895
|
ws.define_method("_data_validation", method!(Worksheet::data_validation, 5))?;
|
|
1596
|
-
ws.define_method("
|
|
1896
|
+
ws.define_method("_freeze_panes", method!(Worksheet::freeze_panes, 2))?;
|
|
1597
1897
|
ws.define_method("row_height", method!(Worksheet::set_row_height, 2))?;
|
|
1598
1898
|
ws.define_method("page_breaks", method!(Worksheet::set_page_breaks, 1))?;
|
|
1599
1899
|
ws.define_method("_page_header", method!(Worksheet::set_header, 1))?;
|
|
@@ -1603,6 +1903,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1603
1903
|
"vertical_page_breaks",
|
|
1604
1904
|
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
1605
1905
|
)?;
|
|
1906
|
+
ws.define_method("activate", method!(Worksheet::activate, 0))?;
|
|
1907
|
+
ws.define_method("hide", method!(Worksheet::hide, 0))?;
|
|
1908
|
+
ws.define_method("zoom", method!(Worksheet::zoom, 1))?;
|
|
1909
|
+
ws.define_method("tab_color", method!(Worksheet::tab_color, 1))?;
|
|
1910
|
+
ws.define_method("hide_gridlines", method!(Worksheet::hide_gridlines, 0))?;
|
|
1911
|
+
ws.define_method("_page_setup", method!(Worksheet::page_setup, 1))?;
|
|
1606
1912
|
ws.define_method("_group_rows", method!(Worksheet::group_rows, 3))?;
|
|
1607
1913
|
ws.define_method("_group_columns", method!(Worksheet::group_columns, 3))?;
|
|
1608
1914
|
ws.define_method("_protect", method!(Worksheet::protect, 2))?;
|
data/lib/fast_xlsx/version.rb
CHANGED
data/lib/fast_xlsx.rb
CHANGED
|
@@ -102,10 +102,15 @@ module FastXlsx
|
|
|
102
102
|
|
|
103
103
|
# Cell writer for one sheet; create with Workbook#add_worksheet.
|
|
104
104
|
class Worksheet
|
|
105
|
-
|
|
105
|
+
# write(row, col, value, format = nil) or write("B2", value, format = nil)
|
|
106
|
+
# is native: the (row, col) form runs once per cell, so it skips a Ruby
|
|
107
|
+
# wrapper. Other forms come here.
|
|
108
|
+
def _write_ref(*args)
|
|
109
|
+
row, col, (value, format) = CellRange.cell(args, 1..2)
|
|
106
110
|
_write(row, col, value, format)
|
|
107
111
|
self
|
|
108
112
|
end
|
|
113
|
+
private :_write_ref
|
|
109
114
|
|
|
110
115
|
def append(values, format: nil)
|
|
111
116
|
_append(values, format)
|
|
@@ -158,14 +163,16 @@ module FastXlsx
|
|
|
158
163
|
end
|
|
159
164
|
|
|
160
165
|
# Adds a comment (Excel "note") to a cell.
|
|
161
|
-
def write_comment(
|
|
166
|
+
def write_comment(*args, author: nil)
|
|
167
|
+
row, col, (text, *) = CellRange.cell(args, 1..1)
|
|
162
168
|
_write_comment(row, col, text, author)
|
|
163
169
|
end
|
|
164
170
|
|
|
165
171
|
# Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
|
|
166
172
|
# cell. source is a file path or an IO (anything responding to #read).
|
|
167
173
|
# Options: scale: or width:/height: (pixels), x_offset:, y_offset: (pixels), alt_text:.
|
|
168
|
-
def insert_image(
|
|
174
|
+
def insert_image(*args, **)
|
|
175
|
+
row, col, (source, *) = CellRange.cell(args, 1..1)
|
|
169
176
|
bytes = source.respond_to?(:read) ? source.read : File.binread(source)
|
|
170
177
|
_insert_image(row, col, bytes, { ** })
|
|
171
178
|
end
|
|
@@ -173,7 +180,8 @@ module FastXlsx
|
|
|
173
180
|
# Inserts a chart with its top-left corner in the cell. series is an Array
|
|
174
181
|
# of { values:, categories:, name: } with Excel ranges such as
|
|
175
182
|
# "Sheet1!$B$2:$B$13". Options: title:, x_axis:, y_axis:, width:, height:.
|
|
176
|
-
def insert_chart(
|
|
183
|
+
def insert_chart(*cell, type:, series:, **)
|
|
184
|
+
row, col, = CellRange.cell(cell)
|
|
177
185
|
_insert_chart(row, col, { type: type, series: series, ** })
|
|
178
186
|
end
|
|
179
187
|
|
|
@@ -197,6 +205,18 @@ module FastXlsx
|
|
|
197
205
|
margin ? margins(footer: margin) : self
|
|
198
206
|
end
|
|
199
207
|
|
|
208
|
+
# Printing: landscape:, paper: (:letter, :legal, :tabloid, :a3, :a4, :a5 or
|
|
209
|
+
# Excel's paper number), fit_width:/fit_height: (pages; 0 or left out =
|
|
210
|
+
# as many as needed), repeat_rows:/repeat_columns: (an index or Range,
|
|
211
|
+
# printed on every page), print_area: (any cell range), gridlines:.
|
|
212
|
+
def page_setup(repeat_rows: nil, repeat_columns: nil, print_area: nil, **options)
|
|
213
|
+
options[:repeat_rows] = CellRange.bounds(repeat_rows) if repeat_rows
|
|
214
|
+
options[:repeat_columns] = CellRange.bounds(repeat_columns) if repeat_columns
|
|
215
|
+
options[:print_area] = CellRange.split(print_area.is_a?(Array) ? print_area : [print_area]).first if print_area
|
|
216
|
+
_page_setup(options)
|
|
217
|
+
self
|
|
218
|
+
end
|
|
219
|
+
|
|
200
220
|
# Print margins in inches; margins not given keep their current value.
|
|
201
221
|
def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
202
222
|
_margins(*[left, right, top, bottom, header, footer].map { |m| m || -1.0 })
|
|
@@ -214,6 +234,13 @@ module FastXlsx
|
|
|
214
234
|
_group_columns(*CellRange.bounds(columns), collapsed)
|
|
215
235
|
end
|
|
216
236
|
|
|
237
|
+
# Keeps the rows above and the columns left of the cell visible while
|
|
238
|
+
# scrolling: (1, 0) or "A2" freezes the first row.
|
|
239
|
+
def freeze_panes(*cell)
|
|
240
|
+
_freeze_panes(*CellRange.cell(cell).first(2))
|
|
241
|
+
self
|
|
242
|
+
end
|
|
243
|
+
|
|
217
244
|
# Locks the sheet against editing. Cells whose format has locked: false
|
|
218
245
|
# stay editable. allow: actions users may still take, any of :format_cells,
|
|
219
246
|
# :format_columns, :format_rows, :insert_columns, :insert_rows,
|
|
@@ -236,6 +263,7 @@ module FastXlsx
|
|
|
236
263
|
module CellRange
|
|
237
264
|
REF = /\A\$?([A-Za-z]{1,3})\$?([1-9]\d*)\z/ # ASCII only: /i also matches the Kelvin sign
|
|
238
265
|
FORMS = 'a range is (first_row, first_col, last_row, last_col), "A1:D10" or (rows, cols)'
|
|
266
|
+
CELL_FORMS = 'a cell is (row, col) or "B2"'
|
|
239
267
|
|
|
240
268
|
module_function
|
|
241
269
|
|
|
@@ -243,12 +271,7 @@ module FastXlsx
|
|
|
243
271
|
# Returns [[first_row, first_col, last_row, last_col], the args after it].
|
|
244
272
|
def split(args, following = 0..0)
|
|
245
273
|
range, rest = parse(args)
|
|
246
|
-
|
|
247
|
-
expected = following.minmax.uniq.join("..")
|
|
248
|
-
raise ArgumentError, "wrong number of arguments after the cell range (given #{rest.size}, " \
|
|
249
|
-
"expected #{expected}); #{FORMS}"
|
|
250
|
-
end
|
|
251
|
-
|
|
274
|
+
check_following(rest, following, "cell range", FORMS)
|
|
252
275
|
[range, rest]
|
|
253
276
|
end
|
|
254
277
|
|
|
@@ -263,6 +286,30 @@ module FastXlsx
|
|
|
263
286
|
end
|
|
264
287
|
end
|
|
265
288
|
|
|
289
|
+
# (row, col) or a single-cell reference ("B2") off the front of args, as
|
|
290
|
+
# [row, col, the args after it].
|
|
291
|
+
def cell(args, following = 0..0)
|
|
292
|
+
row, col, *rest = args.first.is_a?(String) ? [*single_cell(args.first), *args.drop(1)] : args
|
|
293
|
+
raise ArgumentError, "expected a cell, got #{args.inspect}; #{CELL_FORMS}" if col.nil?
|
|
294
|
+
|
|
295
|
+
check_following(rest, following, "cell", CELL_FORMS)
|
|
296
|
+
[row, col, rest]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def single_cell(ref)
|
|
300
|
+
first_row, first_col, last_row, last_col = excel(ref)
|
|
301
|
+
return [first_row, first_col] if first_row == last_row && first_col == last_col
|
|
302
|
+
|
|
303
|
+
raise ArgumentError, "expected a single cell like \"B2\", got #{ref.inspect}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def check_following(rest, following, what, forms)
|
|
307
|
+
return if following.cover?(rest.size)
|
|
308
|
+
|
|
309
|
+
raise ArgumentError, "wrong number of arguments after the #{what} " \
|
|
310
|
+
"(given #{rest.size}, expected #{following.minmax.uniq.join("..")}); #{forms}"
|
|
311
|
+
end
|
|
312
|
+
|
|
266
313
|
def rows_and_cols(rows, cols)
|
|
267
314
|
first_row, last_row = bounds(rows)
|
|
268
315
|
first_col, last_col = bounds(cols)
|
data/sig/fast_xlsx.rbs
CHANGED
|
@@ -57,6 +57,7 @@ module FastXlsx
|
|
|
57
57
|
|
|
58
58
|
class Worksheet
|
|
59
59
|
def write: (Integer row, Integer col, cell value, ?Format? format) -> self
|
|
60
|
+
| (String cell, cell value, ?Format? format) -> self
|
|
60
61
|
def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
|
|
61
62
|
def <<: (Array[cell] row) -> self
|
|
62
63
|
def concat: (Array[Array[cell]] rows) -> self
|
|
@@ -72,11 +73,20 @@ module FastXlsx
|
|
|
72
73
|
| (String range) -> self
|
|
73
74
|
| (indexes rows, indexes cols) -> self
|
|
74
75
|
def freeze_panes: (Integer row, Integer col) -> self
|
|
76
|
+
| (String cell) -> self
|
|
75
77
|
def row_height: (Integer row, Numeric height) -> self
|
|
76
78
|
def page_breaks: (Array[Integer] rows) -> self
|
|
77
79
|
def vertical_page_breaks: (Array[Integer] cols) -> self
|
|
78
80
|
def page_header: (String text, ?margin: Numeric?) -> self
|
|
79
81
|
def page_footer: (String text, ?margin: Numeric?) -> self
|
|
82
|
+
def activate: () -> self
|
|
83
|
+
def hide: () -> self
|
|
84
|
+
def zoom: (Integer percent) -> self
|
|
85
|
+
def tab_color: (String | Integer color) -> self
|
|
86
|
+
def hide_gridlines: () -> self
|
|
87
|
+
def page_setup: (?landscape: bool, ?paper: :letter | :legal | :tabloid | :a3 | :a4 | :a5 | Integer,
|
|
88
|
+
?fit_width: Integer, ?fit_height: Integer, ?repeat_rows: indexes, ?repeat_columns: indexes,
|
|
89
|
+
?print_area: String | Array[cell_range], ?gridlines: bool) -> self
|
|
80
90
|
def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
|
|
81
91
|
?header: Numeric?, ?footer: Numeric?) -> self
|
|
82
92
|
type protect_action = :format_cells | :format_columns | :format_rows | :insert_columns | :insert_rows
|
|
@@ -98,13 +108,19 @@ module FastXlsx
|
|
|
98
108
|
?input_title: String, ?input_message: String,
|
|
99
109
|
?error_title: String, ?error_message: String) -> self
|
|
100
110
|
def write_comment: (Integer row, Integer col, String text, ?author: String?) -> self
|
|
111
|
+
| (String cell, String text, ?author: String?) -> self
|
|
101
112
|
def insert_image: (Integer row, Integer col, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
|
|
102
113
|
?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
|
|
114
|
+
| (String cell, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
|
|
115
|
+
?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
|
|
103
116
|
type chart_series = { values: String, ?categories: String, ?name: String }
|
|
104
117
|
|
|
105
118
|
def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
|
|
106
119
|
?title: String, ?x_axis: String, ?y_axis: String,
|
|
107
120
|
?width: Integer, ?height: Integer) -> self
|
|
121
|
+
| (String cell, type: Symbol, series: Array[chart_series],
|
|
122
|
+
?title: String, ?x_axis: String, ?y_axis: String,
|
|
123
|
+
?width: Integer, ?height: Integer) -> self
|
|
108
124
|
type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
|
|
109
125
|
|
|
110
126
|
def add_table: (*cell_range range,
|