ironcalc 0.7.1.5 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e511f58c879d0fdac27a5c4b2d06f424d836bb2c995f4a9562d61bef955f7ea4
4
- data.tar.gz: 3761670586bb8ed3518e91d270391e51c1f3f9590a8e5aba5e100791de5446c0
3
+ metadata.gz: 99ad95c481d32e19c1ac19db71a37fac4b4c0239911d28909ba4e55aa1e3e28b
4
+ data.tar.gz: 3ee57b654e4633a521ef5e0f0e323437150d5a4c4663497c4a244b877f8e278d
5
5
  SHA512:
6
- metadata.gz: 6728ca159b613acd5dbcd604c286b674a9c2903040a78d2bfdbb51273d31be5e77f0942f8f20587299778586c06fc83cf2e7ae0c2f92f7d2e1b70e1ba5f2aeac
7
- data.tar.gz: 4e7b42714f19977be65194cd1af3ffadbf1ac370fc2572613f166c84229f29710b95d95baf0b3688a4387a6e519611e81816f0dd29feda48030ba603f2d18944
6
+ metadata.gz: ba91db3f951436ec7434c4f6439c216fbb6bd876dbb32ba1f30aa5dbd14d8be2503c955c98861b9a8cd9ecde2d98b744d60973ef7ce3fa180595a266a3dcbe5d
7
+ data.tar.gz: 13353e79cfac4675511ec1fd1bb1bb20912344ef5d1b8d395422b4012ae54d7f6daa4266c98251e3c2cf94c87be384535e0a0e26c303c6db99129cf22ef5b511
data/.yardopts CHANGED
@@ -3,6 +3,7 @@
3
3
  --readme README.md
4
4
  --output-dir doc
5
5
  --no-private
6
+ --hide-api private
6
7
  lib/**/*.rb
7
8
  -
8
9
  README.md
data/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ ## 0.8.3.0
2
+
3
+ - update the IronCalc engine to 0.8.3
4
+ - **breaking**: `fill` styles use a single `color` key; `fg_color` and
5
+ `bg_color` are gone
6
+ - **breaking**: the default font is now Inter 12, was Calibri 13
7
+ - **breaking**: `UserModel#update_range_style` takes the engine's area as a
8
+ Hash, `update_range_style({sheet:, row:, column:, width:, height:}, path,
9
+ value)`, and styles the whole range; it took a single cell before
10
+ - **breaking**: every native method has the engine's name and signature, with
11
+ `nil` for an optional parameter and Hashes for engine structs. Renamed:
12
+ - `Model`: `clear_cell_contents(sheet, row, column)` →
13
+ `range_clear_contents(range)`, `get_cell_content` →
14
+ `get_localized_cell_content`, `get_cell_style` → `get_style_for_cell`,
15
+ `set_frozen_rows_count` / `set_frozen_columns_count` → `set_frozen_rows` /
16
+ `set_frozen_columns`, `rename_sheet(sheet, name)` →
17
+ `rename_sheet_by_index`; `rename_sheet(old_name, new_name)` renames by name
18
+ - `UserModel`: `clear_cell_contents(sheet, row, column)` →
19
+ `range_clear_contents(range)`, `set_column_width(sheet, column, width)` →
20
+ `set_columns_width(sheet, column_start, column_end, width)`, and likewise
21
+ `set_rows_height`
22
+ - **breaking**: style Hashes have Symbol keys (`style[:font][:b]`); setters
23
+ accept Symbol or String keys, but no longer a JSON String
24
+ - **breaking**: `UserModel#set_cell_style` is removed; style a `UserModel` per
25
+ property with `update_range_style`. It could not write back a style read with
26
+ `get_cell_style`, and failed part-way through.
27
+ - `set_sheet_color` accepts nil to clear a sheet's color
28
+ - sheet colors read back as nil rather than absent when unset
29
+ - defined names on `Model` and `UserModel`: `defined_names`,
30
+ `new_defined_name`, `update_defined_name`, `delete_defined_name`,
31
+ `is_valid_defined_name` (raises with the reason) and `valid_defined_name?`
32
+ - typed cell setters on `Model`: `update_cell_with_text`, `_number`, `_bool`,
33
+ `_formula`
34
+ - typed cell readers on `Model`: `get_cell_value_by_index`,
35
+ `get_cell_value_by_ref`, `get_cell_formula`, `is_empty_cell`, `get_all_cells`
36
+ - row and column default styles on `Model`: `get`/`set`/`delete_column_style`,
37
+ `get`/`set`/`delete_row_style`, plus `copy_cell_style`
38
+ - `range_clear_all` on both APIs, `range_clear_formatting` on `UserModel`
39
+ - `insert_sheet` and `set_sheet_state` on `Model`
40
+ - `pause_evaluation` / `resume_evaluation` on `UserModel`, and `batch` to
41
+ recalculate once after a block
42
+ - hidden rows and columns: `is_column_hidden` / `is_row_hidden` on both APIs,
43
+ `set_column_hidden` / `set_row_hidden` on `Model`, `set_columns_hidden` /
44
+ `set_rows_hidden` on `UserModel`
45
+ - xlsx in memory: `to_xlsx_bytes` on both APIs, `load_from_xlsx_bytes` and
46
+ `create_user_model_from_xlsx_bytes`
47
+ - fix a memory leak: every constructor leaked its string arguments on each call
48
+ - package only README, CHANGELOG and licenses instead of every `*.md`
49
+
1
50
  ## 0.7.1.5
