xlsxrb 0.1.14 → 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.
@@ -6,6 +6,7 @@ require "tempfile"
6
6
  require_relative "style_builder"
7
7
  require_relative "chart_builder"
8
8
  require_relative "dsl_helpers"
9
+ require_relative "worksheet_builder"
9
10
  require_relative "ooxml/writer"
10
11
  require_relative "ooxml/worksheet_writer"
11
12
  require_relative "ooxml/workbook_writer"
@@ -70,9 +71,12 @@ module Xlsxrb
70
71
  @current_images = []
71
72
  @current_shapes = []
72
73
  @current_sheet_properties = {}
74
+ @current_sheet_format = {}
73
75
  @current_sheet_view = {}
74
76
  @current_row_breaks = []
75
77
  @current_col_breaks = []
78
+ @current_protected_ranges = []
79
+ @current_cell_watches = []
76
80
  @current_cells = {}
77
81
  @styles = {} # { style_name => StyleBuilder }
78
82
  @style_writer = Ooxml::Writer.new
@@ -359,15 +363,24 @@ module Xlsxrb
359
363
 
360
364
  # Adds data validation rules to a range.
361
365
  #
366
+ # Adds data validation to a cell or range.
367
+ #
362
368
  # @example Dropdown list validation
363
369
  # s.validate_data("B2:B100", type: "list", formula1: '"High,Medium,Low"')
364
370
  #
365
- # @param range [String] The cell range (e.g. "B2:B10").
366
- # @param type [String, Symbol] Validation type ("list", "whole", "decimal", "date", "time", "textLength", "custom").
371
+ # @example Block syntax
372
+ # s.validate_data("B2:B100") do |dv|
373
+ # dv.type :list
374
+ # dv.formula1 '"High,Medium,Low"'
375
+ # end
376
+ #
377
+ # @param range [String, nil] The cell range (e.g. "B2:B10").
378
+ # @param type [String, Symbol, nil] Validation type ("list", "whole", "decimal", "date", "time", "textLength", "custom").
367
379
  # @param opts [Hash] Validation options.
380
+ # @yield [data_validation_builder] Optional configuration block.
368
381
  # @return [void]
369
382
  # @api public
370
- #: (String range, ?type: String | Symbol, **untyped opts) -> void
383
+ #: (?String? range, ?type: (String | Symbol)?, **untyped opts) ?{ (DataValidationBuilder) -> void } -> void
371
384
  def validate_data(...)
372
385
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
373
386
 
@@ -393,18 +406,25 @@ module Xlsxrb
393
406
 
394
407
  # Adds a formatted Excel Table to the sheet.
395
408
  #
396
- # @example
409
+ # @example Standard keyword arguments
397
410
  # s.table("A1:C10", columns: ["ID", "Name", "Total"], name: "SalesTable", style: "TableStyleMedium9")
398
411
  #
399
- # @param ref [String] The cell range for the table (e.g. "A1:D10").
400
- # @param columns [Array<String>, Array<Hash>] Column names or definitions.
412
+ # @example Block syntax
413
+ # s.table("A1:C10") do |t|
414
+ # t.columns ["ID", "Name", "Total"]
415
+ # t.style "TableStyleMedium9"
416
+ # end
417
+ #
418
+ # @param ref [String, nil] The cell range for the table (e.g. "A1:D10").
419
+ # @param columns [Array<String>, Array<Hash>, nil] Column names or definitions.
401
420
  # @param name [String, nil] Table name.
402
421
  # @param display_name [String, nil] Display name.
403
422
  # @param style [String, nil] Table style name.
404
423
  # @param opts [Hash] Additional options.
424
+ # @yield [table_builder] Optional configuration block.
405
425
  # @return [void]
406
426
  # @api public
407
- #: (String ref, columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> void
427
+ #: (?String? ref, ?columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) ?{ (TableBuilder) -> void } -> void
408
428
  def table(...)
409
429
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
410
430
 
@@ -830,6 +850,20 @@ module Xlsxrb
830
850
  @writer.sheet_view(...)
831
851
  end
832
852
 
