xlsxrb 0.1.16 → 0.1.17

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +2 -2
  4. data/lib/xlsxrb/chart_builder.rb +41 -2
  5. data/lib/xlsxrb/dsl_helpers.rb +522 -6
  6. data/lib/xlsxrb/elements/cell.rb +34 -9
  7. data/lib/xlsxrb/elements/column.rb +2 -2
  8. data/lib/xlsxrb/elements/types.rb +1 -0
  9. data/lib/xlsxrb/elements/workbook.rb +48 -0
  10. data/lib/xlsxrb/elements/worksheet.rb +158 -11
  11. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +22 -1
  12. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
  13. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +108 -1
  14. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +16 -11
  15. data/lib/xlsxrb/ooxml/reader.rb +88 -0
  16. data/lib/xlsxrb/ooxml/styles_parser.rb +19 -11
  17. data/lib/xlsxrb/ooxml/workbook_parser.rb +18 -3
  18. data/lib/xlsxrb/ooxml/workbook_writer.rb +266 -46
  19. data/lib/xlsxrb/ooxml/worksheet_parser.rb +18 -4
  20. data/lib/xlsxrb/ooxml/worksheet_writer.rb +289 -88
  21. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +579 -441
  22. data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
  23. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +23 -9
  24. data/lib/xlsxrb/ooxml/writer.rb +268 -31
  25. data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
  26. data/lib/xlsxrb/stream_sheet.rb +157 -3
  27. data/lib/xlsxrb/stream_writer.rb +946 -74
  28. data/lib/xlsxrb/style_builder.rb +59 -6
  29. data/lib/xlsxrb/utils.rb +65 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +197 -14
  32. data/lib/xlsxrb/worksheet_builder.rb +1137 -44
  33. data/lib/xlsxrb.rb +302 -47
  34. data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
  35. data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
  36. data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
  37. data/sig/generated/xlsxrb/elements/column.rbs +3 -3
  38. data/sig/generated/xlsxrb/elements/types.rbs +2 -0
  39. data/sig/generated/xlsxrb/elements/workbook.rbs +37 -0
  40. data/sig/generated/xlsxrb/elements/worksheet.rbs +82 -2
  41. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
  43. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +3 -0
  45. data/sig/generated/xlsxrb/ooxml/reader.rbs +17 -0
  46. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +3 -0
  47. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +8 -0
  48. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +8 -4
  49. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +1 -1
  50. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +12 -1
  51. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
  52. data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
  53. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
  54. data/sig/generated/xlsxrb/stream_sheet.rbs +101 -1
  55. data/sig/generated/xlsxrb/stream_writer.rbs +592 -23
  56. data/sig/generated/xlsxrb/style_builder.rbs +24 -6
  57. data/sig/generated/xlsxrb/utils.rbs +20 -0
  58. data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
  59. data/sig/generated/xlsxrb/worksheet_builder.rbs +554 -10
  60. data/sig/generated/xlsxrb.rbs +77 -16
  61. metadata +1 -1
@@ -80,6 +80,15 @@ module Xlsxrb
80
80
  workbook_properties[:date1904] ? true : false
81
81
  end
82
82
 
83
+ # Returns the internal VBA or application code name of the workbook, or nil if not specified.
84
+ #
85
+ # @return [String, nil]
86
+ # @api public
87
+ #: () -> String?
88
+ def code_name
89
+ workbook_properties[:code_name]
90
+ end
91
+
83
92
  # Returns the 0-based active worksheet tab index from workbookView, or nil if not set.
84
93
  #
85
94
  # @return [Integer, nil]
@@ -89,6 +98,45 @@ module Xlsxrb
89
98
  workbook_properties[:active_tab]
90
99
  end
91
100
 
