write_xlsx 1.12.3 → 1.13.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/Changes +3 -0
  4. data/README.md +1 -1
  5. data/lib/write_xlsx/chartsheet.rb +4 -1
  6. data/lib/write_xlsx/object_positioning.rb +189 -0
  7. data/lib/write_xlsx/package/app.rb +1 -1
  8. data/lib/write_xlsx/page_setup.rb +190 -0
  9. data/lib/write_xlsx/sheets.rb +3 -3
  10. data/lib/write_xlsx/utility.rb +57 -9
  11. data/lib/write_xlsx/version.rb +1 -1
  12. data/lib/write_xlsx/workbook.rb +34 -34
  13. data/lib/write_xlsx/worksheet/asset_manager.rb +60 -0
  14. data/lib/write_xlsx/worksheet/autofilter.rb +388 -0
  15. data/lib/write_xlsx/worksheet/cell_data.rb +8 -5
  16. data/lib/write_xlsx/worksheet/cell_data_manager.rb +47 -0
  17. data/lib/write_xlsx/worksheet/cell_data_store.rb +61 -0
  18. data/lib/write_xlsx/worksheet/columns.rb +199 -0
  19. data/lib/write_xlsx/worksheet/comments_support.rb +61 -0
  20. data/lib/write_xlsx/worksheet/conditional_formats.rb +30 -0
  21. data/lib/write_xlsx/worksheet/data_writing.rb +990 -0
  22. data/lib/write_xlsx/worksheet/drawing_methods.rb +308 -0
  23. data/lib/write_xlsx/worksheet/drawing_preparation.rb +290 -0
  24. data/lib/write_xlsx/worksheet/drawing_relations.rb +76 -0
  25. data/lib/write_xlsx/worksheet/drawing_xml_writer.rb +50 -0
  26. data/lib/write_xlsx/worksheet/formatting.rb +416 -0
  27. data/lib/write_xlsx/worksheet/initialization.rb +146 -0
  28. data/lib/write_xlsx/worksheet/panes.rb +64 -0
  29. data/lib/write_xlsx/worksheet/print_options.rb +72 -0
  30. data/lib/write_xlsx/worksheet/protection.rb +65 -0
  31. data/lib/write_xlsx/worksheet/rich_text_helpers.rb +78 -0
  32. data/lib/write_xlsx/worksheet/row_col_sizing.rb +67 -0
  33. data/lib/write_xlsx/worksheet/rows.rb +84 -0
  34. data/lib/write_xlsx/worksheet/selection.rb +41 -0
  35. data/lib/write_xlsx/worksheet/xml_writer.rb +1241 -0
  36. data/lib/write_xlsx/worksheet.rb +359 -4530
  37. metadata +26 -3
  38. data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73aab5eb08b0bd985cad2f77620ef0b9c44a383d0e98a598428c1385e544963a
4
- data.tar.gz: 20bdb3eaab4895e2378659137e1eb84b62daee5563b2c7bd350e421a77dd1ff8
3
+ metadata.gz: 8da4e683796759c1d6cdee00aaba72671bb7ea86e7ece2a794628d04289c880a
4
+ data.tar.gz: dd59c677e92a15eb273d6290c093e90c2a560d4c087a937c2ee343f2cf5286f4
5
5
  SHA512:
6
- metadata.gz: 21519073cbab6a3b3397afe389e055334b1b0f415a5d4c2e7196e13b6d31e98c47894dd59fc9646c4eb884a9882fb2ad05d4d50a97222a9a2f9a383197e0d75a
7
- data.tar.gz: aec12c29800ed6c0054bb1c705a3ef40a615ef975ef0911978b56dfe051a3f44b4e8644186d9c2f13458f455cf3a25769a8651bd631ab121094fd4edcb0e0a86
6
+ metadata.gz: 33646d4746630950a08a4f29546682a2c52c04db52df9e201c37b42d31e1033ecd486904009b09b88d637a0d7074f65ac92025c9ea0755e427f3d7d7183da33c
7
+ data.tar.gz: a6f60317b950822df36b6cc3eacfd4922bcff4026f090714964d547eaa8227b9e5cb56c6f49ea33686601bf95d9c6788b8b9d70dc1d8f46f46ce1e16855420bb
data/.rubocop.yml CHANGED
@@ -46,6 +46,10 @@ Lint/DuplicateBranch:
46
46
  IgnoreLiteralBranches: true