853
+ # Configures sheet format properties (e.g. default_row_height, default_col_width).
854
+ #
855
+ # @param name [Symbol, String, Hash, nil] Property name or hash of properties.
856
+ # @param value [Object, nil] Property value when name is provided.
857
+ # @param opts [Hash] Keyword arguments for sheet format properties.
858
+ # @return [Hash[Symbol, untyped]]
859
+ # @api public
860
+ #: (?Symbol | String | Hash[Symbol, untyped] | nil name, ?untyped value, **untyped opts) -> Hash[Symbol, untyped]
861
+ def sheet_format(...)
862
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
863
+
864
+ @writer.sheet_format(...)
865
+ end
866
+
833
867
  # simplecov:disable
834
868
  # Edge case / untested delegation block
835
869
  # Adds a horizontal page break after the given row index.
@@ -859,6 +893,35 @@ module Xlsxrb
859
893
  @writer.page_break_col(...)
860
894
  end
861
895
  # simplecov:enable
896
+
897
+ # Adds a protected range to the sheet.
898
+ #
899
+ # @param name [String] Name of the protected range.
900
+ # @param sqref [String] Cell range (e.g. "A1:B10").
901
+ # @param password [String, nil] Optional password or hash.
902
+ # @param opts [Hash] Additional options.
903
+ # @return [void]
904
+ # @api public
905
+ #: (String name, String sqref, ?password: String?, **untyped opts) -> void
906
+ def protected_range(...)
907
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
908
+
909
+ @writer.protected_range(...)
910
+ end
911
+ alias add_protected_range protected_range
912
+
913
+ # Adds a cell watch to the sheet.
914
+ #
915
+ # @param ref [String] Cell reference (e.g. "A1").
916
+ # @return [void]
917
+ # @api public
918
+ #: (String ref) -> void
919
+ def cell_watch(...)
920
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
921
+
922
+ @writer.cell_watch(...)
923
+ end
924
+ alias add_cell_watch cell_watch
862
925
  end
863
926
 
864
927
  # Adds a new sheet to the workbook and starts streaming rows into it.
@@ -922,9 +985,12 @@ module Xlsxrb
922
985
  @current_images = []
923
986
  @current_shapes = []
924
987
  @current_sheet_properties = {}
988
+ @current_sheet_format = {}
925
989
  @current_sheet_view = {}
926
990
  @current_row_breaks = []
927
991
  @current_col_breaks = []
992
+ @current_protected_ranges = []
993
+ @current_cell_watches = []
928
994
  @current_cells = {}
929
995
 
930
996
  return unless block_given?
@@ -967,7 +1033,8 @@ module Xlsxrb
967
1033
  freeze_pane: @current_freeze_pane,
968
1034
  split_pane: @current_split_pane,
969
1035
  selection: @current_selection,
970
- sheet_view: @current_sheet_view.empty? ? nil : @current_sheet_view
1036
+ sheet_view: @current_sheet_view.empty? ? nil : @current_sheet_view,
1037
+ sheet_format: @current_sheet_format.empty? ? nil : @current_sheet_format
971
1038
  )
972
1039
  @zip.write_data(@current_row_buffer) unless @current_row_buffer.empty?
973
1040
  @current_row_buffer.clear
@@ -1152,13 +1219,22 @@ module Xlsxrb
1152
1219
 
1153
1220
  # Adds a data validation rule.
1154
1221
  #
1155
- # @param sqref [String] Cell range.
1222
+ # @param sqref [String, nil] Cell range.
1156
1223
  # @param opts [Hash] Validation options.
1224
+ # @yield [data_validation_builder] Optional configuration block.
1157
1225
  # @return [void]
1158
1226
  # @api public
1159
- #: (untyped sqref, **untyped opts) -> void
1160
- def validate_data(sqref, **opts)
1227
+ #: (?String? sqref, **untyped opts) ?{ (DataValidationBuilder) -> void } -> void
1228
+ def validate_data(sqref = nil, **opts)
1161
1229
  sheet if @current_sheet.nil?
1230
+ if block_given?
1231
+ builder = DataValidationBuilder.new(sqref, **opts)
1232
+ yield builder
1233
+ opts = builder.to_h
1234
+ sqref = opts.delete(:sqref) || sqref
1235
+ end
1236
+ raise ArgumentError, "data validation sqref must be provided" unless sqref
1237
+
1162
1238
  @current_data_validations << opts.merge(sqref: sqref)