101
+ # Returns the active worksheet in the workbook, or the first worksheet if not set.
102
+ #
103
+ # @return [Elements::Worksheet, StreamSheet, nil]
104
+ # @api public
105
+ #: () -> (Elements::Worksheet | StreamSheet)?
106
+ def active_sheet
107
+ idx = active_tab
108
+ idx ? sheets[idx] : sheets.first
109
+ end
110
+
111
+ # Returns the index of the first sheet tab visible in the sheet tab scroll area.
112
+ #
113
+ # @return [Integer, nil]
114
+ # @api public
115
+ #: () -> Integer?
116
+ def first_sheet
117
+ workbook_properties[:first_sheet]
118
+ end
119
+
120
+ # Returns whether sheet tabs are displayed in the workbook window.
121
+ #
122
+ # @return [Boolean]
123
+ # @api public
124
+ #: () -> bool
125
+ def show_sheet_tabs?
126
+ val = workbook_properties[:show_sheet_tabs]
127
+ val != false
128
+ end
129
+ alias show_sheet_tabs show_sheet_tabs?
130
+
131
+ # Returns the ratio between the sheet tabs bar and the horizontal scroll bar (0-1000).
132
+ #
133
+ # @return [Integer, nil]
134
+ # @api public
135
+ #: () -> Integer?
136
+ def tab_ratio
137
+ workbook_properties[:tab_ratio]
138
+ end
139
+
92
140
  # Returns whether Excel is instructed to perform a full recalculation when loading the workbook.
93
141
  #
94
142
  # @return [Boolean]
@@ -17,9 +17,13 @@ module Xlsxrb
17
17
  class Worksheet
18
18
  [Enumerable, CoordinateAccess].each { |m| include m }
19
19
 
20
- attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches, :selection
20
+ attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches, :ignored_errors, :phonetic_properties, :selection, :sheet_calc_pr, :background_image, :threaded_comments
21
21
  attr_writer :dimension
22
22
  alias conditional_formats conditional_formatting
23
+ alias phonetic_pr phonetic_properties
24
+ alias calc_pr sheet_calc_pr
25
+ alias background_picture background_image
26
+ alias picture background_image
23
27
 
24
28
  # @param name [String] The worksheet name (max 31 characters).
25
29
  # @param rows [Array<Elements::Row>] Rows in the sheet.
@@ -59,9 +63,14 @@ module Xlsxrb
59
63
  # @param col_breaks [Array<Integer | Hash>] Explicit page column breaks.
60
64
  # @param protected_ranges [Array<Hash>] Protected range definitions.
61
65
  # @param cell_watches [Array<String>] Watched cell references.
66
+ # @param ignored_errors [Array<Hash>] Ignored error rules.
67
+ # @param phonetic_properties [Hash, nil] Phonetic text properties.
62
68
  # @param selection [Hash, Array<Hash>, nil] Active cell and selection settings.
63
- #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: (Elements::Styles | Hash[untyped, untyped])?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String], ?selection: (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])?) -> void
64
- def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [], selection: nil)
69
+ # @param sheet_calc_pr [Hash, nil] Worksheet calculation properties.
70
+ # @param background_image [Hash, nil] Worksheet background image specification.
71
+ # @param threaded_comments [Array<Hash>] Threaded comments in the sheet.
72
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: (Elements::Styles | Hash[untyped, untyped])?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String], ?ignored_errors: Array[Hash[Symbol, untyped]], ?phonetic_properties: Hash[Symbol, untyped]?, ?selection: (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])?, ?sheet_calc_pr: Hash[Symbol, untyped]?, ?background_image: Hash[Symbol, untyped]?, ?threaded_comments: Array[Hash[Symbol, untyped]]) -> void
73
+ def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [], ignored_errors: [], phonetic_properties: nil, selection: nil, sheet_calc_pr: nil, background_image: nil, threaded_comments: [])
65
74
  @name = name
66
75
  @rows = (rows || []).freeze
67
76
  @columns = (columns || []).freeze
@@ -85,6 +94,7 @@ module Xlsxrb
85
94
  @trim_empty_rows = trim_empty_rows ? true : false
86
95
  @pad_empty_rows = pad_empty_rows ? true : false