47
47
  Exclude:
48
48
  - 'lib/write_xlsx/worksheet.rb'
49
+ - 'lib/write_xlsx/worksheet/data_writing.rb'
50
+
51
+ Lint/FloatComparison:
52
+ Enabled: false
49
53
 
50
54
  Lint/UnderscorePrefixedVariableName:
51
55
  Enabled: false
@@ -94,6 +98,9 @@ Naming/MethodParameterName:
94
98
  Naming/PredicateMethod:
95
99
  Enabled: false
96
100
 
101
+ Naming/PredicatePrefix:
102
+ Enabled: false
103
+
97
104
  Naming/VariableNumber:
98
105
  Enabled: false
99
106
 
data/Changes CHANGED
@@ -1,5 +1,8 @@
1
1
  Change history of write_xlsx rubygem.
2
2
 
3
+ 2026-03-12 v1.13.0
4
+ fix sheet name quoting with upstream Perl implementation.
5
+
3
6
  2025-10-28 v1.12.3
4
7
  fix compatibility with rubyzip 3.x
5
8
 
data/README.md CHANGED
@@ -85,7 +85,7 @@ the first worksheet in an Excel XML spreadsheet called ruby.xlsx:
85
85
  Original Perl module was written by John McNamara(jmcnamara@cpan.org).
86
86
 
87
87
  Converted to ruby by Hideo NAKAMURA(nakamrua.hideo@gmail.com)
88
- Copyright (c) 2012-2024 Hideo NAKAMURA.
88
+ Copyright (c) 2012-2026 Hideo NAKAMURA.
89
89
 
90
90
  See LICENSE.txt for further details.
91
91
 
@@ -24,11 +24,14 @@ module Writexlsx
24
24
  @drawings = Drawings.new
25
25
  @is_chartsheet = true
26
26
  @chart = nil
27
- @charts = [1]
28
27
  @zoom_scale_normal = 0
29
28
  @page_setup.orientation = false
30
29
  end
31
30
 
31
+ def charts
32
+ [@chart].compact
33
+ end
34
+
32
35
  #
33
36
  # Assemble and write the XML file.
