xlsxrb 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f865582e17bc03ede4f300f348f6166a666d82cd088f42a501a6518979a7b999
4
- data.tar.gz: 25ba8105a6ca0318aa9bb4766a0de59da4818d18d226d212f1ec941fde3082c9
3
+ metadata.gz: bf81256a8c23f02b4ef5a1f2efb91122850960da349cc1c00bce37d4055486ac
4
+ data.tar.gz: c0fdb2a48e67beb7a59730fe947ca819c5ed72196c858b02a430cb5f85e953c5
5
5
  SHA512:
6
- metadata.gz: 6e35bcdabcff60af9084c9860fcf1838abfb507065a2eabb1004f40f16ab6cf6f7a11d136bbd83e97f46f0e460b906601db1584ef23b00c05076bfc240417baf
7
- data.tar.gz: 8d4597dad8fe327f4ad887934a7f2bc18fcd65b606ef265ab38bcef0a0ae2565dc09f9e7f04dd8dc5bf7c3403fadc2a265d3ce074c9fd623489d8f4004065802
6
+ metadata.gz: a4e3b7098c50d3d9e5958333f49ad2b8c22399f383067c01b0481fb82e9d1eaa50481de15afe8b2b883d6f0ee1697144ae55d028771eeca159cc38f46626043f
7
+ data.tar.gz: 25c3979864c017334256986e5697c793f40b22b3cdc7419d18e3ef090925f0297909ca8fafc9188cf37769344e656fa7e900cf8da8f3a310599e1cd9ab61ed8b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.1.11] - 2026-08-19
2
+
3
+ ### Performance
4
+ - **Sub-Second Streaming Write (< 1.0s / 1,000,000 cells)**:
5
+ - Achieved sub-second streaming write performance (**0.97 s** median for 1M cells) while maintaining 100% full SST (Shared String Table) XML deduplication and ISO/IEC 29500 compatibility.
6
+ - Implemented 1-Pass Direct ZIP streaming pipeline, eliminating all intermediate tempfiles, disk seek-backs, and extra deflate flush cycles.
7
+ - Added dedicated unstyled fast-path in `WorksheetWriter#write_row_values` with precomputed coordinate and integer lookup tables.
8
+ - Pre-registered default date/time formatting styles, completely eliminating redundant per-row scan loops.
9
+ - Streamlined `ZipWriter` instance variable lookups to eliminate per-chunk hash overhead.
10
+ - Added `alias << row` to `WorksheetProxy` and `StreamWriter` for idiomatic Ruby streaming.
11
+ - **High-Speed Streaming Read (1.61s / 1,000,000 cells)**:
12
+ - Reduced streaming read execution time from **3.40 s** down to **1.61 s** (over 2x speedup).
13
+ - Refactored `Elements::Cell` into an optimized lightweight class with `Cell.fast_create`, reducing 1M cell instantiation overhead by 5.2x (0.92s → 0.17s) while maintaining full pattern matching (`deconstruct`, `deconstruct_keys`) and immutability.
14
+ - Replaced per-byte Ruby scanning in `WorksheetParser.fast_scan_cells_direct` with an Onigmo C-level regular expression scanner (`CELL_FAST_RE`), eliminating 100,000 intermediate XML substring allocations and nested byte loops.
15
+ - Optimized `StreamRow#cells` to populate cell arrays via direct block traversal, eliminating Enumerator object allocations.
16
+
17
+ ### Documentation
18
+ - Updated benchmark results, performance comparisons, and linear-scale SVG charts across `README.md` and `docs/PEER_LIBRARIES.md`.
19
+
1
20
  ## [0.1.10] - 2026-08-19
2
21
 
3
22
  ### Added
data/benchmark.rb CHANGED
@@ -9,6 +9,7 @@ require "bundler/inline"
9
9
  puts "Ensuring benchmark peer ecosystem gems are available (bundler/inline)..."
10
10
  gemfile(true) do
11
11
  source "https://rubygems.org"
12
+ gem "rubyzip", ">= 2.3.0"
12
13
  gem "caxlsx", "4.5.0"