2
51
 
3
52
  - link to changelog and documentation in gemspec
data/Cargo.lock CHANGED
@@ -449,9 +449,9 @@ dependencies = [
449
449
 
450
450
  [[package]]
451
451
  name = "ironcalc"
452
- version = "0.7.1"
452
+ version = "0.8.3"
453
453
  source = "registry+https://github.com/rust-lang/crates.io-index"
454
- checksum = "8e07dd44af17a9e14e7df372347ee0bfc3f0bbbf38894aba5e9abfb53f32ba95"
454
+ checksum = "384c2a5b1e78741cedfaa4fd3209fdb115c1a52453bc10137dfe970ed92872be"
455
455
  dependencies = [
456
456
  "bitcode",
457
457
  "chrono",
@@ -466,9 +466,9 @@ dependencies = [
466
466
 
467
467
  [[package]]
468
468
  name = "ironcalc_base"
469
- version = "0.7.1"
469
+ version = "0.8.3"
470
470
  source = "registry+https://github.com/rust-lang/crates.io-index"
471
- checksum = "9f208abea2365061ed7ce3f6a70e7c1537181ea11780de4974aae3082bf4c6d9"
471
+ checksum = "ce1dd83cada7da742782076daccda92e33162e9f4396a958ae3e7c5bb64e6a01"
472
472
  dependencies = [
473
473
  "bitcode",
474
474
  "chrono",
@@ -477,20 +477,22 @@ dependencies = [
477
477
  "js-sys",
478
478
  "rand",
479
479
  "regex",
480
+ "regex-lite",
480
481
  "ryu",
481
482
  "serde",
482
483
  "statrs",
484
+ "wasm-bindgen",
483
485
  ]
484
486
 
485
487
  [[package]]
486
488
  name = "ironcalc_ruby"
487
- version = "0.7.1"
489
+ version = "0.0.0"
488
490
  dependencies = [
489
491
  "bitcode",
490
492
  "ironcalc",
491
493
  "magnus",
492
494
  "serde",
493
- "serde_json",
495
+ "serde_magnus",
494
496
  ]
495
497
 
496
498
  [[package]]
@@ -817,6 +819,12 @@ dependencies = [
817
819
  "regex-syntax",
818
820
  ]
819
821
 
822
+ [[package]]
823
+ name = "regex-lite"
824
+ version = "0.1.9"
825
+ source = "registry+https://github.com/rust-lang/crates.io-index"
826
+ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
827
+
820
828
  [[package]]
821
829
  name = "regex-syntax"
822
830
  version = "0.8.11"
@@ -896,6 +904,17 @@ dependencies = [
896
904
  "zmij",
897
905
  ]
898
906
 
907
+ [[package]]
908
+ name = "serde_magnus"
909
+ version = "0.11.0"
910
+ source = "registry+https://github.com/rust-lang/crates.io-index"
911
+ checksum = "8ff64c88ddd26acdcad5a501f18bcc339927b77b69f4a03bfaf2a6fc5ba2ac4b"
912
+ dependencies = [
913
+ "magnus",
914
+ "serde",
915
+ "tap",
916
+ ]
917
+
899
918
  [[package]]
900
919
  name = "sha1"
901
920
  version = "0.10.6"
@@ -981,6 +1000,12 @@ dependencies = [
981
1000
  "unicode-ident",
982
1001
  ]
983
1002
 
1003
+ [[package]]
1004
+ name = "tap"
1005
+ version = "1.0.1"
1006
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1007
+ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
1008
+
984
1009
  [[package]]
985
1010
  name = "thiserror"
986
1011
  version = "1.0.69"
data/README.md CHANGED
@@ -39,24 +39,166 @@ um.get_formatted_cell_value(0, 1, 1) # => "3"
39
39
  diffs = um.flush_send_queue # binary diff to send to peers
40
40
  ```
41
41
 
42
- Coordinates: `sheet` is a 0-based index; `row` and `column` are 1-based. Styles
43
- are exchanged as plain Ruby hashes via `get_cell_style` / `set_cell_style`.
42
+ Coordinates: `sheet` is a 0-based index; `row` and `column` are 1-based. Method
43
+ names and signatures are the engine's own. Engine structs, such as styles and
44
+ cell ranges, are plain Ruby Hashes keyed by their field names.
44
45
 
45
- ### Top-level methods
46
+ ### API overview
46
47
 
47
- `create`, `load_from_xlsx`, `load_from_icalc`, `load_from_bytes`,
48
- `create_user_model`, `create_user_model_from_xlsx`,
49
- `create_user_model_from_icalc`, `create_user_model_from_bytes`.
48
+ The [API reference](https://jvdp.github.io/IronCalc-Ruby/) documents every method; it shows the latest
49
+ release, and [older versions](https://jvdp.github.io/IronCalc-Ruby/versions.html) are kept too.
50
+
51
+ - [`IronCalc`](https://jvdp.github.io/IronCalc-Ruby/IronCalc.html) — module functions: the constructors (see
52
+ [Loading and saving](#loading-and-saving)) and utilities for cell references,
53
+ such as [`number_to_column`](https://jvdp.github.io/IronCalc-Ruby/IronCalc.html#number_to_column-class_method)
54
+ and [`parse_reference_a1`](https://jvdp.github.io/IronCalc-Ruby/IronCalc.html#parse_reference_a1-class_method).
55
+ - [`IronCalc::UserModel`](https://jvdp.github.io/IronCalc-Ruby/IronCalc/UserModel.html) — the recommended API.
56
+ It recalculates after every change, supports `undo` / `redo`, and records
57
+ diffs for collaboration
58
+ ([`flush_send_queue`](https://jvdp.github.io/IronCalc-Ruby/IronCalc/UserModel.html#flush_send_queue-instance_method)).
59
+ Styles are set per property with
60
+ [`update_range_style`](https://jvdp.github.io/IronCalc-Ruby/IronCalc/UserModel.html#update_range_style-instance_method).
61
+ - [`IronCalc::Model`](https://jvdp.github.io/IronCalc-Ruby/IronCalc/Model.html) — the raw API. You call
62
+ `evaluate` yourself; in return it offers typed cell access, whole-style
63
+ reads and writes, and row and column default styles.
64
+
65
+ ### Typed cell access
66
+
67
+ `set_user_input` parses what you give it, the way typing into a cell does — so
68
+ `"007"` becomes the number 7 and `"=1+1"` becomes a formula. The typed setters
69
+ skip that, and the typed reader gives you a Ruby object instead of a String:
70
+
71
+ ```ruby
72
+ model.update_cell_with_text(0, 1, 1, "007") # stays the string "007"
73
+ model.update_cell_with_number(0, 1, 2, 42.5)
74
+ model.update_cell_with_bool(0, 1, 3, true)
75
+ model.update_cell_with_formula(0, 1, 4, "=B1*2")
76
+ model.evaluate
77
+
78
+ model.get_cell_value_by_index(0, 1, 2) # => 42.5 (Float, not "42.5")
79
+ model.get_cell_value_by_ref("Sheet1!D1") # => 85.0
80
+ model.get_cell_formula(0, 1, 4) # => "=B1*2", or nil if not a formula
81
+ model.is_empty_cell(0, 9, 9) # => true
82
+ model.get_all_cells # => [{ index:, row:, column: }, ...]
83
+ ```
84
+
85
+ `get_all_cells` lists every cell the workbook has a record for (`index` is the
86
+ sheet index, the engine's field name). Clearing a
87
+ cell's contents keeps its record, so filter with `is_empty_cell` if you want
88
+ only cells that still hold a value.
89
+
90
+ Clearing works on a range, given as the engine's `Area`:
91
+
92
+ ```ruby
93
+ b2_c10 = {sheet: 0, row: 2, column: 2, width: 2, height: 9}
94
+ model.range_clear_contents(b2_c10) # keeps the style
95
+ model.range_clear_all(b2_c10) # contents and style
96
+ ```
97
+
98
+ On `UserModel`, `range_clear_formatting` does the opposite — style only.
99
+
100
+ ### Rows, columns and sheets
101
+
102
+ Rows and columns can carry a default style. As with `set_cell_style`, a style is
103
+ a complete structure, so read one, change a field, and pass it back:
104
+
105
+ ```ruby
106
+ style = model.get_style_for_cell(0, 1, 1)
107
+ style[:num_fmt] = "0.00"
108
+ model.set_column_style(0, 2, style)
109
+ model.get_column_style(0, 2) # => Hash, or nil if the column has no style
110
+ model.delete_column_style(0, 2)
111
+
112
+ model.copy_cell_style([0, 1, 1], [0, 5, 5]) # style only, works across sheets
113
+
114
+ model.insert_sheet("Summary", 0, nil) # position, rather than appending
115
+ model.set_sheet_state(1, "hidden") # or "visible" / "veryHidden"
116
+ ```
117
+
118
+ ### Bulk loading
119
+
120
+ `UserModel` recalculates after every action. When writing many cells, suspend
121
+ that and evaluate once:
122
+
123
+ ```ruby
124
+ um.batch do |m|
125
+ rows.each { |r, c, v| m.set_user_input(0, r, c, v) }
126
+ end # recalculated once, here
127
+ ```
128
+
129
+ `batch` nests, and recalculates even if the block raises. The underlying
130
+ `pause_evaluation` / `resume_evaluation` pair is still available if you need to
131
+ manage it yourself.
132
+
133
+ ### Defined names
134
+
135
+ Named ranges, on both APIs. `scope` is a 0-based sheet index, or `nil` for a
136
+ workbook-scoped name. They survive both an xlsx round trip and
137
+ editing in Excel/LibreOffice, so they are also a place to keep your own
138
+ metadata about a sheet.
139
+
140
+ ```ruby
141
+ model.new_defined_name("answer", nil, "Sheet1!$C$1")
142
+ model.set_user_input(0, 1, 1, "=answer")
143
+ model.evaluate
144
+ model.get_formatted_cell_value(0, 1, 1) # => the value of C1
145
+
146
+ model.defined_names
147
+ # => [{ name: "answer", scope: nil, formula: "Sheet1!$C$1" }]
148
+
149
+ model.update_defined_name("answer", nil, "result", nil, "Sheet1!$D$1")
150
+ model.delete_defined_name("result", nil)
151
+ ```
152
+
153
+ `valid_defined_name?(name, scope, formula)` reports whether
154
+ `new_defined_name` would succeed; the name must be an identifier (`"A1"` is
155
+ rejected — it parses as a cell reference), unused in that scope, and the formula
156
+ must parse. `is_valid_defined_name` runs the same check but raises
157
+ `IronCalc::Error` with the reason. On `UserModel` all three mutations are
158
+ undoable.
159
+
160
+ ### Loading and saving
161
+
162
+ | | `Model` | `UserModel` | save with |
163
+ |---|---|---|---|
164
+ | new workbook | `create` | `create_user_model` | |
165
+ | xlsx file | `load_from_xlsx` | `create_user_model_from_xlsx` | `save_to_xlsx` |
166
+ | xlsx in memory | `load_from_xlsx_bytes` | `create_user_model_from_xlsx_bytes` | `to_xlsx_bytes` |
167
+ | icalc file | `load_from_icalc` | `create_user_model_from_icalc` | `save_to_icalc` |
168
+ | icalc in memory | `load_from_bytes` | `create_user_model_from_bytes` | `to_bytes` |
169
+
170
+ icalc is IronCalc's own compact format; use xlsx for anything another
171
+ spreadsheet application should open. The constructors take
172
+ `(name_or_path, locale, tz, language_id)`, except that the bytes variants take
173
+ the bytes first, and the icalc ones need only the language.
174
+
175
+ ### Utilities
176
+
177
+ ```ruby
178
+ IronCalc.number_to_column(28) # => "AB"
179
+ IronCalc.column_to_number("AB") # => 28
180
+ IronCalc.parse_reference_a1("$B3") # => {row: 3, column: 2, absolute_row: false, absolute_column: true}
181
+ IronCalc.quote_name("My Sheet") # => "'My Sheet'"
182
+ ```
183
+
184
+ Also `parse_reference_r1c1`, the `is_valid_*` checks, `get_supported_locales`
185
+ and `get_all_timezones`; see the [`IronCalc` module](https://jvdp.github.io/IronCalc-Ruby/IronCalc.html).
50
186
 
51
187
  Errors raised by the engine surface as `IronCalc::Error`.
52
188
 
53
189
  ## Relationship to the Python bindings
54
190
 
55
191
  `ironcalc-ruby` is a thin binding over the same Rust engine as the
56
- [IronCalc Python bindings](https://github.com/ironcalc/ironcalc/tree/main/bindings/python),
57
- and deliberately mirrors their API. Both are compiled native extensions (Python
58
- via **pyo3**, Ruby via **magnus** / **rb-sys**) exposing a module named
59
- `ironcalc` backed by the IronCalc engine.
192
+ [IronCalc Python bindings](https://github.com/ironcalc/ironcalc/tree/main/bindings/python).
193
+ Both are compiled native extensions (Python via **pyo3**, Ruby via **magnus** /
194
+ **rb-sys**) exposing a module named `ironcalc` backed by the IronCalc engine.
195
+
196
+ Both follow the shape of the engine, so the two APIs line up closely and most
197
+ Python examples translate directly. Where Python renamed an engine method, Ruby
198
+ keeps the engine's name: Python's `clear_cell_contents(sheet, row, column)` is
199
+ `range_clear_contents(range)` here, and `Model.get_cell_style` is
200
+ `get_style_for_cell`. They do not cover the same amount of it —
201
+ see [Coverage compared to the Python bindings](#coverage-compared-to-the-python-bindings).
60
202
 
61
203
  ### What's identical
62
204
 
@@ -67,10 +209,10 @@ via **pyo3**, Ruby via **magnus** / **rb-sys**) exposing a module named
67
209
  `load_from_icalc`, `load_from_bytes`, `create_user_model`,
68
210
  `create_user_model_from_xlsx`, `create_user_model_from_icalc`,
69
211
  `create_user_model_from_bytes`.
70
- - **Method names and signatures** on `Model` / `UserModel` — `set_user_input`,
71
- `get_formatted_cell_value`, `evaluate`, `insert_rows`, `set_column_width`,
72
- `new_sheet`, `save_to_xlsx`, `to_bytes`, … (Python's `snake_case` is also
73
- Ruby's convention, so they match exactly).
212
+ - **Method names and signatures** for everything both take from the engine
213
+ unchanged — `set_user_input`, `get_formatted_cell_value`, `evaluate`,
214
+ `insert_rows`, `set_column_width`, `new_sheet`, `save_to_xlsx`, `to_bytes`, …
215
+ (Python's `snake_case` is also Ruby's convention).
74
216
  - **Coordinates**: `sheet` is a 0-based index; `row` and `column` are 1-based.
75
217
  - **Semantics**: the same engine, so the same inputs produce the same results.
76
218
 
@@ -101,16 +243,56 @@ ports:
101
243
  |---|---|---|
102
244
  | Module access | `ic.create(...)` | `IronCalc.create(...)` |
103
245
  | Error type | `WorkbookError` | `IronCalc::Error` |
104
- | Cell style | typed `Style` / `Font` / … objects | plain `Hash` (`get_cell_style` / `set_cell_style`) |
246
+ | Cell style | typed `Style` / `Font` / … objects | `Hash` with Symbol keys (`get_style_for_cell` / `set_cell_style`) |
105
247
  | Cell type | `CellType` enum | `Symbol` (`:number`, `:text`, …) |
106
248
  | Worksheet properties | list of `SheetProperty` objects | array of `Hash`es (`:name`, `:state`, `:sheet_id`, `:color`) |
107
249
  | Sheet dimensions | tuple `(min_row, max_row, min_col, max_col)` | 4-element `Array` |
250
+ | Engine structs (`Area`, `CellIndex`) | spread into separate arguments | `Hash` keyed by the struct's field names |
251
+ | Engine tuples | tuple | `Array` |
108
252
  | Binary data | `bytes` | binary `String` |
109
253
  | Version | `ironcalc.__version__` | `IronCalc::VERSION` |
110
254
 
111
255
  Rather than reconstruct Python's per-field style classes, Ruby exchanges styles
112
- as plain hashes (serialized as JSON across the boundary). Everything else is kept
113
- as close to the Python bindings as the two languages allow.
256
+ as plain Hashes keyed by the engine's field names. A method that shares a
257
+ name with an engine method takes the engine's parameters in the engine's order,
258
+ with `nil` for an `Option`; conveniences get names of their own. So a range is
259
+ one argument, `update_range_style({sheet: 0, row: 2, column: 2, width: 2,
260
+ height: 9}, "font.b", "true")`, where Python spreads it into start and end
261
+ corners.
262
+
263
+ ### Coverage compared to the Python bindings
264
+
265
+ Both bindings wrap the same engine, so anything both expose behaves the same.
266
+ They do not expose the same amount of it. Counted against **IronCalc 0.8.3**,
267
+ the version this gem tracks, and accurate at the time of writing:
268
+
269
+ | | Python | Ruby |
270
+ |---|---|---|
271
+ | `Model` | 69 | 62 |
272
+ | `UserModel` | 102 | 52 |
273
+
274
+ Most of the difference is deliberate. Selection, scrolling, clipboard and
275
+ keyboard state belong to a UI rather than to a library, and are not exposed
276
+ here. Conditional formatting, named styles and themes are a known backlog,
277
+ listed explicitly in `rake parity`'s `ENGINE_DEFERRED` so they stay visible.
278
+
279
+ What Ruby adds is mostly layering rather than extra engine coverage: Hash-based
280
+ styles, `batch`, and xlsx in memory (`to_xlsx_bytes` / `load_from_xlsx_bytes`).
281
+
282
+ These numbers move with every engine release; `rake parity` reports the
283
+ current picture.
284
+
285
+ ## AI-assisted development
286
+
287
+ These bindings are written and maintained with substantial help from
288
+ [Claude Code](https://claude.com/claude-code), Anthropic's coding agent. A
289
+ human maintainer directs the work, reviews every change and makes the releases.
290
+
291
+ Correctness doesn't rest on review alone. The test suite, run in CI on every
292
+ push, re-runs the engine's own formula regression fixtures through the binding.
293
+ `rake parity` checks the API surface and method signatures against the engine
294
+ and the Python bindings. The conventions agents follow here are in
295
+ [AGENTS.md](https://github.com/jvdp/IronCalc-Ruby/blob/main/AGENTS.md).
114
296
 
115
297
  ## License
116
298
 
@@ -10,8 +10,8 @@ crate-type = ["cdylib"]
10
10
  [dependencies]
11
11
  magnus = "0.8"
12
12
  serde = { version = "1.0", features = ["derive"] }
13
- serde_json = "1.0"
13
+ serde_magnus = "0.11"
14
14
  bitcode = "0.6.8"
15
15
  # The IronCalc engine, published on crates.io as `ironcalc`. Renamed to `xlsx`
16
16
  # here so the source can refer to `xlsx::base`, `xlsx::import`, `xlsx::export`.
17
- xlsx = { package = "ironcalc", version = "0.7.1" }
17
+ xlsx = { package = "ironcalc", version = "0.8.3" }