fast_xlsx 0.2.0-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 385d5c9b69ab08b516cfa1f99e4075b691b9a264924c32498303f4d577abfeaa
4
- data.tar.gz: 3c28e89e17339b7f2077d0be122eddf858789be99acd9d18a404c044e309ee44
3
+ metadata.gz: aa33588d163149bd2ff649232a7f10246b81dee9f777f55c38afa7d735d90917
4
+ data.tar.gz: '08f220f1a035657a1ab915c9b73ab67ed316cb15c7cbd532df922e2e9cdc339d'
5
5
  SHA512:
6
- metadata.gz: 02670f7b5ebf4a2cf27955651de5a9a7409202700871cdd493f73788de50c7a0e8cb37d0732f8e5ad99ab13fd9017b4f452fac96c5cb6c361e203e4793c35dc0
7
- data.tar.gz: 3744269cbb53e37c8519a5df27a196d0fbab8672f8ca985a24c74c06486f326707cd1c2f5303ef7498f994ce7f695ff905745f5c31e38db9eb08dd4c9937d308
6
+ metadata.gz: 1a9a6c93be3e311157652e72f8ce0ac34f9f7059ea1c871e746c66e0d5c4503e45e3031f55245bd76dc8e598460831828b2fba51078f2613e9b4a412823f5969
7
+ data.tar.gz: 53f6f6d9e41ff5e1aac020b2139aaf06ae6395cd48bf3ddd6095e451680856208ad292e97f7dbc334f3d615cd282ac6132104dbd691b26d93a8d3eece85efd8a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-10-01
4
+
5
+ ### Changed
6
+
7
+ - `to_xlsx` and `save` release Ruby's global lock while building and compressing the file, so other threads (e.g. in Puma or Sidekiq) keep running. An interrupt (Ctrl-C, `Timeout`, `Thread#raise`) takes effect once the save finishes.
8
+ - Saving is 20-30% faster: files are compressed with zlib-rs instead of C zlib (which also drops the `libz-sys` dependency). Adding rows is about 15% faster from building with link-time optimisation.
9
+
10
+ ### Added
11
+
12
+ - `write`, `write_comment`, `insert_image`, `insert_chart` and `freeze_panes` also take a cell reference such as `"B2"`. `write` is now native, which makes it about 15% faster.
13
+ - `Worksheet#zoom`, `#tab_color`, `#hide_gridlines`, `#activate`, `#hide`, and `#page_setup` (orientation, paper size, fit to pages, rows/columns repeated on every page, print area, printed gridlines).
14
+
15
+ ### Fixed
16
+
17
+ - A `merge_range` that overlaps an earlier merge raises before writing anything; before, it also blanked the earlier merge's value.
18
+
3
19
  ## [0.2.0] - 2026-10-01
4
20
 
5
21
  ### Added
data/README.md CHANGED
@@ -18,6 +18,7 @@ ws = wb.add_worksheet("Report") # later: wb.worksheet("Report")
18
18
  ws << ["id", "name", "created_at"] # append a row
19
19
  ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
20
20
  ws.write(0, 5, 42) # write a single cell (row, col, value)
21
+ ws.write("F1", 42) # or by its Excel reference
21
22
 
22
23
  wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
23
24
  wb.save("report.xlsx") # or wb.to_xlsx => binary String
@@ -91,6 +92,22 @@ ws.autofit # size other columns to the data written so fa
91
92
  ws.autofilter("A1:D101") # filter buttons on A1:D101
92
93
  ```
93
94
 
95
+ ### Sheet view and printing
96
+
97
+ ```ruby
98
+ ws.zoom(150) # 10..400 percent
99
+ ws.tab_color("#C00000")
100
+ ws.hide_gridlines
101
+ ws.activate # Excel opens on this sheet (un-hides it if hidden)
102
+ other.hide # the sheet Excel opens on can't be hidden: activate another one first
103
+
104
+ ws.page_setup(landscape: true, paper: :a4, # or :letter, :legal, :tabloid, :a3, :a5, Excel's paper number, 0 = printer default
105
+ fit_width: 1, # 1 page wide, as many pages tall as needed
106
+ repeat_rows: 0, # print the header row on every page (an index or a Range)
107
+ print_area: "A1:D100", # any cell range style
108
+ gridlines: true) # print the gridlines
109
+ ```
110
+
94
111
  ### Cell ranges
95
112
 
96
113
  `autofilter`, `merge_range`, `conditional_format`, `data_validation` and `add_table` take a range in any of these styles:
@@ -102,6 +119,8 @@ ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
102
119
  ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
103
120
  ```