87
96
  @pad_empty_cells = pad_empty_cells ? true : false
97
+ @threaded_comments = (threaded_comments || []).freeze
88
98
  @merged_cells = (merged_cells || []).freeze
89
99
  @auto_filter = auto_filter
90
100
  @tables = (tables || []).freeze
@@ -102,7 +112,11 @@ module Xlsxrb
102
112
  @col_breaks = (col_breaks || []).freeze
103
113
  @protected_ranges = (protected_ranges || unmapped_data[:protected_ranges] || unmapped_data.dig(:facade, :protected_ranges) || []).freeze
104
114
  @cell_watches = (cell_watches || unmapped_data[:cell_watches] || unmapped_data.dig(:facade, :cell_watches) || []).freeze
115
+ @ignored_errors = (ignored_errors || unmapped_data[:ignored_errors] || unmapped_data.dig(:facade, :ignored_errors) || []).freeze
116
+ @phonetic_properties = (phonetic_properties || unmapped_data[:phonetic_properties] || unmapped_data.dig(:facade, :phonetic_properties))&.freeze
105
117
  @selection = selection&.freeze
118
+ @sheet_calc_pr = (sheet_calc_pr || unmapped_data[:sheet_calc_pr] || unmapped_data.dig(:facade, :sheet_calc_pr) || (@sheet_properties[:full_calc_on_load] ? { full_calc_on_load: true } : nil))&.freeze
119
+ @background_image = (background_image || unmapped_data[:background_image] || unmapped_data.dig(:facade, :background_image))&.freeze
106
120
  end
107
121
 
108
122
  # Returns whether trailing empty rows are omitted during iteration.
@@ -154,6 +168,68 @@ module Xlsxrb
154
168
  @dimension ||= compute_dimension
155
169
  end
156
170
 
171
+ # Returns the worksheet tab color specification, or nil if not set.
172
+ #
173
+ # @return [String, Hash, nil] RGB hex string (e.g. "FFFF0000") or color hash (e.g. { theme: 3, tint: -0.5 }).
174
+ # @api public
175
+ #: () -> (String | Hash[Symbol, untyped])?
176
+ def tab_color
177
+ props = sheet_properties
178
+ return props[:tab_color] if props[:tab_color]
179
+ return { theme: props[:tab_color_theme], tint: props[:tab_color_tint] } if props[:tab_color_theme]
180
+ return { indexed: props[:tab_color_indexed] } if props[:tab_color_indexed]
181
+ return { auto: true } if props[:tab_color_auto]
182
+
183
+ nil
184
+ end
185
+
186
+ # Returns the internal VBA or application code name of the worksheet, or nil if not specified.
187
+ #
188
+ # @return [String, nil]
189
+ # @api public
190
+ #: () -> String?
191
+ def code_name
192
+ sheet_properties[:code_name]
193
+ end
194
+
195
+ # Returns whether fit-to-page is enabled for page setup on this worksheet.
196
+ #
197
+ # @return [Boolean]
198
+ # @api public
199
+ #: () -> bool
200
+ def fit_to_page?
201
+ sheet_properties[:fit_to_page] ? true : false
202
+ end
203
+ alias fit_to_page fit_to_page?
204
+
205
+ # Returns whether full calculation on load is enabled on this worksheet.
206
+ #
207
+ # @return [Boolean]
208
+ # @api public
209
+ #: () -> bool
210
+ def full_calc_on_load?
211
+ (@sheet_calc_pr && @sheet_calc_pr[:full_calc_on_load]) || sheet_properties[:full_calc_on_load] ? true : false
212
+ end
213
+ alias full_calc_on_load full_calc_on_load?
214
+
215
+ # Returns the outline and grouping properties of the worksheet, or an empty hash if none configured.
216
+ #
217
+ # @return [Hash]
218
+ # @api public
219
+ #: () -> Hash[Symbol, untyped]
220
+ def outline_properties
221
+ props = sheet_properties
222
+ return props[:outline_properties] if props[:outline_properties]
223
+
224
+ res = {}
225
+ res[:summary_below] = props[:summary_below] unless props[:summary_below].nil?
226
+ res[:summary_right] = props[:summary_right] unless props[:summary_right].nil?
227
+ res[:show_outline_symbols] = props[:show_outline_symbols] unless props[:show_outline_symbols].nil?
228
+ res[:apply_styles] = props[:apply_styles] unless props[:apply_styles].nil?
229
+ res
230
+ end
231
+ alias outline_pr outline_properties
232
+
157
233
  # Iterate over rows in the worksheet.
