fast_xlsx 0.1.2-x86_64-linux → 0.3.0-x86_64-linux
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/README.md +84 -17
- data/bench/Gemfile.lock +2 -2
- data/lib/fast_xlsx/3.3/fast_xlsx.so +0 -0
- data/lib/fast_xlsx/3.4/fast_xlsx.so +0 -0
- data/lib/fast_xlsx/4.0/fast_xlsx.so +0 -0
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +166 -20
- data/sig/fast_xlsx.rbs +36 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa33588d163149bd2ff649232a7f10246b81dee9f777f55c38afa7d735d90917
|
|
4
|
+
data.tar.gz: '08f220f1a035657a1ab915c9b73ab67ed316cb15c7cbd532df922e2e9cdc339d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a9a6c93be3e311157652e72f8ce0ac34f9f7059ea1c871e746c66e0d5c4503e45e3031f55245bd76dc8e598460831828b2fba51078f2613e9b4a412823f5969
|
|
7
|
+
data.tar.gz: 53f6f6d9e41ff5e1aac020b2139aaf06ae6395cd48bf3ddd6095e451680856208ad292e97f7dbc334f3d615cd282ac6132104dbd691b26d93a8d3eece85efd8a
|
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/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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_xlsx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Writes Excel .xlsx files from Ruby through a native Rust extension (rust_xlsxwriter
|
|
14
14
|
+ magnus).
|