fast_xlsx 0.1.2 → 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 +29 -0
- data/Cargo.lock +1 -48
- data/Cargo.toml +4 -0
- data/README.md +84 -17
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/Cargo.toml +3 -1
- data/ext/fast_xlsx/src/lib.rs +380 -12
- data/ext/fast_xlsx/tests/ruby_suite.rs +3 -0
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +166 -20
- data/sig/fast_xlsx.rbs +36 -4
- 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,34 @@
|
|
|
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
|
+
|
|
19
|
+
## [0.2.0] - 2026-10-01
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Range methods (`autofilter`, `merge_range`, `conditional_format`, `data_validation`, `add_table`) also take an Excel reference (`"A1:D10"`) or rows and columns as Integers or Ranges (`0..9, 0..3`).
|
|
24
|
+
- `Workbook#define_name` for workbook-wide and sheet-scoped defined names.
|
|
25
|
+
- `Worksheet#group_rows` and `#group_columns` (outline groups, optionally collapsed); `group_rows` raises in `:constant` / `:low` mode.
|
|
26
|
+
- `Worksheet#protect(password:, allow:)` locks a sheet; `Format` options `locked: false` and `hidden: true` keep cells editable or hide formulas.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `column_width`, `column_format` and the other methods that take a Range raise `ArgumentError` for an empty, reversed or endless Range, or one that isn't Integers, instead of a `TypeError` or `RangeError`.
|
|
31
|
+
|
|
3
32
|
## [0.1.2] - 2026-09-30
|
|
4
33
|
|
|
5
34
|
### Fixed
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# FastXlsx
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/fast_xlsx)
|
|
3
4
|
[](https://codecov.io/gh/7a6163/fast_xlsx)
|
|
4
5
|
|
|
5
6
|
Fast `.xlsx` writer for Ruby, built on [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) via [magnus](https://github.com/matsadler/magnus).
|
|
@@ -17,6 +18,7 @@ ws = wb.add_worksheet("Report") # later: wb.worksheet("Report")
|
|
|
17
18
|
ws << ["id", "name", "created_at"] # append a row
|
|
18
19
|
ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
|
|
19
20
|
ws.write(0, 5, 42) # write a single cell (row, col, value)
|
|
21
|
+
ws.write("F1", 42) # or by its Excel reference
|
|
20
22
|
|
|
21
23
|
wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
|
|
22
24
|
wb.save("report.xlsx") # or wb.to_xlsx => binary String
|
|
@@ -75,6 +77,8 @@ ws.write(1, 2, Date.today, date) # format one cell
|
|
|
75
77
|
| `valign` | `:top`, `:center`, `:bottom` |
|
|
76
78
|
| `border`, `border_left`, `border_right`, `border_top`, `border_bottom` | `:thin`, `:medium`, `:thick`, `:dashed`, `:dotted`, `:double`, `:hair` |
|
|
77
79
|
| `border_color` | `"#RRGGBB"` or `0xRRGGBB` |
|
|
80
|
+
| `locked` | `false` keeps the cell editable on a protected sheet (default `true`) |
|
|
81
|
+
| `hidden` | `true` hides the cell's formula on a protected sheet |
|
|
78
82
|
|
|
79
83
|
Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
|
|
80
84
|
|
|
@@ -85,9 +89,38 @@ ws.column_width(0, 20) # column A, width in characters
|
|
|
85
89
|
ws.column_width(1..3, 12) # columns B–D
|
|
86
90
|
ws.column_format(4, FastXlsx::Format.new(num_format: "#,##0.00")) # default for cells in E written without a format
|
|
87
91
|
ws.autofit # size other columns to the data written so far; set widths are kept
|
|
88
|
-
ws.autofilter(
|
|
92
|
+
ws.autofilter("A1:D101") # filter buttons on A1:D101
|
|
89
93
|
```
|
|
90
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
|
+
|
|
111
|
+
### Cell ranges
|
|
112
|
+
|
|
113
|
+
`autofilter`, `merge_range`, `conditional_format`, `data_validation` and `add_table` take a range in any of these styles:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
ws.autofilter(0, 0, 100, 3) # four 0-based numbers: first_row, first_col, last_row, last_col
|
|
117
|
+
ws.autofilter("A1:D101") # an Excel reference; "$A$1:$D$101" and a single "B2" work too
|
|
118
|
+
ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
|
|
119
|
+
ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
|
|
120
|
+
```
|
|
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
|
+
|
|
91
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.
|
|
92
125
|
|
|
93
126
|
### Layout
|
|
@@ -103,6 +136,36 @@ ws.page_footer("&L&A", margin: 0.2) # sheet name on the left; margin
|
|
|
103
136
|
ws.margins(left: 0.5, top: 1) # other margins keep Excel's defaults
|
|
104
137
|
```
|
|
105
138
|
|
|
139
|
+
### Outline groups
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
ws.group_rows(1..10) # rows 2–11 get an expand/collapse button
|
|
143
|
+
ws.group_rows(1..4) # grouping again nests them (up to 7 levels)
|
|
144
|
+
ws.group_columns(2..3, collapsed: true) # columns C–D, collapsed until expanded
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`group_rows` needs `memory: :standard`: in `:constant` / `:low` mode rust_xlsxwriter writes rows without their outline level, so it raises. `group_columns` works in every mode.
|
|
148
|
+
|
|
149
|
+
### Defined names
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
wb.define_name("Rate", "=0.96") # workbook-wide; use as =A1*Rate
|
|
153
|
+
wb.define_name("Report!Sales", "=Report!$B$2:$B$13") # only on the Report sheet
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Invalid names raise `FastXlsx::Error` right away; duplicate names, and names for a sheet that doesn't exist, raise when saving.
|
|
157
|
+
|
|
158
|
+
### Protection
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
input = FastXlsx::Format.new(locked: false)
|
|
162
|
+
ws.write(1, 1, 0, input) # B2 stays editable
|
|
163
|
+
ws.protect # lock everything else
|
|
164
|
+
ws.protect(password: "secret", allow: %i[sort use_autofilter]) # or with a password and allowed actions
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`allow:` takes `:format_cells`, `:format_columns`, `:format_rows`, `:insert_columns`, `:insert_rows`, `:insert_links`, `:delete_columns`, `:delete_rows`, `:sort`, `:use_autofilter`, `:use_pivot_tables`, `:edit_scenarios`, `:edit_objects`. The password only stops editing in Excel; it does not encrypt the file.
|
|
168
|
+
|
|
106
169
|
### Conditional formats
|
|
107
170
|
|
|
108
171
|
```ruby
|
|
@@ -210,17 +273,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
|
|
|
210
273
|
|
|
211
274
|
| Library | Time | vs fastest | Ruby objects allocated |
|
|
212
275
|
|---|---:|---:|---:|
|
|
213
|
-
| **fast_xlsx** (`memory: :constant`) | **
|
|
214
|
-
| **fast_xlsx** (`memory: :low`) | **
|
|
215
|
-
| **fast_xlsx** | **
|
|
216
|
-
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 |
|
|
217
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) |
|
|
218
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 |
|
|
219
|
-
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 |
|
|
220
|
-
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 |
|
|
221
|
-
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 |
|
|
222
|
-
|
|
223
|
-
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.
|
|
224
287
|
|
|
225
288
|
### Memory
|
|
226
289
|
|
|
@@ -228,14 +291,18 @@ All outputs are 702–750 KB.
|
|
|
228
291
|
|
|
229
292
|
| Library | Unique strings | Repeated strings |
|
|
230
293
|
|---|---:|---:|
|
|
231
|
-
| **fast_xlsx** (`memory: :constant`) | **+
|
|
232
|
-
| **fast_xlsx** (`memory: :low`) | +
|
|
233
|
-
| **fast_xlsx** | +
|
|
234
|
-
| fast_excel 0.5 (constant_memory) | +10 MB | +
|
|
235
|
-
| 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 |
|
|
236
299
|
|
|
237
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.
|
|
238
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
|
+
|
|
239
306
|
### Reproduce
|
|
240
307
|
|
|
241
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::{
|
|
@@ -15,7 +16,8 @@ use rust_xlsxwriter::{
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
use rust_xlsxwriter::{
|
|
18
|
-
Chart, ChartType, DocProperties, Image, Note, Table, TableColumn,
|
|
19
|
+
Chart, ChartType, DocProperties, Image, Note, ProtectionOptions, Table, TableColumn,
|
|
20
|
+
TableFunction, TableStyle,
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
// These constants are defined in lib/fast_xlsx.rb before this extension loads.
|
|
@@ -38,9 +40,56 @@ fn xerr(e: XlsxError) -> Error {
|
|
|
38
40
|
|
|
39
41
|
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
40
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
|
+
|
|
41
87
|
#[magnus::wrap(class = "FastXlsx::Workbook", free_immediately)]
|
|
42
88
|
struct Workbook {
|
|
43
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>,
|
|
44
93
|
constant_memory: bool,
|
|
45
94
|
low_memory: bool,
|
|
46
95
|
}
|
|
@@ -49,6 +98,7 @@ struct Workbook {
|
|
|
49
98
|
struct Worksheet {
|
|
50
99
|
wb: Shared,
|
|
51
100
|
index: usize,
|
|
101
|
+
active: Arc<AtomicUsize>,
|
|
52
102
|
// Where << / append write next.
|
|
53
103
|
next_row: Cell<u32>,
|
|
54
104
|
// Highest row with cells written. In :constant / :low mode rows above it
|
|
@@ -60,6 +110,8 @@ struct Worksheet {
|
|
|
60
110
|
// after add_table (rust_xlsxwriter only formats cells that already exist).
|
|
61
111
|
// Owned copies, so they don't depend on the Ruby Format objects living on.
|
|
62
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)>>,
|
|
63
115
|
}
|
|
64
116
|
|
|
65
117
|
struct TableColumnFormat {
|
|
@@ -72,6 +124,7 @@ impl Workbook {
|
|
|
72
124
|
fn new(constant_memory: bool, low_memory: bool) -> Self {
|
|
73
125
|
Workbook {
|
|
74
126
|
inner: Arc::new(Mutex::new(rust_xlsxwriter::Workbook::new())),
|
|
127
|
+
active: Arc::new(AtomicUsize::new(0)),
|
|
75
128
|
constant_memory,
|
|
76
129
|
low_memory,
|
|
77
130
|
}
|
|
@@ -120,25 +173,39 @@ impl Workbook {
|
|
|
120
173
|
Ok(Worksheet {
|
|
121
174
|
wb: self.inner.clone(),
|
|
122
175
|
index: wb.worksheets().len() - 1,
|
|
176
|
+
active: self.active.clone(),
|
|
123
177
|
next_row: Cell::new(0),
|
|
124
178
|
last_written_row: Cell::new(0),
|
|
125
179
|
flushes_rows: self.constant_memory || self.low_memory,
|
|
126
180
|
table_formats: RefCell::new(Vec::new()),
|
|
181
|
+
merges: RefCell::new(Vec::new()),
|
|
127
182
|
})
|
|
128
183
|
}
|
|
129
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.
|
|
130
187
|
fn to_xlsx(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
.lock()
|
|
134
|
-
.unwrap()
|
|
135
|
-
.save_to_buffer()
|
|
136
|
-
.map_err(xerr)?;
|
|
188
|
+
let inner = &rb_self.inner;
|
|
189
|
+
let buf = without_gvl(|| inner.lock().unwrap().save_to_buffer()).map_err(xerr)?;
|
|
137
190
|
Ok(ruby.str_from_slice(&buf))
|
|
138
191
|
}
|
|
139
192
|
|
|
140
193
|
fn save(&self, path: String) -> Result<(), Error> {
|
|
141
|
-
self.inner
|
|
194
|
+
let inner = &self.inner;
|
|
195
|
+
without_gvl(|| inner.lock().unwrap().save(path).map(|_| ())).map_err(xerr)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// "Name" for the whole workbook, "Sheet1!Name" for one sheet. Duplicate
|
|
199
|
+
// names and unknown sheets are reported when saving, since sheets can be
|
|
200
|
+
// added after the name.
|
|
201
|
+
fn define_name(rb_self: Obj<Self>, name: String, formula: String) -> Result<Obj<Self>, Error> {
|
|
202
|
+
rb_self
|
|
203
|
+
.inner
|
|
204
|
+
.lock()
|
|
205
|
+
.unwrap()
|
|
206
|
+
.define_name(name, &formula)
|
|
207
|
+
.map_err(xerr)?;
|
|
208
|
+
Ok(rb_self)
|
|
142
209
|
}
|
|
143
210
|
|
|
144
211
|
fn set_properties(ruby: &Ruby, rb_self: &Self, fields: RHash) -> Result<(), Error> {
|
|
@@ -215,8 +282,9 @@ fn emit<T: IntoExcelData>(
|
|
|
215
282
|
|
|
216
283
|
// Excel's limit on the text in a cell.
|
|
217
284
|
const MAX_CHARS: usize = 32_767;
|
|
218
|
-
// Excel's column
|
|
285
|
+
// Excel's column and row counts.
|
|
219
286
|
const MAX_COLS: usize = 16_384;
|
|
287
|
+
const MAX_ROWS: u32 = 1_048_576;
|
|
220
288
|
|
|
221
289
|
// A cell value converted from Ruby. Converting may run Ruby code (to_s, jd,
|
|
222
290
|
// url, ...), so it happens before the workbook lock is taken: Ruby code that
|
|
@@ -653,6 +721,35 @@ fn table_column(
|
|
|
653
721
|
Ok((column, format))
|
|
654
722
|
}
|
|
655
723
|
|
|
724
|
+
// Actions users may still take on a protected sheet (selecting cells is
|
|
725
|
+
// always allowed).
|
|
726
|
+
type ProtectionFlag = fn(&mut ProtectionOptions) -> &mut bool;
|
|
727
|
+
const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
|
|
728
|
+
("format_cells", |o| &mut o.format_cells),
|
|
729
|
+
("format_columns", |o| &mut o.format_columns),
|
|
730
|
+
("format_rows", |o| &mut o.format_rows),
|
|
731
|
+
("insert_columns", |o| &mut o.insert_columns),
|
|
732
|
+
("insert_rows", |o| &mut o.insert_rows),
|
|
733
|
+
("insert_links", |o| &mut o.insert_links),
|
|
734
|
+
("delete_columns", |o| &mut o.delete_columns),
|
|
735
|
+
("delete_rows", |o| &mut o.delete_rows),
|
|
736
|
+
("sort", |o| &mut o.sort),
|
|
737
|
+
("use_autofilter", |o| &mut o.use_autofilter),
|
|
738
|
+
("use_pivot_tables", |o| &mut o.use_pivot_tables),
|
|
739
|
+
("edit_scenarios", |o| &mut o.edit_scenarios),
|
|
740
|
+
("edit_objects", |o| &mut o.edit_objects),
|
|
741
|
+
];
|
|
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
|
+
|
|
656
753
|
const BORDERS: &[(&str, FormatBorder)] = &[
|
|
657
754
|
("thin", FormatBorder::Thin),
|
|
658
755
|
("medium", FormatBorder::Medium),
|
|
@@ -710,6 +807,11 @@ impl Format {
|
|
|
710
807
|
}
|
|
711
808
|
"indent" => taken.set_indent(u8::try_convert(value)?),
|
|
712
809
|
"shrink" if value.to_bool() => taken.set_shrink(),
|
|
810
|
+
// For protected sheets: locked: false leaves a cell editable,
|
|
811
|
+
// hidden: true hides its formula.
|
|
812
|
+
"locked" if value.to_bool() => taken.set_locked(),
|
|
813
|
+
"locked" => taken.set_unlocked(),
|
|
814
|
+
"hidden" if value.to_bool() => taken.set_hidden(),
|
|
713
815
|
"border_color" => taken.set_border_color(color(ruby, value)?),
|
|
714
816
|
"num_format" => taken.set_num_format(String::try_convert(value)?),
|
|
715
817
|
"font_size" => taken.set_font_size(f64::try_convert(value)?),
|
|
@@ -742,7 +844,8 @@ impl Format {
|
|
|
742
844
|
"border_right" => taken.set_border_right(choice(ruby, "border", value, BORDERS)?),
|
|
743
845
|
"border_top" => taken.set_border_top(choice(ruby, "border", value, BORDERS)?),
|
|
744
846
|
"border_bottom" => taken.set_border_bottom(choice(ruby, "border", value, BORDERS)?),
|
|
745
|
-
"bold" | "italic" | "underline" | "text_wrap" | "strikeout" | "shrink"
|
|
847
|
+
"bold" | "italic" | "underline" | "text_wrap" | "strikeout" | "shrink"
|
|
848
|
+
| "hidden" => taken,
|
|
746
849
|
other => {
|
|
747
850
|
return Err(Error::new(
|
|
748
851
|
ruby.exception_arg_error(),
|
|
@@ -883,6 +986,29 @@ impl Worksheet {
|
|
|
883
986
|
Ok(())
|
|
884
987
|
}
|
|
885
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
|
+
|
|
886
1012
|
fn write(
|
|
887
1013
|
ruby: &Ruby,
|
|
888
1014
|
rb_self: &Self,
|
|
@@ -1036,6 +1162,35 @@ impl Worksheet {
|
|
|
1036
1162
|
format: Option<&Format>,
|
|
1037
1163
|
) -> Result<Obj<Self>, Error> {
|
|
1038
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
|
+
}
|
|
1039
1194
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1040
1195
|
let tables = rb_self.table_formats.borrow();
|
|
1041
1196
|
let format = Self::cell_format(&tables, first_row, first_col, format.map(|f| &*f.0));
|
|
@@ -1054,6 +1209,7 @@ impl Worksheet {
|
|
|
1054
1209
|
.map_err(xerr)?;
|
|
1055
1210
|
value.write(ws, first_row, first_col, format)
|
|
1056
1211
|
})?;
|
|
1212
|
+
rb_self.merges.borrow_mut().push(range);
|
|
1057
1213
|
rb_self.advance(last_row);
|
|
1058
1214
|
rb_self.note_written(first_row);
|
|
1059
1215
|
Ok(rb_self)
|
|
@@ -1451,6 +1607,207 @@ impl Worksheet {
|
|
|
1451
1607
|
Ok(rb_self)
|
|
1452
1608
|
}
|
|
1453
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
|
+
|
|
1746
|
+
fn group_rows(
|
|
1747
|
+
ruby: &Ruby,
|
|
1748
|
+
rb_self: Obj<Self>,
|
|
1749
|
+
first: u32,
|
|
1750
|
+
last: u32,
|
|
1751
|
+
collapsed: bool,
|
|
1752
|
+
) -> Result<Obj<Self>, Error> {
|
|
1753
|
+
// rust_xlsxwriter writes no outline levels when it writes rows to
|
|
1754
|
+
// disk as it goes.
|
|
1755
|
+
if rb_self.flushes_rows {
|
|
1756
|
+
return Err(Error::new(
|
|
1757
|
+
ruby.get_inner(&ERROR),
|
|
1758
|
+
"group_rows needs memory: :standard (rows written to disk as they go lose their outline level)",
|
|
1759
|
+
));
|
|
1760
|
+
}
|
|
1761
|
+
rb_self.with_ws(|ws| {
|
|
1762
|
+
if collapsed {
|
|
1763
|
+
ws.group_rows_collapsed(first, last)
|
|
1764
|
+
} else {
|
|
1765
|
+
ws.group_rows(first, last)
|
|
1766
|
+
}
|
|
1767
|
+
.map(|_| ())
|
|
1768
|
+
.map_err(xerr)
|
|
1769
|
+
})?;
|
|
1770
|
+
Ok(rb_self)
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
fn group_columns(
|
|
1774
|
+
rb_self: Obj<Self>,
|
|
1775
|
+
first: u16,
|
|
1776
|
+
last: u16,
|
|
1777
|
+
collapsed: bool,
|
|
1778
|
+
) -> Result<Obj<Self>, Error> {
|
|
1779
|
+
rb_self.with_ws(|ws| {
|
|
1780
|
+
if collapsed {
|
|
1781
|
+
ws.group_columns_collapsed(first, last)
|
|
1782
|
+
} else {
|
|
1783
|
+
ws.group_columns(first, last)
|
|
1784
|
+
}
|
|
1785
|
+
.map(|_| ())
|
|
1786
|
+
.map_err(xerr)
|
|
1787
|
+
})?;
|
|
1788
|
+
Ok(rb_self)
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
fn protect(
|
|
1792
|
+
ruby: &Ruby,
|
|
1793
|
+
rb_self: Obj<Self>,
|
|
1794
|
+
password: Option<String>,
|
|
1795
|
+
allow: RArray,
|
|
1796
|
+
) -> Result<Obj<Self>, Error> {
|
|
1797
|
+
let mut options = ProtectionOptions::new();
|
|
1798
|
+
each_entry(allow, |_, action| {
|
|
1799
|
+
*choice(ruby, "protect action", action, PROTECTION_ALLOW)?(&mut options) = true;
|
|
1800
|
+
Ok(())
|
|
1801
|
+
})?;
|
|
1802
|
+
rb_self.with_ws(|ws| {
|
|
1803
|
+
// Always set the password, so protecting again replaces it ("" is none).
|
|
1804
|
+
ws.protect_with_password(password.as_deref().unwrap_or(""));
|
|
1805
|
+
ws.protect_with_options(&options); // keeps the password
|
|
1806
|
+
Ok(())
|
|
1807
|
+
})?;
|
|
1808
|
+
Ok(rb_self)
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1454
1811
|
fn name(&self) -> Result<String, Error> {
|
|
1455
1812
|
self.with_ws(|ws| Ok(ws.name()))
|
|
1456
1813
|
}
|
|
@@ -1469,15 +1826,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1469
1826
|
wb.define_method("_add_worksheet", method!(Workbook::add_worksheet, 1))?;
|
|
1470
1827
|
wb.define_method("to_xlsx", method!(Workbook::to_xlsx, 0))?;
|
|
1471
1828
|
wb.define_method("_save", method!(Workbook::save, 1))?;
|
|
1829
|
+
wb.define_method("define_name", method!(Workbook::define_name, 2))?;
|
|
1472
1830
|
wb.define_method("_properties", method!(Workbook::set_properties, 1))?;
|
|
1473
1831
|
|
|
1474
1832
|
let ws = module.define_class("Worksheet", ruby.class_object())?;
|
|
1475
1833
|
ws.define_method("_write", method!(Worksheet::write, 4))?;
|
|
1834
|
+
ws.define_method("write", method!(Worksheet::write_any, -1))?;
|
|
1476
1835
|
ws.define_method("_append", method!(Worksheet::append, 2))?;
|
|
1477
1836
|
ws.define_method("_column_width", method!(Worksheet::set_column_width, 3))?;
|
|
1478
1837
|
ws.define_method("_column_format", method!(Worksheet::set_column_format, 3))?;
|
|
1479
1838
|
ws.define_method("_autofit", method!(Worksheet::autofit, 0))?;
|
|
1480
|
-
ws.define_method("
|
|
1839
|
+
ws.define_method("_autofilter", method!(Worksheet::autofilter, 4))?;
|
|
1481
1840
|
ws.define_method("name", method!(Worksheet::name, 0))?;
|
|
1482
1841
|
ws.define_method("_write_comment", method!(Worksheet::write_comment, 4))?;
|
|
1483
1842
|
ws.define_method("_insert_image", method!(Worksheet::insert_image, 4))?;
|
|
@@ -1488,7 +1847,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1488
1847
|
method!(Worksheet::conditional_format, 5),
|
|
1489
1848
|
)?;
|
|
1490
1849
|
ws.define_method("_data_validation", method!(Worksheet::data_validation, 5))?;
|
|
1491
|
-
ws.define_method("
|
|
1850
|
+
ws.define_method("_freeze_panes", method!(Worksheet::freeze_panes, 2))?;
|
|
1492
1851
|
ws.define_method("row_height", method!(Worksheet::set_row_height, 2))?;
|
|
1493
1852
|
ws.define_method("page_breaks", method!(Worksheet::set_page_breaks, 1))?;
|
|
1494
1853
|
ws.define_method("_page_header", method!(Worksheet::set_header, 1))?;
|
|
@@ -1498,6 +1857,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1498
1857
|
"vertical_page_breaks",
|
|
1499
1858
|
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
1500
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))?;
|
|
1866
|
+
ws.define_method("_group_rows", method!(Worksheet::group_rows, 3))?;
|
|
1867
|
+
ws.define_method("_group_columns", method!(Worksheet::group_columns, 3))?;
|
|
1868
|
+
ws.define_method("_protect", method!(Worksheet::protect, 2))?;
|
|
1501
1869
|
ws.define_method("_merge_range", method!(Worksheet::merge_range, 6))?;
|
|
1502
1870
|
|
|
1503
1871
|
let format = module.define_class("Format", ruby.class_object())?;
|
|
@@ -14,6 +14,9 @@ fn ruby_test_suite_passes() {
|
|
|
14
14
|
.args(["exec", "rake", "compile", "test"])
|
|
15
15
|
.current_dir(&root)
|
|
16
16
|
.env("RB_SYS_CARGO_PROFILE", "dev")
|
|
17
|
+
// The suite needs a few CPU seconds; a mutant that loops forever is
|
|
18
|
+
// stopped instead of running on after cargo-mutants gives up on it.
|
|
19
|
+
.env("FAST_XLSX_TEST_CPU_SECONDS", "120")
|
|
17
20
|
.status()
|
|
18
21
|
.expect("could not run `bundle exec rake compile test`");
|
|
19
22
|
assert!(status.success(), "the Ruby test suite failed");
|
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)
|
|
@@ -113,11 +118,11 @@ module FastXlsx
|
|
|
113
118
|
|
|
114
119
|
# columns: a 0-based column index or a Range of them. width is in characters.
|
|
115
120
|
def column_width(columns, width)
|
|
116
|
-
|
|
117
|
-
_column_width(*
|
|
121
|
+
range = CellRange.bounds(columns)
|
|
122
|
+
_column_width(*range, width)
|
|
118
123
|
@fixed_widths ||= {}
|
|
119
|
-
@fixed_widths.delete(
|
|
120
|
-
@fixed_widths[
|
|
124
|
+
@fixed_widths.delete(range) # re-insert so autofit replays calls in order
|
|
125
|
+
@fixed_widths[range] = width
|
|
121
126
|
self
|
|
122
127
|
end
|
|
123
128
|
|
|
@@ -129,32 +134,45 @@ module FastXlsx
|
|
|
129
134
|
self
|
|
130
135
|
end
|
|
131
136
|
|
|
137
|
+
# The methods below take a cell range in any of these styles:
|
|
138
|
+
# (first_row, first_col, last_row, last_col) four 0-based numbers
|
|
139
|
+
# ("A1:D10") or ("B2") an Excel reference
|
|
140
|
+
# (rows, cols) Integers or Ranges, e.g. (0..9, 0..3)
|
|
141
|
+
|
|
142
|
+
# Filter buttons on the range's first row.
|
|
143
|
+
def autofilter(*range)
|
|
144
|
+
_autofilter(*CellRange.split(range).first)
|
|
145
|
+
end
|
|
146
|
+
|
|
132
147
|
# Merges the range and writes value (any cell type) into its first cell.
|
|
133
|
-
def merge_range(
|
|
134
|
-
|
|
148
|
+
def merge_range(*args)
|
|
149
|
+
range, (value, format) = CellRange.split(args, 1..2)
|
|
150
|
+
_merge_range(*range, value, format)
|
|
135
151
|
end
|
|
136
152
|
|
|
137
153
|
# Highlights cells in the range by rule. type: :cell, :text, :formula,
|
|
138
154
|
# :data_bar or :color_scale; see the README for each type's options.
|
|
139
|
-
def conditional_format(
|
|
140
|
-
_conditional_format(
|
|
155
|
+
def conditional_format(*range, type:, **)
|
|
156
|
+
_conditional_format(*CellRange.split(range).first, { type: type, ** })
|
|
141
157
|
end
|
|
142
158
|
|
|
143
159
|
# Restricts what can be entered in the range. type: :list, :whole_number,
|
|
144
160
|
# :decimal or :text_length; see the README for the options.
|
|
145
|
-
def data_validation(
|
|
146
|
-
_data_validation(
|
|
161
|
+
def data_validation(*range, type:, **)
|
|
162
|
+
_data_validation(*CellRange.split(range).first, { type: type, ** })
|
|
147
163
|
end
|
|
148
164
|
|
|
149
165
|
# Adds a comment (Excel "note") to a cell.
|
|
150
|
-
def write_comment(
|
|
166
|
+
def write_comment(*args, author: nil)
|
|
167
|
+
row, col, (text, *) = CellRange.cell(args, 1..1)
|
|
151
168
|
_write_comment(row, col, text, author)
|
|
152
169
|
end
|
|
153
170
|
|
|
154
171
|
# Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
|
|
155
172
|
# cell. source is a file path or an IO (anything responding to #read).
|
|
156
173
|
# Options: scale: or width:/height: (pixels), x_offset:, y_offset: (pixels), alt_text:.
|
|
157
|
-
def insert_image(
|
|
174
|
+
def insert_image(*args, **)
|
|
175
|
+
row, col, (source, *) = CellRange.cell(args, 1..1)
|
|
158
176
|
bytes = source.respond_to?(:read) ? source.read : File.binread(source)
|
|
159
177
|
_insert_image(row, col, bytes, { ** })
|
|
160
178
|
end
|
|
@@ -162,7 +180,8 @@ module FastXlsx
|
|
|
162
180
|
# Inserts a chart with its top-left corner in the cell. series is an Array
|
|
163
181
|
# of { values:, categories:, name: } with Excel ranges such as
|
|
164
182
|
# "Sheet1!$B$2:$B$13". Options: title:, x_axis:, y_axis:, width:, height:.
|
|
165
|
-
def insert_chart(
|
|
183
|
+
def insert_chart(*cell, type:, series:, **)
|
|
184
|
+
row, col, = CellRange.cell(cell)
|
|
166
185
|
_insert_chart(row, col, { type: type, series: series, ** })
|
|
167
186
|
end
|
|
168
187
|
|
|
@@ -170,8 +189,8 @@ module FastXlsx
|
|
|
170
189
|
# into an Excel table. columns: header Strings or { header:, total:,
|
|
171
190
|
# total_label:, format: }; other options: style:, name:, total_row:,
|
|
172
191
|
# banded_rows:, autofilter:.
|
|
173
|
-
def add_table(
|
|
174
|
-
_add_table(
|
|
192
|
+
def add_table(*range, **)
|
|
193
|
+
_add_table(*CellRange.split(range).first, { ** })
|
|
175
194
|
end
|
|
176
195
|
|
|
177
196
|
# Printed page header/footer using Excel codes such as "&CPage &P of &N".
|
|
@@ -186,24 +205,151 @@ module FastXlsx
|
|
|
186
205
|
margin ? margins(footer: margin) : self
|
|
187
206
|
end
|
|
188
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
|
+
|
|
189
220
|
# Print margins in inches; margins not given keep their current value.
|
|
190
221
|
def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
191
222
|
_margins(*[left, right, top, bottom, header, footer].map { |m| m || -1.0 })
|
|
192
223
|
self
|
|
193
224
|
end
|
|
194
225
|
|
|
226
|
+
# Outline group with an expand/collapse button. rows: a 0-based row index
|
|
227
|
+
# or a Range; grouping rows already grouped nests them (up to 7 levels).
|
|
228
|
+
# collapsed: true hides them until expanded.
|
|
229
|
+
def group_rows(rows, collapsed: false)
|
|
230
|
+
_group_rows(*CellRange.bounds(rows), collapsed)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def group_columns(columns, collapsed: false)
|
|
234
|
+
_group_columns(*CellRange.bounds(columns), collapsed)
|
|
235
|
+
end
|
|
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
|
+
|
|
244
|
+
# Locks the sheet against editing. Cells whose format has locked: false
|
|
245
|
+
# stay editable. allow: actions users may still take, any of :format_cells,
|
|
246
|
+
# :format_columns, :format_rows, :insert_columns, :insert_rows,
|
|
247
|
+
# :insert_links, :delete_columns, :delete_rows, :sort, :use_autofilter,
|
|
248
|
+
# :use_pivot_tables, :edit_scenarios, :edit_objects.
|
|
249
|
+
def protect(password: nil, allow: [])
|
|
250
|
+
_protect(password, Array(allow))
|
|
251
|
+
end
|
|
252
|
+
|
|
195
253
|
# Default format for cells in these columns that are written without one.
|
|
196
254
|
def column_format(columns, format)
|
|
197
|
-
_column_format(*
|
|
255
|
+
_column_format(*CellRange.bounds(columns), format)
|
|
198
256
|
self
|
|
199
257
|
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Cell ranges in the styles Worksheet methods accept: four 0-based numbers,
|
|
261
|
+
# an Excel reference ("A1:D10", "B2"), or rows and columns as Integers or
|
|
262
|
+
# Ranges.
|
|
263
|
+
module CellRange
|
|
264
|
+
REF = /\A\$?([A-Za-z]{1,3})\$?([1-9]\d*)\z/ # ASCII only: /i also matches the Kelvin sign
|
|
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"'
|
|
267
|
+
|
|
268
|
+
module_function
|
|
269
|
+
|
|
270
|
+
# Splits a range off the front of args and checks how many args follow.
|
|
271
|
+
# Returns [[first_row, first_col, last_row, last_col], the args after it].
|
|
272
|
+
def split(args, following = 0..0)
|
|
273
|
+
range, rest = parse(args)
|
|
274
|
+
check_following(rest, following, "cell range", FORMS)
|
|
275
|
+
[range, rest]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def parse(args)
|
|
279
|
+
case args
|
|
280
|
+
in [Numeric, Numeric, Numeric, Numeric, *rest] then [args.first(4), rest]
|
|
281
|
+
in [String => ref, *rest] then [excel(ref), rest]
|
|
282
|
+
in [Integer | Range => rows, Integer | Range => cols, *rest]
|
|
283
|
+
[rows_and_cols(rows, cols), rest]
|
|
284
|
+
else
|
|
285
|
+
raise ArgumentError, "expected a cell range, got #{args.inspect}; #{FORMS}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
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
|
+
|
|
313
|
+
def rows_and_cols(rows, cols)
|
|
314
|
+
first_row, last_row = bounds(rows)
|
|
315
|
+
first_col, last_col = bounds(cols)
|
|
316
|
+
[first_row, first_col, last_row, last_col]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# "A1:D10", "$A$1:$D$10" or a single "B2".
|
|
320
|
+
def excel(ref)
|
|
321
|
+
cells = cell_matches(ref)
|
|
322
|
+
rows = cells.map { |m| m[2].to_i - 1 }
|
|
323
|
+
cols = cells.map { |m| column(m[1]) }
|
|
324
|
+
[rows.min, cols.min, rows.max, cols.max]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def cell_matches(ref)
|
|
328
|
+
cells = ref.split(":", -1).map { |cell| REF.match(cell) }
|
|
329
|
+
return cells if (1..2).cover?(cells.size) && cells.all?
|
|
330
|
+
|
|
331
|
+
raise ArgumentError, "invalid cell range #{ref.inspect}: use e.g. \"A1:D10\" or \"B2\""
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# "A" => 0, "AA" => 26.
|
|
335
|
+
def column(letters)
|
|
336
|
+
letters.upcase.each_char.reduce(0) { |n, c| (n * 26) + c.ord - 64 } - 1
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# [first, last] of an index or a Range.
|
|
340
|
+
def bounds(indexes)
|
|
341
|
+
return [indexes, indexes] if indexes.is_a?(Integer)
|
|
342
|
+
unless indexes.is_a?(Range) && indexes.begin.is_a?(Integer) && indexes.end.is_a?(Integer)
|
|
343
|
+
raise ArgumentError, "expected an Integer or a Range of Integers, got #{indexes.inspect}"
|
|
344
|
+
end
|
|
200
345
|
|
|
201
|
-
|
|
346
|
+
first, last = indexes.minmax
|
|
347
|
+
raise ArgumentError, "empty range #{indexes.inspect}" unless first
|
|
202
348
|
|
|
203
|
-
|
|
204
|
-
columns.is_a?(Integer) ? [columns, columns] : columns.minmax
|
|
349
|
+
[first, last]
|
|
205
350
|
end
|
|
206
351
|
end
|
|
352
|
+
private_constant :CellRange
|
|
207
353
|
|
|
208
354
|
# Cell style, e.g. Format.new(bold: true). Pass to Worksheet#write.
|
|
209
355
|
class Format
|
data/sig/fast_xlsx.rbs
CHANGED
|
@@ -36,6 +36,7 @@ module FastXlsx
|
|
|
36
36
|
?company: String, ?category: String, ?keywords: String,
|
|
37
37
|
?comments: String, ?status: String) -> self
|
|
38
38
|
def to_xlsx: () -> String
|
|
39
|
+
def define_name: (String name, String formula) -> self
|
|
39
40
|
def save: (String | _ToPath path) -> void
|
|
40
41
|
end
|
|
41
42
|
|
|
@@ -46,7 +47,7 @@ module FastXlsx
|
|
|
46
47
|
type underline = bool | :single | :double | :single_accounting | :double_accounting
|
|
47
48
|
|
|
48
49
|
def self.new: (?bold: bool, ?italic: bool, ?underline: underline, ?strikeout: bool, ?text_wrap: bool,
|
|
49
|
-
?shrink: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
|
|
50
|
+
?shrink: bool, ?locked: bool, ?hidden: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
|
|
50
51
|
?font_color: color, ?bg_color: color, ?font_script: :superscript | :subscript,
|
|
51
52
|
?align: :left | :center | :right, ?valign: :top | :center | :bottom,
|
|
52
53
|
?rotation: Integer, ?indent: Integer,
|
|
@@ -56,42 +57,73 @@ module FastXlsx
|
|
|
56
57
|
|
|
57
58
|
class Worksheet
|
|
58
59
|
def write: (Integer row, Integer col, cell value, ?Format? format) -> self
|
|
60
|
+
| (String cell, cell value, ?Format? format) -> self
|
|
59
61
|
def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
|
|
60
62
|
def <<: (Array[cell] row) -> self
|
|
61
63
|
def concat: (Array[Array[cell]] rows) -> self
|
|
62
64
|
def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
|
|
63
65
|
def column_format: (Integer | Range[Integer] columns, Format format) -> self
|
|
64
66
|
def autofit: () -> self
|
|
67
|
+
# A cell range: four 0-based numbers, an Excel reference ("A1:D10"), or
|
|
68
|
+
# rows and columns as Integers or Ranges. Methods with options take it as
|
|
69
|
+
# *cell_range so the keywords can follow.
|
|
70
|
+
type indexes = Integer | Range[Integer]
|
|
71
|
+
type cell_range = Integer | String | Range[Integer]
|
|
65
72
|
def autofilter: (Integer first_row, Integer first_col, Integer last_row, Integer last_col) -> self
|
|
73
|
+
| (String range) -> self
|
|
74
|
+
| (indexes rows, indexes cols) -> self
|
|
66
75
|
def freeze_panes: (Integer row, Integer col) -> self
|
|
76
|
+
| (String cell) -> self
|
|
67
77
|
def row_height: (Integer row, Numeric height) -> self
|
|
68
78
|
def page_breaks: (Array[Integer] rows) -> self
|
|
69
79
|
def vertical_page_breaks: (Array[Integer] cols) -> self
|
|
70
80
|
def page_header: (String text, ?margin: Numeric?) -> self
|
|
71
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
|
|
72
90
|
def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
|
|
73
91
|
?header: Numeric?, ?footer: Numeric?) -> self
|
|
92
|
+
type protect_action = :format_cells | :format_columns | :format_rows | :insert_columns | :insert_rows
|
|
93
|
+
| :insert_links | :delete_columns | :delete_rows | :sort | :use_autofilter
|
|
94
|
+
| :use_pivot_tables | :edit_scenarios | :edit_objects
|
|
95
|
+
def group_rows: (Integer | Range[Integer] rows, ?collapsed: bool) -> self
|
|
96
|
+
def group_columns: (Integer | Range[Integer] columns, ?collapsed: bool) -> self
|
|
97
|
+
def protect: (?password: String?, ?allow: protect_action | Array[protect_action] | nil) -> self
|
|
74
98
|
def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
|
|
75
|
-
|
|
99
|
+
| (String range, cell value, ?Format? format) -> self
|
|
100
|
+
| (indexes rows, indexes cols, cell value, ?Format? format) -> self
|
|
101
|
+
def conditional_format: (*cell_range range,
|
|
76
102
|
type: :cell | :text | :formula | :data_bar | :color_scale,
|
|
77
103
|
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
78
104
|
?format: Format, ?colors: 2 | 3) -> self
|
|
79
|
-
def data_validation: (
|
|
105
|
+
def data_validation: (*cell_range range,
|
|
80
106
|
type: :list | :whole_number | :decimal | :text_length,
|
|
81
107
|
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
82
108
|
?input_title: String, ?input_message: String,
|
|
83
109
|
?error_title: String, ?error_message: String) -> self
|
|
84
110
|
def write_comment: (Integer row, Integer col, String text, ?author: String?) -> self
|
|
111
|
+
| (String cell, String text, ?author: String?) -> self
|
|
85
112
|
def insert_image: (Integer row, Integer col, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
|
|
86
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
|
|
87
116
|
type chart_series = { values: String, ?categories: String, ?name: String }
|
|
88
117
|
|
|
89
118
|
def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
|
|
90
119
|
?title: String, ?x_axis: String, ?y_axis: String,
|
|
91
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
|
|
92
124
|
type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
|
|
93
125
|
|
|
94
|
-
def add_table: (
|
|
126
|
+
def add_table: (*cell_range range,
|
|
95
127
|
?columns: Array[table_column], ?style: Symbol, ?name: String,
|
|
96
128
|
?total_row: bool, ?banded_rows: bool, ?autofilter: bool) -> self
|
|
97
129
|
def name: () -> String
|