158
234
  #
159
235
  # @example
@@ -417,6 +493,34 @@ module Xlsxrb
417
493
  end
418
494
  end
419
495
 
496
+ # Returns threaded comments for the given cell reference, or empty array.
497
+ #
498
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
499
+ # @param col [Integer, nil] Optional 0-based column index.
500
+ # @return [Array<Hash[Symbol, untyped]>]
501
+ # @api public
502
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Hash[Symbol, untyped]]
503
+ def threaded_comment(ref_or_row, col = nil)
504
+ ref = if col
505
+ "#{Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
506
+ else
507
+ ref_or_row.to_s.upcase
508
+ end
509
+ threaded_comments_by_ref[ref] || []
510
+ end
511
+
512
+ # Returns threaded comments indexed by cell reference.
513
+ #
514
+ # @return [Hash<String, Array<Hash[Symbol, untyped]>>]
515
+ # @api public
516
+ #: () -> Hash[String, Array[Hash[Symbol, untyped]]]
517
+ def threaded_comments_by_ref
518
+ @threaded_comments_by_ref ||= @threaded_comments.each_with_object({}) do |c, acc|
519
+ r = c[:ref] || c[:cell]
520
+ (acc[r.to_s.upcase] ||= []) << c if r
521
+ end
522
+ end
523
+
420
524
  # Access a cell by reference or 0-based coordinates.
421
525
  #
422
526
  # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
@@ -550,7 +654,26 @@ module Xlsxrb
550
654
  new_col_breaks = changes.key?(:col_breaks) ? changes[:col_breaks] : col_breaks
551
655
  new_protected_ranges = changes.key?(:protected_ranges) ? changes[:protected_ranges] : protected_ranges
552
656
  new_cell_watches = changes.key?(:cell_watches) ? changes[:cell_watches] : cell_watches
657
+ new_ignored_errors = changes.key?(:ignored_errors) ? changes[:ignored_errors] : ignored_errors
658
+ new_phonetic_properties = if changes.key?(:phonetic_properties)
659
+ changes[:phonetic_properties]
660
+ elsif changes.key?(:phonetic_pr)
661
+ changes[:phonetic_pr]
662
+ else
663
+ phonetic_properties
664
+ end
553
665
  new_selection = changes.key?(:selection) ? changes[:selection] : selection
666
+ new_sheet_calc_pr = changes.key?(:sheet_calc_pr) ? changes[:sheet_calc_pr] : sheet_calc_pr
667
+ new_background_image = if changes.key?(:background_image)
668
+ changes[:background_image]
669
+ elsif changes.key?(:background_picture)
670
+ changes[:background_picture]
671
+ elsif changes.key?(:picture)
672
+ changes[:picture]
673
+ else
674
+ background_image
675
+ end
676
+ new_threaded_comments = changes.key?(:threaded_comments) ? changes[:threaded_comments] : threaded_comments
554
677
 
555
678
  self.class.new(
556
679
  name: new_name,
@@ -588,7 +711,12 @@ module Xlsxrb
588
711
  col_breaks: new_col_breaks,
589
712
  protected_ranges: new_protected_ranges,
590
713
  cell_watches: new_cell_watches,
591
- selection: new_selection
714
+ ignored_errors: new_ignored_errors,
715
+ phonetic_properties: new_phonetic_properties,
716
+ selection: new_selection,
717
+ sheet_calc_pr: new_sheet_calc_pr,
718
+ background_image: new_background_image,
719
+ threaded_comments: new_threaded_comments
592
720
  )