104
121
 
122
+ Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_chart`, `freeze_panes`) take `(row, col)` or a reference like `"B2"`.
123
+
105
124
  `autofit` only sees rows still in memory, so in `:constant` / `:low` memory mode it ignores rows already written to disk; set widths with `column_width` instead.
106
125
 
107
126
  ### Layout
@@ -254,17 +273,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
254
273
 
255
274
  | Library | Time | vs fastest | Ruby objects allocated |
256
275
  |---|---:|---:|---:|
257
- | **fast_xlsx** (`memory: :constant`) | **92 ms** | 1.0x | 7 |
258
- | **fast_xlsx** (`memory: :low`) | **101 ms** | 1.1x | 7 |
259
- | **fast_xlsx** | **102 ms** | 1.1x | 10 |
260
- | [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 181 ms | 2.0x | 561,728 |
261
- | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 202 ms | 2.2x | 20,079 |
262
- | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 240 ms | 2.6x | 320,076 |
263
- | [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 610 ms | 6.7x | 1,483,899 |
264
- | [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 678 ms | 7.4x | 745,122 |
265
- | [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2624 ms | 28.6x | 8,700,448 |
266
-
267
- All outputs are 702–750 KB.
276
+ | **fast_xlsx** (`memory: :constant`) | **69 ms** | 1.0x | 7 |
277
+ | **fast_xlsx** (`memory: :low`) | **78 ms** | 1.1x | 7 |
278
+ | **fast_xlsx** | **80 ms** | 1.2x | 10 |
279
+ | [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 171 ms | 2.5x | 561,728 |
280
+ | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 194 ms | 2.8x | 20,079 |
281
+ | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 228 ms | 3.3x | 320,076 |
282
+ | [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 583 ms | 8.4x | 1,483,899 |
283
+ | [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 691 ms | 10.0x | 745,122 |
284
+ | [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2650 ms | 38.3x | 8,700,448 |
285
+
286
+ All outputs are 681–750 KB.
268
287
 
269
288
  ### Memory
270
289
 
@@ -272,14 +291,18 @@ All outputs are 702–750 KB.
272
291
 
273
292
  | Library | Unique strings | Repeated strings |
274
293
  |---|---:|---:|
275
- | **fast_xlsx** (`memory: :constant`) | **+2 MB** | **+2 MB** |
276
- | **fast_xlsx** (`memory: :low`) | +62 MB | **+2 MB** |
277
- | **fast_xlsx** | +270 MB | +217 MB |
278
- | fast_excel 0.5 (constant_memory) | +10 MB | +10 MB |
279
- | fast_excel 0.5 | +183 MB | +151 MB |
294
+ | **fast_xlsx** (`memory: :constant`) | **+1 MB** | **+1 MB** |
295
+ | **fast_xlsx** (`memory: :low`) | +61 MB | **+1 MB** |
296
+ | **fast_xlsx** | +271 MB | +217 MB |
297
+ | fast_excel 0.5 (constant_memory) | +10 MB | +9 MB |
298
+ | fast_excel 0.5 | +182 MB | +151 MB |
280
299
 
281
300
  The `:standard` mode uses more memory than fast_excel's: when saving, rust_xlsxwriter assembles each worksheet's XML in memory (so several worksheets can be built in parallel) instead of streaming it from a temp file. Use `memory: :constant` or `memory: :low` for large exports.
282
301
 
302
+ ### Threads
303
+
304
+ Most of the time goes into saving (building the XML and compressing it). `to_xlsx` and `save` do that without holding Ruby's global lock, so in a threaded server (Puma, Sidekiq) other threads keep running while a large export is saved. An interrupt (Ctrl-C, `Timeout`, `Thread#raise`) takes effect once the save finishes.
305
+
283
306
  ### Reproduce
284
307
 