34
37
  #
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module ObjectPositioning
6
+ include Writexlsx::Utility
7
+
8
+ #
9
+ # Calculate the vertices that define the position of a graphical object
10
+ # within the worksheet in pixels.
11
+ #
12
+ def position_object_pixels(col_start, row_start, x1, y1, width, height, anchor = nil) # :nodoc:
13
+ context = object_positioning_context(anchor)
14
+
15
+ position_object_pixels_with_context(
16
+ col_start, row_start, x1, y1, width, height, context
17
+ )
18
+ end
19
+ # def position_object_pixels(col_start, row_start, x1, y1, width, height, anchor = nil) # :nodoc:
20
+ # col_start, row_start, x1, y1 =
21
+ # adjust_start_position_for_negative_offsets(col_start, row_start, x1, y1)
22
+
23
+ # x_abs, y_abs =
24
+ # calculate_absolute_position(col_start, row_start, x1, y1, anchor)
25
+
26
+ # col_start, row_start, x1, y1 =
27
+ # adjust_start_position_for_cell_offsets(col_start, row_start, x1, y1, anchor)
28
+
29
+ # col_end, row_end, x2, y2 =
30
+ # calculate_object_end_position(col_start, row_start, x1, y1, width, height, anchor)
31
+
32
+ # [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs]
33
+ # end
34
+
35
+ #
36
+ # Calculate the vertices that define the position of a graphical object
37
+ # within the worksheet in EMUs.
38
+ #
39
+ def position_object_emus(graphical_object) # :nodoc:
40
+ object = graphical_object
41
+ col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs =
42
+ position_object_pixels(
43
+ object.col, object.row, object.x_offset, object.y_offset,
44
+ object.scaled_width, object.scaled_height, object.anchor
45
+ )
46
+
47
+ # Convert the pixel values to EMUs. See above.
48
+ x1 = (0.5 + (9_525 * x1)).to_i
49
+ y1 = (0.5 + (9_525 * y1)).to_i
50
+ x2 = (0.5 + (9_525 * x2)).to_i
51
+ y2 = (0.5 + (9_525 * y2)).to_i
52
+ x_abs = (0.5 + (9_525 * x_abs)).to_i
53
+ y_abs = (0.5 + (9_525 * y_abs)).to_i
54
+
55
+ [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs]
56
+ end
57
+
58
+ #
59
+ # Convert the width of a cell from pixels to character units.
60
+ #
61
+ def pixels_to_width(pixels)
62
+ max_digit_width = 7.0
63
+ padding = 5.0
64
+
65
+ if pixels <= 12
66
+ pixels / (max_digit_width + padding)
67
+ else
68
+ (pixels - padding) / max_digit_width
69
+ end
70
+ end
71
+
72
+ #
73
+ # Convert the height of a cell from pixels to character units.
74
+ #
75
+ def pixels_to_height(pixels)
76
+ height = 0.75 * pixels
77
+ height = height.to_i if (height - height.to_i).abs < 0.1
78
+ height
79
+ end
80
+
81
+ private
82
+
83
+ def position_object_pixels_with_context(col_start, row_start, x1, y1, width, height, context)
84
+ col_start, row_start, x1, y1 =
85
+ adjust_start_position_for_negative_offsets(col_start, row_start, x1, y1, context)
86
+
87
+ x_abs, y_abs =
88
+ calculate_absolute_position(col_start, row_start, x1, y1, context)
89
+
90
+ col_start, row_start, x1, y1 =
91
+ adjust_start_position_for_cell_offsets(col_start, row_start, x1, y1, context)
92
+
93
+ col_end, row_end, x2, y2 =
94
+ calculate_object_end_position(col_start, row_start, x1, y1, width, height, context)
95
+
96
+ [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs]
97
+ end
98
+
99
+ def adjust_start_position_for_negative_offsets(col_start, row_start, x1, y1, context)
100
+ # Adjust start column for negative offsets.
101
+ while x1 < 0 && col_start > 0
102
+ x1 += context[:size_col].call(col_start - 1, 0)
103
+ col_start -= 1
104
+ end
105
+
106
+ # Adjust start row for negative offsets.
107
+ while y1 < 0 && row_start > 0
108
+ y1 += context[:size_row].call(row_start - 1, 0)
109
+ row_start -= 1
110
+ end
111
+
112
+ # Ensure that the image isn't shifted off the page at top left.
113
+ x1 = 0 if x1 < 0
114
+ y1 = 0 if y1 < 0
115
+
116
+ [col_start, row_start, x1, y1]
117
+ end
118
+
119
+ def calculate_absolute_position(col_start, row_start, x1, y1, context)
120
+ # Calculate the absolute x offset of the top-left vertex.
121
+ x_abs = if context[:col_size_changed]
122
+ (0..(col_start - 1)).inject(0) do |sum, col|
123
+ sum + context[:size_col].call(col, context[:anchor])
124
+ end
125
+ else
126
+ # Optimisation for when the column widths haven't changed.
127
+ context[:default_col_pixels] * col_start
128
+ end
129
+ x_abs += x1
130
+
131
+ # Calculate the absolute y offset of the top-left vertex.
132
+ y_abs = if context[:row_size_changed]
133
+ (0..(row_start - 1)).inject(0) do |sum, row|
134
+ sum + context[:size_row].call(row, context[:anchor])
135
+ end
136
+ else
137
+ # Optimisation for when the row heights haven't changed.
138
+ context[:default_row_pixels] * row_start
139
+ end
140
+ y_abs += y1
141
+
142
+ [x_abs, y_abs]
143
+ end
144
+
145
+ def adjust_start_position_for_cell_offsets(col_start, row_start, x1, y1, context)
146
+ # Adjust start column for offsets that are greater than the col width.
147
+ while x1 >= context[:size_col].call(col_start, context[:anchor])
148
+ x1 -= context[:size_col].call(col_start, 0)
149
+ col_start += 1
150
+ end
151
+
152
+ # Adjust start row for offsets that are greater than the row height.
153
+ while y1 >= context[:size_row].call(row_start, context[:anchor])
154
+ y1 -= context[:size_row].call(row_start, 0)
155
+ row_start += 1
156
+ end
157
+
158
+ [col_start, row_start, x1, y1]
159
+ end
160
+
161
+ def calculate_object_end_position(col_start, row_start, x1, y1, width, height, context)
162
+ # Initialise end cell to the same as the start cell.
163
+ col_end = col_start
164
+ row_end = row_start
165
+
166
+ # Only offset the image in the cell if the row/col isn't hidden.
167
+ width += x1 if context[:size_col].call(col_start, context[:anchor]) > 0
168
+ height += y1 if context[:size_row].call(row_start, context[:anchor]) > 0
169
+
170
+ # Subtract the underlying cell widths to find the end cell of the object.
171
+ while width >= context[:size_col].call(col_end, context[:anchor])
172
+ width -= context[:size_col].call(col_end, context[:anchor])
173
+ col_end += 1
174
+ end
175
+
176
+ # Subtract the underlying cell heights to find the end cell of the object.
177
+ while height >= context[:size_row].call(row_end, context[:anchor])
178
+ height -= context[:size_row].call(row_end, context[:anchor])
179
+ row_end += 1
180
+ end
181
+
182
+ # The end vertices are whatever is left from the width and height.
183
+ x2 = width
184
+ y2 = height
185
+
186
+ [col_end, row_end, x2, y2]
187
+ end
188
+ end
189
+ end
@@ -62,7 +62,7 @@ module Writexlsx
62
62
  @workbook.worksheets
