fast_xlsx 0.5.0-aarch64-linux → 0.7.0-aarch64-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: d5ce755a7313239e1f8aed891577dd789243053ba8898298449e6f374f7fa1b0
4
- data.tar.gz: 981326856ac95f0b000f6129608b66f9fd426add3b67693b606442f74e8908d7
3
+ metadata.gz: ec17dc1cf2a3a54e4494c1da09283c919878e35c06a524c1ce0bb49130cb3bf1
4
+ data.tar.gz: abab767fab2f54710692b1c6586391fdbcb6a273ce0f17b7ee15c814f830d402
5
5
  SHA512:
6
- metadata.gz: 2004ba52b608f7ce2ed3bd8d2d2245c77c7967c2160a33b129841004443fadd9ea49b5895683e2d758f3cece3987cc6b5bcc5a768d55fc1a53a4f08623211418
7
- data.tar.gz: 60d511eb64ae1935c8bd04b40961c22b74967f8bac929dea16dc2facd01a4eee5c8c5afedd717c30d61fc328b263ccdc79f7c069d53cb4e32109812871642b29
6
+ metadata.gz: d60ec2bfda7ea92b693ce4e9062b0629c2cee0df2556710da57aae1821c0a0a427935102c2ae01470553b925da0c5ade5070f091a1f252403e29cfaa2ade771c
7
+ data.tar.gz: d0c1016bfd4a0eb1bb11d6990e526fe9869a268fcae361d3c0a5d31c2ab1b5d05b4e0f2544dccb1808de26f756b9665fbfcadf93ad9d513b99a9b0a299e52921
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2026-10-02
4
+
5
+ ### Added
6
+
7
+ - A Hash of options works wherever a format goes (`write(0, 0, "x", { bold: true })`, `append(row, format: { bold: true })`, table columns, conditional formats, rich strings); equal Hashes share one format.
8
+ - `ws["B2"] = value` and `ws[row, col] = value` write a cell.
9
+ - `Format#to_h` (its options) and `Format#merge` (a new format with more options, from keywords, a Hash or another Format).
10
+ - Formats with the same options are `==`; `eql?` and `hash` follow `Hash#eql?` (so `10` and `10.0` differ there), letting formats serve as Hash keys. Options may have String keys (e.g. parsed from JSON or YAML); the same option as a String and a Symbol raises `ArgumentError`.
11
+ - `Workbook.new` and `Workbook#add_worksheet` yield the new object to a block.
12
+
13
+ ### Changed
14
+
15
+ - A `Format` is frozen, and keeps frozen copies of its option values: one is shared wherever the same options are used.
16
+
17
+ ### Fixed
18
+
19
+ - `Workbook.new { |wb| ... }` ignored its block.
20
+
21
+ ## [0.6.0] - 2026-10-02
22
+
23
+ ### Added
24
+
25
+ - `Worksheet#selection`, `#top_left_cell` (what is selected and scrolled to when the file opens), `#ignore_error` (turns off Excel's green-triangle warnings in a range) and `#unprotect_range` (a range users can edit on a protected sheet).
26
+ - `Worksheet#hide_rows`, `#hide_columns`, `#row_format` (default format for a row's cells) and `#default_row_height` (call it before `row_format`, `hide_rows` and `group_rows`, which copy the default into their rows; it raises otherwise).
27
+
28
+ ### Fixed
29
+
30
+ - `nil` written with a format (e.g. a border, or a row format in `append`) writes a blank cell with that format; the format was dropped.
31
+
3
32
  ## [0.5.0] - 2026-10-01
4
33
 
5
34
  ### Added
data/README.md CHANGED
@@ -19,11 +19,36 @@ 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
21
  ws.write("F1", 42) # or by its Excel reference
22
+ ws["F1"] = 42 # the same (ws[0, 5] = 42 too)
22
23
 
23
24
  wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
24
25
  wb.save("report.xlsx") # or wb.to_xlsx => binary String
25
26
  ```
26
27
 
28
+ `Workbook.new` and `add_worksheet` also take a block, which gets the new object:
29
+
30
+ ```ruby
31
+ wb = FastXlsx::Workbook.new do |wb|
32
+ wb.add_worksheet("Users") do |ws|
33
+ ws.append(%w[id name email], format: { bold: true })
34
+ User.find_each { |u| ws << u.attributes.values_at("id", "name", "email") } # a Hash's values in column order
35
+ end
36
+ end
37
+ ```
38
+
39
+ ### Rails
40
+
41
+ ```ruby
42
+ def export
43
+ wb = FastXlsx::Workbook.new(memory: :constant) # large exports: memory stays flat
44
+ ws = wb.add_worksheet("Orders")
45
+ ws.append(["Order", "Customer", "Total", "Placed at"], format: { bold: true })
46
+ Order.includes(:customer).find_each { |o| ws << [o.number, o.customer.name, o.total, o.created_at] }
47
+ send_data wb.to_xlsx, filename: "orders.xlsx",
48
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
49
+ end
50
+ ```
51
+
27
52
  ### Memory modes
28
53
 
29
54
  By default every cell stays in memory until the file is saved. For large exports, two modes write each finished row to a temp file instead:
@@ -80,6 +105,15 @@ ws.write(1, 2, Date.today, date) # format one cell
80
105
  | `locked` | `false` keeps the cell editable on a protected sheet (default `true`) |
81
106
  | `hidden` | `true` hides the cell's formula on a protected sheet |
82
107
 
108
+ A Hash of options works wherever a format goes (equal Hashes share one format), and a format can be extended:
109
+
110
+ ```ruby
111
+ ws.append(["Total", 1_234], format: { bold: true })
112
+ title = header.merge(font_size: 16) # header's options plus font_size; header.to_h lists them
113
+ ```
114
+
115
+ In a loop over many cells, create the `Format` once: building one from a Hash on every call is slower.
116
+
83
117
  Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
84
118
 
85
119
  Dates and times are shown as `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss`. A format without a `num_format` (yours, a table column's or `column_format`'s) keeps its look and gets that date format added, so `append(row, format: bold)` gives bold dates; a format with a `num_format` is used as is.
@@ -101,6 +135,9 @@ ws.zoom(150) # 10..400 percent
101
135
  ws.tab_color("#C00000")
102
136
  ws.hide_gridlines
103
137
  ws.activate # Excel opens on this sheet (un-hides it if hidden)
138
+ ws.selection("B2:C3") # the cells selected when it opens
139
+ ws.top_left_cell("A50") # and the cell scrolled to the top left
140
+ ws.ignore_error("A2:A1000", :number_stored_as_text) # no green triangles (one kind per range; don't overlap ranges)
104
141
  other.hide # the sheet Excel opens on can't be hidden: activate another one first
105
142
 
106
143
  ws.page_setup(landscape: true, paper: :a4, # or :letter, :legal, :tabloid, :a3, :a5, Excel's paper number, 0 = printer default
@@ -130,6 +167,9 @@ Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_ch
130
167
  ```ruby
131
168
  ws.freeze_panes(1, 0) # keep the first row visible while scrolling
132
169
  ws.row_height(0, 30) # row 1, height in points
170
+ ws.default_row_height(18) # every other row; call it before row_format, hide_rows, group_rows
171
+ ws.row_format(0, header) # default for cells in row 1 written without a format
172
+ ws.hide_rows(10..20) # also hide_columns(3), an index or a Range
133
173
  ws.merge_range(0, 0, 0, 3, "Q3 report", title) # merge A1:D1; the value can be any cell type
134
174
  ws.page_breaks([50, 100]) # print a new page before rows 51 and 101
135
175
  ws.vertical_page_breaks([8]) # and before column I
@@ -164,6 +204,7 @@ input = FastXlsx::Format.new(locked: false)
164
204
  ws.write(1, 1, 0, input) # B2 stays editable
165
205
  ws.protect # lock everything else
166
206
  ws.protect(password: "secret", allow: %i[sort use_autofilter]) # or with a password and allowed actions
207
+ ws.unprotect_range("B2:D10", name: "Inputs") # a range users can still edit (optional password:)
167
208
  ```
168
209
 
169
210
  `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.
@@ -258,7 +299,7 @@ Values are mapped by type:
258
299
  | `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
259
300
  | `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
260
301
  | `true` / `false` | boolean |
261
- | `nil` | empty cell |
302
+ | `nil` | empty cell (a blank cell with the format, when one is given) |
262
303
  | anything else | `to_s` as string |
263
304
 
264
305
  `<<` and `concat` append after the last row written to that worksheet. In `:constant` / `:low` memory mode rows are written to disk as you go, so fill each worksheet top to bottom.
data/bench/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fast_xlsx (0.5.0)
4
+ fast_xlsx (0.7.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.5.0)
80
+ fast_xlsx (0.7.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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FastXlsx
4
4
  # The gem version.
5
- VERSION = "0.5.0"
5
+ VERSION = "0.7.0"
6
6
  end
data/lib/fast_xlsx.rb CHANGED
@@ -74,7 +74,9 @@ module FastXlsx
74
74
  def initialize(*parts)
75
75
  raise ArgumentError, "RichString needs at least one segment" if parts.empty?
76
76
 
77
- @segments = parts.map { |part| part.is_a?(Array) ? [part[0].to_s, part[1]] : [part.to_s, nil] }.freeze
77
+ @segments = parts.map do |part|
78
+ part.is_a?(Array) ? [part[0].to_s, Format._coerce(part[1])] : [part.to_s, nil]
79
+ end.freeze
78
80
  end
79
81
  end
80
82
 
@@ -111,21 +113,33 @@ module FastXlsx
111
113
  # must be filled top to bottom: :constant stores strings inline (memory
112
114
  # stays flat), :low keeps Excel's shared string table (memory grows with
113
115
  # the number of unique strings).
116
+ # @yieldparam workbook [Workbook] when a block is given
117
+ # @return [Workbook]
114
118
  # @raise [ArgumentError] for an unknown mode
119
+ # @example
120
+ # wb = FastXlsx::Workbook.new do |wb|
121
+ # wb.add_worksheet("Report") { |ws| ws << %w[id name] }
122
+ # end
115
123
  def self.new(memory: :standard)
116
124
  unless MEMORY_MODES.include?(memory)
117
125
  raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
118
126
  end
119
127
 
120
- _new(memory == :constant, memory == :low)
128
+ workbook = _new(memory == :constant, memory == :low)
129
+ yield workbook if block_given?
130
+ workbook
121
131
  end
122
132
 
123
133
  # @param name [String, nil] nil takes the first free "SheetN"
134
+ # @yieldparam worksheet [Worksheet] when a block is given
124
135
  # @return [Worksheet]
125
136
  # @raise [FastXlsx::Error] for an invalid name or one already used
126
137
  # (names ignore case)
127
138
  def add_worksheet(name = nil)
128
- _add_worksheet(name).tap { |ws| worksheets << ws }
139
+ worksheet = _add_worksheet(name)
140
+ worksheets << worksheet
141
+ yield worksheet if block_given?
142
+ worksheet
129
143
  end
130
144
 
131
145
  # The same Worksheet objects add_worksheet returned, so their append
@@ -175,7 +189,7 @@ module FastXlsx
175
189
  # @param value [Numeric, String, Time, Date, DateTime, true, false, nil,
176
190
  # Formula, URL, RichString, #to_s] dates get yyyy-mm-dd (hh:mm:ss) unless
177
191
  # the format has a num_format
178
- # @param format [Format, nil]
192
+ # @param format [Format, Hash, nil] a Hash of options works too
179
193
  # @return [self]
180
194
  #
181
195
  # @!method <<(values)
@@ -226,6 +240,14 @@ module FastXlsx
226
240
  # @param color [String, Integer] "#RRGGBB" or 0xRRGGBB
227
241
  # @return [self]
228
242
  #
243
+ # @!method default_row_height(height)
244
+ # Height of rows not given one with {#row_height}. Call it before
245
+ # {#row_format}, {#hide_rows} and {#group_rows}: rows given those keep
246
+ # the earlier default.
247
+ # @param height [Numeric] points, above 0 and up to 409
248
+ # @return [self]
249
+ # @raise [FastXlsx::Error] when called after those
250
+ #
229
251
  # @!method hide_gridlines
230
252
  # Hides the gridlines on screen (see {#page_setup} for printing).
231
253
  # @return [self]
@@ -235,18 +257,28 @@ module FastXlsx
235
257
  # wrapper. Other forms come here.
236
258
  def _write_ref(*args)
237
259
  row, col, (value, format) = CellRange.cell(args, 1..2)
238
- _write(row, col, value, format)
260
+ _write(row, col, value, Format._coerce(format))
239
261
  self
240
262
  end
241
263
  private :_write_ref
242
264
 
265
+ # Writes one cell: ws["B2"] = 42 or ws[1, 1] = 42. Use {#write} to give
266
+ # it a format.
267
+ # @overload []=(ref, value)
268
+ # @overload []=(row, col, value)
269
+ def []=(*cell, value)
270
+ row, col = CellRange.cell(cell)
271
+ write(row, col, value)
272
+ value
273
+ end
274
+
243
275
  # Appends a row after the last row written.
244
276
  # @param values [Array] cell values, as for {#write}
245
- # @param format [Format, Array<Format, nil>, nil] one for every cell, or
277
+ # @param format [Format, Hash, Array<Format, Hash, nil>, nil] one for every cell, or
246
278
  # one per cell
247
279
  # @return [self]
248
280
  def append(values, format: nil)
249
- _append(values, format)
281
+ _append(values, format.is_a?(Array) ? format.map { |f| Format._coerce(f) } : Format._coerce(format))
250
282
  end
251
283
 
252
284
  # @param columns [Integer, Range<Integer>]
@@ -283,7 +315,7 @@ module FastXlsx
283
315
  # @raise [FastXlsx::Error] when it overlaps an earlier merge
284
316
  def merge_range(*args)
285
317
  range, (value, format) = CellRange.split(args, 1..2)
286
- _merge_range(*range, value, format)
318
+ _merge_range(*range, value, Format._coerce(format))
287
319
  end
288
320
 
289
321
  # Highlights cells in the range by rule; see the README for each type's
@@ -292,11 +324,12 @@ module FastXlsx
292
324
  # @param type [Symbol] :cell, :text, :formula, :data_bar or :color_scale
293
325
  # @option options [Symbol] :criteria e.g. :>, :between, :contains
294
326
  # @option options [Numeric, String, Array] :value
295
- # @option options [Format] :format
327
+ # @option options [Format, Hash] :format
296
328
  # @option options [Integer] :colors 2 or 3 (:color_scale)
297
329
  # @return [self]
298
- def conditional_format(*range, type:, **)
299
- _conditional_format(*CellRange.split(range).first, { type: type, ** })
330
+ def conditional_format(*range, type:, **options)
331
+ options[:format] = Format._coerce(options[:format]) if options.key?(:format)
332
+ _conditional_format(*CellRange.split(range).first, { type: type, **options })
300
333
  end
301
334
 
302
335
  # Restricts what can be entered in the range; see the README.
@@ -356,8 +389,13 @@ module FastXlsx
356
389
  # @option options [String] :name
357
390
  # @option options [Boolean] :total_row, :banded_rows, :autofilter
358
391
  # @return [self]
359
- def add_table(*range, **)
360
- _add_table(*CellRange.split(range).first, { ** })
392
+ def add_table(*range, **options)
393
+ if options[:columns].is_a?(Array)
394
+ options[:columns] = options[:columns].map do |column|
395
+ column.is_a?(Hash) && column.key?(:format) ? column.merge(format: Format._coerce(column[:format])) : column
396
+ end
397
+ end
398
+ _add_table(*CellRange.split(range).first, options)
361
399
  end
362
400
 
363
401
  # Printed page header, using Excel codes such as "&CPage &P of &N".
@@ -441,12 +479,77 @@ module FastXlsx
441
479
  _protect(password, Array(allow))
442
480
  end
443
481
 
482
+ # A range users can still edit on a protected sheet ({#protect}),
483
+ # optionally with its own password. Give each range its own name.
484
+ # @overload unprotect_range(*range, name: nil, password: nil)
485
+ # @param name [String, nil] shown in Excel's "Allow Edit Ranges"
486
+ # @return [self]
487
+ def unprotect_range(*range, name: nil, password: nil)
488
+ _unprotect_range(*CellRange.split(range).first, name, password)
489
+ end
490
+
491
+ # The cells selected when the file opens.
492
+ # @param range a cell range (see {Worksheet})
493
+ # @return [self]
494
+ def selection(*range)
495
+ _selection(*CellRange.split(range).first)
496
+ end
497
+
498
+ # The cell scrolled to the top left when the file opens.
499
+ # @param cell (row, col) or "B2"
500
+ # @return [self]
501
+ def top_left_cell(*cell)
502
+ _top_left_cell(*CellRange.cell(cell).first(2))
503
+ end
504
+
505
+ # Turns off one of Excel's warnings (green triangles) in the range, e.g.
506
+ # for codes stored as text. One kind per range (a rust_xlsxwriter limit);
507
+ # overlapping ranges are accepted, but Excel then uses only one of them.
508
+ # @overload ignore_error(*range, error)
509
+ # @param error [Symbol] :number_stored_as_text, :formula_error,
510
+ # :formula_differs, :formula_refers_to_empty_cells,
511
+ # :formula_omits_cells, :data_validation_error,
512
+ # :unlocked_cells_with_formula, :inconsistent_column_formula
513
+ # @return [self]
514
+ # @raise [FastXlsx::Error] when the range already has one
515
+ def ignore_error(*args)
516
+ range, (error, *) = CellRange.split(args, 1..1)
517
+ _ignore_error(*range, error)
518
+ end
519
+
520
+ # Each row in the range is kept (about 1 KB) until saving.
521
+ # @param rows [Integer, Range<Integer>]
522
+ # @return [self]
523
+ # @raise [FastXlsx::Error] in :constant / :low mode, for rows already on disk
524
+ def hide_rows(rows)
525
+ _hide_rows(*CellRange.bounds(rows))
526
+ end
527
+
528
+ # @param columns [Integer, Range<Integer>]
529
+ # @return [self]
530
+ def hide_columns(columns)
531
+ _hide_columns(*CellRange.bounds(columns))
532
+ end
533
+
534
+ # Default format for cells in these rows that are written without one.
535
+ # Use it or {#column_format} for a cell, not both: where both apply, Excel
536
+ # may combine them or show the row's. Each row is kept (about 1 KB) until
537
+ # saving, so style a whole sheet with {#column_format}.
538
+ # @param rows [Integer, Range<Integer>]
539
+ # @param format [Format, Hash]
540
+ # @return [self]
541
+ # @raise [FastXlsx::Error] in :constant / :low mode, for rows already on disk
542
+ def row_format(rows, format)
543
+ _row_format(*CellRange.bounds(rows), Format._coerce(format))
544
+ self
545
+ end
546
+
444
547
  # Default format for cells in these columns that are written without one.
445
548
  # @param columns [Integer, Range<Integer>]
446
- # @param format [Format]
549
+ # @param format [Format, Hash]
447
550
  # @return [self]
448
551
  def column_format(columns, format)
449
- _column_format(*CellRange.bounds(columns), format)
552
+ _column_format(*CellRange.bounds(columns), Format._coerce(format))
450
553
  self
451
554
  end
452
555
  end
@@ -560,9 +663,88 @@ module FastXlsx
560
663
  # border_color:, locked:, hidden:
561
664
  # @raise [ArgumentError] for an unknown option or an invalid value
562
665
  def self.new(**options)
666
+ options = _options(options)
563
667
  # Apply border: first so border_left: etc. override it whatever the order.
564
- options = { border: options[:border], **options.except(:border) } if options.key?(:border)
565
- _new(options)
668
+ ordered = options.key?(:border) ? { border: options[:border], **options.except(:border) } : options
669
+ format = _new(ordered)
670
+ format.instance_variable_set(:@options, options)
671
+ format.freeze
672
+ end
673
+
674
+ # Options with Symbol keys (String ones come from JSON or YAML) and frozen
675
+ # copies of the values, so the format can't change behind the file's back.
676
+ # @api private
677
+ def self._options(given)
678
+ options = given.to_h { |key, value| [key.to_sym, _frozen(value)] }.freeze
679
+ return options if options.size == given.size
680
+
681
+ twice = given.keys.map(&:to_sym).tally.select { |_, count| count > 1 }.keys
682
+ raise ArgumentError, "format option given twice (as a String and a Symbol): #{twice.join(", ")}"
683
+ end
684
+
685
+ # @api private
686
+ def self._frozen(value)
687
+ value.frozen? ? value : value.dup.freeze
688
+ end
689
+
690
+ # The options this format was created with.
691
+ # @return [Hash{Symbol => Object}] frozen
692
+ def to_h
693
+ @options
694
+ end
695
+
696
+ # Formats with the same options are equal; 10 and 10.0 count as the same.
697
+ # @return [Boolean]
698
+ def ==(other)
699
+ other.is_a?(Format) && to_h == other.to_h
700
+ end
701
+
702
+ # Equal as Hash keys: like Hash#eql?, 10 and 10.0 differ, matching {#hash}.
703
+ # @return [Boolean]
704
+ def eql?(other)
705
+ other.is_a?(Format) && to_h.eql?(other.to_h)
706
+ end
707
+
708
+ # @return [Integer]
709
+ def hash
710
+ [Format, to_h].hash
711
+ end
712
+
713
+ # A new format with these options added to (or replacing) this one's.
714
+ # @param other [Format, Hash, nil] options to add, like Hash#merge
715
+ # @example
716
+ # title = header.merge(font_size: 16)
717
+ # bold_money = bold.merge(money)
718
+ # @return [Format]
719
+ # @raise [TypeError] when other has no #to_h
720
+ def merge(other = nil, **)
721
+ unless other.respond_to?(:to_h)
722
+ raise TypeError,
723
+ "can't merge #{other.class} into a Format: use a Hash or a Format"
724
+ end
725
+
726
+ Format.new(**to_h, **other.to_h, **)
727
+ end
728
+
729
+ # Distinct Hashes kept by {._coerce}; past this, the oldest is dropped.
730
+ CACHE_SIZE = 1_024
731
+
732
+ # A Format for a Hash of options; equal Hashes share one, so a Hash written
733
+ # with every row costs about as much as a Format made once. Other values
734
+ # are returned as they are. An invalid Hash raises and isn't kept.
735
+ # @api private
736
+ def self._coerce(format)
737
+ return format unless format.is_a?(Hash)
738
+
739
+ # ponytail: evicts the oldest entry, not the least used one, since
740
+ # moving each hit costs every write; a Hash still in use is rebuilt at
741
+ # worst once per CACHE_SIZE new Hashes.
742
+ cache = (@cache ||= {})
743
+ cache.fetch(format) do
744
+ coerced = new(**format)
745
+ cache.shift if cache.size >= CACHE_SIZE
746
+ cache[format.dup.freeze] = coerced
747
+ end
566
748
  end
567
749
  end
568
750
  end
data/sig/fast_xlsx.rbs CHANGED
@@ -8,11 +8,14 @@ module FastXlsx
8
8
  def read: () -> String
9
9
  end
10
10
 
11
+ # A Format, or a Hash of its options.
12
+ type format = Format | Hash[Symbol | String, untyped]
13
+
11
14
  type cell = Numeric | String | Time | Date | Formula | URL | RichString | bool | nil | _ToS
12
15
 
13
16
  class RichString
14
17
  attr_reader segments: Array[[String, Format?]]
15
- def initialize: (*(_ToS | [_ToS, Format?]) parts) -> void
18
+ def initialize: (*(_ToS | [_ToS, format?]) parts) -> void
16
19
  end
17
20
 
18
21
  class Formula
@@ -28,8 +31,8 @@ module FastXlsx
28
31
 
29
32
  class Workbook
30
33
  MEMORY_MODES: Array[Symbol]
31
- def self.new: (?memory: :standard | :constant | :low) -> Workbook
32
- def add_worksheet: (?String? name) -> Worksheet
34
+ def self.new: (?memory: :standard | :constant | :low) ?{ (Workbook) -> void } -> Workbook
35
+ def add_worksheet: (?String? name) ?{ (Worksheet) -> void } -> Worksheet
33
36
  def worksheets: () -> Array[Worksheet]
34
37
  def worksheet: (String name) -> Worksheet?
35
38
  def properties: (?title: String, ?subject: String, ?author: String, ?manager: String,
@@ -53,16 +56,35 @@ module FastXlsx
53
56
  ?rotation: Integer, ?indent: Integer,
54
57
  ?border: border, ?border_left: border, ?border_right: border,
55
58
  ?border_top: border, ?border_bottom: border, ?border_color: color) -> Format
59
+ def to_h: () -> Hash[Symbol, untyped]
60
+ def merge: (?format? other, **untyped options) -> Format
61
+ def ==: (untyped other) -> bool
62
+ def eql?: (untyped other) -> bool
63
+ def hash: () -> Integer
56
64
  end
57
65
 
58
66
  class Worksheet
59
- def write: (Integer row, Integer col, cell value, ?Format? format) -> self
60
- | (String cell, cell value, ?Format? format) -> self
61
- def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
67
+ def []=: (String cell, cell value) -> cell
68
+ | (Integer row, Integer col, cell value) -> cell
69
+ def write: (Integer row, Integer col, cell value, ?format? format) -> self
70
+ | (String cell, cell value, ?format? format) -> self
71
+ def append: (Array[cell] values, ?format: format? | Array[format?]) -> self
62
72
  def <<: (Array[cell] row) -> self
63
73
  def concat: (Array[Array[cell]] rows) -> self
64
74
  def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
65
- def column_format: (Integer | Range[Integer] columns, Format format) -> self
75
+ def hide_rows: (Integer | Range[Integer] rows) -> self
76
+ def hide_columns: (Integer | Range[Integer] columns) -> self
77
+ def row_format: (Integer | Range[Integer] rows, format format) -> self
78
+ def default_row_height: (Numeric height) -> self
79
+ def selection: (*cell_range range) -> self
80
+ def top_left_cell: (Integer row, Integer col) -> self
81
+ | (String cell) -> self
82
+ type ignorable_error = :number_stored_as_text | :formula_error | :formula_differs | :formula_refers_to_empty_cells
83
+ | :formula_omits_cells | :data_validation_error | :unlocked_cells_with_formula
84
+ | :inconsistent_column_formula
85
+ def ignore_error: (*cell_range | ignorable_error args) -> self
86
+ def unprotect_range: (*cell_range range, ?name: String?, ?password: String?) -> self
87
+ def column_format: (Integer | Range[Integer] columns, format format) -> self
66
88
  def autofit: () -> self
67
89
  # A cell range: four 0-based numbers, an Excel reference ("A1:D10"), or
68
90
  # rows and columns as Integers or Ranges. Methods with options take it as
@@ -95,13 +117,13 @@ module FastXlsx
95
117
  def group_rows: (Integer | Range[Integer] rows, ?collapsed: bool) -> self
96
118
  def group_columns: (Integer | Range[Integer] columns, ?collapsed: bool) -> self
97
119
  def protect: (?password: String?, ?allow: protect_action | Array[protect_action] | nil) -> self
98
- def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
99
- | (String range, cell value, ?Format? format) -> self
100
- | (indexes rows, indexes cols, cell value, ?Format? format) -> self
120
+ def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?format? format) -> self
121
+ | (String range, cell value, ?format? format) -> self
122
+ | (indexes rows, indexes cols, cell value, ?format? format) -> self
101
123
  def conditional_format: (*cell_range range,
102
124
  type: :cell | :text | :formula | :data_bar | :color_scale,
103
125
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
104
- ?format: Format, ?colors: 2 | 3) -> self
126
+ ?format: format, ?colors: 2 | 3) -> self
105
127
  def data_validation: (*cell_range range,
106
128
  type: :list | :whole_number | :decimal | :text_length,
107
129
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
@@ -121,7 +143,7 @@ module FastXlsx
121
143
  | (String cell, type: Symbol, series: Array[chart_series],
122
144
  ?title: String, ?x_axis: String, ?y_axis: String,
123
145
  ?width: Integer, ?height: Integer) -> self
124
- type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
146
+ type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: format }
125
147
 
126
148
  def add_table: (*cell_range range,
127
149
  ?columns: Array[table_column], ?style: Symbol, ?name: String,
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.5.0
4
+ version: 0.7.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Zac