285
308
  ```bash
data/bench/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fast_xlsx (0.2.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.2.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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastXlsx
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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
- def write(row, col, value, format = nil)
105
+ # write(row, col, value, format = nil) or write("B2", value, format = nil)
106
+ # is native: the (row, col) form runs once per cell, so it skips a Ruby
107
+ # wrapper. Other forms come here.
108
+ def _write_ref(*args)
109
+ row, col, (value, format) = CellRange.cell(args, 1..2)
106
110
  _write(row, col, value, format)
107
111
  self
108
112
  end
113
+ private :_write_ref
109
114
 
110
115
  def append(values, format: nil)
111
116
  _append(values, format)
@@ -158,14 +163,16 @@ module FastXlsx
158
163
  end
159
164
 
160
165
  # Adds a comment (Excel "note") to a cell.
161
- def write_comment(row, col, text, author: nil)
166
+ def write_comment(*args, author: nil)
167
+ row, col, (text, *) = CellRange.cell(args, 1..1)
162
168
  _write_comment(row, col, text, author)
163
169
  end
164
170
 
165
171
  # Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
166
172
  # cell. source is a file path or an IO (anything responding to #read).
167
173
  # Options: scale: or width:/height: (pixels), x_offset:, y_offset: (pixels), alt_text:.
168
- def insert_image(row, col, source, **)
174
+ def insert_image(*args, **)
175
+ row, col, (source, *) = CellRange.cell(args, 1..1)
169
176
  bytes = source.respond_to?(:read) ? source.read : File.binread(source)
170
177
  _insert_image(row, col, bytes, { ** })
171
178
  end
@@ -173,7 +180,8 @@ module FastXlsx
173
180
  # Inserts a chart with its top-left corner in the cell. series is an Array
174
181
  # of { values:, categories:, name: } with Excel ranges such as
175
182
  # "Sheet1!$B$2:$B$13". Options: title:, x_axis:, y_axis:, width:, height:.
176
- def insert_chart(row, col, type:, series:, **)
183
+ def insert_chart(*cell, type:, series:, **)
184
+ row, col, = CellRange.cell(cell)
177
185
  _insert_chart(row, col, { type: type, series: series, ** })
178
186
  end
179
187
 
@@ -197,6 +205,18 @@ module FastXlsx
197
205
  margin ? margins(footer: margin) : self
198
206
  end
199
207
 
208
+ # Printing: landscape:, paper: (:letter, :legal, :tabloid, :a3, :a4, :a5 or
209
+ # Excel's paper number), fit_width:/fit_height: (pages; 0 or left out =
210
+ # as many as needed), repeat_rows:/repeat_columns: (an index or Range,
211
+ # printed on every page), print_area: (any cell range), gridlines:.
212
+ def page_setup(repeat_rows: nil, repeat_columns: nil, print_area: nil, **options)
213
+ options[:repeat_rows] = CellRange.bounds(repeat_rows) if repeat_rows
214
+ options[:repeat_columns] = CellRange.bounds(repeat_columns) if repeat_columns
215
+ options[:print_area] = CellRange.split(print_area.is_a?(Array) ? print_area : [print_area]).first if print_area
216
+ _page_setup(options)
217
+ self
218
+ end
219
+
200
220
  # Print margins in inches; margins not given keep their current value.
201
221
  def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
202
222
  _margins(*[left, right, top, bottom, header, footer].map { |m| m || -1.0 })
@@ -214,6 +234,13 @@ module FastXlsx
214
234
  _group_columns(*CellRange.bounds(columns), collapsed)
215
235
  end
216
236
 
237
+ # Keeps the rows above and the columns left of the cell visible while
238
+ # scrolling: (1, 0) or "A2" freezes the first row.
239
+ def freeze_panes(*cell)
240
+ _freeze_panes(*CellRange.cell(cell).first(2))
241
+ self
242
+ end
243
+
217
244
  # Locks the sheet against editing. Cells whose format has locked: false
218
245
  # stay editable. allow: actions users may still take, any of :format_cells,
219
246
  # :format_columns, :format_rows, :insert_columns, :insert_rows,
@@ -236,6 +263,7 @@ module FastXlsx
236
263
  module CellRange
237
264
  REF = /\A\$?([A-Za-z]{1,3})\$?([1-9]\d*)\z/ # ASCII only: /i also matches the Kelvin sign
238
265
  FORMS = 'a range is (first_row, first_col, last_row, last_col), "A1:D10" or (rows, cols)'
266
+ CELL_FORMS = 'a cell is (row, col) or "B2"'
239
267
 
240
268
  module_function
241
269
 
@@ -243,12 +271,7 @@ module FastXlsx
243
271
  # Returns [[first_row, first_col, last_row, last_col], the args after it].
244
272
  def split(args, following = 0..0)
245
273
  range, rest = parse(args)
246
- unless following.cover?(rest.size)
247
- expected = following.minmax.uniq.join("..")
248
- raise ArgumentError, "wrong number of arguments after the cell range (given #{rest.size}, " \
249
- "expected #{expected}); #{FORMS}"
250
- end
251
-
274
+ check_following(rest, following, "cell range", FORMS)
252
275
  [range, rest]
253
276
  end
254
277
 
@@ -263,6 +286,30 @@ module FastXlsx
263
286
  end
264
287
  end
265
288
 
289
+ # (row, col) or a single-cell reference ("B2") off the front of args, as
290
+ # [row, col, the args after it].
291
+ def cell(args, following = 0..0)
292
+ row, col, *rest = args.first.is_a?(String) ? [*single_cell(args.first), *args.drop(1)] : args
293
+ raise ArgumentError, "expected a cell, got #{args.inspect}; #{CELL_FORMS}" if col.nil?
294
+
295
+ check_following(rest, following, "cell", CELL_FORMS)
296
+ [row, col, rest]
297
+ end
298
+
299
+ def single_cell(ref)
300
+ first_row, first_col, last_row, last_col = excel(ref)
301
+ return [first_row, first_col] if first_row == last_row && first_col == last_col
302
+
303
+ raise ArgumentError, "expected a single cell like \"B2\", got #{ref.inspect}"
304
+ end
305
+
306
+ def check_following(rest, following, what, forms)
307
+ return if following.cover?(rest.size)
308
+
309
+ raise ArgumentError, "wrong number of arguments after the #{what} " \
310
+ "(given #{rest.size}, expected #{following.minmax.uniq.join("..")}); #{forms}"
311
+ end
312
+
266
313
  def rows_and_cols(rows, cols)
267
314
  first_row, last_row = bounds(rows)
268
315
  first_col, last_col = bounds(cols)
data/sig/fast_xlsx.rbs CHANGED
@@ -57,6 +57,7 @@ module FastXlsx
57
57
 
58
58
  class Worksheet
59
59
  def write: (Integer row, Integer col, cell value, ?Format? format) -> self
60
+ | (String cell, cell value, ?Format? format) -> self
60
61
  def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
61
62
  def <<: (Array[cell] row) -> self
62
63
  def concat: (Array[Array[cell]] rows) -> self
@@ -72,11 +73,20 @@ module FastXlsx
72
73
  | (String range) -> self
73
74
  | (indexes rows, indexes cols) -> self
74
75
  def freeze_panes: (Integer row, Integer col) -> self
76
+ | (String cell) -> self
75
77
  def row_height: (Integer row, Numeric height) -> self
76
78
  def page_breaks: (Array[Integer] rows) -> self
77
79
  def vertical_page_breaks: (Array[Integer] cols) -> self
78
80
  def page_header: (String text, ?margin: Numeric?) -> self
79
81
  def page_footer: (String text, ?margin: Numeric?) -> self
82
+ def activate: () -> self
83
+ def hide: () -> self
84
+ def zoom: (Integer percent) -> self
85
+ def tab_color: (String | Integer color) -> self
86
+ def hide_gridlines: () -> self
87
+ def page_setup: (?landscape: bool, ?paper: :letter | :legal | :tabloid | :a3 | :a4 | :a5 | Integer,
88
+ ?fit_width: Integer, ?fit_height: Integer, ?repeat_rows: indexes, ?repeat_columns: indexes,
89
+ ?print_area: String | Array[cell_range], ?gridlines: bool) -> self
80
90
  def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
81
91
  ?header: Numeric?, ?footer: Numeric?) -> self
82
92
  type protect_action = :format_cells | :format_columns | :format_rows | :insert_columns | :insert_rows
@@ -98,13 +108,19 @@ module FastXlsx
98
108
  ?input_title: String, ?input_message: String,
99
109
  ?error_title: String, ?error_message: String) -> self
100
110
  def write_comment: (Integer row, Integer col, String text, ?author: String?) -> self
111
+ | (String cell, String text, ?author: String?) -> self
101
112
  def insert_image: (Integer row, Integer col, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
102
113
  ?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
114
+ | (String cell, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
115
+ ?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
103
116
  type chart_series = { values: String, ?categories: String, ?name: String }
104
117
 
105
118
  def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
106
119
  ?title: String, ?x_axis: String, ?y_axis: String,
107
120
  ?width: Integer, ?height: Integer) -> self
121
+ | (String cell, type: Symbol, series: Array[chart_series],
122
+ ?title: String, ?x_axis: String, ?y_axis: String,
123
+ ?width: Integer, ?height: Integer) -> self
108
124
  type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
109
125
 
110
126
  def add_table: (*cell_range range,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Zac