63
63
  .reject { |sheet| sheet.is_chartsheet? || sheet.very_hidden? }
64
64
  .each do |sheet|
65
- add_part_name(sheet.name)
65
+ add_part_name(sheet.name)
66
66
  end
67
67
  end
68
68
 
@@ -0,0 +1,190 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ class PageSetup # :nodoc:
6
+ include Writexlsx::Utility
7
+
8
+ attr_accessor :margin_left, :margin_right, :margin_top, :margin_bottom # :nodoc:
9
+ attr_accessor :margin_header, :margin_footer # :nodoc:
10
+ attr_accessor :repeat_rows, :repeat_cols, :print_area # :nodoc:
11
+ attr_accessor :hbreaks, :vbreaks, :scale # :nodoc:
12
+ attr_accessor :fit_page, :fit_width, :fit_height, :page_setup_changed # :nodoc:
13
+ attr_writer :across # :nodoc:
14
+ attr_accessor :orientation, :print_options_changed, :black_white # :nodoc:
15
+ attr_accessor :header, :footer, :header_footer_changed, :header_footer_aligns, :header_footer_scales
16
+ attr_writer :page_start
17
+ attr_writer :horizontal_dpi, :vertical_dpi
18
+
19
+ def initialize # :nodoc:
20
+ @margin_left = 0.7
21
+ @margin_right = 0.7
22
+ @margin_top = 0.75
23
+ @margin_bottom = 0.75
24
+ @margin_header = 0.3
25
+ @margin_footer = 0.3
26
+ @repeat_rows = ''
27
+ @repeat_cols = ''
28
+ @print_area = ''
29
+ @hbreaks = []
30
+ @vbreaks = []
31
+ @scale = 100
32
+ @fit_page = false
33
+ @fit_width = nil
34
+ @fit_height = nil
35
+ @page_setup_changed = false
36
+ @across = false
37
+ @orientation = true
38
+ @header_footer_aligns = true
39
+ @header_footer_scales = true
40
+ end
41
+
42
+ def paper=(paper_size)
43
+ if paper_size
44
+ @paper_size = paper_size
45
+ @page_setup_changed = true
46
+ end
47
+ end
48
+
49
+ def center_horizontally
50
+ @print_options_changed = true
51
+ @hcenter = true
52
+ end
53
+
54
+ def center_vertically
55
+ @print_options_changed = true
56
+ @vcenter = true
57
+ end
58
+
59
+ def print_row_col_headers(headers)
60
+ if headers
61
+ @print_headers = true
62
+ @print_options_changed = true
63
+ else
64
+ @print_headers = false
65
+ end
66
+ end
67
+
68
+ def hide_gridlines(option)
69
+ if option == 0 || !option
70
+ @print_gridlines = true
71
+ @print_options_changed = true
72
+ else
73
+ @print_gridlines = false
74
+ end
75
+ end
76
+
77
+ #
78
+ # Write the <pageSetup> element.
79
+ #
80
+ # The following is an example taken from Excel.
81
+ #
82
+ # <pageSetup
83
+ # paperSize="9"
84
+ # scale="110"
85
+ # fitToWidth="2"
86
+ # fitToHeight="2"
87
+ # pageOrder="overThenDown"
88
+ # orientation="portrait"
89
+ # useFirstPageNumber="1"
90
+ # blackAndWhite="1"
91
+ # draft="1"
92
+ # horizontalDpi="200"
93
+ # verticalDpi="200"
94
+ # r:id="rId1"
95
+ # />
96
+ #
97
+ def write_page_setup(writer) # :nodoc:
98
+ return unless @page_setup_changed
99
+
100
+ attributes = []
101
+ attributes << ['paperSize', @paper_size] if @paper_size
102
+ attributes << ['scale', @scale] if @scale != 100
103
+ attributes << ['fitToWidth', @fit_width] if @fit_page && @fit_width != 1
104
+ attributes << ['fitToHeight', @fit_height] if @fit_page && @fit_height != 1
105
+ attributes << %w[pageOrder overThenDown] if @across
106
+ attributes << ['firstPageNumber', @page_start] if @page_start && @page_start > 1
107
+ attributes << ['orientation',
108
+ if @orientation
109
+ 'portrait'
110
+ else
111
+ 'landscape'
112
+ end]
113
+ attributes << ['blackAndWhite', 1] if @black_white
114
+ attributes << ['useFirstPageNumber', 1] if ptrue?(@page_start)
115
+
116
+ # Set the DPI. Mainly only for testing.
117
+ attributes << ['horizontalDpi', @horizontal_dpi] if @horizontal_dpi
118
+ attributes << ['verticalDpi', @vertical_dpi] if @vertical_dpi
119
+
120
+ writer.empty_tag('pageSetup', attributes)
121
+ end
122
+
123
+ #
124
+ # Write the <pageMargins> element.
125
+ #
126
+ def write_page_margins(writer) # :nodoc:
127
+ writer.empty_tag('pageMargins', margin_attributes)
128
+ end
129
+
130
+ #
131
+ # Write the <printOptions> element.
132
+ #
133
+ def write_print_options(writer) # :nodoc:
134
+ return unless @print_options_changed
135
+
136
+ attributes = []
137
+ attributes << ['horizontalCentered', 1] if @hcenter
138
+ attributes << ['verticalCentered', 1] if @vcenter
139
+ attributes << ['headings', 1] if @print_headers
140
+ attributes << ['gridLines', 1] if @print_gridlines
141
+ writer.empty_tag('printOptions', attributes)
142
+ end
143
+
144
+ #
145
+ # Write the <headerFooter> element.
146
+ #
147
+ def write_header_footer(writer, excel2003_style) # :nodoc:
148
+ tag = 'headerFooter'
149
+ attributes = []
150
+ attributes << ['scaleWithDoc', 0] unless ptrue?(@header_footer_scales)
151
+ attributes << ['alignWithMargins', 0] unless ptrue?(@header_footer_aligns)
152
+
153
+ if @header_footer_changed
154
+ writer.tag_elements(tag, attributes) do
155
+ write_odd_header(writer) if @header && @header != ''
156
+ write_odd_footer(writer) if @footer && @footer != ''
157
+ end
158
+ elsif excel2003_style
159
+ writer.empty_tag(tag, attributes)
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ #
166
+ # Write the <oddHeader> element.
167
+ #
168
+ def write_odd_header(writer) # :nodoc:
169
+ writer.data_element('oddHeader', @header)
170
+ end
171
+
172
+ #
173
+ # Write the <oddFooter> element.
174
+ #
175
+ def write_odd_footer(writer) # :nodoc:
176
+ writer.data_element('oddFooter', @footer)
177
+ end
178
+
179
+ def margin_attributes # :nodoc:
180
+ [
181
+ ['left', @margin_left],
182
+ ['right', @margin_right],
183
+ ['top', @margin_top],
184
+ ['bottom', @margin_bottom],
185
+ ['header', @margin_header],
186
+ ['footer', @margin_footer]
187
+ ]
188
+ end
189
+ end
190
+ end
@@ -91,9 +91,9 @@ module Writexlsx
91
91
  def write_comment_files(package_dir)
