fast_xlsx 0.1.0-arm-linux-musl

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db5d2a9a270df5498d3e79f69a4a41eea77018f814374552e926929ce7f094fc
4
+ data.tar.gz: ef4b4fa4695c280c15836067e4150aed908950c035eb6c3a74cc45c754d26133
5
+ SHA512:
6
+ metadata.gz: 302ca0e6c60e9d5d676bbaff8d4d96d95e9099ed76e881dcae5987fbb1a26d1dd17da5994186dc2077c0177602b02dbb80fbe3ca89ce704d862e063a49f41c89
7
+ data.tar.gz: e94fd22371e384bf400d1e07c0cfc392a9beb2e7382ecf4ab661e483caf14e882f743385fb1fc84eb47776d9374841c767aede9369841a0197cfc9e8b23d5c56
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-09-30
4
+
5
+ First release: a fast `.xlsx` writer built on rust_xlsxwriter. Early API; it may still change before 1.0.
6
+
7
+ ### Workbooks and worksheets
8
+
9
+ - `FastXlsx::Workbook.new(memory: :standard | :constant | :low)`: keep every cell in memory, or write finished rows to disk with strings inline (`:constant`) or in the shared string table (`:low`)
10
+ - `Workbook#add_worksheet`, `#worksheet(name)`, `#worksheets`, `#properties` (document properties), `#to_xlsx`, `#save`
11
+ - `Worksheet#<<`, `#append`, `#concat`, `#write` (all return the worksheet); writes to rows already on disk raise `FastXlsx::Error`
12
+ - Rows and columns are 0-based
13
+
14
+ ### Cell values
15
+
16
+ - Numeric, String, Time, Date/DateTime, boolean and nil
17
+ - `FastXlsx::Formula`, `FastXlsx::URL` (optionally showing other text), `FastXlsx::RichString` (a format per text segment)
18
+
19
+ ### Formatting
20
+
21
+ - `FastXlsx::Format`: fonts, colors, number formats, alignment, wrapping, rotation, indent, borders and underline styles; unknown options and invalid values raise `ArgumentError`
22
+ - One format per row or per cell in `append`; `Worksheet#column_format` for column defaults
23
+
24
+ ### Layout and features
25
+
26
+ - `column_width`, `autofit` (keeps widths set explicitly), `row_height`, `freeze_panes`, `merge_range`, `autofilter`
27
+ - `conditional_format` (cell, text, formula, data bar, color scale), `data_validation` (lists, numbers, text length, messages)
28
+ - `add_table` (styles, total row, column totals), `insert_chart`, `insert_image` (path or IO, scale or pixel size), `write_comment`
29
+ - Printing: `page_header`, `page_footer`, `margins`, `page_breaks`, `vertical_page_breaks`
30
+ - Unknown options to the option-hash methods raise `ArgumentError`
31
+
32
+ ### Packaging
33
+
34
+ - Precompiled gems for Linux (glibc and musl, x86_64 and aarch64, arm), macOS (arm64 and x86_64) and Windows (x64); requires CRuby 3.3+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Zac
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
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
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds one workbook with a worksheet per feature, to check the output in
4
+ # Excel (or LibreOffice / Numbers) before a release.
5
+ #
6
+ # bundle exec rake compile
7
+ # ruby -Ilib examples/showcase.rb [showcase.xlsx]
8
+ require "date"
9
+ require "stringio"
10
+ require "zlib"
11
+ require "fast_xlsx"
12
+
13
+ path = ARGV[0] || "showcase.xlsx"
14
+
15
+ # A small gradient PNG, generated so the example needs no image file.
16
+ def gradient_png(width, height)
17
+ rows = (0...height).map do |y|
18
+ "\0".b + (0...width).map { |x| [x * 255 / width, y * 255 / height, 200].pack("C3") }.join
19
+ end
20
+ chunk = ->(type, data) { [data.bytesize].pack("N") + type + data + [Zlib.crc32(type + data)].pack("N") }
21
+ "\x89PNG\r\n\x1A\n".b +
22
+ chunk.call("IHDR", [width, height, 8, 2, 0, 0, 0].pack("NNC5")) +
23
+ chunk.call("IDAT", Zlib::Deflate.deflate(rows.join)) +
24
+ chunk.call("IEND", "".b)
25
+ end
26
+
27
+ wb = FastXlsx::Workbook.new
28
+ wb.properties(title: "fast_xlsx showcase", author: "fast_xlsx", keywords: "example")
29
+
30
+ bold = FastXlsx::Format.new(bold: true)
31
+ header = FastXlsx::Format.new(bold: true, bg_color: "#DDEBF7", border_bottom: :thin, align: :center)
32
+ money = FastXlsx::Format.new(num_format: "#,##0.00")
33
+ date = FastXlsx::Format.new(num_format: "yyyy-mm-dd")
34
+ datetime = FastXlsx::Format.new(num_format: "yyyy-mm-dd hh:mm")
35
+
36
+ # Values: every cell type.
37
+ ws = wb.add_worksheet("Values")
38
+ ws.column_width(0, 22).column_width(1, 40)
39
+ ws.append(%w[Type Value], format: header)
40
+ ws << ["Integer", 42]
41
+ ws << ["Float", 3.14159]
42
+ ws.append(["Money format", 1_234_567.891], format: [nil, money])
43
+ ws.append(["Date", Date.new(2024, 2, 29)], format: [nil, date])
44
+ ws.append(["Time", Time.new(2024, 2, 29, 13, 45)], format: [nil, datetime])
45
+ ws << ["Boolean", true]
46
+ ws << ["nil (empty cell)", nil]
47
+ ws << ["Formula =SUM(B2:B3)", FastXlsx::Formula.new("SUM(B2:B3)")]
48
+ ws << ["URL", FastXlsx::URL.new("https://github.com/7a6163/fast_xlsx")]
49
+ ws << ["URL with text", FastXlsx::URL.new("https://github.com/7a6163/fast_xlsx", text: "fast_xlsx on GitHub")]
50
+ ws << ["Rich string",
51
+ FastXlsx::RichString.new(["Bold ", bold], "plain ", ["italic", FastXlsx::Format.new(italic: true)])]
52
+ ws << ["Symbol (to_s)", :symbol]
53
+
54
+ # Formats: one option per row.
55
+ ws = wb.add_worksheet("Formats")
56
+ ws.column_width(0, 24).column_width(1, 30)
57
+ ws.append(%w[Option Sample], format: header)
58
+ {
59
+ "bold" => { bold: true }, "italic" => { italic: true }, "strikeout" => { strikeout: true },
60
+ "underline :double" => { underline: :double }, "font_script :superscript" => { font_script: :superscript },
61
+ "font_size 16" => { font_size: 16 }, "font_name Courier New" => { font_name: "Courier New" },
62
+ "font_color #C00000" => { font_color: "#C00000" }, "bg_color #FFF2CC" => { bg_color: "#FFF2CC" },
63
+ "align :right" => { align: :right }, "valign :top" => { valign: :top }, "indent 2" => { indent: 2 },
64
+ "rotation 45" => { rotation: 45 }, "text_wrap" => { text_wrap: true }, "shrink" => { shrink: true },
65
+ "border :medium + color" => { border: :medium, border_color: "#2F5597" },
66
+ "border_bottom :double" => { border_bottom: :double }
67
+ }.each do |label, options|
68
+ text = label == "text_wrap" ? "long text that wraps inside the cell width" : "Sample text"
69
+ ws.append([label, text], format: [nil, FastXlsx::Format.new(**options)])
70
+ end
71
+ ws.row_height(14, 40)
72
+
73
+ # Layout: widths, autofit, merged title, frozen header, filter.
74
+ ws = wb.add_worksheet("Layout")
75
+ ws.merge_range(0, 0, 0, 3, "Merged title across A1:D1", FastXlsx::Format.new(bold: true, font_size: 14, align: :center))
76
+ ws.append(%w[Region Rep Amount Note], format: header)
77
+ 20.times { |i| ws << [%w[North South East West][i % 4], "Rep #{i + 1}", (i + 1) * 125.5, "row #{i + 1}"] }
78
+ ws.column_format(2, money)
79
+ ws.column_width(3, 30) # kept by autofit
80
+ ws.autofit
81
+ ws.freeze_panes(2, 0)
82
+ ws.autofilter(1, 0, 21, 3)
83
+
84
+ # Conditional formats.
85
+ ws = wb.add_worksheet("Conditional")
86
+ red = FastXlsx::Format.new(font_color: "#9C0006", bg_color: "#FFC7CE")
87
+ green = FastXlsx::Format.new(font_color: "#006100", bg_color: "#C6EFCE")
88
+ ws.append(["Cell < 0", "Text contains 'error'", "Data bar", "Color scale", "Formula (A > 50)"], format: header)
89
+ values = [-30, 80, 15, -5, 60, 95, 40, -10, 70, 25]
90
+ values.each_with_index { |v, i| ws << [v, i.even? ? "ok" : "error #{i}", v.abs, v, v] }
91
+ ws.conditional_format(1, 0, 10, 0, type: :cell, criteria: :<, value: 0, format: red)
92
+ ws.conditional_format(1, 1, 10, 1, type: :text, criteria: :contains, value: "error", format: red)
93
+ ws.conditional_format(1, 2, 10, 2, type: :data_bar)
94
+ ws.conditional_format(1, 3, 10, 3, type: :color_scale)
95
+ ws.conditional_format(1, 4, 10, 4, type: :formula, value: "=$A2>50", format: green)
96
+ ws.column_width(0..4, 22)
97
+
98
+ # Data validation.
99
+ ws = wb.add_worksheet("Validation")
100
+ ws.append(["Status (dropdown)", "Quantity 1-10", "Price >= 0", "Code <= 5 chars"], format: header)
101
+ ws.data_validation(1, 0, 20, 0, type: :list, value: %w[Open Pending Closed],
102
+ input_title: "Status", input_message: "Pick a status")
103
+ ws.data_validation(1, 1, 20, 1, type: :whole_number, criteria: :between, value: [1, 10],
104
+ error_title: "Invalid quantity", error_message: "Enter a whole number from 1 to 10")
105
+ ws.data_validation(1, 2, 20, 2, type: :decimal, criteria: :>=, value: 0)
106
+ ws.data_validation(1, 3, 20, 3, type: :text_length, criteria: :<=, value: 5)
107
+ ws.column_width(0..3, 20)
108
+
109
+ # Table with a total row.
110
+ ws = wb.add_worksheet("Table")
111
+ sales = [%w[North Ann 1200], %w[South Bob 950], %w[East Cai 1430], %w[West Dee 780]].map { |r, n, a| [r, n, a.to_i] }
112
+ ws.add_table(0, 0, sales.size + 1, 2, total_row: true, style: :medium2,
113
+ columns: [{ header: "Region", total_label: "Total" }, "Rep",
114
+ { header: "Sales", total: :sum, format: money }])
115
+ ws.concat(sales)
116
+ ws.column_width(0..2, 14)
117
+
118
+ # Charts.
119
+ ws = wb.add_worksheet("Chart")
120
+ ws.append(%w[Month Sales Costs], format: header)
121
+ ws.concat([["Jan", 10, 7], ["Feb", 25, 12], ["Mar", 18, 11], ["Apr", 30, 16], ["May", 27, 14]])
122
+ series = [{ name: "Sales", categories: "Chart!$A$2:$A$6", values: "Chart!$B$2:$B$6" },
123
+ { name: "Costs", categories: "Chart!$A$2:$A$6", values: "Chart!$C$2:$C$6" }]
124
+ ws.insert_chart(0, 4, type: :column, series: series, title: "Column", x_axis: "Month", y_axis: "Amount")
125
+ ws.insert_chart(16, 4, type: :line, series: series, title: "Line", width: 600, height: 300)
126
+ ws.insert_chart(16, 0, type: :pie, series: [series.first], title: "Pie", width: 300, height: 300)
127
+
128
+ # Image and comment.
129
+ ws = wb.add_worksheet("Media")
130
+ ws << ["Generated PNG at scale 1, pixel size 240x60, and with an offset. B1 has a comment."]
131
+ ws.write_comment(0, 1, "This is a note (comment).", author: "fast_xlsx")
132
+ png = gradient_png(120, 40)
133
+ ws.insert_image(2, 0, StringIO.new(png), alt_text: "Gradient")
134
+ ws.insert_image(2, 3, StringIO.new(png), width: 240, height: 60)
135
+ ws.insert_image(8, 0, StringIO.new(png), x_offset: 20, y_offset: 10, scale: 1.5)
136
+
137
+ # Printing: check File > Print preview.
138
+ ws = wb.add_worksheet("Printing")
139
+ ws.page_header("&CPage &P of &N").page_footer("&L&A&R&D", margin: 0.2).margins(left: 0.5, right: 0.5)
140
+ ws.page_breaks([30])
141
+ ws.vertical_page_breaks([4])
142
+ ws.append(%w[Row A B C D E F], format: header)
143
+ 60.times { |i| ws << [i + 1, *Array.new(6) { |c| (i + 1) * (c + 1) }] }
144
+
145
+ wb.save(path)
146
+ puts "wrote #{path}"
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: data/lib/fast_xlsx/3.3/fast_xlsx.so
3
+ size: 33943032 bytes
4
+ sha256: 261882173f94edd46d754f7eac0b7938388c6f670b16f1ffdbc7977305e688ee
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: data/lib/fast_xlsx/3.4/fast_xlsx.so
3
+ size: 33915280 bytes
4
+ sha256: 76348560391dcfd884ff751b6af72ba2717822fbe21c27e59aba5a2775cf9ec3
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: data/lib/fast_xlsx/4.0/fast_xlsx.so
3
+ size: 33890116 bytes
4
+ sha256: c9241b2aee4fc4bdbccf8ae2ea3d559df08de4c56620e80cf8798b5e56ef860c
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastXlsx
4
+ VERSION = "0.1.0"
5
+ end
data/lib/fast_xlsx.rb ADDED
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fast_xlsx/version"
4
+
5
+ # Fast .xlsx writer backed by rust_xlsxwriter.
6
+ module FastXlsx
7
+ class Error < StandardError; end
8
+
9
+ # Cell value written as an Excel formula, e.g. Formula.new("SUM(A1:A9)").
10
+ Formula = Data.define(:expression) do
11
+ def initialize(expression:)
12
+ super(expression: expression.to_s)
13
+ end
14
+ end
15
+
16
+ # Cell value written as a hyperlink, e.g. URL.new("https://example.com").
17
+ # text: shown in the cell instead of the URL itself.
18
+ # Subclassed (not a Data.define block) so .new can call super, which lets it
19
+ # accept URL.new(url, text: ...) as well as the usual Data forms.
20
+ class URL < Data.define(:url, :text) # rubocop:disable Style/DataInheritance
21
+ def self.new(*args, **kwargs)
22
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..2)" if args.size > 2
23
+
24
+ kwargs[:url] = args[0] unless args.empty?
25
+ kwargs[:text] = args[1] if args.size > 1
26
+ super(**kwargs)
27
+ end
28
+
29
+ # Also reached by URL[...] and #with, so values are normalized here.
30
+ def initialize(url:, text: nil)
31
+ super(url: url.to_s, text: text&.to_s)
32
+ end
33
+ end
34
+
35
+ # Text with a format per segment, e.g. RichString.new(["Total: ", bold], "1,234").
36
+ # Each segment is a String (default font) or [String, Format].
37
+ class RichString
38
+ attr_reader :segments
39
+
40
+ def initialize(*parts)
41
+ raise ArgumentError, "RichString needs at least one segment" if parts.empty?
42
+
43
+ @segments = parts.map { |part| part.is_a?(Array) ? [part[0].to_s, part[1]] : [part.to_s, nil] }.freeze
44
+ end
45
+ end
46
+
47
+ # The native extension looks up FastXlsx::Error, so load it after Error is defined.
48
+ require "fast_xlsx/fast_xlsx"
49
+
50
+ # Owns the worksheets; serialize with #to_xlsx or #save.
51
+ class Workbook
52
+ MEMORY_MODES = %i[standard constant low].freeze
53
+
54
+ # memory: :standard keeps every cell in memory until saving. :constant and
55
+ # :low write each finished row to disk, so each worksheet must be filled
56
+ # top to bottom. :constant stores strings inline (memory stays flat); :low
57
+ # keeps Excel's shared string table (memory grows with the number of
58
+ # unique strings, output is standard).
59
+ def self.new(memory: :standard)
60
+ unless MEMORY_MODES.include?(memory)
61
+ raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
62
+ end
63
+
64
+ _new(memory == :constant, memory == :low)
65
+ end
66
+
67
+ def add_worksheet(name = nil)
68
+ _add_worksheet(name).tap { |ws| worksheets << ws }
69
+ end
70
+
71
+ # The same Worksheet objects add_worksheet returned, so their append
72
+ # position is shared.
73
+ def worksheets
74
+ @worksheets ||= []
75
+ end
76
+
77
+ def worksheet(name)
78
+ worksheets.find { |ws| ws.name == name }
79
+ end
80
+
81
+ # Document properties shown in Excel's File > Info: title:, subject:,
82
+ # author:, manager:, company:, category:, keywords:, comments:, status:.
83
+ # Later calls add to earlier ones.
84
+ def properties(**fields)
85
+ merged = (@properties || {}).merge(fields)
86
+ _properties(merged) # validates before anything is remembered
87
+ @properties = merged
88
+ self
89
+ end
90
+ end
91
+
92
+ # Cell writer for one sheet; create with Workbook#add_worksheet.
93
+ class Worksheet
94
+ def write(row, col, value, format = nil)
95
+ _write(row, col, value, format)
96
+ self
97
+ end
98
+
99
+ def append(values, format: nil)
100
+ _append(values, format)
101
+ end
102
+
103
+ # columns: a 0-based column index or a Range of them. width is in characters.
104
+ def column_width(columns, width)
105
+ bounds = column_bounds(columns)
106
+ _column_width(*bounds, width)
107
+ @fixed_widths ||= {}
108
+ @fixed_widths.delete(bounds) # re-insert so autofit replays calls in order
109
+ @fixed_widths[bounds] = width
110
+ self
111
+ end
112
+
113
+ # Sizes columns to the data written so far. Widths set with
114
+ # column_width are kept.
115
+ def autofit
116
+ _autofit
117
+ @fixed_widths&.each { |bounds, width| _column_width(*bounds, width) }
118
+ self
119
+ end
120
+
121
+ # Merges the range and writes value (any cell type) into its first cell.
122
+ def merge_range(first_row, first_col, last_row, last_col, value, format = nil)
123
+ _merge_range(first_row, first_col, last_row, last_col, value, format)
124
+ end
125
+
126
+ # Highlights cells in the range by rule. type: :cell, :text, :formula,
127
+ # :data_bar or :color_scale; see the README for each type's options.
128
+ def conditional_format(first_row, first_col, last_row, last_col, type:, **)
129
+ _conditional_format(first_row, first_col, last_row, last_col, { type: type, ** })
130
+ end
131
+
132
+ # Restricts what can be entered in the range. type: :list, :whole_number,
133
+ # :decimal or :text_length; see the README for the options.
134
+ def data_validation(first_row, first_col, last_row, last_col, type:, **)
135
+ _data_validation(first_row, first_col, last_row, last_col, { type: type, ** })
136
+ end
137
+
138
+ # Adds a comment (Excel "note") to a cell.
139
+ def write_comment(row, col, text, author: nil)
140
+ _write_comment(row, col, text, author)
141
+ end
142
+
143
+ # Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
144
+ # cell. source is a file path or an IO (anything responding to #read).
145
+ # Options: scale: or width:/height: (pixels), x_offset:, y_offset: (pixels), alt_text:.
146
+ def insert_image(row, col, source, **)
147
+ bytes = source.respond_to?(:read) ? source.read : File.binread(source)
148
+ _insert_image(row, col, bytes, { ** })
149
+ end
150
+
151
+ # Inserts a chart with its top-left corner in the cell. series is an Array
152
+ # of { values:, categories:, name: } with Excel ranges such as
153
+ # "Sheet1!$B$2:$B$13". Options: title:, x_axis:, y_axis:, width:, height:.
154
+ def insert_chart(row, col, type:, series:, **)
155
+ _insert_chart(row, col, { type: type, series: series, ** })
156
+ end
157
+
158
+ # Turns the range (header row included, total row too when total_row: true)
159
+ # into an Excel table. columns: header Strings or { header:, total:,
160
+ # total_label:, format: }; other options: style:, name:, total_row:,
161
+ # banded_rows:, autofilter:.
162
+ def add_table(first_row, first_col, last_row, last_col, **)
163
+ _add_table(first_row, first_col, last_row, last_col, { ** })
164
+ end
165
+
166
+ # Printed page header/footer using Excel codes such as "&CPage &P of &N".
167
+ # margin: is in inches.
168
+ def page_header(text, margin: nil)
169
+ _page_header(text)
170
+ margin ? margins(header: margin) : self
171
+ end
172
+
173
+ def page_footer(text, margin: nil)
174
+ _page_footer(text)
175
+ margin ? margins(footer: margin) : self
176
+ end
177
+
178
+ # Print margins in inches; margins not given keep their current value.
179
+ def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
180
+ _margins(*[left, right, top, bottom, header, footer].map { |m| m || -1.0 })
181
+ self
182
+ end
183
+
184
+ # Default format for cells in these columns that are written without one.
185
+ def column_format(columns, format)
186
+ _column_format(*column_bounds(columns), format)
187
+ self
188
+ end
189
+
190
+ private
191
+
192
+ def column_bounds(columns)
193
+ columns.is_a?(Integer) ? [columns, columns] : columns.minmax
194
+ end
195
+ end
196
+
197
+ # Cell style, e.g. Format.new(bold: true). Pass to Worksheet#write.
198
+ class Format
199
+ def self.new(**options)
200
+ # Apply border: first so border_left: etc. override it whatever the order.
201
+ options = { border: options[:border], **options.except(:border) } if options.key?(:border)
202
+ _new(options)
203
+ end
204
+ end
205
+ end
data/sig/fast_xlsx.rbs ADDED
@@ -0,0 +1,100 @@
1
+ module FastXlsx
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ interface _Reader
8
+ def read: () -> String
9
+ end
10
+
11
+ type cell = Numeric | String | Time | Date | Formula | URL | RichString | bool | nil | _ToS
12
+
13
+ class RichString
14
+ attr_reader segments: Array[[String, Format?]]
15
+ def initialize: (*(_ToS | [_ToS, Format?]) parts) -> void
16
+ end
17
+
18
+ class Formula
19
+ attr_reader expression: String
20
+ def self.new: (_ToS expression) -> Formula
21
+ end
22
+
23
+ class URL
24
+ attr_reader url: String
25
+ attr_reader text: String?
26
+ def self.new: (_ToS url, ?text: _ToS?) -> URL
27
+ end
28
+
29
+ class Workbook
30
+ MEMORY_MODES: Array[Symbol]
31
+ def self.new: (?memory: :standard | :constant | :low) -> Workbook
32
+ def add_worksheet: (?String? name) -> Worksheet
33
+ def worksheets: () -> Array[Worksheet]
34
+ def worksheet: (String name) -> Worksheet?
35
+ def properties: (?title: String, ?subject: String, ?author: String, ?manager: String,
36
+ ?company: String, ?category: String, ?keywords: String,
37
+ ?comments: String, ?status: String) -> self
38
+ def to_xlsx: () -> String
39
+ def save: (String path) -> void
40
+ end
41
+
42
+ class Format
43
+ type color = String | Integer
44
+ type border = :thin | :medium | :thick | :dashed | :dotted | :double | :hair
45
+
46
+ type underline = bool | :single | :double | :single_accounting | :double_accounting
47
+
48
+ def self.new: (?bold: bool, ?italic: bool, ?underline: underline, ?strikeout: bool, ?text_wrap: bool,
49
+ ?shrink: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
50
+ ?font_color: color, ?bg_color: color, ?font_script: :superscript | :subscript,
51
+ ?align: :left | :center | :right, ?valign: :top | :center | :bottom,
52
+ ?rotation: Integer, ?indent: Integer,
53
+ ?border: border, ?border_left: border, ?border_right: border,
54
+ ?border_top: border, ?border_bottom: border, ?border_color: color) -> Format
55
+ end
56
+
57
+ class Worksheet
58
+ def write: (Integer row, Integer col, cell value, ?Format? format) -> self
59
+ def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
60
+ def <<: (Array[cell] row) -> self
61
+ def concat: (Array[Array[cell]] rows) -> self
62
+ def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
63
+ def column_format: (Integer | Range[Integer] columns, Format format) -> self
64
+ def autofit: () -> self
65
+ def autofilter: (Integer first_row, Integer first_col, Integer last_row, Integer last_col) -> self
66
+ def freeze_panes: (Integer row, Integer col) -> self
67
+ def row_height: (Integer row, Numeric height) -> self
68
+ def page_breaks: (Array[Integer] rows) -> self
69
+ def vertical_page_breaks: (Array[Integer] cols) -> self
70
+ def page_header: (String text, ?margin: Numeric?) -> self
71
+ def page_footer: (String text, ?margin: Numeric?) -> self
72
+ def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
73
+ ?header: Numeric?, ?footer: Numeric?) -> self
74
+ def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
75
+ def conditional_format: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
76
+ type: :cell | :text | :formula | :data_bar | :color_scale,
77
+ ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
78
+ ?format: Format, ?colors: 2 | 3) -> self
79
+ def data_validation: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
80
+ type: :list | :whole_number | :decimal | :text_length,
81
+ ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
82
+ ?input_title: String, ?input_message: String,
83
+ ?error_title: String, ?error_message: String) -> self
84
+ def write_comment: (Integer row, Integer col, String text, ?author: String?) -> self
85
+ def insert_image: (Integer row, Integer col, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
86
+ ?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
87
+ type chart_series = { values: String, ?categories: String, ?name: String }
88
+
89
+ def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
90
+ ?title: String, ?x_axis: String, ?y_axis: String,
91
+ ?width: Integer, ?height: Integer) -> self
92
+ type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
93
+
94
+ def add_table: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
95
+ ?columns: Array[table_column], ?style: Symbol, ?name: String,
96
+ ?total_row: bool, ?banded_rows: bool, ?autofilter: bool) -> self
97
+ def name: () -> String
98
+ def next_row: () -> Integer
99
+ end
100
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_xlsx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: arm-linux-musl
6
+ authors:
7
+ - Zac
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Writes Excel .xlsx files from Ruby through a native Rust extension (rust_xlsxwriter
14
+ + magnus).
15
+ email:
16
+ - 579103+7a6163@users.noreply.github.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - bench/Gemfile
26
+ - bench/Gemfile.lock
27
+ - bench/compare.rb
28
+ - bench/memory.rb
29
+ - bench/write.rb
30
+ - examples/showcase.rb
31
+ - lib/fast_xlsx.rb
32
+ - lib/fast_xlsx/3.3/fast_xlsx.so
33
+ - lib/fast_xlsx/3.4/fast_xlsx.so
34
+ - lib/fast_xlsx/4.0/fast_xlsx.so
35
+ - lib/fast_xlsx/version.rb
36
+ - sig/fast_xlsx.rbs
37
+ homepage: https://github.com/7a6163/fast_xlsx
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ github_repo: ssh://github.com/7a6163/fast_xlsx
42
+ homepage_uri: https://github.com/7a6163/fast_xlsx
43
+ source_code_uri: https://github.com/7a6163/fast_xlsx
44
+ changelog_uri: https://github.com/7a6163/fast_xlsx/blob/main/CHANGELOG.md
45
+ rubygems_mfa_required: 'true'
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ - - "<"
56
+ - !ruby/object:Gem::Version
57
+ version: 4.1.dev
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 3.3.22
63
+ requirements: []
64
+ rubygems_version: 3.5.23
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Fast xlsx writer for Ruby, powered by rust_xlsxwriter
68
+ test_files: []