xlsxrb 0.1.13 → 0.1.14

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +2 -0
  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 +85 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +424 -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.rb +12 -14
  20. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  21. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  22. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  23. data/lib/xlsxrb/ooxml/workbook_parser.rb +53 -15
  24. data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
  25. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  26. data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
  27. data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
  28. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  29. data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
  30. data/lib/xlsxrb/stream_row.rb +243 -16
  31. data/lib/xlsxrb/stream_sheet.rb +414 -15
  32. data/lib/xlsxrb/stream_writer.rb +213 -17
  33. data/lib/xlsxrb/style_builder.rb +308 -22
  34. data/lib/xlsxrb/utils.rb +158 -0
  35. data/lib/xlsxrb/version.rb +1 -1
  36. data/lib/xlsxrb/workbook_builder.rb +73 -3
  37. data/lib/xlsxrb/worksheet_builder.rb +227 -16
  38. data/lib/xlsxrb.rb +578 -79
  39. data/sig/generated/xlsxrb/colors.rbs +40 -0
  40. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  41. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  42. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  43. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  44. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  45. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  46. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  47. data/sig/generated/xlsxrb/elements/workbook.rbs +51 -2
  48. data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -18
  49. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  50. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  51. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  52. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  53. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  54. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  55. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  56. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  57. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
  59. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
  61. data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
  62. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  63. data/sig/generated/xlsxrb/utils.rbs +119 -0
  64. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  65. data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
  66. data/sig/generated/xlsxrb.rbs +186 -27
  67. metadata +11 -1
@@ -11,10 +11,12 @@ module Xlsxrb
11
11
  class WorksheetBuilder
12
12
  # @param name [String] The worksheet name.
13
13
  # @param strict_excel_mode [Boolean] Whether to enforce Microsoft Excel limits.
14
- #: (String name, ?strict_excel_mode: bool) -> void
15
- def initialize(name, strict_excel_mode: true)
14
+ # @param state [Symbol] Sheet visibility state (:visible, :hidden, or :very_hidden).
15
+ #: (String name, ?strict_excel_mode: bool, ?state: Symbol) -> void
16
+ def initialize(name, strict_excel_mode: true, state: :visible)
16
17
  @name = name
17
18
  @strict_excel_mode = strict_excel_mode
19
+ @state = state ? state.to_sym : :visible
18
20
  @rows = []
19
21
  @columns = []
20
22
  @charts = []
@@ -44,8 +46,12 @@ module Xlsxrb
44
46
  @sheet_view = {}
45
47
  @row_breaks = []
46
48
  @col_breaks = []
49
+ @print_area = nil
50
+ @print_titles = nil
47
51
  end
48
52
 
53
+ attr_reader :name
54
+
49
55
  # Defines or configures a named cell style.
50
56
  #
51
57
  # @example Define a bold header style
@@ -81,11 +87,12 @@ module Xlsxrb
81
87
  # @param hidden [Boolean] Whether the row is hidden.
82
88
  # @param custom_height [Boolean] Whether custom row height is enforced.
83
89
  # @param outline_level [Integer, nil] Grouping/outline hierarchy level.
90
+ # @param collapsed [Boolean] Whether the row is collapsed.
84
91
  # @return [void]
85
92
  # @raise [ArgumentError] If limits are exceeded when strict_excel_mode is enabled.
86
93
  # @api public
87
- #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
88
- def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
94
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
95
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
89
96
  row_index = @rows.size
90
97
  DslHelpers.validate_row_bounds!(row_index, height, strict_excel_mode: @strict_excel_mode)
91
98
 
@@ -149,6 +156,10 @@ module Xlsxrb
149
156
  col_index += 1
150
157
  next
151
158
  end
159
+ sb = @styles[style_name]
160
+ fmt_code = if sb&.num_fmt_id
161
+ sb.num_fmt_id.is_a?(String) ? sb.num_fmt_id : Ooxml::Utils::BUILTIN_NUM_FMT_CODES[sb.num_fmt_id]
162
+ end
152
163
  # If value is a Formula object or Hash with :formula, store it as the cell's formula
153
164
  cells[col_index] = if val.is_a?(Elements::Formula)
