xlsxrb 0.1.13 → 0.1.15

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +17 -14
  4. data/lib/xlsxrb/colors.rb +118 -0
  5. data/lib/xlsxrb/elements/cell.rb +205 -16
  6. data/lib/xlsxrb/elements/column.rb +6 -4
  7. data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
  8. data/lib/xlsxrb/elements/image.rb +128 -0
  9. data/lib/xlsxrb/elements/row.rb +206 -9
  10. data/lib/xlsxrb/elements/styles.rb +157 -0
  11. data/lib/xlsxrb/elements/types.rb +95 -1
  12. data/lib/xlsxrb/elements/workbook.rb +121 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +519 -29
  14. data/lib/xlsxrb/elements.rb +2 -0
  15. data/lib/xlsxrb/number_formatter.rb +429 -0
  16. data/lib/xlsxrb/ooxml/cfb.rb +1 -1
  17. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
  18. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
  19. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
  20. data/lib/xlsxrb/ooxml/reader.rb +14 -17
  21. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  22. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  23. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  24. data/lib/xlsxrb/ooxml/workbook_parser.rb +74 -16
  25. data/lib/xlsxrb/ooxml/workbook_writer.rb +82 -23
  26. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  27. data/lib/xlsxrb/ooxml/worksheet_writer.rb +263 -66
  28. data/lib/xlsxrb/ooxml/writer/features_xml.rb +5 -3
  29. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  30. data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
  31. data/lib/xlsxrb/ooxml/zip_reader.rb +144 -28
  32. data/lib/xlsxrb/stream_row.rb +243 -16
  33. data/lib/xlsxrb/stream_sheet.rb +647 -16
  34. data/lib/xlsxrb/stream_writer.rb +375 -34
  35. data/lib/xlsxrb/style_builder.rb +308 -22
  36. data/lib/xlsxrb/utils.rb +211 -0
  37. data/lib/xlsxrb/version.rb +1 -1
  38. data/lib/xlsxrb/workbook_builder.rb +72 -11
  39. data/lib/xlsxrb/worksheet_builder.rb +738 -24
  40. data/lib/xlsxrb.rb +1073 -83
  41. data/sig/generated/xlsxrb/colors.rbs +40 -0
  42. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  43. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  44. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  45. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  46. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  47. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  48. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  49. data/sig/generated/xlsxrb/elements/workbook.rbs +79 -2
  50. data/sig/generated/xlsxrb/elements/worksheet.rbs +282 -18
  51. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  52. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  53. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  54. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  55. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  56. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  57. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
  58. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  59. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
  60. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  61. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
  62. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +13 -0
  63. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  64. data/sig/generated/xlsxrb/stream_sheet.rbs +358 -14
  65. data/sig/generated/xlsxrb/stream_writer.rbs +220 -30
  66. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  67. data/sig/generated/xlsxrb/utils.rbs +151 -0
  68. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  69. data/sig/generated/xlsxrb/worksheet_builder.rbs +356 -18
  70. data/sig/generated/xlsxrb.rbs +223 -27
  71. metadata +11 -1
@@ -20,6 +20,281 @@ module Xlsxrb
20
20
  grey: "FF808080"
21
21
  }.freeze
22
22
 