1163
1239
  end
1164
1240
 
@@ -1176,18 +1252,33 @@ module Xlsxrb
1176
1252
 
1177
1253
  # Adds an Excel Table (ListObject).
1178
1254
  #
1179
- # @param ref [String] Table range.
1180
- # @param columns [Array<String>] Column names.
1255
+ # @param ref [String, nil] Table range.
1256
+ # @param columns [Array<String>, nil] Column names.
1181
1257
  # @param name [String, nil] Table name.
1182
1258
  # @param display_name [String, nil] Display name.
1183
1259
  # @param style [String, nil] Table style.
1184
1260
  # @param opts [Hash] Additional options.
1261
+ # @yield [table_builder] Optional configuration block.
1185
1262
  # @return [void]
1186
1263
  # @api public
1187
- #: (untyped ref, columns: untyped, ?name: untyped, ?display_name: untyped, ?style: untyped, **untyped opts) -> void
1188
- def table(ref, columns:, name: nil, display_name: nil, style: nil, **)
1264
+ #: (?String? ref, ?columns: untyped, ?name: untyped, ?display_name: untyped, ?style: untyped, **untyped opts) ?{ (TableBuilder) -> void } -> void
1265
+ def table(ref = nil, columns: nil, name: nil, display_name: nil, style: nil, **opts)
1189
1266
  sheet if @current_sheet.nil?
1190
- @current_tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **)
1267
+ opts = opts.dup
1268
+ if block_given?
1269
+ builder = TableBuilder.new(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
1270
+ yield builder
1271
+ opts = builder.to_h
1272
+ ref = opts.delete(:ref) || ref
1273
+ columns = opts.delete(:columns) || columns
1274
+ name = opts.delete(:name) || name
1275
+ display_name = opts.delete(:display_name) || display_name
1276
+ style = opts.delete(:style) || style
1277
+ end
1278
+ raise ArgumentError, "table ref must be provided" unless ref
1279
+ raise ArgumentError, "table columns must be provided" if columns.nil?
1280
+
1281
+ @current_tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
1191
1282
  end
1192
1283
 
1193
1284
  # Adds a Pivot Table.
@@ -1464,6 +1555,25 @@ module Xlsxrb
1464
1555
  @current_sheet_view[name] = value
1465
1556
  end
1466
1557
 
1558
+ # Configures sheet format properties (e.g. default_row_height, default_col_width).
1559
+ #
1560
+ # @param name [Symbol, String, Hash, nil] Property name or hash of properties.
1561
+ # @param value [Object, nil] Property value when name is provided.
1562
+ # @param opts [Hash] Keyword arguments for sheet format properties.
1563
+ # @return [Hash[Symbol, untyped]]
1564
+ # @api public
1565
+ #: (?Symbol | String | Hash[Symbol, untyped] | nil name, ?untyped value, **untyped opts) -> Hash[Symbol, untyped]
1566
+ def sheet_format(name = nil, value = nil, **opts)
1567
+ sheet if @current_sheet.nil?
1568
+ if name.is_a?(Hash)
1569
+ @current_sheet_format.merge!(name)
1570
+ elsif name && value
1571
+ @current_sheet_format[name.to_sym] = value
1572
+ end
1573
+ @current_sheet_format.merge!(opts) unless opts.empty?
1574
+ @current_sheet_format
1575
+ end
1576
+
1467
1577
  # Inserts a horizontal page break before a row.
1468
1578
  #
1469
1579
  # @param row_num [Integer] 1-based row number.
@@ -1493,6 +1603,36 @@ module Xlsxrb
1493
1603
  # simplecov:enable
1494
1604
  end
1495
1605
 
1606
+ # Adds a protected range to the current sheet.
1607
+ #
1608
+ # @param name [String] Name of the protected range.
1609
+ # @param sqref [String] Cell range (e.g. "A1:B10").
1610
+ # @param password [String, nil] Optional password or hash.
1611
+ # @param opts [Hash] Additional options.
1612
+ # @return [void]
1613
+ # @api public
1614
+ #: (String name, String sqref, ?password: String?, **untyped opts) -> void
1615
+ def protected_range(name, sqref, password: nil, **opts)
1616
+ sheet if @current_sheet.nil?
1617
+ entry = { name: name.to_s, sqref: sqref.to_s }
1618
+ entry[:password] = password if password
1619
+ entry.merge!(opts) unless opts.empty?
1620
+ @current_protected_ranges << DslHelpers.normalize_protection_options(entry)
1621
+ end
1622
+ alias add_protected_range protected_range
1623
+
1624
+ # Adds a cell watch to the current sheet.
1625
+ #
1626
+ # @param ref [String] Cell reference (e.g. "A1").
1627
+ # @return [void]
1628
+ # @api public
1629
+ #: (String ref) -> void
1630
+ def cell_watch(ref)
1631
+ sheet if @current_sheet.nil?
1632
+ @current_cell_watches << ref.to_s
1633
+ end
1634
+ alias add_cell_watch cell_watch
1635
+
1496
1636
  # --- Workbook-Level Methods ---
1497
1637
 
1498
1638
  # Adds a defined name.
@@ -1819,9 +1959,12 @@ module Xlsxrb
1819
1959
  images: @current_images,
1820
1960
  shapes: @current_shapes,
1821
1961
  sheet_properties: @current_sheet_properties,
1962
+ sheet_format: @current_sheet_format,
1822
1963
  sheet_view: @current_sheet_view,
1823
1964
  row_breaks: @current_row_breaks,
1824
- col_breaks: @current_col_breaks
1965
+ col_breaks: @current_col_breaks,
1966
+ protected_ranges: @current_protected_ranges,
1967
+ cell_watches: @current_cell_watches
1825
1968
  }