92
92
  self.select { |sheet| sheet.has_comments? }
93
93
  .each_with_index do |sheet, index|
94
- FileUtils.mkdir_p("#{package_dir}/xl/drawings")
95
- sheet.comments.set_xml_writer("#{package_dir}/xl/comments#{index + 1}.xml")
96
- sheet.comments.assemble_xml_file
94
+ FileUtils.mkdir_p("#{package_dir}/xl/drawings")
95
+ sheet.comments.set_xml_writer("#{package_dir}/xl/comments#{index + 1}.xml")
96
+ sheet.comments.assemble_xml_file
97
97
  end
98
98
  end
99
99
 
@@ -99,7 +99,7 @@ module Writexlsx
99
99
  def xl_range_formula(sheetname, row_1, row_2, col_1, col_2)
100
100
  # Use Excel's conventions and quote the sheet name if it contains any
101
101
  # non-word character or if it isn't already quoted.
102
- sheetname = "'#{sheetname}'" if sheetname =~ /\W/ && !(sheetname =~ /^'/)
102
+ sheetname = quote_sheetname(sheetname)
103
103
 
104
104
  range1 = xl_rowcol_to_cell(row_1, col_1, 1, 1)
105
105
  range2 = xl_rowcol_to_cell(row_2, col_2, 1, 1)
@@ -128,15 +128,11 @@ module Writexlsx
128
128
  # TODO. We need to handle more special cases.
129
129
  #
130
130
  def quote_sheetname(sheetname) # :nodoc:
131
- # Use Excel's conventions and quote the sheet name if it comtains any
132
- # non-word character or if it isn't already quoted.
133
131
  name = sheetname.dup
134
- if name =~ /\W/ && !(name =~ /^'/)
135
- # Double quote and single quoted strings.
136
- name = name.gsub("'", "''")
137
- name = "'#{name}'"
138
- end
139
- name
132
+ return name if already_quoted_sheetname?(name)
133
+ return name unless sheetname_needs_quoting?(name)
134
+
135
+ "'#{escape_sheetname(name)}'"
140
136
  end
141
137
 
142
138
  def check_dimensions(row, col)
@@ -965,6 +961,58 @@ module Writexlsx
965
961
  def write_a_end_para_rpr # :nodoc:
966
962
  @writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
967
963
  end
964
+
965
+ private
966
+
967
+ def already_quoted_sheetname?(name)
968
+ name.start_with?("'")
969
+ end
970
+
971
+ def escape_sheetname(name)
972
+ name.gsub("'", "''")
973
+ end
974
+
975
+ def sheetname_needs_quoting?(name)
976
+ contains_non_identifier_chars?(name) ||
977
+ starts_with_digit_or_dot?(name) ||
978
+ valid_a1_reference_name?(name) ||
979
+ starts_with_rc_reference?(name) ||
980
+ single_rc_reference?(name)
981
+ end
982
+
983
+ def contains_non_identifier_chars?(name)
984
+ name.match?(/[^\p{L}\p{N}_.]/)
985
+ end
986
+
987
+ def starts_with_digit_or_dot?(name)
988
+ name.match?(/^[\p{N}.]/)
989
+ end
990
+
991
+ def valid_a1_reference_name?(name)
992
+ upcased = name.upcase
993
+ return false unless upcased.match?(/^[A-Z]{1,3}\d+$/)
994
+
995
+ row, col = xl_cell_to_rowcol(upcased)
996
+ row.between?(0, 1_048_575) && col.between?(0, 16_383)
997
+ end
998
+
999
+ def starts_with_rc_reference?(name)
1000
+ upcased = name.upcase
1001
+
1002
+ if (match = upcased.match(/^R(\d+)/))
1003
+ return match[1].to_i.between?(1, 1_048_576)
1004
+ end
1005
+
1006
+ if (match = upcased.match(/^R?C(\d+)/))
1007
+ return match[1].to_i.between?(1, 16_384)
1008
+ end
1009
+
1010
+ false
1011
+ end
1012
+
1013
+ def single_rc_reference?(name)
1014
+ %w[R C RC].include?(name.upcase)
1015
+ end
968
1016
  end
969
1017
 
970
1018
  module WriteDPtPoint
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WriteXLSX_VERSION = "1.12.3"
3
+ WriteXLSX_VERSION = "1.13.0"
@@ -1303,40 +1303,40 @@ module Writexlsx
1303
1303
  # Build an array of the worksheet charts including any combined charts.
1304
1304
  @charts.collect { |chart| [chart, chart.combined] }.flatten.compact
1305
1305
  .each do |chart|
1306
- chart.formula_ids.each do |range, id|
1307
- # Skip if the series has user defined data.
1308
- if chart.formula_data[id]
1309
- seen_ranges[range] = chart.formula_data[id] unless seen_ranges[range]
1310
- next
1311
- # Check to see if the data is already cached locally.
1312
- elsif seen_ranges[range]
1313
- chart.formula_data[id] = seen_ranges[range]
1314
- next
1315
- end
1316
-
1317
- # Convert the range formula to a sheet name and cell range.
1318
- sheetname, *cells = get_chart_range(range)
1319
-
1320
- # Skip if we couldn't parse the formula.
1321
- next unless sheetname
1322
-
1323
- # Handle non-contiguous ranges: (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5).
1324
- # We don't try to parse the ranges. We just return an empty list.
1325
- if sheetname =~ /^\([^,]+,/
1326
- chart.formula_data[id] = []
1327
- seen_ranges[range] = []
1328
- next
1329
- end
1330
-
1331
- # Raise if the name is unknown since it indicates a user error in
1332
- # a chart series formula.
1333
- raise "Unknown worksheet reference '#{sheetname} in range '#{range}' passed to add_series()\n" unless worksheets[sheetname]
1334
-
1335
- # Add the data to the chart.
1336
- # And store range data locally to avoid lookup if seen agein.
1337
- chart.formula_data[id] =
1338
- seen_ranges[range] = chart_data(worksheets[sheetname], cells)
1339
- end
1306
+ chart.formula_ids.each do |range, id|
1307
+ # Skip if the series has user defined data.
1308
+ if chart.formula_data[id]
1309
+ seen_ranges[range] = chart.formula_data[id] unless seen_ranges[range]
1310
+ next
1311
+ # Check to see if the data is already cached locally.
1312
+ elsif seen_ranges[range]
1313
+ chart.formula_data[id] = seen_ranges[range]
1314
+ next
1315
+ end
1316
+
1317
+ # Convert the range formula to a sheet name and cell range.
1318
+ sheetname, *cells = get_chart_range(range)
1319
+
1320
+ # Skip if we couldn't parse the formula.
1321
+ next unless sheetname
1322
+
1323
+ # Handle non-contiguous ranges: (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5).
1324
+ # We don't try to parse the ranges. We just return an empty list.
1325
+ if sheetname =~ /^\([^,]+,/
1326
+ chart.formula_data[id] = []
1327
+ seen_ranges[range] = []
1328
+ next
1329
+ end
1330
+
1331
+ # Raise if the name is unknown since it indicates a user error in
1332
+ # a chart series formula.
1333
+ raise "Unknown worksheet reference '#{sheetname}' in range '#{range}' passed to add_series()\n" unless worksheets[sheetname]
1334
+
1335
+ # Add the data to the chart.
1336
+ # And store range data locally to avoid lookup if seen agein.
1337
+ chart.formula_data[id] =
1338
+ seen_ranges[range] = chart_data(worksheets[sheetname], cells)
1339
+ end
1340
1340
  end
1341
1341
  end
1342
1342