xlsxrb 0.1.4 → 0.1.6
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 +4 -4
- data/.devcontainer/Dockerfile +1 -1
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
- data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
- data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
- data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +42 -3
- data/README.md +130 -49
- data/Rakefile +23 -2
- data/Steepfile +17 -0
- data/benchmark.rb +324 -348
- data/docs/ARCHITECTURE.md +24 -0
- data/docs/DEVELOPMENT.md +58 -0
- data/docs/QUALITY_ASSURANCE.md +26 -0
- data/docs/visual/VisualGallery.md +893 -938
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +113 -8
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +52 -4
- data/lib/xlsxrb/elements/types.rb +6 -0
- data/lib/xlsxrb/elements/workbook.rb +29 -0
- data/lib/xlsxrb/elements/worksheet.rb +98 -1
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/ooxml/reader.rb +8 -6
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +106 -40
- data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/utils.rb +2 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +156 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
- data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
- data/lib/xlsxrb/ooxml/zip_reader.rb +179 -57
- data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +284 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1761 -275
- data/rbs_collection.lock.yaml +252 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
- data/sig/generated/xlsxrb/elements/column.rbs +30 -0
- data/sig/generated/xlsxrb/elements/row.rbs +32 -0
- data/sig/generated/xlsxrb/elements/types.rbs +74 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +250 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- data/sig/generated/xlsxrb.rbs +1276 -0
- data/sig/rexml.rbs +4 -0
- metadata +145 -4
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
data/README.md
CHANGED
|
@@ -6,17 +6,22 @@ A Ruby library for reading and writing XLSX files with streaming support.
|
|
|
6
6
|
|
|
7
7
|
The Ruby ecosystem already has great XLSX libraries. Each is well-designed for its purpose:
|
|
8
8
|
|
|
9
|
-
| Library | Read | Write |
|
|
10
|
-
| -------------------------------------------------- | ---- | ----- |
|
|
11
|
-
| [roo](https://rubygems.org/gems/roo) | ✅ | ❌ |
|
|
12
|
-
| [creek](https://rubygems.org/gems/creek) | ✅ | ❌ |
|
|
13
|
-
| [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ |
|
|
14
|
-
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ |
|
|
15
|
-
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ |
|
|
16
|
-
| [
|
|
17
|
-
| [
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
| Library | Read | Write | Model | Write String Storage | Rich Formatting |
|
|
10
|
+
| -------------------------------------------------- | ---- | ----- | --------------------- | -------------------- | --------------- |
|
|
11
|
+
| [roo](https://rubygems.org/gems/roo) | ✅ | ❌ | Streaming | N/A (Read-only) | ⚠️ (Formulas, Basic styles) |
|
|
12
|
+
| [creek](https://rubygems.org/gems/creek) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Raw cell values) |
|
|
13
|
+
| [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Fast plain text) |
|
|
14
|
+
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ | In-Memory | Inline (opt: SST) | ✅ (Charts, Styles) |
|
|
15
|
+
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | Streaming | Inline (opt: SST) | ❌ (Plain data only) |
|
|
16
|
+
| [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | Streaming (C Ext) | SST (opt: Inline) | ⚠️ (Basic styles) |
|
|
17
|
+
| [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | In-Memory | Inline / Direct | ✅ (DOM editing) |
|
|
18
|
+
| **[xlsxrb](https://github.com/niku/xlsxrb)** | ✅ | ✅ | **Streaming / In-Memory** | **SST** | ✅ **(Full Features)** |
|
|
19
|
+
|
|
20
|
+
Each of these libraries makes deliberate tradeoffs, and they do so thoughtfully:
|
|
21
|
+
* **Memory & Execution Model (Streaming vs In-Memory)**: Streaming libraries write or read rows sequentially on-the-fly to maintain a constant, low-memory footprint regardless of row count. In-memory libraries build complete document object trees, offering flexible random access and cell updates at the cost of high RAM usage on large sheets.
|
|
22
|
+
* **String Storage Architecture (SST vs Inline Strings)**:
|
|
23
|
+
* **SST (Shared String Table)**: De-duplicates strings into a central dictionary (`xl/sharedStrings.xml`), referencing them by numeric IDs in cell entries (`<c t="s"><v>0</v></c>`). This is standard Microsoft Excel behavior, producing significantly smaller raw XML documents (50–100% smaller) and reducing Excel's memory footprint when opening spreadsheets.
|
|
24
|
+
* **Inline Strings**: Writes text directly into cell payloads (`<c t="inlineStr"><is><t>...</t></is></c>`). Bypassing the dictionary enables blazing-fast raw throughput for simple data exports, but inflates uncompressed XML size and limits advanced formatting (e.g. styling, cell merges, charts).
|
|
20
25
|
|
|
21
26
|
Traditionally, attempting to build a "complete package" that offers both reading and writing, rich features, high performance, strict compatibility, and comprehensive documentation presents an inherent open-source challenge: the cumulative maintenance overhead often exceeds the capacity of individual human maintainers.
|
|
22
27
|
|
|
@@ -77,11 +82,11 @@ Generate large files efficiently by writing data directly to the file stream:
|
|
|
77
82
|
```ruby
|
|
78
83
|
require "xlsxrb"
|
|
79
84
|
|
|
80
|
-
Xlsxrb.generate("large_output.xlsx") do |
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
Xlsxrb.generate("large_output.xlsx") do |wb|
|
|
86
|
+
wb.sheet("Sales Data") do |sheet|
|
|
87
|
+
sheet.row(["Date", "Amount", "Status"])
|
|
88
|
+
sheet.row([Date.today, 100, true])
|
|
89
|
+
sheet.column(0, width: 15.5)
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
```
|
|
@@ -91,12 +96,56 @@ Read rows one at a time without loading the entire file into memory:
|
|
|
91
96
|
```ruby
|
|
92
97
|
require "xlsxrb"
|
|
93
98
|
|
|
94
|
-
Xlsxrb.foreach("large_file.xlsx"
|
|
95
|
-
|
|
99
|
+
Xlsxrb.foreach("large_file.xlsx") do |sheet|
|
|
100
|
+
sheet.each do |row|
|
|
101
|
+
puts "Row #{row.index}: #{row.cells.map(&:value).join(', ')}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### In-Memory Building & Modifying
|
|
107
|
+
|
|
108
|
+
`xlsxrb` provides a powerful, immutable-by-default API for modifying existing Excel files or building templates in-memory.
|
|
109
|
+
|
|
110
|
+
#### Modifying an Existing File
|
|
111
|
+
You can update specific cells or sheets using the functional `Xlsxrb.modify` API, which yields the parsed `Workbook`.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
require "xlsxrb"
|
|
115
|
+
|
|
116
|
+
# Create a dummy template.xlsx for this example
|
|
117
|
+
Xlsxrb.build { |w| w.sheet("Invoice") }.write("template.xlsx")
|
|
118
|
+
|
|
119
|
+
Xlsxrb.modify("template.xlsx", "output.xlsx") do |wb|
|
|
120
|
+
wb.update_sheet("Invoice") do |sheet|
|
|
121
|
+
# Update a specific cell
|
|
122
|
+
sheet = sheet.update_cell("C4", value: "INV-10042")
|
|
123
|
+
sheet = sheet.update_cell("C5", value: Date.today)
|
|
124
|
+
|
|
125
|
+
# Or append new rows
|
|
126
|
+
sheet.with(rows: sheet.rows + [
|
|
127
|
+
Xlsxrb::Elements::Row.new(index: sheet.rows.size, cells: [])
|
|
128
|
+
])
|
|
129
|
+
end
|
|
96
130
|
end
|
|
97
131
|
```
|
|
98
132
|
|
|
99
|
-
|
|
133
|
+
#### Hash & Range Styling (Syntactic Sugar)
|
|
134
|
+
You can directly apply inline styles or use Ranges for multiple columns without boilerplate:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
Xlsxrb.build do |wb|
|
|
138
|
+
# Use [] accessor for sheets
|
|
139
|
+
wb["Report"].row(
|
|
140
|
+
["ID", "Name", "Score", "Rank"],
|
|
141
|
+
# Apply 'header' style to first two columns, and bold inline style to the third
|
|
142
|
+
styles: { 0..1 => "header", 2 => { font: { bold: true, color: "red" } } }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Set multiple column widths at once using Ranges
|
|
146
|
+
wb["Report"].column("A".."D", width: 15.0)
|
|
147
|
+
end
|
|
148
|
+
```
|
|
100
149
|
|
|
101
150
|
## Feature Support & ECMA-376 Compliance
|
|
102
151
|
|
|
@@ -112,61 +161,93 @@ For detailed specification references and policies, see [SPEC_SOURCES.md](docs/S
|
|
|
112
161
|
|
|
113
162
|
## Benchmarks
|
|
114
163
|
|
|
115
|
-
The following benchmarks measure the time
|
|
164
|
+
The following benchmarks measure the time, peak memory, and GC count required to process a 1,000,000 cells (100,000 rows × 10 columns) spreadsheet across popular Ruby Excel libraries. Each test is executed across 3 independent runs in isolated subprocesses; median values are reported along with the mean execution time.
|
|
116
165
|
|
|
117
166
|
### Write Performance (1,000,000 cells)
|
|
118
167
|
|
|
119
|
-
| Library | Time
|
|
120
|
-
| ---------------------- |
|
|
121
|
-
| xlsxtream
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
| xlsxrb (
|
|
125
|
-
|
|
|
126
|
-
| rubyXL
|
|
168
|
+
| Library | Model | Write String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |
|
|
169
|
+
| ---------------------- | ----------- | -------------------- | ------------- | ----------- | ----------- | -------- |
|
|
170
|
+
| xlsxtream 3.1.0 | Streaming | Inline String | 1.23 s | 1.25 s | 18.1 MB | 1061.0 |
|
|
171
|
+
| xlsxrb (Streaming) | Streaming | SST (Shared) | 1.62 s | 1.62 s | 94.5 MB | 39.0 |
|
|
172
|
+
| fast_excel 0.5.0 (C) | Streaming | SST (Shared) | 2.03 s | 2.03 s | 147.9 MB | 263.0 |
|
|
173
|
+
| xlsxrb (In-Memory) | In-Memory | SST (Shared) | 4.06 s | 4.06 s | 280.3 MB | 32.0 |
|
|
174
|
+
| caxlsx 4.5.0 | In-Memory | Inline String | 5.36 s | 5.35 s | 188.7 MB | 23.0 |
|
|
175
|
+
| rubyXL 3.4.38 | In-Memory | Inline String | 38.02 s | 38.03 s | 2166.0 MB | 104.0 |
|
|
176
|
+
|
|
177
|
+
> **Note**: All libraries are evaluated in their **default, out-of-the-box configuration**. Under the same Microsoft Excel-standard Shared String Table (SST) architecture, Pure Ruby `xlsxrb` (Streaming: 1.62s, In-Memory: 4.06s) writes 1,000,000 cells faster than the C-extension `fast_excel` (2.03s) and in-memory gems like `caxlsx` (5.36s).
|
|
127
178
|
|
|
128
179
|
### Read Performance (1,000,000 cells)
|
|
129
180
|
|
|
130
|
-
| Library
|
|
131
|
-
|
|
|
132
|
-
| xlsxrb (Streaming) |
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
| xsv
|
|
137
|
-
| rubyXL
|
|
181
|
+
| Library | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |
|
|
182
|
+
| ---------------------- | ----------- | ------------- | ----------- | ----------- | -------- |
|
|
183
|
+
| xlsxrb (Streaming) | Streaming | 3.38 s | 3.37 s | 90.6 MB | 40.0 |
|
|
184
|
+
| xlsxrb (In-Memory) | In-Memory | 5.75 s | 5.94 s | 250.9 MB | 55.0 |
|
|
185
|
+
| creek 2.6.3 | Streaming | 7.90 s | 7.93 s | 835.7 MB | 481.0 |
|
|
186
|
+
| roo 3.0.0 | Streaming | 10.34 s | 10.28 s | 139.0 MB | 107.0 |
|
|
187
|
+
| xsv 1.4.1 | Streaming | 16.39 s | 17.28 s | 75.4 MB | 2215.0 |
|
|
188
|
+
| rubyXL 3.4.38 | In-Memory | 35.45 s | 35.38 s | 2281.3 MB | 146.0 |
|
|
138
189
|
|
|
139
|
-
### Running the Benchmarks Locally
|
|
190
|
+
### Running the Benchmarks Locally (Reproducibility)
|
|
140
191
|
|
|
141
|
-
The benchmark
|
|
192
|
+
The benchmark suite leverages [`bundler/inline`](https://bundler.io/v2.5/guides/bundler_in_a_single_file_ruby_script.html) to automatically manage and download all peer ecosystem gems without modifying the project's core `Gemfile` or requiring manual global `gem install` steps. Each library is executed in an isolated subprocess (`Bundler.with_unbundled_env`) across multiple runs with standard business dataset rows (integers, strings, floats, booleans, dates) to ensure clean memory and GC measurements without cross-contamination.
|
|
142
193
|
|
|
143
|
-
To run the benchmark
|
|
194
|
+
To run the complete benchmark suite:
|
|
144
195
|
```bash
|
|
145
|
-
ruby benchmark.rb 100000
|
|
196
|
+
ruby benchmark.rb 100000 10
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Security (Protection against CSV/Excel Injection)
|
|
200
|
+
|
|
201
|
+
Unlike CSV files which lack type definitions and force Excel to guess types (often inadvertently executing strings starting with `=`), `.xlsx` files generated by `xlsxrb` are strictly typed.
|
|
202
|
+
|
|
203
|
+
When you pass a Ruby `String` to `xlsxrb`, it explicitly writes it as a `String` (`t="s"`) into the OOXML file. Therefore, **even if a string starts with `=`, Excel will never evaluate it as a formula**. To write a formula, you must explicitly use `Xlsxrb::Elements::Formula.new`. This design completely mitigates CSV/Formula Injection vulnerabilities by default without requiring additional sanitization.
|
|
204
|
+
|
|
205
|
+
### External Link Updates (`update_links`)
|
|
206
|
+
|
|
207
|
+
As an extra layer of "defense in depth", `xlsxrb` configures the workbook to **never automatically update external links** when opened (`updateLinks="never"`). This is intentionally set to `never` by default to prevent Excel from silently reaching out to external resources or executing DDE (Dynamic Data Exchange) links, which is a known vector for malware.
|
|
208
|
+
|
|
209
|
+
If you absolutely need external links to update automatically, you can explicitly override this (though **it is highly discouraged due to security risks**):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
Xlsxrb.generate("file.xlsx") do |wb|
|
|
213
|
+
# WARNING: Enabling this can expose users to malicious external reference vulnerabilities!
|
|
214
|
+
wb.workbook_property(:update_links, "always")
|
|
215
|
+
# ...
|
|
216
|
+
end
|
|
146
217
|
```
|
|
147
218
|
|
|
148
219
|
## Testing & Quality Assurance
|
|
149
220
|
|
|
150
|
-
To support reliability, compliance with the ECMA-376 specification, and consistent updates, `xlsxrb` is backed by a
|
|
221
|
+
To support reliability, compliance with the ECMA-376 specification, and consistent updates, `xlsxrb` is backed by a highly rigorous, enterprise-grade Quality Assurance (QA) and testing architecture.
|
|
222
|
+
|
|
223
|
+
### Multi-Tier Testing Strategy
|
|
224
|
+
* **Round-Trip Testing**: Unit tests verify that every generated sheet can be reliably parsed back by the reader with identical content and styling.
|
|
225
|
+
* **Contract Consistency**: Ensures semantic output consistency between the Streaming (`Xlsxrb.generate`) and In-Memory (`Xlsxrb.build`) APIs.
|
|
226
|
+
* **Property-Based Testing (PBT)**: Automatically generates random data to catch edge cases (e.g., huge numbers, special characters) preventing unexpected crashes.
|
|
227
|
+
* **Concurrency Validation**: Thread and Ractor safety checks to guarantee no global variable pollution during parallel execution.
|
|
228
|
+
* **Security & DoS Protection**: Hardened against malicious files, including memory exhaustion (ZIP Bombs) and infinite parsing loops.
|
|
229
|
+
|
|
230
|
+
### Strict Interoperability & Rendering
|
|
231
|
+
* **Official Open XML SDK Validation (E2E)**: Every generated spreadsheet is structurally validated against the official Microsoft Open XML SDK to prevent file corruption warnings in Microsoft Excel.
|
|
232
|
+
* **Visual Regression Testing (VRT)**: Spreadsheets are rendered via a headless LibreOffice Calc engine and compared pixel-by-pixel against visual baselines to catch subtle rendering regressions.
|
|
151
233
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
4. Visual Regression Testing (VRT): To guarantee rendering correctness, generated XLSX files are rendered via a headless LibreOffice Calc engine, and compared pixel-by-pixel against visual baselines.
|
|
234
|
+
### Performance & Types
|
|
235
|
+
* **Continuous Benchmarking**: Memory usage and processing speeds are profiled in CI on large datasets to prevent performance regressions and OOM leaks.
|
|
236
|
+
* **Runtime Type Validation**: Strong dynamic typing using `RBS::Test` to ensure the library's types are perfectly sound at runtime.
|
|
156
237
|
|
|
157
|
-
For
|
|
238
|
+
For a comprehensive breakdown of our QA matrix, see [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md). For details on running tests locally, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
158
239
|
|
|
159
240
|
## Development
|
|
160
241
|
|
|
161
242
|
We welcome contributions! The project is configured with a ready-to-use Dev Container to streamline local environment setup.
|
|
162
243
|
|
|
163
|
-
For contribution guidelines, E2E testing policies, and the step-by-step development workflow, please refer to [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
244
|
+
For contribution guidelines, E2E testing policies, and the step-by-step development workflow (including how to run the Dev Container from your terminal), please refer to [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
164
245
|
|
|
165
246
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
166
247
|
|
|
167
248
|
## Contributing
|
|
168
249
|
|
|
169
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/niku/xlsxrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](
|
|
250
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/niku/xlsxrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
170
251
|
|
|
171
252
|
## License
|
|
172
253
|
|
|
@@ -174,4 +255,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
174
255
|
|
|
175
256
|
## Code of Conduct
|
|
176
257
|
|
|
177
|
-
Everyone interacting in the Xlsxrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](
|
|
258
|
+
Everyone interacting in the Xlsxrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -88,6 +88,12 @@ end
|
|
|
88
88
|
task test: %i[build_sdk_runner ensure_reader_fixtures]
|
|
89
89
|
|
|
90
90
|
namespace :test do
|
|
91
|
+
desc "Run tests with runtime type checking enabled (RBS_TEST=1)"
|
|
92
|
+
task :rbs do
|
|
93
|
+
ENV["RBS_TEST"] = "1"
|
|
94
|
+
Rake::Task["test:unit"].invoke
|
|
95
|
+
Rake::Task["test:contract"].invoke
|
|
96
|
+
end
|
|
91
97
|
Rake::TestTask.new(:unit) do |t|
|
|
92
98
|
t.libs << "test"
|
|
93
99
|
t.libs << "lib"
|
|
@@ -139,7 +145,10 @@ require "rubocop/rake_task"
|
|
|
139
145
|
|
|
140
146
|
RuboCop::RakeTask.new
|
|
141
147
|
|
|
142
|
-
task
|
|
148
|
+
require "bundler/audit/task"
|
|
149
|
+
Bundler::Audit::Task.new
|
|
150
|
+
|
|
151
|
+
task default: %i[bundle:audit rubocop typecheck test]
|
|
143
152
|
|
|
144
153
|
namespace :visual do
|
|
145
154
|
desc "Generate docs/visual/ README.md explanation gallery"
|
|
@@ -210,6 +219,8 @@ task :wasm do
|
|
|
210
219
|
|
|
211
220
|
# A. Write custom static stubs and gateways dynamically
|
|
212
221
|
File.write(File.join(bundle_assets_dir, "openssl.rb"), "# frozen_string_literal: true\n")
|
|
222
|
+
time_rb_path = $LOAD_PATH.lazy.map { |p| File.join(p, "time.rb") }.find { |f| File.exist?(f) }
|
|
223
|
+
FileUtils.cp(time_rb_path, bundle_assets_dir) if time_rb_path
|
|
213
224
|
|
|
214
225
|
File.write(File.join(bundle_assets_dir, "opentelemetry.rb"), <<~RUBY)
|
|
215
226
|
# frozen_string_literal: true
|
|
@@ -410,7 +421,7 @@ task doc: %i[wasm fetch_assets] do
|
|
|
410
421
|
"--exclude 'docs/zeta\\.js' " \
|
|
411
422
|
"--exclude 'docs/office_thread\\.js' " \
|
|
412
423
|
"--exclude 'docs/preview\\.html' " \
|
|
413
|
-
"--title 'xlsxrb Documentation' --main README.md README.md \"docs/visual/VisualGallery.md\" lib/"
|
|
424
|
+
"--title 'xlsxrb Documentation' --main README.md README.md CHANGELOG.md CODE_OF_CONDUCT.md docs/*.md \"docs/visual/VisualGallery.md\" lib/"
|
|
414
425
|
|
|
415
426
|
# Copy visual gallery images and files so they are available in RDoc output
|
|
416
427
|
FileUtils.mkdir_p("doc/test/visual/baselines")
|
|
@@ -514,3 +525,13 @@ namespace :doc do
|
|
|
514
525
|
end
|
|
515
526
|
end
|
|
516
527
|
end
|
|
528
|
+
desc "Generate RBS signature files from inline annotations"
|
|
529
|
+
task :sig do
|
|
530
|
+
files = FileList["lib/**/*.rb"].to_a
|
|
531
|
+
sh "bundle", "exec", "rbs-inline", "--output=sig/generated", "--base=lib", *files
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
desc "Run static type checking with Steep"
|
|
535
|
+
task typecheck: :sig do
|
|
536
|
+
sh "bundle exec steep check"
|
|
537
|
+
end
|
data/Steepfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
target :lib do
|
|
4
|
+
signature "sig"
|
|
5
|
+
|
|
6
|
+
library "date"
|
|
7
|
+
library "time"
|
|
8
|
+
library "securerandom"
|
|
9
|
+
library "openssl"
|
|
10
|
+
library "pathname"
|
|
11
|
+
library "tempfile"
|
|
12
|
+
library "bigdecimal"
|
|
13
|
+
|
|
14
|
+
repo_path "sig"
|
|
15
|
+
|
|
16
|
+
configure_code_diagnostics(Steep::Diagnostic::Ruby.lenient)
|
|
17
|
+
end
|