154
165
  Elements::Cell.new(
@@ -156,7 +167,9 @@ module Xlsxrb
156
167
  column_index: col_index,
157
168
  value: val.cached_value,
158
169
  formula: val,
159
- style_index: style_name
170
+ style_index: style_name,
171
+ raw_value: val.cached_value&.to_s,
172
+ format_code: fmt_code
160
173
  )
161
174
  elsif val.is_a?(Hash) && val.key?(:formula)
162
175
  f_obj = Elements::Formula.new(
@@ -169,14 +182,18 @@ module Xlsxrb
169
182
  column_index: col_index,
170
183
  value: val[:value],
171
184
  formula: f_obj,
172
- style_index: style_name
185
+ style_index: style_name,
186
+ raw_value: val[:value]&.to_s,
187
+ format_code: fmt_code
173
188
  )
174
189
  else
175
190
  Elements::Cell.new(
176
191
  row_index: row_index,
177
192
  column_index: col_index,
178
193
  value: val,
179
- style_index: style_name
194
+ style_index: style_name,
195
+ raw_value: val&.to_s,
196
+ format_code: fmt_code
180
197
  )
181
198
  end
182
199
  col_index += 1
@@ -189,9 +206,25 @@ module Xlsxrb
189
206
  height: height,
190
207
  hidden: hidden,
191
208
  custom_height: custom_height || !height.nil?,
209
+ collapsed: collapsed,
192
210
  outline_level: outline_level
193
211
  )
194
212
  end
213
+ alias add_row row
214
+
215
+ # Appends a row to the worksheet and returns self for chaining.
216
+ #
217
+ # @example
218
+ # sheet << ["A", 1] << ["B", 2]
219
+ #
220
+ # @param values [Array, Hash] Row cell values.
221
+ # @return [self]
222
+ # @api public
223
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
224
+ def <<(values)
225
+ row(values)
226
+ self
227
+ end
195
228
 
196
229
  # Sets column formatting and properties for one or multiple columns.
197
230
  #
@@ -203,23 +236,24 @@ module Xlsxrb
203
236
  #
204
237
  # @param index [Integer, String, Range, Array] Column index (0-based), letter ("A"), or range ("A".."D").
205
238
  # @param width [Float, Integer, nil] Column width in character units (0 to 255).
206
- # @param hidden [Boolean] Whether the column is hidden.
207
- # @param custom_width [Boolean] Whether custom width is explicitly set.
208
- # @param outline_level [Integer, nil] Grouping/outline level.
239
+ # @param style [Integer, Symbol, String, nil] Column style format or index.
209
240
  # @return [void]
210
241
  # @raise [ArgumentError] If width exceeds 255 in strict mode.
211
242
  # @api public
212
- #: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil) -> void
213
- def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
243
+ #: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil, ?style: (Integer | Symbol | String)?) -> void
244
+ def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil, style: nil)
214
245
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
215
246
 
216
247
  DslHelpers.normalize_column_indices(index).each do |idx|
248
+ unmapped = style ? { style_index: style } : {}
217
249
  @columns << Elements::Column.new(
218
250
  index: idx,
219
251
  width: width,
220
252
  hidden: hidden,
221
253
  custom_width: custom_width || !width.nil?,
222
- outline_level: outline_level
254
+ outline_level: outline_level,
255
+ style_index: style,
256
+ unmapped_data: unmapped
223
257
  )
224
258
  end
225
259
  end
@@ -250,8 +284,17 @@ module Xlsxrb
250
284
  # @param location [String, nil] Optional internal sheet location (e.g. "Sheet2!A1").
251
285
  # @return [void]
252
286
  # @api public
253
- #: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
287
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
254
288
  def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
289
+ if url.is_a?(Elements::Hyperlink)
290
+ link = { cell: cell }
291
+ link[:url] = url.url if url.url
292
+ link[:display] = url.display || display if url.display || display
293
+ link[:tooltip] = url.tooltip || tooltip if url.tooltip || tooltip
294
+ link[:location] = url.location || location if url.location || location
295
+ @hyperlinks << link
296
+ return
297
+ end
255
298
  link = { cell: cell }
