write_xlsx 1.12.3 → 1.15.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +22 -0
  3. data/Changes +23 -0
  4. data/README.md +1 -1
  5. data/lib/write_xlsx/chart/area.rb +2 -2
  6. data/lib/write_xlsx/chart/axis.rb +55 -32
  7. data/lib/write_xlsx/chart/axis_writer.rb +528 -0
  8. data/lib/write_xlsx/chart/bar.rb +2 -2
  9. data/lib/write_xlsx/chart/caption.rb +16 -9
  10. data/lib/write_xlsx/chart/chart_area.rb +121 -0
  11. data/lib/write_xlsx/chart/column.rb +2 -2
  12. data/lib/write_xlsx/chart/d_pt_point_writer.rb +14 -0
  13. data/lib/write_xlsx/chart/doughnut.rb +0 -3
  14. data/lib/write_xlsx/chart/formatting_writer.rb +652 -0
  15. data/lib/write_xlsx/chart/initialization.rb +100 -0
  16. data/lib/write_xlsx/chart/line.rb +4 -3
  17. data/lib/write_xlsx/chart/pie.rb +6 -2
  18. data/lib/write_xlsx/chart/radar.rb +2 -2
  19. data/lib/write_xlsx/chart/scatter.rb +4 -3
  20. data/lib/write_xlsx/chart/series.rb +35 -15
  21. data/lib/write_xlsx/chart/series_data.rb +132 -0
  22. data/lib/write_xlsx/chart/series_writer.rb +318 -0
  23. data/lib/write_xlsx/chart/settings.rb +226 -0
  24. data/lib/write_xlsx/chart/stock.rb +2 -2
  25. data/lib/write_xlsx/chart/table.rb +50 -0
  26. data/lib/write_xlsx/chart/xml_writer.rb +305 -0
  27. data/lib/write_xlsx/chart.rb +286 -2477
  28. data/lib/write_xlsx/chartsheet.rb +35 -83
  29. data/lib/write_xlsx/constants.rb +11 -0
  30. data/lib/write_xlsx/drawing.rb +5 -3
  31. data/lib/write_xlsx/format/alignment_state.rb +39 -0
  32. data/lib/write_xlsx/format/alignment_style.rb +92 -0
  33. data/lib/write_xlsx/format/border_state.rb +47 -0
  34. data/lib/write_xlsx/format/border_style.rb +116 -0
  35. data/lib/write_xlsx/format/fill_state.rb +26 -0
  36. data/lib/write_xlsx/format/fill_style.rb +52 -0
  37. data/lib/write_xlsx/format/font_state.rb +74 -0
  38. data/lib/write_xlsx/format/font_style.rb +172 -0
  39. data/lib/write_xlsx/format/format_state.rb +65 -0
  40. data/lib/write_xlsx/format/number_format_state.rb +20 -0
  41. data/lib/write_xlsx/format/number_format_style.rb +28 -0
  42. data/lib/write_xlsx/format/protection_state.rb +20 -0
  43. data/lib/write_xlsx/format/protection_style.rb +28 -0
  44. data/lib/write_xlsx/format.rb +1093 -426
  45. data/lib/write_xlsx/formats.rb +0 -2
  46. data/lib/write_xlsx/image_property.rb +4 -1
  47. data/lib/write_xlsx/inserted_chart.rb +1 -1
  48. data/lib/write_xlsx/object_positioning.rb +203 -0
  49. data/lib/write_xlsx/package/app.rb +3 -3
  50. data/lib/write_xlsx/package/button.rb +6 -2
  51. data/lib/write_xlsx/package/comments.rb +11 -3
  52. data/lib/write_xlsx/package/conditional_format.rb +7 -3
  53. data/lib/write_xlsx/package/content_types.rb +2 -2
  54. data/lib/write_xlsx/package/core.rb +2 -2
  55. data/lib/write_xlsx/package/custom.rb +3 -2
  56. data/lib/write_xlsx/package/metadata.rb +2 -2
  57. data/lib/write_xlsx/package/packager.rb +0 -3
  58. data/lib/write_xlsx/package/relationships.rb +2 -2
  59. data/lib/write_xlsx/package/rich_value.rb +4 -2
  60. data/lib/write_xlsx/package/rich_value_rel.rb +2 -2
  61. data/lib/write_xlsx/package/rich_value_structure.rb +2 -2
  62. data/lib/write_xlsx/package/rich_value_types.rb +3 -3
  63. data/lib/write_xlsx/package/shared_strings.rb +2 -2
  64. data/lib/write_xlsx/package/styles.rb +13 -9
  65. data/lib/write_xlsx/package/table.rb +8 -2
  66. data/lib/write_xlsx/package/theme.rb +0 -3
  67. data/lib/write_xlsx/package/vml.rb +2 -2
  68. data/lib/write_xlsx/page_setup.rb +192 -0
  69. data/lib/write_xlsx/shape.rb +97 -100
  70. data/lib/write_xlsx/sheets.rb +9 -4
  71. data/lib/write_xlsx/sparkline.rb +2 -2
  72. data/lib/write_xlsx/utility/cell_reference.rb +124 -0
  73. data/lib/write_xlsx/utility/chart_formatting.rb +262 -0
  74. data/lib/write_xlsx/utility/common.rb +44 -0
  75. data/lib/write_xlsx/utility/date_time.rb +113 -0
  76. data/lib/write_xlsx/utility/dimensions.rb +40 -0
  77. data/lib/write_xlsx/utility/drawing.rb +136 -0
  78. data/lib/write_xlsx/utility/rich_text.rb +184 -0
  79. data/lib/write_xlsx/utility/sheetname_quoting.rb +73 -0
  80. data/lib/write_xlsx/utility/string_width.rb +45 -0
  81. data/lib/write_xlsx/utility/url.rb +27 -0
  82. data/lib/write_xlsx/utility/xml_primitives.rb +32 -0
  83. data/lib/write_xlsx/version.rb +1 -1
  84. data/lib/write_xlsx/workbook/chart_data.rb +188 -0
  85. data/lib/write_xlsx/workbook/format_preparation.rb +199 -0
  86. data/lib/write_xlsx/workbook/initialization.rb +223 -0
  87. data/lib/write_xlsx/workbook/package_preparation.rb +231 -0
  88. data/lib/write_xlsx/workbook/workbook_writer.rb +164 -0
  89. data/lib/write_xlsx/workbook.rb +143 -981
  90. data/lib/write_xlsx/worksheet/asset_manager.rb +60 -0
  91. data/lib/write_xlsx/worksheet/autofilter.rb +390 -0
  92. data/lib/write_xlsx/worksheet/cell_data.rb +13 -6
  93. data/lib/write_xlsx/worksheet/cell_data_manager.rb +47 -0
  94. data/lib/write_xlsx/worksheet/cell_data_store.rb +61 -0
  95. data/lib/write_xlsx/worksheet/columns.rb +204 -0
  96. data/lib/write_xlsx/worksheet/comments_support.rb +61 -0
  97. data/lib/write_xlsx/worksheet/conditional_formats.rb +30 -0
  98. data/lib/write_xlsx/worksheet/data_validation.rb +9 -1
  99. data/lib/write_xlsx/worksheet/data_writing.rb +1017 -0
  100. data/lib/write_xlsx/worksheet/drawing_methods.rb +308 -0
  101. data/lib/write_xlsx/worksheet/drawing_preparation.rb +290 -0
  102. data/lib/write_xlsx/worksheet/drawing_relations.rb +76 -0
  103. data/lib/write_xlsx/worksheet/drawing_xml_writer.rb +50 -0
  104. data/lib/write_xlsx/worksheet/formatting.rb +418 -0
  105. data/lib/write_xlsx/worksheet/hyperlink.rb +9 -1
  106. data/lib/write_xlsx/worksheet/initialization.rb +146 -0
  107. data/lib/write_xlsx/worksheet/panes.rb +64 -0
  108. data/lib/write_xlsx/worksheet/print_options.rb +72 -0
  109. data/lib/write_xlsx/worksheet/protection.rb +65 -0
  110. data/lib/write_xlsx/worksheet/rich_text_helpers.rb +78 -0
  111. data/lib/write_xlsx/worksheet/row_col_sizing.rb +69 -0
  112. data/lib/write_xlsx/worksheet/rows.rb +84 -0
  113. data/lib/write_xlsx/worksheet/selection.rb +41 -0
  114. data/lib/write_xlsx/worksheet/xml_writer.rb +1246 -0
  115. data/lib/write_xlsx/worksheet.rb +376 -4530
  116. metadata +66 -4
  117. data/lib/write_xlsx/utility.rb +0 -986
  118. data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