593
721
  end
594
722
 
@@ -608,6 +736,7 @@ module Xlsxrb
608
736
  state: state,
609
737
  hyperlinks: hyperlinks,
610
738
  comments: comments,
739
+ threaded_comments: threaded_comments,
611
740
  styles: styles,
612
741
  date1904: date1904,
613
742
  dimension: dimension,
@@ -630,7 +759,18 @@ module Xlsxrb
630
759
  sheet_format: sheet_format,
631
760
  row_breaks: row_breaks,
632
761
  col_breaks: col_breaks,
633
- selection: selection
762
+ protected_ranges: protected_ranges,
763
+ cell_watches: cell_watches,
764
+ ignored_errors: ignored_errors,
765
+ phonetic_properties: phonetic_properties,
766
+ phonetic_pr: phonetic_properties,
767
+ selection: selection,
768
+ sheet_calc_pr: sheet_calc_pr,
769
+ calc_pr: sheet_calc_pr,
770
+ code_name: code_name,
771
+ background_image: background_image,
772
+ background_picture: background_image,
773
+ picture: background_image
634
774
  }
635
775
  end
636
776
 
@@ -642,6 +782,7 @@ module Xlsxrb
642
782
  name == other.name && rows == other.rows && columns == other.columns && charts == other.charts &&
643
783
  conditional_formatting == other.conditional_formatting && data_validations == other.data_validations &&
644
784
  state == other.state && hyperlinks == other.hyperlinks && comments == other.comments &&
785
+ threaded_comments == other.threaded_comments &&
645
786
  merged_cells == other.merged_cells && auto_filter == other.auto_filter &&
646
787
  tables == other.tables && shapes == other.shapes && freeze_pane == other.freeze_pane &&
647
788
  page_setup == other.page_setup && page_margins == other.page_margins &&
@@ -649,7 +790,10 @@ module Xlsxrb
649
790
  sheet_protection == other.sheet_protection && sheet_view == other.sheet_view &&
650
791
  sheet_properties == other.sheet_properties && sheet_format == other.sheet_format &&
651
792
  row_breaks == other.row_breaks && col_breaks == other.col_breaks &&
652
- selection == other.selection
793
+ protected_ranges == other.protected_ranges && cell_watches == other.cell_watches &&
794
+ ignored_errors == other.ignored_errors && phonetic_properties == other.phonetic_properties &&
795
+ selection == other.selection && sheet_calc_pr == other.sheet_calc_pr &&
796
+ background_image == other.background_image
653
797
  end
654
798
  alias eql? ==
655
799
 
@@ -657,9 +801,10 @@ module Xlsxrb
657
801
  def hash
658
802
  [
659
803
  self.class, name, rows, columns, charts, conditional_formatting, data_validations,
660
- state, hyperlinks, comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
804
+ state, hyperlinks, comments, threaded_comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
661
805
  page_setup, page_margins, header_footer, print_options, sheet_protection, sheet_view,
662
- sheet_properties, sheet_format, row_breaks, col_breaks, selection
806
+ sheet_properties, sheet_format, row_breaks, col_breaks, protected_ranges,
807
+ cell_watches, ignored_errors, phonetic_properties, selection, sheet_calc_pr, background_image
663
808
  ].hash
664
809
  end
665
810
 
@@ -673,7 +818,7 @@ module Xlsxrb
673
818
  end
674
819
  alias to_worksheet load
675
820
 
676
- FORBIDDEN_NAME_CHARS = %r{[\\/?*\[\]]}
821
+ FORBIDDEN_NAME_CHARS = %r{[\\/?*\[\]:]|[\x00-\x08\x0B\x0C\x0E-\x1F]}
677
822
 
678
823
  # Returns whether the worksheet name is valid according to OOXML specifications (1..31 chars, no forbidden chars).
