roo-oxide 0.0.2-x64-mingw-ucrt
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 +7 -0
- data/CHANGELOG.md +63 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/lib/roo/calamine/version.rb +5 -0
- data/lib/roo/calamine.rb +3 -0
- data/lib/roo/oxide/3.3/roo_oxide.so +0 -0
- data/lib/roo/oxide/3.4/roo_oxide.so +0 -0
- data/lib/roo/oxide/4.0/roo_oxide.so +0 -0
- data/lib/roo/oxide/context.rb +74 -0
- data/lib/roo/oxide/csv.rb +157 -0
- data/lib/roo/oxide/deferred_sheets.rb +284 -0
- data/lib/roo/oxide/excelx.rb +156 -0
- data/lib/roo/oxide/formatters.rb +114 -0
- data/lib/roo/oxide/row_index.rb +93 -0
- data/lib/roo/oxide/row_iteration.rb +33 -0
- data/lib/roo/oxide/shared_strings.rb +46 -0
- data/lib/roo/oxide/sheet.rb +99 -0
- data/lib/roo/oxide/sheet_doc.rb +138 -0
- data/lib/roo/oxide/sheet_parts.rb +78 -0
- data/lib/roo/oxide/version.rb +7 -0
- data/lib/roo/oxide.rb +100 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e1af8f92835d68f057b646dd72d8d44585c00be6121bd2768450bdbbc0b2156d
|
|
4
|
+
data.tar.gz: d86051eabbb82cd4137e3114fa3d7b90074ff9f6f3ecf7b668454d43083679ca
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 98b9882d803e627badd20da08506ad6c19d4ebe5a9becde651bc933e7c8ec0405eff2530ff4f13dac481618f853d7391b1aad08c70812f3d9ac1e352903d32fd
|
|
7
|
+
data.tar.gz: 33ad164c32fbd2131a7618ee1159eaceedf0378ac36215c8147211aa680003b9e31c37bcb727250a58b49aafd44d8105872f27ce26f253d79cff86cfef1de459
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.2](https://github.com/salasebas/roo-oxide/compare/v0.0.1...v0.0.2) (2026-11-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* name the native Roo accelerator roo-oxide ([#78](https://github.com/salasebas/roo-oxide/issues/78)) ([6a67945](https://github.com/salasebas/roo-oxide/commit/6a6794599b3fe2ff3c68d11b82b0c78f9b812c1f))
|
|
9
|
+
* read values 2x faster than streaming with an opt-in Roo::Calamine.each_row_values ([#12](https://github.com/salasebas/roo-oxide/issues/12)) ([c4d02de](https://github.com/salasebas/roo-oxide/commit/c4d02de37458641a9e24db0de0d3b5eef7da5927))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Performance Improvements
|
|
13
|
+
|
|
14
|
+
* build cell coordinates in Rust for 3-13% faster reads ([#13](https://github.com/salasebas/roo-oxide/issues/13)) ([42a57f5](https://github.com/salasebas/roo-oxide/commit/42a57f5d044000fda04022e8de500d7fbb5deb53))
|
|
15
|
+
* build cell coordinates in Rust instead of Coordinate.new ([42a57f5](https://github.com/salasebas/roo-oxide/commit/42a57f5d044000fda04022e8de500d7fbb5deb53))
|
|
16
|
+
* build roo cells without running their Ruby constructors ([5f4561a](https://github.com/salasebas/roo-oxide/commit/5f4561a3821fc94f3c63c8eb1d3bfd273f7cffc7))
|
|
17
|
+
* cut clean row iteration time by 62% ([#70](https://github.com/salasebas/roo-oxide/issues/70)) ([43914e9](https://github.com/salasebas/roo-oxide/commit/43914e90cc018ba07fcc29f92d3e2a866ab3eccb))
|
|
18
|
+
* cut each_row_values peak memory by up to 30% by converting rows straight to values ([#23](https://github.com/salasebas/roo-oxide/issues/23)) ([82c2d69](https://github.com/salasebas/roo-oxide/commit/82c2d693b905df0d1aba4b195e930b898b92b34d))
|
|
19
|
+
* cut IO workbook streaming memory by 53-58% ([#56](https://github.com/salasebas/roo-oxide/issues/56)) ([735e7b0](https://github.com/salasebas/roo-oxide/commit/735e7b00b6c8e335704492a5fdc5fa92d27312ed))
|
|
20
|
+
* cut parse allocations by 200k and small cell lookup time by 20% ([#42](https://github.com/salasebas/roo-oxide/issues/42)) ([fdb6a93](https://github.com/salasebas/roo-oxide/commit/fdb6a9335d53d7c8117bce8d47b1e448dce81cf5))
|
|
21
|
+
* cut peak memory 11–19% (65 MiB) on workbooks with many unique strings by extracting sharedStrings.xml natively ([#37](https://github.com/salasebas/roo-oxide/issues/37)) ([7722a73](https://github.com/salasebas/roo-oxide/commit/7722a733eef5ca146d6ea4d5639aaf36fe22d1f8))
|
|
22
|
+
* cut peak memory of row, column, and cell reads by up to 26% with 20-byte native records ([#32](https://github.com/salasebas/roo-oxide/issues/32)) ([f165dc5](https://github.com/salasebas/roo-oxide/commit/f165dc5048e5061c9f5a05ba60256f0309670ee9))
|
|
23
|
+
* cut row reads by parsing plain cells directly ([fd71931](https://github.com/salasebas/roo-oxide/commit/fd7193170749a7462e8450d131e501317aad5c9b))
|
|
24
|
+
* cut small-workbook opening time by 37% ([#60](https://github.com/salasebas/roo-oxide/issues/60)) ([2cd40eb](https://github.com/salasebas/roo-oxide/commit/2cd40eb5e3c63839dcfdd9b28ca76567f21916ac))
|
|
25
|
+
* cut temporal CSV and formatted read times by 46–66% ([#71](https://github.com/salasebas/roo-oxide/issues/71)) ([21f9da6](https://github.com/salasebas/roo-oxide/commit/21f9da66f1260eb41469f1b6ef57a98b8d57f871))
|
|
26
|
+
* cut unique-string document reads by 25–42% ([#61](https://github.com/salasebas/roo-oxide/issues/61)) ([2660f9a](https://github.com/salasebas/roo-oxide/commit/2660f9a113800fdb6819cd6ecf77193f329227d2))
|
|
27
|
+
* cut unique-string peak memory 23–43% by deferring HTML strings ([#44](https://github.com/salasebas/roo-oxide/issues/44)) ([7806c6f](https://github.com/salasebas/roo-oxide/commit/7806c6fe40f9f52e9aa048be186d4a79ec24016a))
|
|
28
|
+
* export YAML, XML and Matrix 19–51× faster ([#58](https://github.com/salasebas/roo-oxide/issues/58)) ([e979933](https://github.com/salasebas/roo-oxide/commit/e979933c97338a768bc606d7b374f50621081481))
|
|
29
|
+
* hand each_row_streaming the native row array directly ([bf37bc7](https://github.com/salasebas/roo-oxide/commit/bf37bc717c51f43f1b9a8745ff066f42673d55fd))
|
|
30
|
+
* make each 8× faster on sparse sheets built whole by indexing their cells by row ([#30](https://github.com/salasebas/roo-oxide/issues/30)) ([f915c14](https://github.com/salasebas/roo-oxide/commit/f915c14ad69325988b7006627652ba736ca9738f))
|
|
31
|
+
* make each and parse(headers: true) 2–2.6× faster with half the memory by reading rows from the native records ([#25](https://github.com/salasebas/roo-oxide/issues/25)) ([e5ae0a9](https://github.com/salasebas/roo-oxide/commit/e5ae0a91ab814e5aa6eb2cd70cd4e31fcfc34b87))
|
|
32
|
+
* make each without options up to 47% faster on large sheets ([#18](https://github.com/salasebas/roo-oxide/issues/18)) ([739f02c](https://github.com/salasebas/roo-oxide/commit/739f02c5fed5f46c074a91776ba62180a75bc0d3))
|
|
33
|
+
* make header iteration as fast as parse ([#57](https://github.com/salasebas/roo-oxide/issues/57)) ([03f8520](https://github.com/salasebas/roo-oxide/commit/03f8520a59ceb938a2f357ab2dad6a8fb0e27811))
|
|
34
|
+
* make parse(headers: true) 2x faster by grouping the cells hash in one pass ([#14](https://github.com/salasebas/roo-oxide/issues/14)) ([1d15aae](https://github.com/salasebas/roo-oxide/commit/1d15aae5d0ef83413be288157f012e517c35e54f))
|
|
35
|
+
* make single-cell reads 2x faster with 3x less memory by building cells lazily ([#17](https://github.com/salasebas/roo-oxide/issues/17)) ([c2417fb](https://github.com/salasebas/roo-oxide/commit/c2417fb3238d76da4fe30bd437ea95b2859bc604))
|
|
36
|
+
* read columns 2.6–8.4× faster with half the memory from the native records ([#31](https://github.com/salasebas/roo-oxide/issues/31)) ([8b0b683](https://github.com/salasebas/roo-oxide/commit/8b0b683935b3ec7f78513f360221d4eb208a5638))
|
|
37
|
+
* read hyperlinks and merged ranges natively instead of loading the whole sheet into Nokogiri ([29e5fd6](https://github.com/salasebas/roo-oxide/commit/29e5fd64ce4e1a36d11b5eee7a48fc75fa49e792))
|
|
38
|
+
* read row values and random cells 30-40% faster by parsing each tag's attributes once ([#22](https://github.com/salasebas/roo-oxide/issues/22)) ([afd5ad1](https://github.com/salasebas/roo-oxide/commit/afd5ad13b88670eefa4647ce5b41e960d500b04f))
|
|
39
|
+
* read sheets with hyperlinks or expanded merged ranges 30-60% faster in up to 20x less memory ([#26](https://github.com/salasebas/roo-oxide/issues/26)) ([29e5fd6](https://github.com/salasebas/roo-oxide/commit/29e5fd64ce4e1a36d11b5eee7a48fc75fa49e792))
|
|
40
|
+
* read small workbook rows 8–13% faster with in-memory metadata ([#45](https://github.com/salasebas/roo-oxide/issues/45)) ([eb7c4e9](https://github.com/salasebas/roo-oxide/commit/eb7c4e92ee31f981a6ffeaf32b7528ddba40125e))
|
|
41
|
+
* read small worksheets 11% faster natively with lazy ZIP inflation ([#43](https://github.com/salasebas/roo-oxide/issues/43)) ([65bd561](https://github.com/salasebas/roo-oxide/commit/65bd56137023b87c5e588ee6f9817191fe84f646))
|
|
42
|
+
* read workbooks full of unique or rich-text strings 4-17x faster by parsing shared strings natively ([#27](https://github.com/salasebas/roo-oxide/issues/27)) ([de00e34](https://github.com/salasebas/roo-oxide/commit/de00e3429d3d6d8b2a4a76e7db6f5ea3d9520f52))
|
|
43
|
+
* read worksheets from a copy of the xlsx instead of roo's extracted files ([77c232b](https://github.com/salasebas/roo-oxide/commit/77c232bfd736e85257782c767a48c73b6adec433))
|
|
44
|
+
* read worksheets straight from a copy of the xlsx zip, with up to 60% less peak memory on large files ([#28](https://github.com/salasebas/roo-oxide/issues/28)) ([77c232b](https://github.com/salasebas/roo-oxide/commit/77c232bfd736e85257782c767a48c73b6adec433))
|
|
45
|
+
* reduce Array row iteration overhead ([#59](https://github.com/salasebas/roo-oxide/issues/59)) ([5ab8230](https://github.com/salasebas/roo-oxide/commit/5ab823065464f9c38dbbe9f6f55b87e4ac5eb76f))
|
|
46
|
+
* reduce native cell construction overhead ([c974479](https://github.com/salasebas/roo-oxide/commit/c9744797ca42b46787479195201a6863e32b6856))
|
|
47
|
+
* reduce row-read overhead by parsing plain cells directly ([#69](https://github.com/salasebas/roo-oxide/issues/69)) ([fd71931](https://github.com/salasebas/roo-oxide/commit/fd7193170749a7462e8450d131e501317aad5c9b))
|
|
48
|
+
* reduce single-cell read latency on small sheets ([#73](https://github.com/salasebas/roo-oxide/issues/73)) ([194b149](https://github.com/salasebas/roo-oxide/commit/194b149d64a2682fa43fc54319a640dfe4eb300e))
|
|
49
|
+
* speed up each_row_values 7–9% and each, cell and parse 3–9% by reading common cell attributes on a fast path ([#36](https://github.com/salasebas/roo-oxide/issues/36)) ([d98ff8a](https://github.com/salasebas/roo-oxide/commit/d98ff8ac9ba96e30badab264e5e37a41029fe9cc))
|
|
50
|
+
* speed up hyperlink reads and row iteration ([#72](https://github.com/salasebas/roo-oxide/issues/72)) ([4f47c26](https://github.com/salasebas/roo-oxide/commit/4f47c264ae9ead7f83dbc5e3c4596d53f3172b55))
|
|
51
|
+
* speed up initial temporal scans 40–42% with 31–33% less memory ([#41](https://github.com/salasebas/roo-oxide/issues/41)) ([c974479](https://github.com/salasebas/roo-oxide/commit/c9744797ca42b46787479195201a6863e32b6856))
|
|
52
|
+
* stream rows 7–18% faster by yielding the native row array without copying it ([#35](https://github.com/salasebas/roo-oxide/issues/35)) ([bf37bc7](https://github.com/salasebas/roo-oxide/commit/bf37bc717c51f43f1b9a8745ff066f42673d55fd))
|
|
53
|
+
* stream tall sheets ~10% faster by handing Ruby one row at a time ([#24](https://github.com/salasebas/roo-oxide/issues/24)) ([4370a4b](https://github.com/salasebas/roo-oxide/commit/4370a4bac1656b01ff976b3815f9eb382101c83b))
|
|
54
|
+
* stream xlsx 30% faster by building roo cells without their constructors ([#15](https://github.com/salasebas/roo-oxide/issues/15)) ([5f4561a](https://github.com/salasebas/roo-oxide/commit/5f4561a3821fc94f3c63c8eb1d3bfd273f7cffc7))
|
|
55
|
+
* write to_csv 2–22× faster with byte-identical output by walking the cells once ([#29](https://github.com/salasebas/roo-oxide/issues/29)) ([2094b39](https://github.com/salasebas/roo-oxide/commit/2094b39d9259dd1787b457a4d4c6dff9df7e6cf1))
|
|
56
|
+
* write to_csv 2.2–3.9× faster with up to 83% less peak memory from the native records ([#38](https://github.com/salasebas/roo-oxide/issues/38)) ([8293459](https://github.com/salasebas/roo-oxide/commit/829345998667a208d1c76abc26f4242f2c1a2bd8))
|
|
57
|
+
|
|
58
|
+
## 0.0.1 (2026-11-10)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Features
|
|
62
|
+
|
|
63
|
+
* make roo read xlsx files 4-12x faster with identical results ([#1](https://github.com/salasebas/roo-calamine/issues/1)) ([dce0aa4](https://github.com/salasebas/roo-calamine/commit/dce0aa4737b6e376ef01eb4f2aa8f602331d4d0b))
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Sala
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# roo-oxide
|
|
2
|
+
|
|
3
|
+
Native acceleration for [Roo](https://github.com/roo-rb/roo), powered by Rust.
|
|
4
|
+
|
|
5
|
+
roo-oxide is built on top of Roo and installs it as a dependency. It replaces
|
|
6
|
+
Roo's busiest `.xlsx` and `.xlsm` read paths with a native engine, while you
|
|
7
|
+
keep using `Roo::Spreadsheet`, `Roo::Excelx` and the same cell/value APIs.
|
|
8
|
+
On the measured large sheets, reads are **16–36× faster** with **2–31× less
|
|
9
|
+
memory**. Your existing Roo code stays the same.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "roo-oxide"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
xlsx = Roo::Spreadsheet.open("orders.xlsx") # or Roo::Excelx.new("orders.xlsx")
|
|
18
|
+
xlsx.each_row_streaming { |row| ... } # read by the native reader
|
|
19
|
+
xlsx.parse(headers: true) # this too
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **Unofficial.** roo-oxide is not affiliated with or endorsed by roo-rb or
|
|
23
|
+
> the roo maintainers.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Add only `gem "roo-oxide"` to your Gemfile. Bundler installs Roo automatically
|
|
28
|
+
and loads the acceleration layer on `Bundler.require` (as Rails does).
|
|
29
|
+
Without Bundler, install `roo-oxide` and `require "roo/oxide"`; it loads Roo
|
|
30
|
+
for you.
|
|
31
|
+
|
|
32
|
+
### Migrating from roo-calamine
|
|
33
|
+
|
|
34
|
+
Replace `gem "roo-calamine"` with `gem "roo-oxide"`. Roo is installed as a
|
|
35
|
+
dependency, so a separate `gem "roo"` line is optional.
|
|
36
|
+
Use `require "roo/oxide"` and `Roo::Oxide` in new code. The old
|
|
37
|
+
`require "roo/calamine"`, version require and `Roo::Calamine` constant remain
|
|
38
|
+
available as compatibility aliases in the new gem. Load one plugin gem at a
|
|
39
|
+
time; loading the old plugin before roo-oxide raises a migration error.
|
|
40
|
+
|
|
41
|
+
`ROO_CALAMINE` still works when `ROO_OXIDE` is unset. When both are set,
|
|
42
|
+
`ROO_OXIDE` takes precedence.
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Ruby | CRuby 3.3, 3.4, 4.0 |
|
|
47
|
+
| roo | `~> 4.0` |
|
|
48
|
+
| Precompiled | Linux x86_64/aarch64 (glibc, musl), macOS x86_64/arm64, Windows x64 |
|
|
49
|
+
| Other platforms | the source gem compiles with Rust 1.86 or later |
|
|
50
|
+
| JRuby | installs without Rust and stays inert: roo reads every file itself |
|
|
51
|
+
|
|
52
|
+
## Benchmarks
|
|
53
|
+
|
|
54
|
+
100,000 rows × 20 mixed columns, Apple M5 Pro, Ruby 3.4.12. Time and peak
|
|
55
|
+
memory include opening the workbook.
|
|
56
|
+
|
|
57
|
+
| | roo 4.0.0 | roo + roo-oxide | Faster |
|
|
58
|
+
|---|---:|---:|---:|
|
|
59
|
+
| `each_row_streaming` | 13.49 s · 119 MB | **0.59 s · 51 MB** | 23× |
|
|
60
|
+
| `parse(headers: true)` | 19.61 s · 3300 MB | **0.75 s · 282 MB** | 26× |
|
|
61
|
+
| `each` | 18.81 s · 3282 MB | **0.62 s · 174 MB** | 30× |
|
|
62
|
+
| 10,000 random `cell(row, col)` | 17.25 s · 3272 MB | **0.48 s · 104 MB** | 36× |
|
|
63
|
+
| `to_csv` | 21.72 s · 3369 MB | **0.77 s · 118 MB** | 28× |
|
|
64
|
+
| `to_yaml` | 24.26 s · 3430 MB | **1.28 s · 322 MB** | 19× |
|
|
65
|
+
| `to_xml` | 50.81 s · 3991 MB | **1.04 s · 711 MB** | 49× |
|
|
66
|
+
| `to_matrix` | 19.97 s · 3287 MB | **0.65 s · 216 MB** | 31× |
|
|
67
|
+
| [`each_row_values`](#values-only) | 13.43 s · 118 MB | **0.44 s · 47 MB** | 31× |
|
|
68
|
+
|
|
69
|
+
On the other test files it is 18–43× faster with shared strings, 16–33× with
|
|
70
|
+
unique strings, and 2.6–10× on a 100-row sheet. See
|
|
71
|
+
[bench/README.md](bench/README.md) for every file, the method, and how to run
|
|
72
|
+
it. [bench/RESULTS.md](bench/RESULTS.md) times every public roo read. It
|
|
73
|
+
ties roo only where roo reads the file itself: CSV, and XML roo-oxide
|
|
74
|
+
hands back.
|
|
75
|
+
|
|
76
|
+
## Values only
|
|
77
|
+
|
|
78
|
+
If you only need values, `Roo::Oxide.each_row_values` skips roo's cell
|
|
79
|
+
objects. It is roo-oxide's own API, not roo's, and using it changes nothing
|
|
80
|
+
in what roo does.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Roo::Oxide.each_row_values("orders.xlsx", sheet: "Orders") do |values|
|
|
84
|
+
values # => [1001, "Ada", #<Date: 2024-05-01>, 19.5, nil, true]
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Each row equals, value for value, what this roo code yields:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Roo::Excelx.new("orders.xlsx").each_row_streaming(sheet: "Orders", pad_cells: true) do |cells|
|
|
92
|
+
cells.map { |cell| cell&.value }
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Arguments and value rules</summary>
|
|
98
|
+
|
|
99
|
+
- **Arguments:** a path or an IO; `sheet:` is a name or index (default: the
|
|
100
|
+
first sheet); other keywords are `Roo::Excelx.new` options.
|
|
101
|
+
- **Return:** an Enumerator without a block. It closes the workbook when it is
|
|
102
|
+
done.
|
|
103
|
+
- **Rows:** one entry per column from A to the row's last cell, with `nil`
|
|
104
|
+
where there is no cell.
|
|
105
|
+
- **Values** follow roo's rules: Integer or Float by the written text and
|
|
106
|
+
number format; Date, DateTime and Time by the style; roo's shared strings
|
|
107
|
+
(HTML for rich text); `Roo::Link` for hyperlinks; error text such as
|
|
108
|
+
`"#DIV/0!"`.
|
|
109
|
+
- With the reader off, or on JRuby, it reads through roo and yields the same
|
|
110
|
+
rows.
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
## Turning it off
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Roo::Oxide.enabled = false # from the next sheet read on
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
ROO_OXIDE=0 bundle exec rails server # also: false, off
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Either one makes roo read exactly as it does without the gem.
|
|
125
|
+
`Roo::Oxide.enabled?` tells whether the next read uses the native reader,
|
|
126
|
+
and `Roo::Oxide.available?` whether the extension is loaded.
|
|
127
|
+
|
|
128
|
+
## How it works
|
|
129
|
+
|
|
130
|
+
roo-oxide prepends modules to roo's classes. A read such as
|
|
131
|
+
`each_row_streaming` reaches roo-oxide first, which either does the work in
|
|
132
|
+
a Rust extension and returns the objects roo would return, or hands the call
|
|
133
|
+
back to roo.
|
|
134
|
+
|
|
135
|
+
- **Opening:** worksheets stay in the zip until they are read.
|
|
136
|
+
- **Parsing:** worksheet XML is streamed with
|
|
137
|
+
[quick-xml](https://github.com/tafia/quick-xml) instead of loaded into a
|
|
138
|
+
Nokogiri document.
|
|
139
|
+
- **Lazy cells:** the sheet lives in compact native records, and a
|
|
140
|
+
`Roo::Excelx::Cell` is built only when something reads it.
|
|
141
|
+
- **Output:** `to_csv`, `to_yaml`, `to_xml`, and `to_matrix` are written from
|
|
142
|
+
those records.
|
|
143
|
+
- **Fallback:** XML the reader cannot reproduce exactly the way Nokogiri does
|
|
144
|
+
goes back to roo.
|
|
145
|
+
|
|
146
|
+
Styles, number formats, comments, images, and every open option stay roo's
|
|
147
|
+
code. roo is the test oracle: roo 4.0.0's own suite, every roo fixture, and the
|
|
148
|
+
edge-case suites must give the same results with and without roo-oxide.
|
|
149
|
+
[docs/how-it-works.md](docs/how-it-works.md) has the details and the test
|
|
150
|
+
coverage.
|
|
151
|
+
|
|
152
|
+
### Known differences
|
|
153
|
+
|
|
154
|
+
These are by design.
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>Overrides of roo's private methods</summary>
|
|
158
|
+
|
|
159
|
+
- Under roo 4.0.0, overrides of roo's private shared-string parsing
|
|
160
|
+
(`SharedStrings#extract_shared_strings`, `#extract_html`,
|
|
161
|
+
`#fix_invalid_shared_strings`, `#create_html`) are bypassed while the reader
|
|
162
|
+
is on. Other roo versions build their shared strings themselves.
|
|
163
|
+
- Overrides of roo's private cell parsing (`SheetDoc#cell_from_xml`,
|
|
164
|
+
`#extract_cells`, `#extract_hyperlinks`, `#expand_merged_ranges`,
|
|
165
|
+
`Cell.cell_class`) are bypassed while the reader is on. So are:
|
|
166
|
+
- a `Roo::Excelx` subclass's `#cell` or `#each` inside `parse` and `each`
|
|
167
|
+
with options;
|
|
168
|
+
- its `#cell`, `#celltype`, `#empty?`, or roo's private `#cell_to_csv`
|
|
169
|
+
inside `to_csv`;
|
|
170
|
+
- redefinitions of the
|
|
171
|
+
[single-cell reads](docs/how-it-works.md#cells-and-values) in `Roo::Excelx`
|
|
172
|
+
or `Roo::Excelx::Sheet` themselves (subclasses work);
|
|
173
|
+
- under roo 4.0.0, an override of `initialize` on a `Cell` class or on
|
|
174
|
+
`Roo::Excelx::Coordinate`;
|
|
175
|
+
- under roo 4.0.0, `Sheet#pad_cells` redefined in `Roo::Excelx::Sheet`
|
|
176
|
+
itself (subclasses and prepended overrides work).
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary>Behavior and performance</summary>
|
|
182
|
+
|
|
183
|
+
- **Building `Sheet#cells` late costs more.** Building the whole hash (`#set`,
|
|
184
|
+
for example) after a read that does not need it (a single-cell read,
|
|
185
|
+
`#last_row`, `#row`, `#each`, `#parse`) costs more than building it from the
|
|
186
|
+
start, because the sheet is parsed before any cell is built rather than
|
|
187
|
+
alongside.
|
|
188
|
+
- **Non-UTF-8 text set by hand.** A cell value set to non-ASCII text in an
|
|
189
|
+
encoding other than UTF-8 can make `to_csv` raise
|
|
190
|
+
`Encoding::CompatibilityError`, where roo writes it, when its line also
|
|
191
|
+
holds non-ASCII UTF-8 text. Cells read from a file are always UTF-8.
|
|
192
|
+
- **Writes straight into `cells`.** On a
|
|
193
|
+
[sheet built whole](docs/how-it-works.md#cells-and-values), inside an `each`
|
|
194
|
+
block without options, a write straight into `sheet_for(...).cells` (not
|
|
195
|
+
through `set`) does not show in later rows.
|
|
196
|
+
- **Broken XML.** On XML that breaks partway, `each_row_streaming` raises the
|
|
197
|
+
same `Nokogiri::XML::SyntaxError` as roo but yields a few more rows before
|
|
198
|
+
it.
|
|
199
|
+
- **Errors come when a sheet is read.** A workbook leaves its worksheets in
|
|
200
|
+
the zip, so a worksheet whose compressed data is broken, or larger than the
|
|
201
|
+
zip says, raises roo's error when it is read rather than in
|
|
202
|
+
`Roo::Excelx.new`. The paths in `#sheet_files` exist only once roo's own
|
|
203
|
+
code has read them.
|
|
204
|
+
|
|
205
|
+
</details>
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
bundle install
|
|
211
|
+
bundle exec rake compile # build the extension
|
|
212
|
+
bundle exec rake test # unit, fixture, and edge-case tests
|
|
213
|
+
bundle exec rake parity # roo 4.0.0's own suites, with and without roo-oxide
|
|
214
|
+
bundle exec rubocop
|
|
215
|
+
cargo test && cargo clippy --all-targets -- -D warnings && cargo fmt --check
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT. See [LICENSE](LICENSE).
|
data/lib/roo/calamine.rb
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# The answers the native reader needs from roo while it builds cells. Each
|
|
6
|
+
# one comes from the roo object or method that SheetDoc#cell_from_xml uses,
|
|
7
|
+
# so formats, shared strings, and errors are roo's own.
|
|
8
|
+
class Context
|
|
9
|
+
# roo versions whose cell constructors the native reader reproduces:
|
|
10
|
+
# under them it sets each cell's instance variables itself, and under
|
|
11
|
+
# any other version it calls the constructors.
|
|
12
|
+
CELL_LAYOUT_ROO_VERSIONS = %w[4.0.0].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :hyperlinks, :empty_cell
|
|
15
|
+
|
|
16
|
+
def initialize(shared, hyperlinks, empty_cell)
|
|
17
|
+
@shared = shared
|
|
18
|
+
@hyperlinks = hyperlinks
|
|
19
|
+
@empty_cell = empty_cell
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_date
|
|
23
|
+
@shared.base_date
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def base_timestamp
|
|
27
|
+
@shared.base_timestamp
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_cells?
|
|
31
|
+
CELL_LAYOUT_ROO_VERSIONS.include?(Roo::VERSION)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The format string, its type, and whether Cell::Number#create_numeric
|
|
35
|
+
# always calls Float under it (nil when matching it raises).
|
|
36
|
+
def style(index)
|
|
37
|
+
format = @shared.styles.style_format(index)
|
|
38
|
+
[format.to_s, Roo::Excelx::Format.to_type(format), float_format(format.to_s)]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def style_index(text)
|
|
42
|
+
text.to_i
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def coordinate(reference)
|
|
46
|
+
Roo::Utils.extract_coordinate(reference)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Roo's plain strings and deferred or public HTML table, nil when disabled.
|
|
50
|
+
def shared_strings
|
|
51
|
+
strings = @shared.shared_strings
|
|
52
|
+
html_disabled = strings.instance_variable_get(:@options)[:disable_html_wrapper]
|
|
53
|
+
plain = strings.to_a
|
|
54
|
+
[plain, (strings.oxide_html unless html_disabled)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def shared_string(text)
|
|
58
|
+
shared_string_at(@shared.shared_strings, text.to_i)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def float_format(format)
|
|
64
|
+
format.match?(/%|\.0/)
|
|
65
|
+
rescue ArgumentError, EncodingError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def shared_string_at(strings, index)
|
|
70
|
+
strings.use_html?(index) ? strings.to_html[index] : strings[index]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module Roo
|
|
6
|
+
module Oxide
|
|
7
|
+
# Prepended to Roo::Excelx. roo's #to_csv writes every slot from the first
|
|
8
|
+
# row and column to the last through #empty?, #cell and #celltype, and each
|
|
9
|
+
# of them looks up the sheet and the cell again. This writes the same text
|
|
10
|
+
# from one pass over the sheet's cells: natively from the lazy cells while
|
|
11
|
+
# the sheet has them, else a line at a time, asking each cell what roo's
|
|
12
|
+
# Roo::Formatters::CSV#cell_to_csv asks it, in the same order.
|
|
13
|
+
module CSV
|
|
14
|
+
# roo versions whose Roo::Formatters::CSV this reproduces. Under any other
|
|
15
|
+
# version roo writes the CSV itself.
|
|
16
|
+
ROO_VERSIONS = %w[4.0.0].freeze
|
|
17
|
+
private_constant :ROO_VERSIONS
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def write_csv_content(file = nil, sheet = nil, separator = ",")
|
|
22
|
+
return super unless csv_lines?(separator.to_s)
|
|
23
|
+
|
|
24
|
+
file ||= STDOUT # rubocop:disable Style/GlobalStdStream
|
|
25
|
+
return unless first_row(sheet)
|
|
26
|
+
|
|
27
|
+
target = sheet_for(sheet)
|
|
28
|
+
area = [last_row(sheet), last_column(sheet), target.first_row, target.last_row, target.first_column,
|
|
29
|
+
target.last_column]
|
|
30
|
+
store = target.lazy_cells
|
|
31
|
+
if store && area.all?(Integer)
|
|
32
|
+
store.write_csv(file, self, separator.to_s, area)
|
|
33
|
+
else
|
|
34
|
+
write_csv_lines(file, target, separator.to_s, *area.first(2))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# roo prints each field on its own; a line joins them, so the separator
|
|
39
|
+
# has to join UTF-8 text, and IO#print must add no $\ between fields.
|
|
40
|
+
def csv_lines?(separator)
|
|
41
|
+
Oxide.enabled? && ROO_VERSIONS.include?(Roo::VERSION) && $OUTPUT_RECORD_SEPARATOR.nil? &&
|
|
42
|
+
(separator.ascii_only? || separator.encoding == Encoding::UTF_8)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# roo writes rows 1..last_row and columns 1..last_column, and prints the
|
|
46
|
+
# separator before every column but the first, then the field, so after
|
|
47
|
+
# a field raises the file holds the line up to it.
|
|
48
|
+
def write_csv_lines(file, sheet, separator, rows, width) # rubocop:disable Metrics
|
|
49
|
+
first_row = sheet.first_row
|
|
50
|
+
last_row = sheet.last_row
|
|
51
|
+
first_column = sheet.first_column
|
|
52
|
+
last_column = sheet.last_column
|
|
53
|
+
separators = Hash.new { |runs, count| runs[count] = separator * count }
|
|
54
|
+
blank_line = "#{separators[[width - 1, 0].max]}\n"
|
|
55
|
+
line = +""
|
|
56
|
+
line_row = written = 0
|
|
57
|
+
each_csv_cell(sheet.cells, rows, width) do |row, column, cell|
|
|
58
|
+
if row != line_row
|
|
59
|
+
file.print(line << separators[width - 1 - written] << "\n") if line_row.positive?
|
|
60
|
+
(row - line_row - 1).times { file.print(blank_line) }
|
|
61
|
+
line.clear
|
|
62
|
+
line_row = row
|
|
63
|
+
written = 0
|
|
64
|
+
end
|
|
65
|
+
line << separators[column - 1 - written]
|
|
66
|
+
written = column - 1
|
|
67
|
+
begin
|
|
68
|
+
next if !cell || cell.empty? || row < first_row || row > last_row ||
|
|
69
|
+
column < first_column || column > last_column
|
|
70
|
+
|
|
71
|
+
line << csv_field(cell)
|
|
72
|
+
rescue StandardError
|
|
73
|
+
file.print(line)
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
file.print(line << separators[width - 1 - written] << "\n") if line_row.positive?
|
|
78
|
+
(line_row + 1).upto(rows) { file.print(blank_line) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Yields the cells roo visits, by row and then column: those with an
|
|
82
|
+
# Integer row in 1..rows and an Integer column in 1..columns.
|
|
83
|
+
def each_csv_cell(cells, rows, columns)
|
|
84
|
+
if csv_keys_sorted?(cells)
|
|
85
|
+
cells.each_pair do |(row, column), cell|
|
|
86
|
+
break if row > rows
|
|
87
|
+
|
|
88
|
+
yield row, column, cell if row.positive? && column.between?(1, columns)
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
sorted_csv_keys(cells, rows, columns).each { |key| yield key[0], key[1], cells[key] }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sorted_csv_keys(cells, rows, columns)
|
|
96
|
+
keys = cells.keys.select do |key|
|
|
97
|
+
key.is_a?(Array) && key.size == 2 && key.all?(Integer) &&
|
|
98
|
+
key[0].between?(1, rows) && key[1].between?(1, columns)
|
|
99
|
+
end
|
|
100
|
+
keys.sort!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether every key is a [row, column] of Integers, in increasing order
|
|
104
|
+
# from row 0.
|
|
105
|
+
def csv_keys_sorted?(cells) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
106
|
+
previous_row = previous_column = 0
|
|
107
|
+
cells.each_key do |key|
|
|
108
|
+
return false unless key.is_a?(Array) && key.size == 2
|
|
109
|
+
|
|
110
|
+
row, column = key
|
|
111
|
+
return false unless row.is_a?(Integer) && column.is_a?(Integer)
|
|
112
|
+
|
|
113
|
+
if row == previous_row
|
|
114
|
+
return false if column < previous_column
|
|
115
|
+
elsif row < previous_row
|
|
116
|
+
return false
|
|
117
|
+
end
|
|
118
|
+
previous_row = row
|
|
119
|
+
previous_column = column
|
|
120
|
+
end
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Roo::Formatters::CSV#cell_to_csv after its #empty? check.
|
|
125
|
+
def csv_field(cell) # rubocop:disable Metrics/CyclomaticComplexity
|
|
126
|
+
value = cell.value
|
|
127
|
+
case (type = cell.type)
|
|
128
|
+
when :string then csv_quote(value) unless value.empty?
|
|
129
|
+
when :boolean then %("#{cell.formatted_value.gsub('"', '""').downcase}")
|
|
130
|
+
when :float, :percentage then csv_number(value)
|
|
131
|
+
when :formula then csv_formula_value(value)
|
|
132
|
+
when :date, :datetime then value.to_s
|
|
133
|
+
when :time then integer_to_timestring(value)
|
|
134
|
+
when :link then csv_quote(value.url)
|
|
135
|
+
else raise "unhandled celltype #{type}"
|
|
136
|
+
end || ""
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def csv_formula_value(value)
|
|
140
|
+
case value
|
|
141
|
+
when String then csv_quote(value) unless value.empty?
|
|
142
|
+
when Integer, Date, DateTime, TrueClass, FalseClass then value.to_s
|
|
143
|
+
when Float then csv_number(value)
|
|
144
|
+
else raise "unhandled onecell-class #{value.class}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def csv_quote(text)
|
|
149
|
+
text.include?('"') ? %("#{text.gsub('"', '""')}") : %("#{text}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def csv_number(value)
|
|
153
|
+
value == value.to_i ? value.to_i.to_s : value.to_s
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|