13
14
  gem "xlsxtream", "3.1.0"
14
15
  gem "fast_excel", "0.5.0", platform: :mri
@@ -187,7 +188,7 @@ RUNNER_SCRIPT = <<~'RUBY'
187
188
  when ["xlsxrb_inmemory", "read"]
188
189
  require_relative "lib/xlsxrb"
189
190
  measure do
190
- wb = Xlsxrb.read(filename)
191
+ wb = Xlsxrb.read(filename).load
191
192
  count = 0
192
193
  wb.sheets.each do |sheet|
193
194
  sheet.rows.each do |row|
@@ -237,6 +238,7 @@ RUNNER_SCRIPT = <<~'RUBY'
237
238
  end
238
239
  end
239
240
  when ["simple_xlsx_reader", "read"]
241
+ gem "rubyzip", "~> 1.3.0"
240
242
  require "simple_xlsx_reader"
241
243
  measure do
242
244
  doc = SimpleXlsxReader.open(filename)
@@ -88,25 +88,25 @@ The following benchmarks evaluate processing **1,000,000 cells** (100,000 rows
88
88
 
89
89
  | Library | Version | Model | String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |
90
90
  | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
91
- | **xlsxtream** | 3.1.0 | Streaming | Inline String | **1.19 s** | 1.20 s | **18.2 MB** | 1072.0 |
92
- | **xlsxrb (Streaming)** | - | Streaming | SST (Shared) | **1.73 s** | 1.65 s | 94.4 MB | 39.0 |
93
- | **fast_excel** | 0.5.0 | Streaming | SST (Shared) | 1.89 s | 1.89 s | 148.2 MB | 245.0 |
94
- | **xlsxrb (In-Memory)** | - | In-Memory | SST (Shared) | 3.84 s | 3.83 s | 278.3 MB | 32.0 |
95
- | **write_xlsx** | 1.15.0 | In-Memory | SST (Shared) | 4.32 s | 4.34 s | 201.2 MB | 33.0 |
96
- | **caxlsx** | 4.5.0 | In-Memory | Inline String | 5.15 s | 5.12 s | 188.6 MB | 23.0 |
97
- | **rubyXL** | 3.4.38 | In-Memory | Inline String | 38.81 s | 37.82 s | 2186.8 MB | 103.0 |
91
+ | **xlsxrb (Streaming)** | - | Streaming | SST (Shared) | **0.95 s** | **0.95 s** | 67.6 MB | **34.0** |
92
+ | **xlsxtream** | 3.1.0 | Streaming | Inline String | 1.16 s | 1.17 s | **17.0 MB** | 323.0 |
93
+ | **fast_excel** | 0.5.0 | Streaming | SST (Shared) | 2.02 s | 2.14 s | 148.3 MB | 210.0 |
94
+ | **xlsxrb (In-Memory)** | - | In-Memory | SST (Shared) | 2.42 s | 2.57 s | 209.0 MB | 16.0 |
95
+ | **write_xlsx** | 1.15.0 | In-Memory | SST (Shared) | 4.39 s | 4.42 s | 217.4 MB | 25.0 |
96
+ | **caxlsx** | 4.5.0 | In-Memory | Inline String | 5.28 s | 5.28 s | 177.4 MB | 24.0 |
97
+ | **rubyXL** | 3.4.38 | In-Memory | Inline String | 43.13 s | 43.65 s | 2167.2 MB | 101.0 |
98
98
 
99
99
  ### Read Performance (1,000,000 cells)
100
100
 
101
101
  | Library | Version | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |
102
102
  | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
103
- | **xlsxrb (Streaming)** | - | Streaming | **3.17 s** | 3.22 s | 91.4 MB | 43.0 |
104
- | **simple_xlsx_reader** | 5.1.0 | Streaming | 4.48 s | 4.45 s | **38.5 MB** | 1669.0 |
105
- | **xlsxrb (In-Memory)** | - | In-Memory | 5.71 s | 5.89 s | 224.8 MB | 63.0 |
106
- | **creek** | 2.6.3 | Streaming | 8.14 s | 8.02 s | 834.6 MB | 477.0 |
107
- | **xsv** | 1.4.1 | Streaming | 14.61 s | 14.50 s | 76.1 MB | 2224.0 |
108
- | **roo** | 3.0.0 | Streaming | 15.69 s | 13.36 s | 119.7 MB | 441.0 |
109
- | **rubyXL** | 3.4.38 | In-Memory | 37.13 s | 40.35 s | 2537.6 MB | 146.0 |
103
+ | **xlsxrb (Streaming)** | - | Streaming | **1.73 s** | **1.72 s** | 136.2 MB | **45.0** |
104
+ | **xlsxrb (In-Memory)** | - | In-Memory | 4.33 s | 4.46 s | 304.3 MB | 77.0 |
105
+ | **simple_xlsx_reader** | 5.1.0 | Streaming | 4.38 s | 4.41 s | **32.8 MB** | 918.0 |
106
+ | **creek** | 2.6.3 | Streaming | 7.12 s | 7.05 s | 840.1 MB | 419.0 |
107
+ | **roo** | 3.0.0 | Streaming | 10.95 s | 10.33 s | 123.4 MB | 196.0 |
108
+ | **xsv** | 1.4.1 | Streaming | 14.56 s | 14.59 s | 73.6 MB | 2028.0 |
109
+ | **rubyXL** | 3.4.38 | In-Memory | 30.74 s | 30.78 s | 2289.6 MB | 146.0 |
110
110
 
111
111
  ---
112
112
 
@@ -46,40 +46,40 @@
46
46
  <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
47
47
  <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
48
48
 
49
- <!-- xlsxtream 3.1.0: 1.19s -->
50
- <text x="150" y="85" text-anchor="end" class="label">xlsxtream 3.1.0</text>
51
- <rect x="160" y="72" width="6.5" height="18" rx="3" fill="#64748b"/>
52
- <text x="174.5" y="85" class="value">1.19 s</text>
49
+ <!-- xlsxrb (Streaming): 0.95s -->
50
+ <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
51
+ <rect x="160" y="72" width="5.2" height="18" rx="3" fill="#3b82f6"/>
52
+ <text x="173.2" y="85" class="value">0.95 s</text>
53
53
 
54
- <!-- xlsxrb (Streaming): 1.73s -->
55
- <text x="150" y="120" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
56
- <rect x="160" y="107" width="9.5" height="18" rx="3" fill="#3b82f6"/>
57
- <text x="177.5" y="120" class="value">1.73 s</text>
54
+ <!-- xlsxtream 3.1.0: 1.16s -->
55
+ <text x="150" y="120" text-anchor="end" class="label">xlsxtream 3.1.0</text>
56
+ <rect x="160" y="107" width="6.4" height="18" rx="3" fill="#64748b"/>
57
+ <text x="174.4" y="120" class="value">1.16 s</text>
58
58
 
59
- <!-- fast_excel 0.5.0: 1.89s -->
59
+ <!-- fast_excel 0.5.0: 2.02s -->
60
60
  <text x="150" y="155" text-anchor="end" class="label">fast_excel 0.5.0</text>
61
- <rect x="160" y="142" width="10.4" height="18" rx="3" fill="#64748b"/>
62
- <text x="178.4" y="155" class="value">1.89 s</text>
61
+ <rect x="160" y="142" width="11.1" height="18" rx="3" fill="#64748b"/>
62
+ <text x="179.1" y="155" class="value">2.02 s</text>
63
63
 
64
- <!-- xlsxrb (In-Memory): 3.84s -->
64
+ <!-- xlsxrb (In-Memory): 2.42s -->
65
65
  <text x="150" y="190" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
66
- <rect x="160" y="177" width="21.1" height="18" rx="3" fill="#3b82f6"/>
67
- <text x="189.1" y="190" class="value">3.84 s</text>
66
+ <rect x="160" y="177" width="13.3" height="18" rx="3" fill="#3b82f6"/>
67
+ <text x="181.3" y="190" class="value">2.42 s</text>
68
68
 
69
- <!-- write_xlsx 1.15.0: 4.32s -->
69
+ <!-- write_xlsx 1.15.0: 4.39s -->
70
70
  <text x="150" y="225" text-anchor="end" class="label">write_xlsx 1.15.0</text>
71
- <rect x="160" y="212" width="23.8" height="18" rx="3" fill="#64748b"/>
72
- <text x="191.8" y="225" class="value">4.32 s</text>
71
+ <rect x="160" y="212" width="24.1" height="18" rx="3" fill="#64748b"/>
72
+ <text x="192.1" y="225" class="value">4.39 s</text>
73
73
 
74
- <!-- caxlsx 4.5.0: 5.15s -->
74
+ <!-- caxlsx 4.5.0: 5.28s -->
75
75
  <text x="150" y="260" text-anchor="end" class="label">caxlsx 4.5.0</text>
76
- <rect x="160" y="247" width="28.3" height="18" rx="3" fill="#64748b"/>
77
- <text x="196.3" y="260" class="value">5.15 s</text>
76
+ <rect x="160" y="247" width="29.0" height="18" rx="3" fill="#64748b"/>
77
+ <text x="197.0" y="260" class="value">5.28 s</text>
78
78
 
79
- <!-- rubyXL 3.4.38: 38.81s -->
79
+ <!-- rubyXL 3.4.38: 43.13s -->
80
80
  <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
81
- <rect x="160" y="282" width="213.5" height="18" rx="3" fill="#64748b"/>
82
- <text x="381.5" y="295" class="value">38.81 s</text>
81
+ <rect x="160" y="282" width="220.0" height="18" rx="3" fill="#64748b"/>
82
+ <text x="388.0" y="295" class="value">43.13 s</text>
83
83
  </g>
84
84
 
85
85
  <!-- Right: Read Performance -->
@@ -103,42 +103,42 @@
103
103
  <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
104
104
  <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
105
105
 
106
- <!-- xlsxrb (Streaming): 3.17s -->
106
+ <!-- xlsxrb (Streaming): 1.73s -->
107
107
  <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
108
- <rect x="160" y="72" width="17.4" height="18" rx="3" fill="#3b82f6"/>
109
- <text x="185.4" y="85" class="value">3.17 s</text>
108
+ <rect x="160" y="72" width="9.5" height="18" rx="3" fill="#3b82f6"/>
109
+ <text x="177.5" y="85" class="value">1.73 s</text>
110
110
 
111
- <!-- simple_xlsx_reader: 4.48s -->
112
- <text x="150" y="120" text-anchor="end" class="label">simple_xlsx_reader</text>
113
- <rect x="160" y="107" width="24.6" height="18" rx="3" fill="#64748b"/>
114
- <text x="192.6" y="120" class="value">4.48 s</text>
111
+ <!-- xlsxrb (In-Memory): 4.33s -->
112
+ <text x="150" y="120" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
113
+ <rect x="160" y="107" width="23.8" height="18" rx="3" fill="#3b82f6"/>
114
+ <text x="191.8" y="120" class="value">4.33 s</text>
115
115
 
116
- <!-- xlsxrb (In-Memory): 5.71s -->
117
- <text x="150" y="155" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
118
- <rect x="160" y="142" width="31.4" height="18" rx="3" fill="#3b82f6"/>
119
- <text x="199.4" y="155" class="value">5.71 s</text>
116
+ <!-- simple_xlsx_reader: 4.38s -->
117
+ <text x="150" y="155" text-anchor="end" class="label">simple_xlsx_reader</text>
118
+ <rect x="160" y="142" width="24.1" height="18" rx="3" fill="#64748b"/>
119
+ <text x="192.1" y="155" class="value">4.38 s</text>
120
120
 
121
- <!-- creek 2.6.3: 8.14s -->
121
+ <!-- creek 2.6.3: 7.12s -->
122
122
  <text x="150" y="190" text-anchor="end" class="label">creek 2.6.3</text>
123
- <rect x="160" y="177" width="44.8" height="18" rx="3" fill="#64748b"/>
124
- <text x="212.8" y="190" class="value">8.14 s</text>
123
+ <rect x="160" y="177" width="39.2" height="18" rx="3" fill="#64748b"/>
124
+ <text x="207.2" y="190" class="value">7.12 s</text>
125
125
 
126
- <!-- xsv 1.4.1: 14.61s -->
127
- <text x="150" y="225" text-anchor="end" class="label">xsv 1.4.1</text>
128
- <rect x="160" y="212" width="80.4" height="18" rx="3" fill="#64748b"/>
129
- <text x="248.4" y="225" class="value">14.61 s</text>
126
+ <!-- roo 3.0.0: 10.95s -->
127
+ <text x="150" y="225" text-anchor="end" class="label">roo 3.0.0</text>
128
+ <rect x="160" y="212" width="60.2" height="18" rx="3" fill="#64748b"/>
129
+ <text x="228.2" y="225" class="value">10.95 s</text>
130
130
 
131
- <!-- roo 3.0.0: 15.69s -->
132
- <text x="150" y="260" text-anchor="end" class="label">roo 3.0.0</text>
133
- <rect x="160" y="247" width="86.3" height="18" rx="3" fill="#64748b"/>
134
- <text x="254.3" y="260" class="value">15.69 s</text>
131
+ <!-- xsv 1.4.1: 14.56s -->
132
+ <text x="150" y="260" text-anchor="end" class="label">xsv 1.4.1</text>
133
+ <rect x="160" y="247" width="80.1" height="18" rx="3" fill="#64748b"/>
134
+ <text x="248.1" y="260" class="value">14.56 s</text>
135
135
 
136
- <!-- rubyXL 3.4.38: 37.13s -->
136
+ <!-- rubyXL 3.4.38: 30.74s -->
137
137
  <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
138
- <rect x="160" y="282" width="204.2" height="18" rx="3" fill="#64748b"/>
139
- <text x="372.2" y="295" class="value">37.13 s</text>
138
+ <rect x="160" y="282" width="169.1" height="18" rx="3" fill="#64748b"/>
139
+ <text x="337.1" y="295" class="value">30.74 s</text>
140
140
  </g>
141
141
 
142
142
  <!-- Footer -->
143
- <text x="480" y="455" text-anchor="middle" class="footer">Tested on Ruby 3.4+ in isolated subprocesses • Full GC and peak memory metrics in docs/PEER_LIBRARIES.md</text>
143
+ <text x="480" y="455" text-anchor="middle" class="footer">Tested on Ruby 4.0+ in isolated subprocesses • Full GC and peak memory metrics in docs/PEER_LIBRARIES.md</text>
144
144
  </svg>
@@ -15,7 +15,9 @@ module Xlsxrb
15
15
  # cell.to_date # Date value
16
16
  #
17
17
  # @api public
18
- Cell = Data.define(:row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors) do
18
+ class Cell
19
+ attr_reader :row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors
20
+
19
21
  # @param row_index [Integer] 0-based row index.
20
22
  # @param column_index [Integer] 0-based column index.
21
23
  # @param value [Object, nil] The cell's value.
@@ -23,12 +25,66 @@ module Xlsxrb
23
25
  # @param style_index [Integer, String, nil] Style identifier.
24
26
  # @param unmapped_data [Hash] Additional metadata.
25
27
  # @param errors [Array<String>, nil] Validation errors.
26
- #: (row_index: Integer, column_index: Integer, ?value: untyped, ?formula: Elements::Formula?, ?style_index: Integer | String | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
28
+ #: (row_index: untyped, column_index: untyped, ?value: untyped, ?formula: (Elements::Formula | String)?, ?style_index: (Integer | String)?, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
27
29
  def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: EMPTY_HASH, errors: nil)