23
+ CSS_COLORS = {
24
+ aliceblue: "F0F8FF",
25
+ antiquewhite: "FAEBD7",
26
+ aqua: "00FFFF",
27
+ aquamarine: "7FFFD4",
28
+ azure: "F0FFFF",
29
+ beige: "F5F5DC",
30
+ bisque: "FFE4C4",
31
+ black: "000000",
32
+ blanchedalmond: "FFEBCD",
33
+ blue: "0000FF",
34
+ blueviolet: "8A2BE2",
35
+ brown: "A52A2A",
36
+ burlywood: "DEB887",
37
+ cadetblue: "5F9EA0",
38
+ chartreuse: "7FFF00",
39
+ chocolate: "D2691E",
40
+ coral: "FF7F50",
41
+ cornflowerblue: "6495ED",
42
+ cornsilk: "FFF8DC",
43
+ crimson: "DC143C",
44
+ cyan: "00FFFF",
45
+ darkblue: "00008B",
46
+ darkcyan: "008B8B",
47
+ darkgoldenrod: "B8860B",
48
+ darkgray: "A9A9A9",
49
+ darkgrey: "A9A9A9",
50
+ darkgreen: "006400",
51
+ darkkhaki: "BDB76B",
52
+ darkmagenta: "8B008B",
53
+ darkolivegreen: "556B2F",
54
+ darkorange: "FF8C00",
55
+ darkorchid: "9932CC",
56
+ darkred: "8B0000",
57
+ darksalmon: "E9967A",
58
+ darkseagreen: "8FBC8F",
59
+ darkslateblue: "483D8B",
60
+ darkslategray: "2F4F4F",
61
+ darkslategrey: "2F4F4F",
62
+ darkturquoise: "00CED1",
63
+ darkviolet: "9400D3",
64
+ deeppink: "FF1493",
65
+ deepskyblue: "00BFFF",
66
+ dimgray: "696969",
67
+ dimgrey: "696969",
68
+ dodgerblue: "1E90FF",
69
+ firebrick: "B22222",
70
+ floralwhite: "FFFAF0",
71
+ forestgreen: "228B22",
72
+ fuchsia: "FF00FF",
73
+ gainsboro: "DCDCDC",
74
+ ghostwhite: "F8F8FF",
75
+ gold: "FFD700",
76
+ goldenrod: "DAA520",
77
+ gray: "808080",
78
+ grey: "808080",
79
+ green: "00FF00",
80
+ greenyellow: "ADFF2F",
81
+ honeydew: "F0FFF0",
82
+ hotpink: "FF69B4",
83
+ indianred: "CD5C5C",
84
+ indigo: "4B0082",
85
+ ivory: "FFFFF0",
86
+ khaki: "F0E68C",
87
+ lavender: "E6E6FA",
88
+ lavenderblush: "FFF0F5",
89
+ lawngreen: "7CFC00",
90
+ lemonchiffon: "FFFACD",
91
+ lightblue: "ADD8E6",
92
+ lightcoral: "F08080",
93
+ lightcyan: "E0FFFF",
94
+ lightgoldenrodyellow: "FAFAD2",
95
+ lightgray: "D3D3D3",
96
+ lightgrey: "D3D3D3",
97
+ lightgreen: "90EE90",
98
+ lightpink: "FFB6C1",
99
+ lightsalmon: "FFA07A",
100
+ lightseagreen: "20B2AA",
101
+ lightskyblue: "87CEFA",
102
+ lightslategray: "778899",
103
+ lightslategrey: "778899",
104
+ lightsteelblue: "B0C4DE",
105
+ lightyellow: "FFFFE0",
106
+ lime: "00FF00",
107
+ limegreen: "32CD32",
108
+ linen: "FAF0E6",
109
+ magenta: "FF00FF",
110
+ maroon: "800000",
111
+ mediumaquamarine: "66CDAA",
112
+ mediumblue: "0000CD",
113
+ mediumorchid: "BA55D3",
114
+ mediumpurple: "9370DB",
115
+ mediumseagreen: "3CB371",
116
+ mediumslateblue: "7B68EE",
117
+ mediumspringgreen: "00FA9A",
118
+ mediumturquoise: "48D1CC",
119
+ mediumvioletred: "C71585",
120
+ midnightblue: "191970",
121
+ mintcream: "F5FFFA",
122
+ mistyrose: "FFE4E1",
123
+ moccasin: "FFE4B5",
124
+ navajowhite: "FFDEAD",
125
+ navy: "000080",
126
+ oldlace: "FDF5E6",
127
+ olive: "808000",
128
+ olivedrab: "6B8E23",
129
+ orange: "FFA500",
130
+ orangered: "FF4500",
131
+ orchid: "DA70D6",
132
+ palegoldenrod: "EEE8AA",
133
+ palegreen: "98FB98",
134
+ paleturquoise: "AFEEEE",
135
+ palevioletred: "DB7093",
136
+ papayawhip: "FFEFD5",
137
+ peachpuff: "FFDAB9",
138
+ peru: "CD853F",
139
+ pink: "FFC0CB",
140
+ plum: "DDA0DD",
141
+ powderblue: "B0E0E6",
142
+ purple: "800080",
143
+ rebeccapurple: "663399",
144
+ red: "FF0000",
145
+ rosybrown: "BC8F8F",
146
+ royalblue: "4169E1",
147
+ saddlebrown: "8B4513",
148
+ salmon: "FA8072",
149
+ sandybrown: "F4A460",
150
+ seagreen: "2E8B57",
151
+ seashell: "FFF5EE",
152
+ sienna: "A0522D",
153
+ silver: "C0C0C0",
154
+ skyblue: "87CEEB",
155
+ slateblue: "6A5ACD",
156
+ slategray: "708090",
157
+ slategrey: "708090",
158
+ snow: "FFFAFA",
159
+ springgreen: "00FF7F",
160
+ steelblue: "4682B4",
161
+ tan: "D2B48C",
162
+ teal: "008080",
163
+ thistle: "D8BFD8",
164
+ tomato: "FF6347",
165
+ turquoise: "40E0D0",
166
+ violet: "EE82EE",
167
+ wheat: "F5DEB3",
168
+ white: "FFFFFF",
169
+ whitesmoke: "F5F5F5",
170
+ yellow: "FFFF00",
171
+ yellowgreen: "9ACD32"
172
+ }.freeze
173
+
174
+ BORDER_STYLES = {
175
+ none: :none,
176
+ thin: :thin,
177
+ medium: :medium,
178
+ dashed: :dashed,
179
+ dotted: :dotted,
180
+ thick: :thick,
181
+ double: :double,
182
+ hair: :hair,
183
+ medium_dashed: :mediumDashed,
184
+ mediumdashed: :mediumDashed,
185
+ mediumDashed: :mediumDashed,
186
+ dash_dot: :dashDot,
187
+ dashdot: :dashDot,
188
+ dashDot: :dashDot,
189
+ medium_dash_dot: :mediumDashDot,
190
+ mediumdashdot: :mediumDashDot,
191
+ mediumDashDot: :mediumDashDot,
192
+ dash_dot_dot: :dashDotDot,
193
+ dashdotdot: :dashDotDot,
194
+ dashDotDot: :dashDotDot,
195
+ medium_dash_dot_dot: :mediumDashDotDot,
196
+ mediumdashdotdot: :mediumDashDotDot,
197
+ mediumDashDotDot: :mediumDashDotDot,
198
+ slant_dash_dot: :slantDashDot,
199
+ slantdashdot: :slantDashDot,
200
+ slantDashDot: :slantDashDot
201
+ }.freeze
202
+
203
+ BORDER_INDEX_MAP = {
204
+ 0 => :none,
205
+ 1 => :thin,
206
+ 2 => :medium,
207
+ 3 => :dashed,
208
+ 4 => :dotted,
209
+ 5 => :thick,
210
+ 6 => :double,
211
+ 7 => :hair,
212
+ 8 => :mediumDashed,
213
+ 9 => :dashDot,
214
+ 10 => :mediumDashDot,
215
+ 11 => :dashDotDot,
216
+ 12 => :mediumDashDotDot,
217
+ 13 => :slantDashDot
218
+ }.freeze
219
+
220
+ # Normalizes a color definition (CSS color name, hex code, or RGB integer) to an OpenXML hex color string.
221
+ #
222
+ # @example
223
+ # StyleBuilder.normalize_color(:navy) #=> "FF000080"
224
+ # StyleBuilder.normalize_color("#FF0000") #=> "FFFF0000"
225
+ # StyleBuilder.normalize_color("#F00") #=> "FFFF0000"
226
+ # StyleBuilder.normalize_color("FF0000", alpha: false) #=> "FF0000"
227
+ # StyleBuilder.normalize_color(0xFF0000) #=> "FFFF0000"
228
+ #
229
+ # @param color [String, Symbol, Integer, nil]
230
+ # @param alpha [Boolean] Whether to include the alpha channel ("AARRGGBB" vs "RRGGBB").
231
+ # @return [String, nil]
232
+ # @api public
233
+ #: (String | Symbol | Integer | nil color, ?alpha: bool) -> String?
234
+ def self.normalize_color(color, alpha: true)
235
+ return nil if color.nil?
236
+
237
+ hex = case color
238
+ when Symbol, defined?(String) && String
239
+ str = color.to_s.strip
240
+ return nil if str.empty?
241
+
242
+ # Strip leading colon (e.g. ":red")
243
+ str = str.sub(/^:/, "")
244
+ return nil if str.empty?
245
+
246
+ sym_key = str.downcase.to_sym
247
+ if CSS_COLORS.key?(sym_key)
248
+ CSS_COLORS[sym_key]
249
+ else
250
+ # Hex string parsing
251
+ clean_hex = str.sub(/^#/, "")
252
+ case clean_hex.length
253
+ when 3
254
+ if clean_hex.match?(/\A[0-9a-fA-F]{3}\z/)
255
+ r, g, b = clean_hex.chars
256
+ "#{r}#{r}#{g}#{g}#{b}#{b}".upcase
257
+ end
258
+ when 6
259
+ clean_hex.upcase if clean_hex.match?(/\A[0-9a-fA-F]{6}\z/)
260
+ when 8
261
+ if clean_hex.match?(/\A[0-9a-fA-F]{8}\z/)
262
+ return alpha ? clean_hex.upcase : clean_hex[2..7].upcase
263
+ end
264
+ end
265
+ end
266
+ when Integer
267
+ format("%06X", color & 0xFFFFFF)
268
+ end
269
+
270
+ return nil unless hex
271
+
272
+ alpha ? "FF#{hex}" : hex
273
+ end
274
+
275
+ # Normalizes a border style symbol, string, or integer index to an OOXML ST_BorderStyle symbol.
276
+ #
277
+ # @example
278
+ # StyleBuilder.normalize_border_style(:border_thin) #=> :thin
279
+ # StyleBuilder.normalize_border_style(1) #=> :thin
280
+ # StyleBuilder.normalize_border_style(:medium_dashed) #=> :mediumDashed
281
+ # StyleBuilder.normalize_border_style("double") #=> :double
282
+ # StyleBuilder.normalize_border_style(0) #=> :none
283
+ #
284
+ # @param style [Symbol, String, Integer, nil]
285
+ # @return [Symbol, nil]
286
+ # @api public
287
+ #: (Symbol | String | Integer | nil style) -> Symbol?
288
+ def self.normalize_border_style(style)
289
+ return nil if style.nil?
290
+
291
+ return BORDER_INDEX_MAP[style] if style.is_a?(Integer)
292
+
293
+ str = style.to_s.sub(/^border_/, "")
294
+ sym = str.to_sym
295
+ BORDER_STYLES[sym] || BORDER_STYLES[str.downcase.to_sym]
296
+ end
297
+
23
298
  # Resolves a color symbol, string, or prefix to an aRGB hex string.
24
299
  #
25
300
  # @param color [Symbol, String, Object, nil]
@@ -28,10 +303,15 @@ module Xlsxrb
28
303
  def self.resolve_color(color)
29
304
  return nil unless color
30
305
 
306
+ return Colors.palette_color(color, alpha: true) if color.is_a?(Integer) && defined?(Colors) && color >= 0 && color < Colors::INDEXED_COLORS.size
307
+
31
308
  if color.is_a?(Symbol) || (color.is_a?(String) && color.start_with?(":") && color.length > 1)
32
309
  key = color.to_s.sub(/^:/, "").to_sym
33
- return COLORS[key] || color.to_s
310
+ return COLORS[key] || (CSS_COLORS[key] ? "FF#{CSS_COLORS[key]}" : nil) || color.to_s
34
311
  end
312
+
313
+ return normalize_color(color, alpha: true) || color.to_s if color.is_a?(String) && color.start_with?("#")
314
+
35
315
  color.to_s
36
316
  end
37
317
 
@@ -314,75 +594,81 @@ module Xlsxrb
314
594
  end
315
595
 
316
596
  # Sets all borders.
317
- # @param style [String, Symbol] The border style.
597
+ # @param style [String, Symbol, Integer] The border style.
318
598
  # @param color [String, Symbol, nil] The color.
319
599
  # @return [self]
320
600
  # @api public
321
- #: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
601
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
322
602
  def border_all(style: "thin", color: nil)
603
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
323
604
  color_opt = color ? { color: resolve_color(color) } : {}
324
- @border_props[:left] = { style: style, **color_opt }
325
- @border_props[:right] = { style: style, **color_opt }
326
- @border_props[:top] = { style: style, **color_opt }
327
- @border_props[:bottom] = { style: style, **color_opt }
605
+ @border_props[:left] = { style: norm_style, **color_opt }
606
+ @border_props[:right] = { style: norm_style, **color_opt }
607
+ @border_props[:top] = { style: norm_style, **color_opt }
608
+ @border_props[:bottom] = { style: norm_style, **color_opt }
328
609
  self
329
610
  end
330
611
 
331
612
  # Sets the left border.
332
- # @param style [String, Symbol] The border style.
613
+ # @param style [String, Symbol, Integer] The border style.
333
614
  # @param color [String, Symbol, nil] The color.
334
615
  # @return [self]
335
616
  # @api public
336
- #: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
617
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
337
618
  def border_left(style: "thin", color: nil)
338
- @border_props[:left] = { style: style, color: resolve_color(color) }.compact
619
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
620
+ @border_props[:left] = { style: norm_style, color: resolve_color(color) }.compact
339
621
  self
340
622
  end
341
623
 
342
624
  # Sets the right border.
343
- # @param style [String, Symbol] The border style.
625
+ # @param style [String, Symbol, Integer] The border style.
344
626
  # @param color [String, Symbol, nil] The color.
345
627
  # @return [self]
346
628
  # @api public
347
- #: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
629
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
348
630
  def border_right(style: "thin", color: nil)
349
- @border_props[:right] = { style: style, color: resolve_color(color) }.compact
631
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
632
+ @border_props[:right] = { style: norm_style, color: resolve_color(color) }.compact
350
633
  self
351
634
  end
352
635
 
353
636
  # Sets the top border.
354
- # @param style [String, Symbol] The border style.
637
+ # @param style [String, Symbol, Integer] The border style.
355
638
  # @param color [String, Symbol, nil] The color.
356
639
  # @return [self]
357
640
  # @api public
358
- #: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
641
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
359
642
  def border_top(style: "thin", color: nil)
360
- @border_props[:top] = { style: style, color: resolve_color(color) }.compact
643
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
644
+ @border_props[:top] = { style: norm_style, color: resolve_color(color) }.compact
361
645
  self
362
646
  end
363
647
 
364
648
  # Sets the bottom border.
365
- # @param style [String, Symbol] The border style.
649
+ # @param style [String, Symbol, Integer] The border style.
366
650
  # @param color [String, Symbol, nil] The color.
367
651
  # @return [self]
368
652
  # @api public
369
- #: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
653
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
370
654
  def border_bottom(style: "thin", color: nil)
371
- @border_props[:bottom] = { style: style, color: resolve_color(color) }.compact
655
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
656
+ @border_props[:bottom] = { style: norm_style, color: resolve_color(color) }.compact
372
657
  self
373
658
  end
374
659
 
375
660
  # rubocop:disable Naming/MethodParameterName
376
661
  # Sets diagonal borders.
377
- # @param style [String, Symbol] The border style.
662
+ # @param style [String, Symbol, Integer] The border style.
378
663
  # @param color [String, Symbol, nil] The color.
379
664
  # @param up [Boolean] Diagonal up.
380
665
  # @param down [Boolean] Diagonal down.
381
666
  # @return [self]
382
667
  # @api public
383
- #: (?style: String | Symbol, ?color: String | Symbol | nil, ?up: bool, ?down: bool) -> self
668
+ #: (?style: String | Symbol | Integer, ?color: String | Symbol | nil, ?up: bool, ?down: bool) -> self
384
669
  def border_diagonal(style: "thin", color: nil, up: false, down: false)
385
- @border_props[:diagonal] = { style: style, color: resolve_color(color) }.compact
670
+ norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
671
+ @border_props[:diagonal] = { style: norm_style, color: resolve_color(color) }.compact
386
672
  @border_props[:diagonal_up] = true if up
387
673
  @border_props[:diagonal_down] = true if down
388
674
  self
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ require_relative "elements/cell"
6
+ require_relative "ooxml/utils"
7
+
8
+ module Xlsxrb
9
+ # Public coordinate conversion and reference parsing utilities.
10
+ #
11
+ # @api public
12
+ module Utils
13
+ # Converts a cell reference string (e.g. "A1", "BC100") to 0-based [row, col] coordinates.
14
+ #
15
+ # @example
16
+ # Xlsxrb::Utils.ref_to_row_col("A1") #=> [0, 0]
17
+ # Xlsxrb::Utils.ref_to_row_col("BC100") #=> [99, 54]
18
+ #
19
+ # @param cell_ref [String, nil] Excel cell reference string.
20
+ # @return [Array(Integer, Integer), nil] 0-based [row_idx, col_idx] tuple, or nil if invalid.
21
+ # @api public
22
+ #: (String? cell_ref) -> [Integer, Integer]?
23
+ def self.ref_to_row_col(cell_ref)
24
+ Elements::Cell.parse_ref(cell_ref)
25
+ end
26
+
27
+ # Converts 0-based [row_idx, col_idx] coordinates to an A1-style reference string.
28
+ #
29
+ # @example
30
+ # Xlsxrb::Utils.row_col_to_ref(0, 0) #=> "A1"
31
+ # Xlsxrb::Utils.row_col_to_ref(99, 54) #=> "BC100"
32
+ #
33
+ # @param row_idx [Integer] 0-based row index.
34
+ # @param col_idx [Integer] 0-based column index.
35
+ # @return [String] A1-style reference string.
36
+ # @api public
37
+ #: (Integer row_idx, Integer col_idx) -> String
38
+ def self.row_col_to_ref(row_idx, col_idx)
39
+ "#{Elements::Cell.column_letter(col_idx)}#{row_idx + 1}"
40
+ end
41
+
42
+ # Converts a cell range reference string (e.g. "A1:C10", "B2:D5", "A1") to 0-based
43
+ # [[top_row, left_col], [bottom_row, right_col]] coordinate pairs.
44
+ #
45
+ # @example Range reference
46
+ # Xlsxrb::Utils.parse_range("A1:C10") #=> [[0, 0], [9, 2]]
47
+ #
48
+ # @example Single cell reference
49
+ # Xlsxrb::Utils.parse_range("B5") #=> [[4, 1], [4, 1]]
50
+ #
51
+ # @param range_ref [String, nil] Cell range reference string.
52
+ # @return [Array([Integer, Integer], [Integer, Integer]), nil] 0-based coordinate pairs, or nil if invalid.
53
+ # @api public
54
+ #: (String? range_ref) -> [[Integer, Integer], [Integer, Integer]]?
55
+ def self.parse_range(range_ref)
56
+ return nil if range_ref.nil? || range_ref.empty?
57
+
58
+ parts = range_ref.split(":", 2)
59
+ first = Elements::Cell.parse_ref(parts[0])
60
+ return nil unless first
61
+
62
+ second = parts[1] ? Elements::Cell.parse_ref(parts[1]) : first
63
+ return nil unless second
64
+
65
+ top_row = [first[0], second[0]].min
66
+ bottom_row = [first[0], second[0]].max
67
+ left_col = [first[1], second[1]].min
68
+ right_col = [first[1], second[1]].max
69
+ [[top_row, left_col], [bottom_row, right_col]]
70
+ end
71
+
72
+ # Converts 0-based bounding coordinates to an A1-style range reference string.
73
+ #
74
+ # @example Range coordinates
75
+ # Xlsxrb::Utils.range_to_ref(0, 0, 9, 2) #=> "A1:C10"
76
+ #
77
+ # @example Single cell coordinates
78
+ # Xlsxrb::Utils.range_to_ref(4, 1, 4, 1) #=> "B5"
79
+ #
80
+ # @param top_row [Integer] 0-based start row index.
81
+ # @param left_col [Integer] 0-based start column index.
82
+ # @param bottom_row [Integer] 0-based end row index.
83
+ # @param right_col [Integer] 0-based end column index.
84
+ # @return [String] A1-style range reference string.
85
+ # @api public
86
+ #: (Integer top_row, Integer left_col, Integer bottom_row, Integer right_col) -> String
87
+ def self.range_to_ref(top_row, left_col, bottom_row, right_col)
88
+ if top_row == bottom_row && left_col == right_col
89
+ row_col_to_ref(top_row, left_col)
90
+ else
91
+ "#{row_col_to_ref(top_row, left_col)}:#{row_col_to_ref(bottom_row, right_col)}"
92
+ end
93
+ end
94
+
95
+ # Converts a column letter name (e.g. "A", "BC") to a 0-based integer index.
96
+ #
97
+ # @example
98
+ # Xlsxrb::Utils.col_name_to_index("A") #=> 0
99
+ # Xlsxrb::Utils.col_name_to_index("BC") #=> 54
100
+ #
101
+ # @param col_name [String, Symbol, Integer] Column letter or index.
102
+ # @return [Integer] 0-based column index.
103
+ # @api public
104
+ #: (String | Symbol | Integer col_name) -> Integer
105
+ def self.col_name_to_index(col_name)
106
+ Elements::Cell.column_index(col_name)
107
+ end
108
+
109
+ # Converts a 0-based column integer index to a column letter name (e.g. 0 -> "A", 54 -> "BC").
110
+ #
111
+ # @example
112
+ # Xlsxrb::Utils.col_index_to_name(0) #=> "A"
113
+ # Xlsxrb::Utils.col_index_to_name(54) #=> "BC"
114
+ #
115
+ # @param col_idx [Integer] 0-based column index.
116
+ # @return [String] Column letter name.
117
+ # @api public
118
+ #: (Integer col_idx) -> String
119
+ def self.col_index_to_name(col_idx)
120
+ Elements::Cell.column_letter(col_idx)
121
+ end
122
+
123
+ # Splits an A1-style cell reference into its column name and 1-based row number.
124
+ #
125
+ # @example
126
+ # Xlsxrb::Utils.split_coordinate("A1") #=> ["A", 1]
127
+ # Xlsxrb::Utils.split_coordinate("BC100") #=> ["BC", 100]
128
+ #
129
+ # @param cell_ref [String, nil] Excel cell reference string.
130
+ # @return [Array(String, Integer), nil] [col_name, row_number] tuple (1-based row), or nil if invalid.
131
+ # @api public
132
+ #: (String? cell_ref) -> [String, Integer]?
133
+ def self.split_coordinate(cell_ref)
134
+ return nil unless cell_ref
135
+
136
+ coords = Elements::Cell.parse_ref(cell_ref)
137
+ return nil unless coords
138
+
139
+ row_idx, col_idx = coords
140
+ [Elements::Cell.column_letter(col_idx), row_idx + 1]
141
+ end
142
+
143
+ # Converts a Date or Time to an Excel numeric serial number (1900 or 1904 system).
144
+ #
145
+ # @example
146
+ # Xlsxrb::Utils.date_to_serial(Date.new(2026, 1, 1)) #=> 46023
147
+ #
148
+ # @param date_or_time [Date, Time] Date or Time instance.
149
+ # @param date1904 [Boolean] Whether to use the 1904 date system.
150
+ # @return [Integer, Float] Serial number.
151
+ # @api public
152
+ #: (Date | Time date_or_time, ?date1904: bool) -> (Integer | Float)
153
+ def self.date_to_serial(date_or_time, date1904: false)
154
+ if date_or_time.is_a?(Time)
155
+ Ooxml::Utils.datetime_to_serial(date_or_time, date1904: date1904)
156
+ else
157
+ Ooxml::Utils.date_to_serial(date_or_time, date1904: date1904)
158
+ end
159
+ end
160
+
161
+ # Converts an Excel serial number to a Date.
162
+ #
163
+ # @example
164
+ # Xlsxrb::Utils.serial_to_date(46023) #=> #<Date: 2026-01-01>
165
+ #
166
+ # @param serial [Numeric] Excel serial number.
167
+ # @param date1904 [Boolean] Whether to use the 1904 date system.
168
+ # @return [Date]
169
+ # @api public
170
+ #: (Numeric serial, ?date1904: bool) -> Date
171
+ def self.serial_to_date(serial, date1904: false)
172
+ Ooxml::Utils.serial_to_date(serial, date1904: date1904)
173
+ end
174
+
175
+ # Converts a Time or Date to a fractional Excel serial number.
176
+ #
177
+ # @example
178
+ # Xlsxrb::Utils.datetime_to_serial(Time.utc(2026, 1, 1, 12, 0, 0)) #=> 46023.5
179
+ #
180
+ # @param time [Time, Date] Time or Date instance.
181
+ # @param date1904 [Boolean] Whether to use the 1904 date system.
182
+ # @return [Float] Fractional serial number.
183
+ # @api public
184
+ #: (Time | Date time, ?date1904: bool) -> Float
185
+ def self.datetime_to_serial(time, date1904: false)
186
+ if time.is_a?(Time)
187
+ Ooxml::Utils.datetime_to_serial(time, date1904: date1904)
188
+ else
189
+ Ooxml::Utils.date_to_serial(time, date1904: date1904).to_f
190
+ end
191
+ end
192
+
193
+ # Converts an Excel serial number to a Time (UTC).
194
+ #
195
+ # @example
196
+ # Xlsxrb::Utils.serial_to_time(46023.5) #=> 2026-01-01 12:00:00 UTC
197
+ #
198
+ # @param serial [Numeric] Excel serial number.
199
+ # @param date1904 [Boolean] Whether to use the 1904 date system.
200
+ # @return [Time]
201
+ # @api public
202
+ #: (Numeric serial, ?date1904: bool) -> Time
203
+ def self.serial_to_time(serial, date1904: false)
204
+ Ooxml::Utils.serial_to_datetime(serial, date1904: date1904)
205
+ end
206
+
207
+ class << self
208
+ alias serial_to_datetime serial_to_time
209
+ end
210
+ end
211
+ end
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module Xlsxrb
6
- VERSION = "0.1.13"
6
+ VERSION = "0.1.15"
7
7
  end