256
299
  link[:url] = url if url
257
300
  link[:display] = display if display
@@ -380,6 +423,38 @@ module Xlsxrb
380
423
  @sparkline_groups << group
381
424
  end
382
425
 
426
+ # Adds a single sparkline to the sheet.
427
+ #
428
+ # @example Add a line sparkline
429
+ # sheet.add_sparkline(location: "F1", range: "A1:E1")
430
+ #
431
+ # @example Add a column sparkline with high/low markers
432
+ # sheet.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
433
+ #
434
+ # @example Add a win/loss sparkline
435
+ # sheet.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
436
+ #
437
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
438
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
439
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
440
+ # @param opts [Hash] Additional sparkline options.
441
+ # @return [void]
442
+ # @api public
443
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
444
+ def add_sparkline(location: nil, range: nil, type: nil, **opts)
445
+ loc = location || opts.delete(:location_ref)
446
+ rng = range || opts.delete(:data_ref) || opts.delete(:data)
447
+ raise ArgumentError, "Missing required location: parameter for sparkline" unless loc
448
+ raise ArgumentError, "Missing required range: parameter for sparkline" unless rng
449
+
450
+ mapped_type = case type&.to_s
451
+ when "win_loss" then "stacked"
452
+ when nil, "" then nil
453
+ else type.to_s
454
+ end
455
+ sparkline_group(sparklines: [{ data_ref: rng.to_s, location_ref: loc.to_s }], type: mapped_type, **opts)
456
+ end
457
+
383
458
  # Merges a range of cells into a single cell.
384
459
  #
385
460
  # @example Merge with string range
@@ -492,6 +567,98 @@ module Xlsxrb
492
567
  @print_options[name] = value
493
568
  end
494
569
 
570
+ # Sets or gets the print area for the worksheet.
571
+ #
572
+ # @example Set print area range
573
+ # sheet.print_area("A1:H50")
574
+ #
575
+ # @example Get print area range
576
+ # sheet.print_area #=> "A1:H50"
577
+ #
578
+ # @param range [String, nil] Cell range (e.g. "A1:H50").
579
+ # @return [String, nil]
580
+ # @api public
581
+ #: (?String? range) -> String?
582
+ def print_area(range = nil)
583
+ return @print_area if range.nil?
584
+
585
+ @print_area = range
586
+ end
587
+
588
+ # Sets or gets repeating print titles (rows and/or columns) for pagination.
589
+ #
590
+ # @example Set repeating header rows
591
+ # sheet.print_titles(rows: "1:2")
592
+ #
593
+ # @example Get repeating print titles
594
+ # sheet.print_titles #=> { rows: "1:2", cols: nil }
595
+ #
596
+ # @param rows [String, nil] Repeating row range (e.g. "1:2").
597
+ # @param cols [String, nil] Repeating column range (e.g. "A:B").
598
+ # @return [Hash, nil]
599
+ # @api public
600
+ #: (?rows: String?, ?cols: String?) -> Hash[Symbol, untyped]?
601
+ def print_titles(rows: nil, cols: nil)
602
+ return @print_titles if rows.nil? && cols.nil?
603
+
604
+ @print_titles = { rows: rows, cols: cols }
605
+ end
606
+
607
+ # Sets repeating title rows to appear at the top of each printed page.
608
+ # Accepts 0-indexed row numbers (WriteXLSX / xlsxrb convention) or Excel range string ("1:2").
609
+ #
610
+ # @example Repeat first row
611
+ # sheet.repeat_rows(0)
612
+ #
613
+ # @example Repeat rows 1 through 3
614
+ # sheet.repeat_rows(0, 2)
615
+ #
616
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
617
+ # @param last [Integer, String, nil] Optional last row index (0-based, defaults to first).
618
+ # @return [void]
619
+ # @api public
620
+ #: (Integer | String first, ?(Integer | String)? last) -> void
621
+ def repeat_rows(first, last = nil)
622
+ rows_str = if first.is_a?(String)
623
+ first.include?(":") ? first : "#{first}:#{last || first}"
624
+ else
625
+ last ||= first
626
+ "#{first + 1}:#{last + 1}"
627
+ end
628
+ @print_titles = (@print_titles || {}).merge(rows: rows_str)
629
+ end
630
+
631
+ # Sets repeating title columns to appear at the left of each printed page.
632
+ # Accepts 0-indexed column numbers (0 => "A"), column names ("A", "B"), or range string ("A:B").
633
+ #
634
+ # @example Repeat column A
635
+ # sheet.repeat_columns(0)
636
+ #
637
+ # @example Repeat columns A through C
638
+ # sheet.repeat_columns("A", "C")
639
+ #
640
+ # @param first [Integer, String] First column index (0-based) or column name/range.
641
+ # @param last [Integer, String, nil] Optional last column index/name (defaults to first).
642
+ # @return [void]
643
+ # @api public
644
+ #: (Integer | String first, ?(Integer | String)? last) -> void
645
+ def repeat_columns(first, last = nil)
646
+ cols_str = if first.is_a?(String)
647
+ first.include?(":") ? first : "#{first}:#{last || first}"
648
+ else
649
+ first_name = Utils.col_index_to_name(first)
650
+ last_name = if last.is_a?(Integer)
651
+ Utils.col_index_to_name(last)
652
+ elsif last
653
+ last.to_s
654
+ else
655
+ first_name
656
+ end
657
+ "#{first_name}:#{last_name}"
658
+ end
659
+ @print_titles = (@print_titles || {}).merge(cols: cols_str)
660
+ end
661
+
495
662
  # Sets sheet-level protection with optional password hashing.
