fast_xlsx 0.1.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.
data/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # FastXlsx
2
+
3
+ [![codecov](https://codecov.io/gh/7a6163/fast_xlsx/graph/badge.svg)](https://codecov.io/gh/7a6163/fast_xlsx)
4
+
5
+ Fast `.xlsx` writer for Ruby, built on [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) via [magnus](https://github.com/matsadler/magnus).
6
+
7
+ > **Status: early.** The API may still change before 1.0.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ require "fast_xlsx"
13
+
14
+ wb = FastXlsx::Workbook.new # or memory: :constant / :low, see below
15
+ ws = wb.add_worksheet("Report") # later: wb.worksheet("Report"), wb.worksheets
16
+
17
+ ws << ["id", "name", "created_at"] # append a row
18
+ ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
19
+ ws.write(0, 5, 42) # write a single cell (row, col, value)
20
+
21
+ wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
22
+ wb.save("report.xlsx") # or wb.to_xlsx => binary String
23
+ ```
24
+
25
+ ### Memory modes
26
+
27
+ 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:
28
+
29
+ ```ruby
30
+ FastXlsx::Workbook.new # memory: :standard (default): everything in memory, any write order
31
+ FastXlsx::Workbook.new(memory: :constant) # rows on disk, strings stored inline in each cell
32
+ FastXlsx::Workbook.new(memory: :low) # rows on disk, strings in Excel's shared string table
33
+ ```
34
+
35
+ | | `:standard` (default) | `:constant` | `:low` |
36
+ |---|---|---|---|
37
+ | Finished rows | kept in memory | written to disk | written to disk |
38
+ | Memory grows with | all cells | nothing (flat) | the number of unique strings |
39
+ | Write order | any | top to bottom only | top to bottom only |
40
+ | `autofit` | full | only sees rows still in memory | only sees rows still in memory |
41
+ | Strings | shared string table | inline in each cell | shared string table |
42
+ | Output | standard | some readers (e.g. xsv) don't support inline strings | standard |
43
+
44
+ Which one:
45
+
46
+ - **`:standard`** for normal reports, when you need to go back and change earlier rows, or rely on `autofit`.
47
+ - **`:constant`** for large exports written row by row: memory stays flat whatever the data, and it is the fastest mode.
48
+ - **`:low`** for large exports that other programs will read: memory stays low when strings repeat (regions, statuses, …) and the file uses the standard shared string table. With many unique strings it keeps those strings in memory until `save`.
49
+
50
+ In both disk-backed modes, writing to a row that was already written to disk raises `FastXlsx::Error`, and tables must be added before their data (see [Tables](#tables)). An unknown mode raises `ArgumentError`.
51
+
52
+ ### Formats
53
+
54
+ ```ruby
55
+ header = FastXlsx::Format.new(bold: true, bg_color: "#DDEBF7", border_bottom: :thin, align: :center)
56
+ date = FastXlsx::Format.new(num_format: "yyyy-mm-dd")
57
+
58
+ ws.append(["id", "name", "created_at"], format: header) # format every cell in the row
59
+ ws.append([1, "a", Time.now], format: [nil, nil, date]) # or one format (or nil) per cell
60
+ ws.write(1, 2, Date.today, date) # format one cell
61
+ ```
62
+
63
+ | Option | Values |
64
+ |---|---|
65
+ | `bold`, `italic`, `strikeout`, `text_wrap`, `shrink` | `true` / `false` |
66
+ | `underline` | `true` (single), `:single`, `:double`, `:single_accounting`, `:double_accounting` |
67
+ | `font_script` | `:superscript`, `:subscript` |
68
+ | `rotation` | degrees, `-90..90`, or `270` for stacked text |
69
+ | `indent` | indent level, e.g. `2` |
70
+ | `font_size` | number, e.g. `14` |
71
+ | `font_name` | e.g. `"Arial"` |
72
+ | `font_color`, `bg_color` | `"#RRGGBB"` or `0xRRGGBB` |
73
+ | `num_format` | Excel number format, e.g. `"#,##0.00"`, `"yyyy-mm-dd"` |
74
+ | `align` | `:left`, `:center`, `:right` |
75
+ | `valign` | `:top`, `:center`, `:bottom` |
76
+ | `border`, `border_left`, `border_right`, `border_top`, `border_bottom` | `:thin`, `:medium`, `:thick`, `:dashed`, `:dotted`, `:double`, `:hair` |
77
+ | `border_color` | `"#RRGGBB"` or `0xRRGGBB` |
78
+
79
+ Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
80
+
81
+ ### Columns and filters
82
+
83
+ ```ruby
84
+ ws.column_width(0, 20) # column A, width in characters
85
+ ws.column_width(1..3, 12) # columns B–D
86
+ ws.column_format(4, FastXlsx::Format.new(num_format: "#,##0.00")) # default for cells in E written without a format
87
+ ws.autofit # size other columns to the data written so far; set widths are kept
88
+ ws.autofilter(0, 0, 100, 3) # filter buttons on A1:D101 (first_row, first_col, last_row, last_col)
89
+ ```
90
+
91
+ `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
+
93
+ ### Layout
94
+
95
+ ```ruby
96
+ ws.freeze_panes(1, 0) # keep the first row visible while scrolling
97
+ ws.row_height(0, 30) # row 1, height in points
98
+ ws.merge_range(0, 0, 0, 3, "Q3 report", title) # merge A1:D1; the value can be any cell type
99
+ ws.page_breaks([50, 100]) # print a new page before rows 51 and 101
100
+ ws.vertical_page_breaks([8]) # and before column I
101
+ ws.page_header("&CPage &P of &N") # printed header, Excel header/footer codes
102
+ ws.page_footer("&L&A", margin: 0.2) # sheet name on the left; margin in inches
103
+ ws.margins(left: 0.5, top: 1) # other margins keep Excel's defaults
104
+ ```
105
+
106
+ ### Conditional formats
107
+
108
+ ```ruby
109
+ red = FastXlsx::Format.new(font_color: "#9C0006", bg_color: "#FFC7CE")
110
+
111
+ # rows 1–100 of column B (first_row, first_col, last_row, last_col)
112
+ ws.conditional_format(0, 1, 99, 1, type: :cell, criteria: :<, value: 0, format: red)
113
+ ws.conditional_format(0, 1, 99, 1, type: :cell, criteria: :between, value: [1, 10], format: red)
114
+ ws.conditional_format(0, 0, 99, 0, type: :text, criteria: :contains, value: "error", format: red)
115
+ ws.conditional_format(0, 0, 99, 3, type: :formula, value: "=$D1>100", format: red)
116
+ ws.conditional_format(0, 2, 99, 2, type: :data_bar)
117
+ ws.conditional_format(0, 2, 99, 2, type: :color_scale) # 3-color; colors: 2 for 2-color
118
+ ```
119
+
120
+ | `type` | `criteria` | `value` |
121
+ |---|---|---|
122
+ | `:cell` | `:==`, `:!=`, `:>`, `:>=`, `:<`, `:<=`, `:between`, `:not_between` | number or string; `[min, max]` for the range criteria |
123
+ | `:text` | `:contains`, `:not_contains`, `:begins_with`, `:ends_with` | string |
124
+ | `:formula` | — | formula string, relative to the top-left cell |
125
+ | `:data_bar`, `:color_scale` | — | — |
126
+
127
+ ### Data validation
128
+
129
+ ```ruby
130
+ ws.data_validation(1, 2, 100, 2, type: :list, value: %w[Open Closed]) # dropdown in C2:C101
131
+ ws.data_validation(1, 2, 100, 2, type: :list, value: "=$Z$1:$Z$10") # dropdown from a range
132
+ ws.data_validation(1, 3, 100, 3, type: :whole_number, criteria: :between, value: [1, 10],
133
+ input_title: "Quantity", input_message: "1 to 10",
134
+ error_title: "Invalid", error_message: "Enter a whole number from 1 to 10")
135
+ ```
136
+
137
+ `type` is `:list`, `:whole_number`, `:decimal` or `:text_length`; the number types take the same `criteria` as `:cell` conditional formats.
138
+
139
+ ### Comments
140
+
141
+ ```ruby
142
+ ws.write_comment(0, 0, "Checked by finance", author: "Zac") # Excel shows it as a note on A1
143
+ ```
144
+
145
+ ### Images
146
+
147
+ ```ruby
148
+ ws.insert_image(0, 0, "logo.png") # top-left corner in A1
149
+ ws.insert_image(0, 5, StringIO.new(blob.download), scale: 0.5, x_offset: 10, y_offset: 4, alt_text: "Logo")
150
+ ws.insert_image(10, 0, "chart.png", width: 320, height: 180) # pixel size; one of them keeps the aspect ratio
151
+ ```
152
+
153
+ A String is always treated as a path, so wrap raw bytes (such as Active Storage's `blob.download`) in a `StringIO`.
154
+
155
+ The source is a file path or any IO responding to `#read` (PNG, JPEG, GIF or BMP). Offsets are in pixels. Data that is not a supported image raises `FastXlsx::Error`.
156
+
157
+ ### Tables
158
+
159
+ ```ruby
160
+ ws.concat([%w[Region Rep Sales], *sales]) # header row, then the data
161
+ ws.add_table(0, 0, sales.size + 1, 2, total_row: true, style: :medium2, # +1 row for the totals
162
+ columns: [{ header: "Region", total_label: "Total" }, "Rep", { header: "Sales", total: :sum }])
163
+ ```
164
+
165
+ The range includes the header row and, with `total_row: true`, the total row; the table writes the headers. `columns` must match the range width. Options: `style` (`:light1`–`:light21`, `:medium1`–`:medium28`, `:dark1`–`:dark11`, `:none`), `name`, `total_row`, `banded_rows`, `autofilter`. Column totals: `:sum`, `:average`, `:count`, `:count_numbers`, `:max`, `:min`, `:std_dev`, `:var`.
166
+
167
+ You can also add the table first and then append the data: after `add_table`, `<<` / `append` / `concat` continue right under the header row. In `:constant` / `:low` memory mode this is the only order that works; adding a table whose header row was already written to disk raises `FastXlsx::Error`.
168
+
169
+ ### Charts
170
+
171
+ ```ruby
172
+ ws.concat([%w[Month Sales Costs], ["Jan", 10, 7], ["Feb", 25, 12], ["Mar", 18, 11]])
173
+
174
+ ws.insert_chart(1, 4, type: :column,
175
+ series: [
176
+ { name: "Sales", categories: "Sheet1!$A$2:$A$4", values: "Sheet1!$B$2:$B$4" },
177
+ { name: "Costs", categories: "Sheet1!$A$2:$A$4", values: "Sheet1!$C$2:$C$4" }
178
+ ],
179
+ title: "Q1", x_axis: "Month", y_axis: "Amount", width: 600, height: 360)
180
+ ```
181
+
182
+ `type`: `:column`, `:column_stacked`, `:bar`, `:bar_stacked`, `:line`, `:line_stacked`, `:area`, `:area_stacked`, `:pie`, `:doughnut`, `:radar`, `:scatter`. Ranges use Excel syntax, so a chart can plot data from another worksheet. Sizes are in pixels (default 480 × 288).
183
+
184
+ Values are mapped by type:
185
+
186
+ | Ruby | Excel |
187
+ |---|---|
188
+ | `Integer`, `Float`, any `Numeric` | number |
189
+ | `String` | string |
190
+ | `Time` | number (Excel serial date, local time) |
191
+ | `Date`, `DateTime` | number (Excel serial date, own offset) |
192
+ | `FastXlsx::Formula.new("SUM(A1:A9)")` | formula |
193
+ | `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
194
+ | `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
195
+ | `true` / `false` | boolean |
196
+ | `nil` | empty cell |
197
+ | anything else | `to_s` as string |
198
+
199
+ `<<` 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.
200
+
201
+ Errors from the writer (invalid sheet names, writes to rows already on disk, …) raise `FastXlsx::Error`.
202
+
203
+ ## Performance
204
+
205
+ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; xlsxtream is a streaming writer with fewer features.
206
+
207
+ ### Speed
208
+
209
+ 20,000 rows × 5 columns (integer, string, integer, `Time`, float), build + serialize to a String, median of 7 runs (3 for rubyXL):
210
+
211
+ | Library | Time | vs fastest | Ruby objects allocated |
212
+ |---|---:|---:|---:|
213
+ | **fast_xlsx** (`memory: :constant`) | **92 ms** | 1.0x | 7 |
214
+ | **fast_xlsx** (`memory: :low`) | **101 ms** | 1.1x | 7 |
215
+ | **fast_xlsx** | **102 ms** | 1.1x | 10 |
216
+ | [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 181 ms | 2.0x | 561,728 |
217
+ | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 202 ms | 2.2x | 20,079 |
218
+ | [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 240 ms | 2.6x | 320,076 |
219
+ | [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 610 ms | 6.7x | 1,483,899 |
220
+ | [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 678 ms | 7.4x | 745,122 |
221
+ | [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2624 ms | 28.6x | 8,700,448 |
222
+
223
+ All outputs are 702–750 KB.
224
+
225
+ ### Memory
226
+
227
+ 200,000 rows × 5 columns saved to a file; extra peak RSS over a process that only builds the data, median of 3 runs. "Unique" gives every row a different 100-character string; "repeated" uses a handful of values (regions, statuses), as most reports do:
228
+
229
+ | Library | Unique strings | Repeated strings |
230
+ |---|---:|---:|
231
+ | **fast_xlsx** (`memory: :constant`) | **+2 MB** | **+2 MB** |
232
+ | **fast_xlsx** (`memory: :low`) | +62 MB | **+2 MB** |
233
+ | **fast_xlsx** | +270 MB | +217 MB |
234
+ | fast_excel 0.5 (constant_memory) | +10 MB | +10 MB |
235
+ | fast_excel 0.5 | +183 MB | +151 MB |
236
+
237
+ 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
+
239
+ ### Reproduce
240
+
241
+ ```bash
242
+ bundle exec rake compile
243
+ BUNDLE_GEMFILE=bench/Gemfile bundle install
244
+ BUNDLE_GEMFILE=bench/Gemfile bundle exec ruby bench/compare.rb # speed; optional row count argument
245
+
246
+ # memory: peak RSS of one run (use /usr/bin/time -v on Linux); subtract the baseline
247
+ BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb baseline unique
248
+ BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb fast_xlsx:low unique
249
+ ```
250
+
251
+ ## Installation
252
+
253
+ ```bash
254
+ bundle add fast_xlsx
255
+ ```
256
+
257
+ Precompiled gems are built for common platforms; other platforms need a Rust toolchain to install. Requires CRuby 3.3+; JRuby and TruffleRuby are not supported because this is a native extension.
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ bin/setup
263
+ bundle exec rake compile # build the Rust extension into lib/fast_xlsx/
264
+ bundle exec rake test
265
+ bundle exec rake # compile + test + rubocop
266
+ ```
267
+
268
+ Coverage of the Rust extension while the Ruby tests run (needs [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) and `rustup component add llvm-tools-preview`):
269
+
270
+ ```bash
271
+ rm -rf tmp lib/fast_xlsx/fast_xlsx.bundle # force an instrumented rebuild
272
+ eval "$(cargo llvm-cov show-env --export-prefix)"
273
+ bundle exec rake compile test
274
+ cargo llvm-cov report --release # or --lcov / --html
275
+ ```
276
+
277
+ Rebuild in a clean shell afterwards (`rm -rf tmp && bundle exec rake compile`) so the everyday build is not instrumented.
278
+
279
+ ### Releasing
280
+
281
+ First generate the showcase workbook and open it in Excel to check every feature renders (the test suite only inspects the XML):
282
+
283
+ ```bash
284
+ bundle exec rake compile
285
+ ruby -Ilib examples/showcase.rb showcase.xlsx
286
+ ```
287
+
288
+ Then bump `FastXlsx::VERSION`, update `CHANGELOG.md`, commit, and push a matching tag:
289
+
290
+ ```bash
291
+ git tag v0.1.0 && git push origin v0.1.0
292
+ ```
293
+
294
+ The `Build gems` workflow builds the source gem plus precompiled gems for each platform and pushes them to RubyGems (trusted publishing) and GitHub Packages. It refuses to publish if the tag and `VERSION` differ.
295
+
296
+ ## License
297
+
298
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ require "rb_sys/extensiontask"
13
+
14
+ task build: :compile
15
+
16
+ GEMSPEC = Gem::Specification.load("fast_xlsx.gemspec")
17
+
18
+ RbSys::ExtensionTask.new("fast_xlsx", GEMSPEC) do |ext|
19
+ ext.lib_dir = "lib/fast_xlsx"
20
+ end
21
+
22
+ task default: %i[compile test rubocop]
data/bench/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Libraries compared in bench/compare.rb. Kept out of the main Gemfile so CI
4
+ # does not install them. Usage: BUNDLE_GEMFILE=bench/Gemfile bundle exec ruby bench/compare.rb
5
+
6
+ source "https://rubygems.org"
7
+
8
+ gem "fast_xlsx", path: ".."
9
+
10
+ gem "caxlsx"
11
+ gem "fast_excel"
12
+ gem "rubyXL"
13
+ gem "write_xlsx"
14
+ gem "xlsxtream"
@@ -0,0 +1,110 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ fast_xlsx (0.1.0)
5
+ rb_sys (~> 0.9.130)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ caxlsx (4.5.0)
11
+ htmlentities (~> 4.3, >= 4.3.4)
12
+ marcel (~> 1.0)
13
+ nokogiri (~> 1.10, >= 1.10.4)
14
+ rubyzip (>= 2.4, < 4)
15
+ fast_excel (0.5.0)
16
+ ffi (> 1.9, < 2)
17
+ ffi (1.17.4-aarch64-linux-gnu)
18
+ ffi (1.17.4-aarch64-linux-musl)
19
+ ffi (1.17.4-arm-linux-gnu)
20
+ ffi (1.17.4-arm-linux-musl)
21
+ ffi (1.17.4-arm64-darwin)
22
+ ffi (1.17.4-x86_64-darwin)
23
+ ffi (1.17.4-x86_64-linux-gnu)
24
+ ffi (1.17.4-x86_64-linux-musl)
25
+ htmlentities (4.4.2)
26
+ marcel (1.2.1)
27
+ nkf (0.3.0)
28
+ nokogiri (1.19.4-aarch64-linux-gnu)
29
+ racc (~> 1.4)
30
+ nokogiri (1.19.4-aarch64-linux-musl)
31
+ racc (~> 1.4)
32
+ nokogiri (1.19.4-arm-linux-gnu)
33
+ racc (~> 1.4)
34
+ nokogiri (1.19.4-arm-linux-musl)
35
+ racc (~> 1.4)
36
+ nokogiri (1.19.4-arm64-darwin)
37
+ racc (~> 1.4)
38
+ nokogiri (1.19.4-x86_64-darwin)
39
+ racc (~> 1.4)
40
+ nokogiri (1.19.4-x86_64-linux-gnu)
41
+ racc (~> 1.4)
42
+ nokogiri (1.19.4-x86_64-linux-musl)
43
+ racc (~> 1.4)
44
+ racc (1.8.1)
45
+ rake-compiler-dock (1.12.0)
46
+ rb_sys (0.9.130)
47
+ rake-compiler-dock (= 1.12.0)
48
+ rubyXL (3.4.38)
49
+ nokogiri (>= 1.10.8)
50
+ rubyzip (>= 3.2.2)
51
+ rubyzip (3.7.0)
52
+ write_xlsx (1.15.1)
53
+ nkf
54
+ rubyzip (>= 2.4.0, < 4.0)
55
+ xlsxtream (3.1.0)
56
+ zip_kit (>= 6.2, < 7)
57
+ zip_kit (6.3.4)
58
+
59
+ PLATFORMS
60
+ aarch64-linux-gnu
61
+ aarch64-linux-musl
62
+ arm-linux-gnu
63
+ arm-linux-musl
64
+ arm64-darwin
65
+ x86_64-darwin
66
+ x86_64-linux-gnu
67
+ x86_64-linux-musl
68
+
69
+ DEPENDENCIES
70
+ caxlsx
71
+ fast_excel
72
+ fast_xlsx!
73
+ rubyXL
74
+ write_xlsx
75
+ xlsxtream
76
+
77
+ CHECKSUMS
78
+ caxlsx (4.5.0) sha256=e3d98d859f148df05d5462086b5079b523f29c1766b569535f1d68629ce743ff
79
+ fast_excel (0.5.0) sha256=59c418bdcf586a6030798d4e3a9f575742badaceaf7bb90f38eee5feb5bdc478
80
+ fast_xlsx (0.1.0)
81
+ ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
82
+ ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
83
+ ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
84
+ ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
85
+ ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
86
+ ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
87
+ ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
88
+ ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
89
+ htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
90
+ marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f
91
+ nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895
92
+ nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
93
+ nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af
94
+ nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
95
+ nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb
96
+ nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527
97
+ nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551
98
+ nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a
99
+ nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b
100
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
101
+ rake-compiler-dock (1.12.0) sha256=f13205c2738f3d2053afcd03491a9e4541b22a59a0bfc53fc8bc883bd8188023
102
+ rb_sys (0.9.130) sha256=7d486d99c1da02635515deaf9860fc5aea90bb4ab2589b2deec7fdc7d3548615
103
+ rubyXL (3.4.38) sha256=6b3f46a5ff8ec9903a562604a379a6b79b67cdec73515162b1785bd6092e6ce6
104
+ rubyzip (3.7.0) sha256=65c19294da75297a939006f3516deacc33185fbd721ff1954f7a231db6d3e121
105
+ write_xlsx (1.15.1) sha256=6a97a5ea9af2fd2f248af4aa61cdced0933b6887c96826b6a04f8384a656d3a2
106
+ xlsxtream (3.1.0) sha256=68ce0cac504d86beb9bc626defd5977146c68e5ca2a4ab76c5c61c48e0a66fcd
107
+ zip_kit (6.3.4) sha256=407c6d39feef818678fae2d129077bd2f920c9806072af3482e671eeae3c48ea
108
+
109
+ BUNDLED WITH
110
+ 4.0.10
data/bench/compare.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Compares fast_xlsx with other Ruby xlsx writers on the same data.
4
+ #
5
+ # BUNDLE_GEMFILE=bench/Gemfile bundle install
6
+ # bundle exec rake compile
7
+ # BUNDLE_GEMFILE=bench/Gemfile bundle exec ruby bench/compare.rb [rows]
8
+ require "stringio"
9
+
10
+ require "fast_xlsx"
11
+ require "fast_excel"
12
+ require "caxlsx"
13
+ require "write_xlsx"
14
+ require "xlsxtream"
15
+ require "rubyXL"
16
+ require "rubyXL/convenience_methods"
17
+
18
+ def elapsed
19
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ yield
21
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
22
+ end
23
+
24
+ ROWS = Integer(ARGV[0] || 20_000)
25
+ DATA = Array.new(ROWS) do |n|
26
+ [n, "String string #{n}" * 5, n * 7 % 1000, Time.at((n * 1000) + 1_492_922_688), n * 100.5]
27
+ end
28
+
29
+ # Each writer builds one worksheet from DATA and returns the .xlsx bytes.
30
+ WRITERS = {
31
+ "fast_xlsx" => lambda {
32
+ wb = FastXlsx::Workbook.new
33
+ wb.add_worksheet.concat(DATA)
34
+ wb.to_xlsx
35
+ },
36
+ "fast_xlsx (memory: :constant)" => lambda {
37
+ wb = FastXlsx::Workbook.new(memory: :constant)
38
+ wb.add_worksheet.concat(DATA)
39
+ wb.to_xlsx
40
+ },
41
+ "fast_xlsx (memory: :low)" => lambda {
42
+ wb = FastXlsx::Workbook.new(memory: :low)
43
+ wb.add_worksheet.concat(DATA)
44
+ wb.to_xlsx
45
+ },
46
+ "fast_excel" => lambda {
47
+ wb = FastExcel.open
48
+ ws = wb.add_worksheet
49
+ DATA.each { |r| ws << r }
50
+ wb.read_string
51
+ },
52
+ "fast_excel (constant_memory)" => lambda {
53
+ wb = FastExcel.open(constant_memory: true)
54
+ ws = wb.add_worksheet
55
+ DATA.each { |r| ws << r }
56
+ wb.read_string
57
+ },
58
+ "write_xlsx" => lambda {
59
+ io = StringIO.new
60
+ wb = WriteXLSX.new(io)
61
+ ws = wb.add_worksheet
62
+ DATA.each_with_index { |r, i| ws.write_row(i, 0, r) }
63
+ wb.close
64
+ io.string
65
+ },
66
+ "xlsxtream" => lambda {
67
+ io = StringIO.new
68
+ Xlsxtream::Workbook.open(io) do |xlsx|
69
+ xlsx.write_worksheet("Sheet1") { |ws| DATA.each { |r| ws << r } }
70
+ end
71
+ io.string
72
+ },
73
+ "caxlsx" => lambda {
74
+ package = Axlsx::Package.new
75
+ package.workbook.add_worksheet { |ws| DATA.each { |r| ws.add_row(r) } }
76
+ package.to_stream.read
77
+ },
78
+ "rubyXL" => lambda {
79
+ wb = RubyXL::Workbook.new
80
+ ws = wb[0]
81
+ DATA.each_with_index { |r, i| r.each_with_index { |v, j| ws.add_cell(i, j, v) } }
82
+ wb.stream.read
83
+ }
84
+ }.freeze
85
+
86
+ def measure(writer, runs)
87
+ bytes = writer.call # warm up
88
+ times = Array.new(runs) do
89
+ GC.start
90
+ elapsed { writer.call }
91
+ end
92
+ GC.start
93
+ before = GC.stat(:total_allocated_objects)
94
+ writer.call
95
+ { ms: times.sort[runs / 2] * 1000, allocs: GC.stat(:total_allocated_objects) - before, kb: bytes.bytesize / 1024 }
96
+ end
97
+
98
+ puts "#{ROWS} rows x 5 columns (integer, string, integer, Time, float), Ruby #{RUBY_VERSION}, #{RUBY_PLATFORM}"
99
+ puts
100
+
101
+ results = WRITERS.to_h do |name, writer|
102
+ runs = name == "rubyXL" ? 3 : 7
103
+ [name, measure(writer, runs)]
104
+ end
105
+
106
+ fastest = results.values.map { |r| r[:ms] }.min
107
+ puts "| Library | Time (median) | vs fastest | Ruby objects allocated | Output |"
108
+ puts "|---|---:|---:|---:|---:|"
109
+ results.sort_by { |_, r| r[:ms] }.each do |name, r|
110
+ puts format("| %<name>s | %<ms>.0f ms | %<x>.1fx | %<allocs>s | %<kb>d KB |",
111
+ name: name, ms: r[:ms], x: r[:ms] / fastest,
112
+ allocs: r[:allocs].to_s.reverse.scan(/\d{1,3}/).join(",").reverse, kb: r[:kb])
113
+ end
data/bench/memory.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Writes one workbook in this process so its peak memory can be read by the OS:
4
+ #
5
+ # BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb fast_xlsx:low unique # macOS
6
+ # BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -v bundle exec ruby bench/memory.rb fast_xlsx:low unique # Linux
7
+ #
8
+ # writer: baseline (build the data only), fast_xlsx[:constant|:low], fast_excel[:constant]
9
+ # data: unique (every string differs) or repeated (a few distinct strings)
10
+ # Subtract the baseline's peak to get what writing the file adds.
11
+ require "fast_xlsx"
12
+ require "fileutils"
13
+ require "tmpdir"
14
+
15
+ writer = ARGV[0]
16
+ kind = ARGV[1] || "unique"
17
+ rows = Integer(ARGV[2] || 200_000)
18
+ regions = %w[North South East West Central]
19
+ statuses = %w[Open Closed Pending Cancelled]
20
+ data = Array.new(rows) do |n|
21
+ label = kind == "unique" ? "String string #{n}" * 5 : regions[n % 5]
22
+ [n, label, statuses[n % 4], Time.at((n * 1000) + 1_492_922_688), n * 100.5]
23
+ end
24
+ out = File.join(Dir.tmpdir, "fast_xlsx_memory_#{Process.pid}.xlsx")
25
+
26
+ case writer
27
+ when "baseline"
28
+ nil
29
+ when /\Afast_xlsx/
30
+ mode = writer.split(":")[1]
31
+ wb = FastXlsx::Workbook.new(memory: (mode || "standard").to_sym)
32
+ wb.add_worksheet.concat(data)
33
+ wb.save(out)
34
+ when /\Afast_excel/
35
+ require "fast_excel"
36
+ wb = FastExcel.open(out, constant_memory: writer.end_with?(":constant"))
37
+ ws = wb.add_worksheet
38
+ data.each { |r| ws << r }
39
+ wb.close
40
+ else
41
+ abort "unknown writer #{writer}"
42
+ end
43
+ FileUtils.rm_f(out)
data/bench/write.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec rake compile && ruby -Ilib bench/write.rb
4
+ # Compares against fast_excel when it can be loaded (FAST_EXCEL=/path/to/fast_excel/lib/fast_excel).
5
+ require "fast_xlsx"
6
+
7
+ def elapsed
8
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
9
+ yield
10
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
11
+ end
12
+
13
+ ROWS = 20_000
14
+ DATA = Array.new(ROWS) do |n|
15
+ [n, "String string #{n}" * 5, n * 7 % 1000, Time.at((n * 1000) + 1_492_922_688), n * 100.5]
16
+ end
17
+
18
+ def report(label, runs = 7, &block)
19
+ times = Array.new(runs) do
20
+ GC.start
21
+ elapsed(&block)
22
+ end
23
+ puts " #{label.ljust(26)} #{(times.sort[runs / 2] * 1000).round(1)} ms"
24
+ end
25
+
26
+ begin
27
+ require ENV.fetch("FAST_EXCEL", "fast_excel")
28
+ rescue LoadError
29
+ nil
30
+ end
31
+
32
+ [false, true].each do |cm|
33
+ puts "memory=#{cm ? "constant" : "standard"}, #{ROWS}x5 cells, median of 7"
34
+ report("fast_xlsx <<") do
35
+ wb = FastXlsx::Workbook.new(memory: cm ? :constant : :standard)
36
+ ws = wb.add_worksheet
37
+ DATA.each { |r| ws << r }
38
+ wb.to_xlsx
39
+ end
40
+ report("fast_xlsx concat") do
41
+ wb = FastXlsx::Workbook.new(memory: cm ? :constant : :standard)
42
+ wb.add_worksheet.concat(DATA)
43
+ wb.to_xlsx
44
+ end
45
+ next unless defined?(FastExcel)
46
+
47
+ report("fast_excel <<") do
48
+ wb = FastExcel.open(constant_memory: cm)
49
+ ws = wb.add_worksheet
50
+ DATA.each { |r| ws << r }
51
+ wb.read_string
52
+ end
53
+ end