30
+ @row_index = row_index
31
+ @column_index = column_index
32
+ @value = value
33
+ @formula = formula
34
+ @style_index = style_index
35
+ @unmapped_data = unmapped_data || EMPTY_HASH
28
36
  computed_errors = errors || self.class.validate(row_index, column_index, value)
29
- computed_errors = computed_errors.freeze unless computed_errors.frozen?
30
- super(row_index: row_index, column_index: column_index, value: value, formula: formula,
31
- style_index: style_index, unmapped_data: unmapped_data, errors: computed_errors)
37
+ @errors = computed_errors.frozen? ? computed_errors : computed_errors.freeze
38
+ end
39
+
40
+ def self.fast_create(row_index, column_index, value, style_index = nil, formula = nil)
41
+ inst = allocate
42
+ inst.instance_variable_set(:@row_index, row_index)
43
+ inst.instance_variable_set(:@column_index, column_index)
44
+ inst.instance_variable_set(:@value, value)
45
+ inst.instance_variable_set(:@formula, formula)
46
+ inst.instance_variable_set(:@style_index, style_index)
47
+ inst.instance_variable_set(:@unmapped_data, EMPTY_HASH)
48
+ inst.instance_variable_set(:@errors, EMPTY_ERRORS)
49
+ inst
50
+ end
51
+
52
+ def ==(other)
53
+ other.is_a?(Cell) &&
54
+ row_index == other.row_index &&
55
+ column_index == other.column_index &&
56
+ value == other.value &&
57
+ formula == other.formula &&
58
+ style_index == other.style_index &&
59
+ unmapped_data == other.unmapped_data &&
60
+ errors == other.errors
61
+ end
62
+ alias eql? ==
63
+
64
+ def hash
65
+ [row_index, column_index, value, formula, style_index, unmapped_data, errors].hash
66
+ end
67
+
68
+ def deconstruct
69
+ [row_index, column_index, value, formula, style_index, unmapped_data, errors]
70
+ end
71
+
72
+ def deconstruct_keys(keys)
73
+ h = { row_index: row_index, column_index: column_index, value: value, formula: formula,
74
+ style_index: style_index, unmapped_data: unmapped_data, errors: errors }
75
+ keys ? h.slice(*keys) : h
76
+ end
77
+
78
+ def with(**changes)
79
+ self.class.new(
80
+ row_index: changes.fetch(:row_index, row_index),
81
+ column_index: changes.fetch(:column_index, column_index),
82
+ value: changes.fetch(:value, value),
83
+ formula: changes.fetch(:formula, formula),
84
+ style_index: changes.fetch(:style_index, style_index),
85
+ unmapped_data: changes.fetch(:unmapped_data, unmapped_data),
86
+ errors: changes.fetch(:errors, errors)
87
+ )
32
88
  end