1826
1969
  sheet_data[:cells] = @current_cells if @current_cells && !@current_cells.empty?
1827
1970
  @current_cells = nil
@@ -1832,6 +1975,7 @@ module Xlsxrb
1832
1975
  @current_row_writer.finish(
1833
1976
  drawing_rid: info[:drawing_rid],
1834
1977
  sheet_protection: @current_sheet_protection,
1978
+ protected_ranges: @current_protected_ranges.empty? ? nil : @current_protected_ranges,
1835
1979
  auto_filter: @current_auto_filter,
1836
1980
  filter_columns: @current_filter_columns.empty? ? nil : @current_filter_columns,
1837
1981
  sort_state: @current_sort_state,
@@ -1845,6 +1989,7 @@ module Xlsxrb
1845
1989
  header_footer: @current_header_footer.empty? ? nil : @current_header_footer,
1846
1990
  row_breaks: @current_row_breaks.empty? ? nil : @current_row_breaks,
1847
1991
  col_breaks: @current_col_breaks.empty? ? nil : @current_col_breaks,
1992
+ cell_watches: @current_cell_watches.empty? ? nil : @current_cell_watches,
1848
1993
  tables: @current_tables.empty? ? nil : @current_tables,
1849
1994
  table_start_rid: info[:table_start_rid],
1850
1995
  legacy_drawing_rid: info[:vml_rid],
data/lib/xlsxrb/utils.rb CHANGED
@@ -39,6 +39,59 @@ module Xlsxrb
39
39
  "#{Elements::Cell.column_letter(col_idx)}#{row_idx + 1}"
40
40
  end
41
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
+
42
95
  # Converts a column letter name (e.g. "A", "BC") to a 0-based integer index.
43
96
  #
44
97
  # @example
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module Xlsxrb
6
- VERSION = "0.1.14"
6
+ VERSION = "0.1.15"
7
7
  end
@@ -348,16 +348,7 @@ module Xlsxrb
348
348
  errors: row.errors
349
349
  )
350
350
  end
351
- Elements::Worksheet.new(
352
- name: sheet.name,
353
- rows: new_rows,
354
- columns: sheet.columns,
355
- charts: sheet.charts,
356
- conditional_formatting: sheet.conditional_formatting,
357
- data_validations: sheet.data_validations,
358
- unmapped_data: sheet.unmapped_data,
359
- errors: sheet.errors
360
- )
351
+ sheet.with(rows: new_rows)
361
352
  end
362
353
 
363
354
  [updated_sheets, styles_definition]