fast_xlsx 0.2.0 → 0.3.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 +16 -0
- data/Cargo.lock +1 -48
- data/Cargo.toml +4 -0
- data/README.md +39 -16
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/Cargo.toml +3 -1
- data/ext/fast_xlsx/src/lib.rs +269 -9
- 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: 14314a7af34dfb1bfd00bac2f8fde41c4fa03447020dfef48571055bdac3151d
|
|
4
|
+
data.tar.gz: 915ca1cd79de28ca34023af4d884eb9c6728bd86abd29aa19bd62ca08e3914fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 346ed04a149c9f6201fdb84af8b5b1a5a1008a17ac1df8faaa26af8d500dc2e13ecd8eb55705638a99a27501d7ca757f08a50825c7faac4a4a2593fb556a94e5
|
|
7
|
+
data.tar.gz: 712d24c13ab60499f91795cdbdd4d3c251e41c6b30b60dc8c9e3d25aaa671da98710d128dd673c859ad2ec336c817d69da6a46ea5e9f7a8015025444db59bcef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `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.
|
|
13
|
+
- `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).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- A `merge_range` that overlaps an earlier merge raises before writing anything; before, it also blanked the earlier merge's value.
|
|
18
|
+
|
|
3
19
|
## [0.2.0] - 2026-10-01
|
|
4
20
|
|
|
5
21
|
### 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
|
|
@@ -91,6 +92,22 @@ ws.autofit # size other columns to the data written so fa
|
|
|
91
92
|
ws.autofilter("A1:D101") # filter buttons on A1:D101
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
### Sheet view and printing
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
ws.zoom(150) # 10..400 percent
|
|
99
|
+
ws.tab_color("#C00000")
|
|
100
|
+
ws.hide_gridlines
|
|
101
|
+
ws.activate # Excel opens on this sheet (un-hides it if hidden)
|
|
102
|
+
other.hide # the sheet Excel opens on can't be hidden: activate another one first
|
|
103
|
+
|
|
104
|
+
ws.page_setup(landscape: true, paper: :a4, # or :letter, :legal, :tabloid, :a3, :a5, Excel's paper number, 0 = printer default
|
|
105
|
+
fit_width: 1, # 1 page wide, as many pages tall as needed
|
|
106
|
+
repeat_rows: 0, # print the header row on every page (an index or a Range)
|
|
107
|
+
print_area: "A1:D100", # any cell range style
|
|
108
|
+
gridlines: true) # print the gridlines
|
|
109
|
+
```
|
|
110
|
+
|
|
94
111
|
### Cell ranges
|
|
95
112
|
|
|
96
113
|
`autofilter`, `merge_range`, `conditional_format`, `data_validation` and `add_table` take a range in any of these styles:
|
|
@@ -102,6 +119,8 @@ ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
|
|
|
102
119
|
ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
|
|
103
120
|
```
|
|
104
121
|
|
|
122
|
+
Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_chart`, `freeze_panes`) take `(row, col)` or a reference like `"B2"`.
|
|
123
|
+
|
|
105
124
|
`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
125
|
|
|
107
126
|
### Layout
|
|
@@ -254,17 +273,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
|
|
|
254
273
|
|
|
255
274
|
| Library | Time | vs fastest | Ruby objects allocated |
|
|
256
275
|
|---|---:|---:|---:|
|
|
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
|
|
276
|
+
| **fast_xlsx** (`memory: :constant`) | **69 ms** | 1.0x | 7 |
|
|
277
|
+
| **fast_xlsx** (`memory: :low`) | **78 ms** | 1.1x | 7 |
|
|
278
|
+
| **fast_xlsx** | **80 ms** | 1.2x | 10 |
|
|
279
|
+
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 171 ms | 2.5x | 561,728 |
|
|
280
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 194 ms | 2.8x | 20,079 |
|
|
281
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 228 ms | 3.3x | 320,076 |
|
|
282
|
+
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 583 ms | 8.4x | 1,483,899 |
|
|
283
|
+
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 691 ms | 10.0x | 745,122 |
|
|
284
|
+
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2650 ms | 38.3x | 8,700,448 |
|
|
285
|
+
|
|
286
|
+
All outputs are 681–750 KB.
|
|
268
287
|
|
|
269
288
|
### Memory
|
|
270
289
|
|
|
@@ -272,14 +291,18 @@ All outputs are 702–750 KB.
|
|
|
272
291
|
|
|
273
292
|
| Library | Unique strings | Repeated strings |
|
|
274
293
|
|---|---:|---:|
|
|
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 | +
|
|
294
|
+
| **fast_xlsx** (`memory: :constant`) | **+1 MB** | **+1 MB** |
|
|
295
|
+
| **fast_xlsx** (`memory: :low`) | +61 MB | **+1 MB** |
|
|
296
|
+
| **fast_xlsx** | +271 MB | +217 MB |
|
|
297
|
+
| fast_excel 0.5 (constant_memory) | +10 MB | +9 MB |
|
|
298
|
+
| fast_excel 0.5 | +182 MB | +151 MB |
|
|
280
299
|
|
|
281
300
|
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
301
|
|
|
302
|
+
### Threads
|
|
303
|
+
|
|
304
|
+
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.
|
|
305
|
+
|
|
283
306
|
### Reproduce
|
|
284
307
|
|
|
285
308
|
```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.3.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.3.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,5 @@
|
|
|
1
1
|
use std::cell::{Cell, RefCell};
|
|
2
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
2
3
|
use std::sync::{Arc, Mutex};
|
|
3
4
|
|
|
4
5
|
use magnus::{
|
|
@@ -39,9 +40,56 @@ fn xerr(e: XlsxError) -> Error {
|
|
|
39
40
|
|
|
40
41
|
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
41
42
|
|
|
43
|
+
// Runs `f` without Ruby's global lock, so other Ruby threads run meanwhile.
|
|
44
|
+
// `f` must not touch any Ruby object, and must release the workbook mutex
|
|
45
|
+
// before returning: a Ruby thread waiting on that mutex holds the lock this
|
|
46
|
+
// thread then needs back.
|
|
47
|
+
//
|
|
48
|
+
// Uses the "2" variant: rb_thread_call_without_gvl raises pending interrupts
|
|
49
|
+
// (Thread#raise, Timeout, Ctrl-C) by longjmp-ing over these Rust frames,
|
|
50
|
+
// which skips their destructors. This one never raises; if an interrupt is
|
|
51
|
+
// already pending it returns without calling `f`, which then runs with the
|
|
52
|
+
// lock held. Either way Ruby raises the interrupt after the method returns.
|
|
53
|
+
// Without an unblocking function, an interrupt waits for `f` to finish.
|
|
54
|
+
fn without_gvl<R>(f: impl FnOnce() -> R) -> R {
|
|
55
|
+
unsafe extern "C" fn call<F: FnOnce() -> R, R>(
|
|
56
|
+
data: *mut std::ffi::c_void,
|
|
57
|
+
) -> *mut std::ffi::c_void {
|
|
58
|
+
let (f, result) = &mut *(data as *mut (Option<F>, Option<std::thread::Result<R>>));
|
|
59
|
+
// A panic must not unwind into Ruby's C code; it is resumed below.
|
|
60
|
+
*result = Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|
|
61
|
+
f.take().unwrap(),
|
|
62
|
+
)));
|
|
63
|
+
std::ptr::null_mut()
|
|
64
|
+
}
|
|
65
|
+
fn run<F: FnOnce() -> R, R>(f: F) -> R {
|
|
66
|
+
let mut data: (Option<F>, Option<std::thread::Result<R>>) = (Some(f), None);
|
|
67
|
+
unsafe {
|
|
68
|
+
rb_sys::rb_thread_call_without_gvl2(
|
|
69
|
+
Some(call::<F, R>),
|
|
70
|
+
&mut data as *mut _ as *mut std::ffi::c_void,
|
|
71
|
+
None,
|
|
72
|
+
std::ptr::null_mut(),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
let Some(result) = data.1 else {
|
|
76
|
+
// An interrupt was pending, so `call` never ran.
|
|
77
|
+
return (data.0.take().unwrap())();
|
|
78
|
+
};
|
|
79
|
+
match result {
|
|
80
|
+
Ok(result) => result,
|
|
81
|
+
Err(panic) => std::panic::resume_unwind(panic),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
run(f)
|
|
85
|
+
}
|
|
86
|
+
|
|
42
87
|
#[magnus::wrap(class = "FastXlsx::Workbook", free_immediately)]
|
|
43
88
|
struct Workbook {
|
|
44
89
|
inner: Shared,
|
|
90
|
+
// Index of the sheet Excel opens on (0 unless one is activated), shared
|
|
91
|
+
// with the worksheets: it can't be hidden.
|
|
92
|
+
active: Arc<AtomicUsize>,
|
|
45
93
|
constant_memory: bool,
|
|
46
94
|
low_memory: bool,
|
|
47
95
|
}
|
|
@@ -50,6 +98,7 @@ struct Workbook {
|
|
|
50
98
|
struct Worksheet {
|
|
51
99
|
wb: Shared,
|
|
52
100
|
index: usize,
|
|
101
|
+
active: Arc<AtomicUsize>,
|
|
53
102
|
// Where << / append write next.
|
|
54
103
|
next_row: Cell<u32>,
|
|
55
104
|
// Highest row with cells written. In :constant / :low mode rows above it
|
|
@@ -61,6 +110,8 @@ struct Worksheet {
|
|
|
61
110
|
// after add_table (rust_xlsxwriter only formats cells that already exist).
|
|
62
111
|
// Owned copies, so they don't depend on the Ruby Format objects living on.
|
|
63
112
|
table_formats: RefCell<Vec<TableColumnFormat>>,
|
|
113
|
+
// Merged ranges as (first_row, first_col, last_row, last_col).
|
|
114
|
+
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
64
115
|
}
|
|
65
116
|
|
|
66
117
|
struct TableColumnFormat {
|
|
@@ -73,6 +124,7 @@ impl Workbook {
|
|
|
73
124
|
fn new(constant_memory: bool, low_memory: bool) -> Self {
|
|
74
125
|
Workbook {
|
|
75
126
|
inner: Arc::new(Mutex::new(rust_xlsxwriter::Workbook::new())),
|
|
127
|
+
active: Arc::new(AtomicUsize::new(0)),
|
|
76
128
|
constant_memory,
|
|
77
129
|
low_memory,
|
|
78
130
|
}
|
|
@@ -121,25 +173,26 @@ impl Workbook {
|
|
|
121
173
|
Ok(Worksheet {
|
|
122
174
|
wb: self.inner.clone(),
|
|
123
175
|
index: wb.worksheets().len() - 1,
|
|
176
|
+
active: self.active.clone(),
|
|
124
177
|
next_row: Cell::new(0),
|
|
125
178
|
last_written_row: Cell::new(0),
|
|
126
179
|
flushes_rows: self.constant_memory || self.low_memory,
|
|
127
180
|
table_formats: RefCell::new(Vec::new()),
|
|
181
|
+
merges: RefCell::new(Vec::new()),
|
|
128
182
|
})
|
|
129
183
|
}
|
|
130
184
|
|
|
185
|
+
// Saving (XML and compression) can take a while, so it runs without
|
|
186
|
+
// Ruby's global lock. The mutex guard is dropped inside the closure.
|
|
131
187
|
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)?;
|
|
188
|
+
let inner = &rb_self.inner;
|
|
189
|
+
let buf = without_gvl(|| inner.lock().unwrap().save_to_buffer()).map_err(xerr)?;
|
|
138
190
|
Ok(ruby.str_from_slice(&buf))
|
|
139
191
|
}
|
|
140
192
|
|
|
141
193
|
fn save(&self, path: String) -> Result<(), Error> {
|
|
142
|
-
self.inner
|
|
194
|
+
let inner = &self.inner;
|
|
195
|
+
without_gvl(|| inner.lock().unwrap().save(path).map(|_| ())).map_err(xerr)
|
|
143
196
|
}
|
|
144
197
|
|
|
145
198
|
// "Name" for the whole workbook, "Sheet1!Name" for one sheet. Duplicate
|
|
@@ -229,8 +282,9 @@ fn emit<T: IntoExcelData>(
|
|
|
229
282
|
|
|
230
283
|
// Excel's limit on the text in a cell.
|
|
231
284
|
const MAX_CHARS: usize = 32_767;
|
|
232
|
-
// Excel's column
|
|
285
|
+
// Excel's column and row counts.
|
|
233
286
|
const MAX_COLS: usize = 16_384;
|
|
287
|
+
const MAX_ROWS: u32 = 1_048_576;
|
|
234
288
|
|
|
235
289
|
// A cell value converted from Ruby. Converting may run Ruby code (to_s, jd,
|
|
236
290
|
// url, ...), so it happens before the workbook lock is taken: Ruby code that
|
|
@@ -686,6 +740,16 @@ const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
|
|
|
686
740
|
("edit_objects", |o| &mut o.edit_objects),
|
|
687
741
|
];
|
|
688
742
|
|
|
743
|
+
// Excel's paper size codes; page_setup also takes the number itself.
|
|
744
|
+
const PAPER_SIZES: &[(&str, u8)] = &[
|
|
745
|
+
("letter", 1),
|
|
746
|
+
("tabloid", 3),
|
|
747
|
+
("legal", 5),
|
|
748
|
+
("a3", 8),
|
|
749
|
+
("a4", 9),
|
|
750
|
+
("a5", 11),
|
|
751
|
+
];
|
|
752
|
+
|
|
689
753
|
const BORDERS: &[(&str, FormatBorder)] = &[
|
|
690
754
|
("thin", FormatBorder::Thin),
|
|
691
755
|
("medium", FormatBorder::Medium),
|
|
@@ -922,6 +986,29 @@ impl Worksheet {
|
|
|
922
986
|
Ok(())
|
|
923
987
|
}
|
|
924
988
|
|
|
989
|
+
// Worksheet#write. The (row, col, value, format = nil) form is handled
|
|
990
|
+
// here rather than in a Ruby wrapper, since it runs once per cell; the
|
|
991
|
+
// rest ("B2", or a wrong argument count) goes to Ruby's _write_ref.
|
|
992
|
+
fn write_any(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
|
|
993
|
+
let index = |i: usize| args.get(i).and_then(|v| Integer::from_value(*v));
|
|
994
|
+
if let (3 | 4, Some(row), Some(col)) = (args.len(), index(0), index(1)) {
|
|
995
|
+
let format = match args.get(3) {
|
|
996
|
+
Some(f) => Option::<&Format>::try_convert(*f)?,
|
|
997
|
+
None => None,
|
|
998
|
+
};
|
|
999
|
+
Self::write(
|
|
1000
|
+
ruby,
|
|
1001
|
+
&rb_self,
|
|
1002
|
+
row.to_u32()?,
|
|
1003
|
+
col.to_u16()?,
|
|
1004
|
+
args[2],
|
|
1005
|
+
format,
|
|
1006
|
+
)?;
|
|
1007
|
+
return Ok(rb_self);
|
|
1008
|
+
}
|
|
1009
|
+
rb_self.funcall("_write_ref", args)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
925
1012
|
fn write(
|
|
926
1013
|
ruby: &Ruby,
|
|
927
1014
|
rb_self: &Self,
|
|
@@ -1075,6 +1162,35 @@ impl Worksheet {
|
|
|
1075
1162
|
format: Option<&Format>,
|
|
1076
1163
|
) -> Result<Obj<Self>, Error> {
|
|
1077
1164
|
rb_self.check_not_flushed(ruby, first_row)?;
|
|
1165
|
+
// rust_xlsxwriter blanks the range before it notices an overlap, which
|
|
1166
|
+
// wipes the earlier merge's value, so check first.
|
|
1167
|
+
// A range rust_xlsxwriter rejects anyway (reversed, too big) is left
|
|
1168
|
+
// to it, so its error names the real problem.
|
|
1169
|
+
// ponytail: linear scan; sheets have few merges.
|
|
1170
|
+
let range = (first_row, first_col, last_row, last_col);
|
|
1171
|
+
let valid = first_row <= last_row
|
|
1172
|
+
&& first_col <= last_col
|
|
1173
|
+
&& last_row < MAX_ROWS
|
|
1174
|
+
&& usize::from(last_col) < MAX_COLS;
|
|
1175
|
+
let overlap = rb_self
|
|
1176
|
+
.merges
|
|
1177
|
+
.borrow()
|
|
1178
|
+
.iter()
|
|
1179
|
+
.copied()
|
|
1180
|
+
.find(|&(fr, fc, lr, lc)| {
|
|
1181
|
+
first_row <= lr && fr <= last_row && first_col <= lc && fc <= last_col
|
|
1182
|
+
})
|
|
1183
|
+
.filter(|_| valid);
|
|
1184
|
+
if let Some((fr, fc, lr, lc)) = overlap {
|
|
1185
|
+
return Err(Error::new(
|
|
1186
|
+
ruby.get_inner(&ERROR),
|
|
1187
|
+
format!(
|
|
1188
|
+
"merge range {} overlaps the earlier merge {}",
|
|
1189
|
+
rust_xlsxwriter::utility::cell_range(first_row, first_col, last_row, last_col),
|
|
1190
|
+
rust_xlsxwriter::utility::cell_range(fr, fc, lr, lc),
|
|
1191
|
+
),
|
|
1192
|
+
));
|
|
1193
|
+
}
|
|
1078
1194
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1079
1195
|
let tables = rb_self.table_formats.borrow();
|
|
1080
1196
|
let format = Self::cell_format(&tables, first_row, first_col, format.map(|f| &*f.0));
|
|
@@ -1093,6 +1209,7 @@ impl Worksheet {
|
|
|
1093
1209
|
.map_err(xerr)?;
|
|
1094
1210
|
value.write(ws, first_row, first_col, format)
|
|
1095
1211
|
})?;
|
|
1212
|
+
rb_self.merges.borrow_mut().push(range);
|
|
1096
1213
|
rb_self.advance(last_row);
|
|
1097
1214
|
rb_self.note_written(first_row);
|
|
1098
1215
|
Ok(rb_self)
|
|
@@ -1490,6 +1607,142 @@ impl Worksheet {
|
|
|
1490
1607
|
Ok(rb_self)
|
|
1491
1608
|
}
|
|
1492
1609
|
|
|
1610
|
+
fn activate(rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1611
|
+
let mut wb = rb_self.wb.lock().unwrap();
|
|
1612
|
+
// rust_xlsxwriter leaves sheets activated earlier selected, which
|
|
1613
|
+
// groups them in Excel (edits then go to all of them).
|
|
1614
|
+
for (i, ws) in wb.worksheets_mut().iter_mut().enumerate() {
|
|
1615
|
+
ws.set_active(i == rb_self.index);
|
|
1616
|
+
ws.set_selected(i == rb_self.index);
|
|
1617
|
+
}
|
|
1618
|
+
rb_self.active.store(rb_self.index, Ordering::Relaxed);
|
|
1619
|
+
Ok(rb_self)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
fn hide(ruby: &Ruby, rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1623
|
+
// rust_xlsxwriter would quietly unhide it when saving.
|
|
1624
|
+
if rb_self.active.load(Ordering::Relaxed) == rb_self.index {
|
|
1625
|
+
return Err(Error::new(
|
|
1626
|
+
ruby.get_inner(&ERROR),
|
|
1627
|
+
"can't hide the sheet Excel opens on (the first one unless another is activated): activate another sheet first",
|
|
1628
|
+
));
|
|
1629
|
+
}
|
|
1630
|
+
rb_self.with_ws(|ws| {
|
|
1631
|
+
ws.set_hidden(true);
|
|
1632
|
+
Ok(())
|
|
1633
|
+
})?;
|
|
1634
|
+
Ok(rb_self)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent: Value) -> Result<Obj<Self>, Error> {
|
|
1638
|
+
// Integers only: converting would quietly truncate 150.9.
|
|
1639
|
+
let percent = Integer::from_value(percent)
|
|
1640
|
+
.ok_or_else(|| {
|
|
1641
|
+
Error::new(
|
|
1642
|
+
ruby.exception_type_error(),
|
|
1643
|
+
format!("zoom must be an Integer, got {}", percent.inspect()),
|
|
1644
|
+
)
|
|
1645
|
+
})?
|
|
1646
|
+
.to_i64()?;
|
|
1647
|
+
// rust_xlsxwriter only prints a warning for these.
|
|
1648
|
+
if !(10..=400).contains(&percent) {
|
|
1649
|
+
return Err(Error::new(
|
|
1650
|
+
ruby.exception_arg_error(),
|
|
1651
|
+
format!("invalid zoom {percent}: use 10..400"),
|
|
1652
|
+
));
|
|
1653
|
+
}
|
|
1654
|
+
rb_self.with_ws(|ws| {
|
|
1655
|
+
ws.set_zoom(percent as u16);
|
|
1656
|
+
Ok(())
|
|
1657
|
+
})?;
|
|
1658
|
+
Ok(rb_self)
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
fn tab_color(ruby: &Ruby, rb_self: Obj<Self>, value: Value) -> Result<Obj<Self>, Error> {
|
|
1662
|
+
let rgb = color(ruby, value)?;
|
|
1663
|
+
rb_self.with_ws(|ws| {
|
|
1664
|
+
ws.set_tab_color(rgb);
|
|
1665
|
+
Ok(())
|
|
1666
|
+
})?;
|
|
1667
|
+
Ok(rb_self)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
fn hide_gridlines(rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
1671
|
+
rb_self.with_ws(|ws| {
|
|
1672
|
+
ws.set_screen_gridlines(false);
|
|
1673
|
+
Ok(())
|
|
1674
|
+
})?;
|
|
1675
|
+
Ok(rb_self)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Ranges arrive as [first, last] / [first_row, first_col, last_row,
|
|
1679
|
+
// last_col], already parsed by lib/fast_xlsx.rb.
|
|
1680
|
+
fn page_setup(ruby: &Ruby, rb_self: &Self, options: RHash) -> Result<(), Error> {
|
|
1681
|
+
check_keys(
|
|
1682
|
+
ruby,
|
|
1683
|
+
options,
|
|
1684
|
+
&[
|
|
1685
|
+
"landscape",
|
|
1686
|
+
"paper",
|
|
1687
|
+
"fit_width",
|
|
1688
|
+
"fit_height",
|
|
1689
|
+
"repeat_rows",
|
|
1690
|
+
"repeat_columns",
|
|
1691
|
+
"print_area",
|
|
1692
|
+
"gridlines",
|
|
1693
|
+
],
|
|
1694
|
+
"page_setup",
|
|
1695
|
+
)?;
|
|
1696
|
+
let landscape = opt::<Value>(ruby, options, "landscape")?.map(|v| v.to_bool());
|
|
1697
|
+
let paper = match opt::<Value>(ruby, options, "paper")? {
|
|
1698
|
+
None => None,
|
|
1699
|
+
Some(v) if Integer::from_value(v).is_some() => Some(
|
|
1700
|
+
Integer::from_value(v)
|
|
1701
|
+
.and_then(|n| n.to_u8().ok())
|
|
1702
|
+
.ok_or_else(|| {
|
|
1703
|
+
Error::new(
|
|
1704
|
+
ruby.exception_arg_error(),
|
|
1705
|
+
format!("invalid paper {}: use a symbol or Excel's paper number (0 for the printer's default)", v.inspect()),
|
|
1706
|
+
)
|
|
1707
|
+
})?,
|
|
1708
|
+
),
|
|
1709
|
+
Some(v) => Some(choice(ruby, "paper", v, PAPER_SIZES)?),
|
|
1710
|
+
};
|
|
1711
|
+
let fit_width = opt::<u16>(ruby, options, "fit_width")?;
|
|
1712
|
+
let fit_height = opt::<u16>(ruby, options, "fit_height")?;
|
|
1713
|
+
let repeat_rows = opt::<(u32, u32)>(ruby, options, "repeat_rows")?;
|
|
1714
|
+
let repeat_columns = opt::<(u16, u16)>(ruby, options, "repeat_columns")?;
|
|
1715
|
+
let print_area = opt::<(u32, u16, u32, u16)>(ruby, options, "print_area")?;
|
|
1716
|
+
let gridlines = opt::<Value>(ruby, options, "gridlines")?.map(|v| v.to_bool());
|
|
1717
|
+
rb_self.with_ws(|ws| {
|
|
1718
|
+
match landscape {
|
|
1719
|
+
Some(true) => ws.set_landscape(),
|
|
1720
|
+
Some(false) => ws.set_portrait(),
|
|
1721
|
+
None => ws,
|
|
1722
|
+
};
|
|
1723
|
+
if let Some(paper) = paper {
|
|
1724
|
+
ws.set_paper_size(paper);
|
|
1725
|
+
}
|
|
1726
|
+
// 0 means as many pages as the content needs.
|
|
1727
|
+
if fit_width.unwrap_or(0) > 0 || fit_height.unwrap_or(0) > 0 {
|
|
1728
|
+
ws.set_print_fit_to_pages(fit_width.unwrap_or(0), fit_height.unwrap_or(0));
|
|
1729
|
+
}
|
|
1730
|
+
if let Some((first, last)) = repeat_rows {
|
|
1731
|
+
ws.set_repeat_rows(first, last).map_err(xerr)?;
|
|
1732
|
+
}
|
|
1733
|
+
if let Some((first, last)) = repeat_columns {
|
|
1734
|
+
ws.set_repeat_columns(first, last).map_err(xerr)?;
|
|
1735
|
+
}
|
|
1736
|
+
if let Some((fr, fc, lr, lc)) = print_area {
|
|
1737
|
+
ws.set_print_area(fr, fc, lr, lc).map_err(xerr)?;
|
|
1738
|
+
}
|
|
1739
|
+
if let Some(gridlines) = gridlines {
|
|
1740
|
+
ws.set_print_gridlines(gridlines);
|
|
1741
|
+
}
|
|
1742
|
+
Ok(())
|
|
1743
|
+
})
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1493
1746
|
fn group_rows(
|
|
1494
1747
|
ruby: &Ruby,
|
|
1495
1748
|
rb_self: Obj<Self>,
|
|
@@ -1578,6 +1831,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1578
1831
|
|
|
1579
1832
|
let ws = module.define_class("Worksheet", ruby.class_object())?;
|
|
1580
1833
|
ws.define_method("_write", method!(Worksheet::write, 4))?;
|
|
1834
|
+
ws.define_method("write", method!(Worksheet::write_any, -1))?;
|
|
1581
1835
|
ws.define_method("_append", method!(Worksheet::append, 2))?;
|
|
1582
1836
|
ws.define_method("_column_width", method!(Worksheet::set_column_width, 3))?;
|
|
1583
1837
|
ws.define_method("_column_format", method!(Worksheet::set_column_format, 3))?;
|
|
@@ -1593,7 +1847,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1593
1847
|
method!(Worksheet::conditional_format, 5),
|
|
1594
1848
|
)?;
|
|
1595
1849
|
ws.define_method("_data_validation", method!(Worksheet::data_validation, 5))?;
|
|
1596
|
-
ws.define_method("
|
|
1850
|
+
ws.define_method("_freeze_panes", method!(Worksheet::freeze_panes, 2))?;
|
|
1597
1851
|
ws.define_method("row_height", method!(Worksheet::set_row_height, 2))?;
|
|
1598
1852
|
ws.define_method("page_breaks", method!(Worksheet::set_page_breaks, 1))?;
|
|
1599
1853
|
ws.define_method("_page_header", method!(Worksheet::set_header, 1))?;
|
|
@@ -1603,6 +1857,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1603
1857
|
"vertical_page_breaks",
|
|
1604
1858
|
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
1605
1859
|
)?;
|
|
1860
|
+
ws.define_method("activate", method!(Worksheet::activate, 0))?;
|
|
1861
|
+
ws.define_method("hide", method!(Worksheet::hide, 0))?;
|
|
1862
|
+
ws.define_method("zoom", method!(Worksheet::zoom, 1))?;
|
|
1863
|
+
ws.define_method("tab_color", method!(Worksheet::tab_color, 1))?;
|
|
1864
|
+
ws.define_method("hide_gridlines", method!(Worksheet::hide_gridlines, 0))?;
|
|
1865
|
+
ws.define_method("_page_setup", method!(Worksheet::page_setup, 1))?;
|
|
1606
1866
|
ws.define_method("_group_rows", method!(Worksheet::group_rows, 3))?;
|
|
1607
1867
|
ws.define_method("_group_columns", method!(Worksheet::group_columns, 3))?;
|
|
1608
1868
|
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,
|