33
89
 
34
90
  # Returns whether the cell is valid according to OOXML specifications.
@@ -57,7 +113,8 @@ module Xlsxrb
57
113
  def [](key)
58
114
  case key
59
115
  when :value then value
60
- when :formula then formula
116
+ when :formula
117
+ formula.is_a?(Formula) ? formula.expression : formula
61
118
  when :style_index then style_index
62
119
  when :ref then ref
63
120
  when :column_index then column_index
@@ -114,9 +171,8 @@ module Xlsxrb
114
171
  def to_date
115
172
  return value if value.is_a?(Date)
116
173
 
117
- # Excel epoch is 1899-12-30
118
174
  if value.is_a?(Numeric)
119
- Date.new(1899, 12, 30) + value.to_i
175
+ Ooxml::Utils.serial_to_date(value)
120
176
  else
121
177
  begin
122
178
  Date.parse(value.to_s)
@@ -135,9 +191,7 @@ module Xlsxrb
135
191
  return value if value.is_a?(Time)
136
192
 
137
193
  if value.is_a?(Numeric)
138
- days = value.to_f
139
- base_time = Time.utc(1899, 12, 30)
140
- base_time + (days * 86_400)
194
+ Ooxml::Utils.serial_to_datetime(value)
141
195
  else
142
196
  begin
143
197
  Time.parse(value.to_s)
@@ -168,7 +222,7 @@ module Xlsxrb
168
222
  # @param index [Integer] 0-based column index.
169
223
  # @return [String] Excel column letter.
170
224
  # @api public
171
- #: (Integer index) -> String
225
+ #: (untyped index) -> String
172
226
  def self.column_letter(index)
173
227
  raise ArgumentError, "Column index must be a non-negative Integer, got #{index.inspect}" unless index.is_a?(Integer) && index >= 0
174
228