@@ -0,0 +1,184 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module RichText
7
+ def underline_attributes(underline)
8
+ case underline
9
+ when 2
10
+ [%w[val double]]
11
+ when 33
12
+ [%w[val singleAccounting]]
13
+ when 34
14
+ [%w[val doubleAccounting]]
15
+ else
16
+ [] # Default to single underline.
17
+ end
18
+ end
19
+
20
+ #
21
+ # Convert user defined font values into private hash values.
22
+ #
23
+ def convert_font_args(params)
24
+ return unless params
25
+
26
+ font = params_to_font(params)
27
+
28
+ # Convert font size units.
29
+ font[:_size] *= 100 if font[:_size] && font[:_size] != 0
30
+
31
+ # Convert rotation into 60,000ths of a degree.
32
+ font[:_rotation] = 60_000 * font[:_rotation].to_i if ptrue?(font[:_rotation])
33
+
34
+ font
35
+ end
36
+
37
+ def params_to_font(params)
38
+ {
39
+ _name: params[:name],
40
+ _color: params[:color],
41
+ _size: params[:size],
42
+ _bold: params[:bold],
43
+ _italic: params[:italic],
44
+ _underline: params[:underline],
45
+ _pitch_family: params[:pitch_family],
46
+ _charset: params[:charset],
47
+ _baseline: params[:baseline] || 0,
48
+ _rotation: params[:rotation]
49
+ }
50
+ end
51
+
52
+ #
53
+ # Get the font style attributes from a font hash.
54
+ #
55
+ def get_font_style_attributes(font)
56
+ return [] unless font
57
+ return [] unless font.respond_to?(:[])
58
+
59
+ attributes = []
60
+ attributes << ['sz', font[:_size]] if ptrue?(font[:_size])
61
+ attributes << ['b', font[:_bold]] if font[:_bold]
62
+ attributes << ['i', font[:_italic]] if font[:_italic]
63
+ attributes << %w[u sng] if font[:_underline]
64
+
65
+ # Turn off baseline when testing fonts that don't have it.
66
+ attributes << ['baseline', font[:_baseline]] if font[:_baseline] != -1
67
+ attributes
68
+ end
69
+
70
+ #
71
+ # Get the font latin attributes from a font hash.
72
+ #
73
+ def get_font_latin_attributes(font)
74
+ return [] unless font
75
+ return [] unless font.respond_to?(:[])
76
+
77
+ attributes = []
78
+ attributes << ['typeface', font[:_name]] if ptrue?(font[:_name])
79
+ attributes << ['pitchFamily', font[:_pitch_family]] if font[:_pitch_family]
80
+ attributes << ['charset', font[:_charset]] if font[:_charset]
81
+
82
+ attributes
83
+ end
84
+
85
+ #
86
+ # Write the <c:txPr> element.
87
+ #
88
+ def write_tx_pr(font, is_y_axis = nil) # :nodoc:
89
+ rotation = nil
90
+ rotation = font[:_rotation] if font && font.respond_to?(:[]) && font[:_rotation]
91
+ @writer.tag_elements('c:txPr') do
92
+ # Write the a:bodyPr element.
93
+ write_a_body_pr(rotation, is_y_axis)
94
+ # Write the a:lstStyle element.
95
+ write_a_lst_style
96
+ # Write the a:p element.
97
+ write_a_p_formula(font)
98
+ end
99
+ end
100
+
101
+ #
102
+ # Write the <a:bodyPr> element.
103
+ #
104
+ def write_a_body_pr(rot, is_y_axis = nil) # :nodoc:
105
+ rot = -5400000 if !rot && ptrue?(is_y_axis)
106
+ attributes = []
107
+ if rot
108
+ if rot == 16_200_000
109
+ # 270 deg/stacked angle.
110
+ attributes << ['rot', 0]
111
+ attributes << %w[vert wordArtVert]
112
+ elsif rot == 16_260_000
113
+ # 271 deg/stacked angle.
114
+ attributes << ['rot', 0]
115
+ attributes << %w[vert eaVert]
116
+ else
117
+ attributes << ['rot', rot]
118
+ attributes << %w[vert horz]
119
+ end
120
+ end
121
+
122
+ @writer.empty_tag('a:bodyPr', attributes)
123
+ end
124
+
125
+ #
126
+ # Write the <a:lstStyle> element.
127
+ #
128
+ def write_a_lst_style # :nodoc:
129
+ @writer.empty_tag('a:lstStyle')
130
+ end
131
+
132
+ #
133
+ # Write the <a:p> element for formula titles.
134
+ #
135
+ def write_a_p_formula(font = nil) # :nodoc:
136
+ @writer.tag_elements('a:p') do
137
+ # Write the a:pPr element.
138
+ write_a_p_pr_formula(font)
139
+ # Write the a:endParaRPr element.
140
+ write_a_end_para_rpr
141
+ end
142
+ end
143
+
144
+ #
145
+ # Write the <a:pPr> element for formula titles.
146
+ #
147
+ def write_a_p_pr_formula(font) # :nodoc:
148
+ @writer.tag_elements('a:pPr') { write_a_def_rpr(font) }
149
+ end
150
+
151
+ #
152
+ # Write the <a:defRPr> element.
153
+ #
154
+ def write_a_def_rpr(font = nil) # :nodoc:
155
+ write_def_rpr_r_pr_common(
156
+ font,
157
+ get_font_style_attributes(font),
158
+ 'a:defRPr'
159
+ )
160
+ end
161
+
162
+ def write_def_rpr_r_pr_common(font, style_attributes, tag) # :nodoc:
163
+ latin_attributes = get_font_latin_attributes(font)
164
+ has_color = ptrue?(font) && ptrue?(font[:_color])
165
+
166
+ if !latin_attributes.empty? || has_color
167
+ @writer.tag_elements(tag, style_attributes) do
168
+ write_a_solid_fill(color: font[:_color]) if has_color
169
+ write_a_latin(latin_attributes) unless latin_attributes.empty?
170
+ end
171
+ else
172
+ @writer.empty_tag(tag, style_attributes)
173
+ end
174
+ end
175
+
176
+ #
177
+ # Write the <a:endParaRPr> element.
178
+ #
179
+ def write_a_end_para_rpr # :nodoc:
180
+ @writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,73 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module SheetnameQuoting
7
+ #
8
+ # Sheetnames used in references should be quoted if they contain any spaces,
9
+ # special characters or if the look like something that isn't a sheet name.
10
+ # TODO. We need to handle more special cases.
11
+ #
12
+ def quote_sheetname(sheetname) # :nodoc:
13
+ name = sheetname.dup
14
+ return name if already_quoted_sheetname?(name)
15
+ return name unless sheetname_needs_quoting?(name)
16
+
17
+ "'#{escape_sheetname(name)}'"
18
+ end
19
+
20
+ private
21
+
22
+ def already_quoted_sheetname?(name)
23
+ name.start_with?("'")
24
+ end
25
+
26
+ def escape_sheetname(name)
27
+ name.gsub("'", "''")
28
+ end
29
+
30
+ def sheetname_needs_quoting?(name)
31
+ contains_non_identifier_chars?(name) ||
32
+ starts_with_digit_or_dot?(name) ||
33
+ valid_a1_reference_name?(name) ||
34
+ starts_with_rc_reference?(name) ||
35
+ single_rc_reference?(name)
36
+ end
37
+
38
+ def contains_non_identifier_chars?(name)
39
+ name.match?(/[^\p{L}\p{N}_.]/)
40
+ end
41
+
42
+ def starts_with_digit_or_dot?(name)
43
+ name.match?(/^[\p{N}.]/)
44
+ end
45
+
46
+ def valid_a1_reference_name?(name)
47
+ upcased = name.upcase
48
+ return false unless upcased.match?(/^[A-Z]{1,3}\d+$/)
49
+
50
+ row, col = xl_cell_to_rowcol(upcased)
51
+ row.between?(0, 1_048_575) && col.between?(0, 16_383)
52
+ end
53
+
54
+ def starts_with_rc_reference?(name)
55
+ upcased = name.upcase
56
+
57
+ if (match = upcased.match(/^R(\d+)/))
58
+ return match[1].to_i.between?(1, 1_048_576)
59
+ end
60
+
61
+ if (match = upcased.match(/^R?C(\d+)/))
62
+ return match[1].to_i.between?(1, 16_384)
63
+ end
64
+
65
+ false
66
+ end
67
+
68
+ def single_rc_reference?(name)
69
+ %w[R C RC].include?(name.upcase)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module StringWidth
7
+ MAX_DIGIT_WIDTH = 7 # For Calabri 11. # :nodoc:
8
+ PADDING = 5 # :nodoc:
9
+ DEFAULT_COL_PIXELS = 64
10
+ CHAR_WIDTHS = {
11
+ ' ' => 3, '!' => 5, '"' => 6, '#' => 7, '$' => 7, '%' => 11,
12
+ '&' => 10, "'" => 3, '(' => 5, ')' => 5, '*' => 7, '+' => 7,
13
+ ',' => 4, '-' => 5, '.' => 4, '/' => 6, '0' => 7, '1' => 7,
14
+ '2' => 7, '3' => 7, '4' => 7, '5' => 7, '6' => 7, '7' => 7,
15
+ '8' => 7, '9' => 7, ':' => 4, ';' => 4, '<' => 7, '=' => 7,
16
+ '>' => 7, '?' => 7, '@' => 13, 'A' => 9, 'B' => 8, 'C' => 8,
17
+ 'D' => 9, 'E' => 7, 'F' => 7, 'G' => 9, 'H' => 9, 'I' => 4,
18
+ 'J' => 5, 'K' => 8, 'L' => 6, 'M' => 12, 'N' => 10, 'O' => 10,
19
+ 'P' => 8, 'Q' => 10, 'R' => 8, 'S' => 7, 'T' => 7, 'U' => 9,
20
+ 'V' => 9, 'W' => 13, 'X' => 8, 'Y' => 7, 'Z' => 7, '[' => 5,
21
+ '\\' => 6, ']' => 5, '^' => 7, '_' => 7, '`' => 4, 'a' => 7,
22
+ 'b' => 8, 'c' => 6, 'd' => 8, 'e' => 8, 'f' => 5, 'g' => 7,
23
+ 'h' => 8, 'i' => 4, 'j' => 4, 'k' => 7, 'l' => 4, 'm' => 12,
24
+ 'n' => 8, 'o' => 8, 'p' => 8, 'q' => 8, 'r' => 5, 's' => 6,
25
+ 't' => 5, 'u' => 8, 'v' => 7, 'w' => 11, 'x' => 7, 'y' => 7,
26
+ 'z' => 6, '{' => 5, '|' => 7, '}' => 5, '~' => 7
27
+ }.freeze
28
+
29
+ #
30
+ # xl_string_pixel_width($string)
31
+ #
32
+ # Get the pixel width of a string based on individual character widths taken
33
+ # from Excel. UTF8 characters are given a default width of 8.
34
+ #
35
+ # Note, Excel adds an additional 7 pixels padding to a cell.
36
+ #
37
+ def xl_string_pixel_width(string)
38
+ length = 0
39
+ string.to_s.chars.each { |char| length += CHAR_WIDTHS[char] || 8 }
40
+
41
+ length
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module Url
7
+ def escape_url(url)
8
+ unless url =~ /%[0-9a-fA-F]{2}/
9
+ # Escape the URL escape symbol.
10
+ url = url.gsub("%", "%25")
11
+
12
+ # Escape whitespae in URL.
13
+ url = url.gsub(/[\s\x00]/, '%20')
14
+
15
+ # Escape other special characters in URL.
16
+ re = /(["<>\[\]`^{}])/
17
+ while re =~ url
18
+ match = $LAST_MATCH_INFO[1]
19
+ url = url.sub(re, sprintf("%%%x", match.ord))
20
+ end
21
+ end
22
+
23
+ url
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module XmlPrimitives
7
+ #
8
+ # Write the <color> element.
9
+ #
10
+ def write_color(name, value, writer = @writer) # :nodoc:
11
+ attributes = [[name, value]]
12
+
13
+ writer.empty_tag('color', attributes)
14
+ end
15
+
16
+ def write_xml_declaration
17
+ @writer.xml_decl
18
+ yield
19
+ @writer.crlf
20
+ @writer.close
21
+ end
22
+
23
+ def r_id_attributes(id)
24
+ ['r:id', "rId#{id}"]
25
+ end
26
+
27
+ def xml_str
28
+ @writer.string
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WriteXLSX_VERSION = "1.12.3"
3
+ WriteXLSX_VERSION = "1.15.0"
@@ -0,0 +1,188 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ class Workbook
6
+ module ChartData
7
+ private
8
+
9
+ #
10
+ # Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name and cell
11
+ # range such as ( 'Sheet1', 0, 1, 4, 1 ).
12
+ #
13
+ def get_chart_range(range) # :nodoc:
14
+ # Split the range formula into sheetname and cells at the last '!'.
15
+ pos = range.rindex('!')
16
+ return nil unless pos
17
+
18
+ if pos > 0
19
+ sheetname = range[0, pos]
20
+ cells = range[(pos + 1)..-1]
21
+ end
22
+
23
+ # Split the cell range into 2 cells or else use single cell for both.
24
+ if cells =~ /:/
25
+ cell_1, cell_2 = cells.split(":")
26
+ else
27
+ cell_1 = cells
28
+ cell_2 = cells
29
+ end
30
+
31
+ # Remove leading/trailing apostrophes and convert escaped quotes to single.
32
+ sheetname.sub!(/^'/, '')
33
+ sheetname.sub!(/'$/, '')
34
+ sheetname.gsub!("''", "'")
35
+
36
+ row_start, col_start = xl_cell_to_rowcol(cell_1)
37
+ row_end, col_end = xl_cell_to_rowcol(cell_2)
38
+
39
+ # Check that we have a 1D range only.
40
+ return nil if row_start != row_end && col_start != col_end
41
+
42
+ [sheetname, row_start, col_start, row_end, col_end]
43
+ end
44
+
45
+ #
46
+ # Add "cached" data to charts to provide the numCache and strCache data for
47
+ # series and title/axis ranges.
48
+ #
49
+ def add_chart_data # :nodoc:
50
+ worksheets = {}
51
+ seen_ranges = {}
52
+
53
+ # Map worksheet names to worksheet objects.
54
+ @worksheets.each { |worksheet| worksheets[worksheet.name] = worksheet }
55
+
56
+ # Build an array of the worksheet charts including any combined charts.
57
+ @charts.collect { |chart| [chart, chart.combined] }.flatten.compact
58
+ .each do |chart|
59
+ chart.formula_ids.each do |range, id|
60
+ # Skip if the series has user defined data.
61
+ if chart.formula_data[id]
62
+ seen_ranges[range] = chart.formula_data[id] unless seen_ranges[range]
63
+ next
64
+ # Check to see if the data is already cached locally.
65
+ elsif seen_ranges[range]
66
+ chart.formula_data[id] = seen_ranges[range]
67
+ next
68
+ end
69
+
70
+ # Convert the range formula to a sheet name and cell range.
71
+ sheetname, *cells = get_chart_range(range)
72
+
73
+ # Skip if we couldn't parse the formula.
74
+ next unless sheetname
75
+
76
+ # Handle non-contiguous ranges: (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5).
77
+ # We don't try to parse the ranges. We just return an empty list.
78
+ if sheetname =~ /^\([^,]+,/
79
+ chart.formula_data[id] = []
80
+ seen_ranges[range] = []
81
+ next
82
+ end
83
+
84
+ # Raise if the name is unknown since it indicates a user error in
85
+ # a chart series formula.
86
+ raise "Unknown worksheet reference '#{sheetname}' in range '#{range}' passed to add_series()\n" unless worksheets[sheetname]
87
+
88
+ # Add the data to the chart.
89
+ # And store range data locally to avoid lookup if seen agein.
90
+ chart.formula_data[id] =
91
+ seen_ranges[range] = chart_data(worksheets[sheetname], cells)
92
+ end
93
+ end
94
+ end
95
+
96
+ def chart_data(worksheet, cells)
97
+ # Get the data from the worksheet table.
98
+ data = worksheet.get_range_data(*cells)
99
+
100
+ # Convert shared string indexes to strings.
101
+ data.collect do |token|
102
+ if token.is_a?(Hash)
103
+ string = @shared_strings.string(token[:sst_id])
104
+
105
+ # Ignore rich strings for now. Deparse later if necessary.
106
+ if string =~ /^<r>/ && string =~ %r{</r>$}
107
+ ''
108
+ else
109
+ string
110
+ end
111
+ else
112
+ token
113
+ end
114
+ end
115
+ end
116
+
117
+ #
118
+ # Sort internal and user defined names in the same order as used by Excel.
119
+ # This may not be strictly necessary but unsorted elements caused a lot of
120
+ # issues in the the Spreadsheet::WriteExcel binary version. Also makes
121
+ # comparison testing easier.
122
+ #
123
+ def sort_defined_names(names) # :nodoc:
124
+ names.sort do |a, b|
125
+ name_a = normalise_defined_name(a[0])
126
+ name_b = normalise_defined_name(b[0])
127
+ sheet_a = normalise_sheet_name(a[2])
128
+ sheet_b = normalise_sheet_name(b[2])
129
+ # Primary sort based on the defined name.
130
+ if name_a > name_b
131
+ 1
132
+ elsif name_a < name_b
133
+ -1
134
+ elsif sheet_a >= sheet_b # name_a == name_b
135
+ # Secondary sort based on the sheet name.
136
+ 1
137
+ else
138
+ -1
139
+ end
140
+ end
141
+ end
142
+
143
+ # Used in the above sort routine to normalise the defined names. Removes any
144
+ # leading '_xmln.' from internal names and lowercases the strings.
145
+ def normalise_defined_name(name) # :nodoc:
146
+ name.sub(/^_xlnm./, '').downcase
147
+ end
148
+
149
+ # Used in the above sort routine to normalise the worksheet names for the
150
+ # secondary sort. Removes leading quote and lowercases the strings.
151
+ def normalise_sheet_name(name) # :nodoc:
152
+ name.sub(/^'/, '').downcase
153
+ end
154
+
155
+ #
156
+ # Extract the named ranges from the sorted list of defined names. These are
157
+ # used in the App.xml file.
158
+ #
159
+ def extract_named_ranges(defined_names) # :nodoc:
160
+ named_ranges = []
161
+
162
+ defined_names.each do |defined_name|
163
+ name, index, range = defined_name
164
+
165
+ # Skip autoFilter ranges.
166
+ next if name == '_xlnm._FilterDatabase'
167
+
168
+ # We are only interested in defined names with ranges.
169
+ next unless range =~ /^([^!]+)!/
170
+
171
+ sheet_name = ::Regexp.last_match(1)
172
+
173
+ # Match Print_Area and Print_Titles xlnm types.
174
+ if name =~ /^_xlnm\.(.*)$/
175
+ xlnm_type = ::Regexp.last_match(1)
176
+ name = "#{sheet_name}!#{xlnm_type}"
177
+ elsif index != -1
178
+ name = "#{sheet_name}!#{name}"
179
+ end
180
+
181
+ named_ranges << name
182
+ end
183
+
184
+ named_ranges
185
+ end
186
+ end
187
+ end
188
+ end