496
663
  #
497
664
  # @param opts [Hash] Protection options (e.g. password: "secret", select_locked_cells: true).
@@ -614,10 +781,54 @@ module Xlsxrb
614
781
  facade_meta[:sheet_view] = @sheet_view unless @sheet_view.empty?
615
782
  facade_meta[:row_breaks] = @row_breaks unless @row_breaks.empty?
616
783
  facade_meta[:col_breaks] = @col_breaks unless @col_breaks.empty?
784
+ facade_meta[:print_area] = @print_area if @print_area
785
+ facade_meta[:print_titles] = @print_titles if @print_titles
786
+
787
+ hl_hash = {}
788
+ @hyperlinks.each do |h|
789
+ ref = h[:cell].to_s.upcase
790
+ entry = {}
791
+ entry[:url] = h[:url] if h[:url]
792
+ entry[:display] = h[:display] if h[:display]
793
+ entry[:tooltip] = h[:tooltip] if h[:tooltip]
794
+ entry[:location] = h[:location] if h[:location]
795
+ hl_hash[ref] = entry unless entry.empty?
796
+ end
797
+
798
+ cm_hash = {}
799
+ @comments.each do |c|
800
+ ref = (c[:ref] || c[:cell]).to_s.upcase
801
+ cm_hash[ref] = c
802
+ end
803
+
804
+ rows = if hl_hash.empty? && cm_hash.empty?
805
+ @rows
806
+ else
807
+ @rows.map do |row|
808
+ new_cells = row.cells.map do |cell|
809
+ hl = hl_hash[cell.ref]
810
+ cm = cm_hash[cell.ref]
811
+ if hl || cm
812
+ cell.with(hyperlink: hl || cell.hyperlink, comment: cm || cell.comment)
813
+ else
814
+ cell
815
+ end
816
+ end
817
+ row.with(cells: new_cells)
818
+ end
819
+ end
617
820
 
618
821
  Elements::Worksheet.new(
619
- name: @name, rows: @rows, columns: @columns, charts: @charts,
620
- unmapped_data: facade_meta.empty? ? {} : { facade: facade_meta }
822
+ name: @name, rows: rows, columns: @columns, charts: @charts,
823
+ conditional_formatting: @conditional_formats,
824
+ data_validations: @data_validations,
825
+ unmapped_data: facade_meta.empty? ? {} : { facade: facade_meta },
826
+ state: @state,
827
+ hyperlinks: hl_hash,
828
+ comments: @comments,
829
+ print_area: @print_area,
830
+ print_titles: @print_titles,
831
+ sparkline_groups: @sparkline_groups
621
832
  )
622
833
  end
623
834