679
824
  #
@@ -683,7 +828,7 @@ module Xlsxrb
683
828
  def self.valid_name?(name)
684
829
  case name
685
830
  when String
686
- !name.empty? && name.size <= 31 && !name.match?(FORBIDDEN_NAME_CHARS)
831
+ !name.empty? && name.size <= 31 && !name.match?(FORBIDDEN_NAME_CHARS) && !name.start_with?("'") && !name.end_with?("'") && !name.casecmp?("History")
687
832
  else
688
833
  false
689
834
  end
@@ -701,7 +846,9 @@ module Xlsxrb
701
846
  errs << "worksheet name must be a non-empty String (got #{name.inspect})"
702
847
  else
703
848
  errs << "worksheet name cannot exceed 31 characters (got #{name.size})" if name.size > 31
704
- errs << "worksheet name cannot contain \\, /, ?, *, [, or ]" if name.match?(FORBIDDEN_NAME_CHARS)
849
+ errs << "worksheet name cannot contain \\, /, ?, *, [, ], or :" if name.match?(FORBIDDEN_NAME_CHARS)
850
+ errs << "worksheet name cannot start or end with an apostrophe" if name.start_with?("'") || name.end_with?("'")
851
+ errs << "worksheet name 'History' is reserved by Excel" if name.casecmp?("History")
705
852
  end
706
853
  errs << "rows must be an Array (got #{rows.class})" unless rows.is_a?(Array)
707
854
  if rows.is_a?(Array)
@@ -150,6 +150,7 @@ module Xlsxrb
150
150
  @value_buffer = +""
151
151
  @inline_text_buffer = +""
152
152
  @formula_buffer = +""
153
+ @shared_formula_masters = {}
153
154
  @is_runs = []
154
155
  @is_has_runs = false
155
156
  @is_current_font = {}
@@ -302,6 +303,8 @@ module Xlsxrb
302
303
  @value_buffer = +""
303
304
  @inline_text_buffer = +""
304
305
  @formula_buffer = +""
306
+ when "worksheet"
307
+ expand_shared_formulas!
305
308
  end
306
309
  end
307
310
 
@@ -339,8 +342,16 @@ module Xlsxrb
339
342
  @value_buffer.dup
340
343
  end
341
344
  f_type = { "shared" => :shared, "array" => :array, "dataTable" => :data_table }[@formula_type]
345
+ expr = @formula_buffer.dup
346
+ if f_type == :shared && @formula_si
347
+ if !expr.empty?
348
+ @shared_formula_masters[@formula_si] = { cell: @current_cell_ref, expression: expr }
349
+ elsif (master = @shared_formula_masters[@formula_si])
350
+ expr = Xlsxrb::Utils.expand_formula(master[:expression], from_ref: master[:cell], to_ref: @current_cell_ref)
351
+ end
352
+ end
342
353
  @cells[@current_cell_ref] = Xlsxrb::Elements::Formula.new(
343
- expression: @formula_buffer.dup,
354
+ expression: expr,
344
355
  cached_value: cached,
345
356
  type: f_type,
346
357
  ref: @formula_ref,
@@ -377,6 +388,16 @@ module Xlsxrb
377
388
  end
378
389
  end
379
390
 
391
+ def expand_shared_formulas!
392
+ @cells.each do |ref, val|
393
+ next unless val.is_a?(Xlsxrb::Elements::Formula) && val.type == :shared && (val.expression.nil? || val.expression.empty?)
394
+ next unless val.shared_index && (master = @shared_formula_masters[val.shared_index])
395
+
396
+ expanded = Xlsxrb::Utils.expand_formula(master[:expression], from_ref: master[:cell], to_ref: ref)
397
+ @cells[ref] = val.with(expression: expanded)
398
+ end
399
+ end
400
+
380
401
  def numeric_value(raw)
381
402
  Xlsxrb::Ooxml::WorksheetParser.numeric_cell_value(raw)
382
403
  end