write_xlsx 1.13.0 → 1.15.1

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +25 -4
  3. data/Changes +23 -0
  4. data/lib/write_xlsx/chart/area.rb +2 -2
  5. data/lib/write_xlsx/chart/axis.rb +55 -32
  6. data/lib/write_xlsx/chart/axis_writer.rb +528 -0
  7. data/lib/write_xlsx/chart/bar.rb +2 -2
  8. data/lib/write_xlsx/chart/caption.rb +16 -9
  9. data/lib/write_xlsx/chart/chart_area.rb +121 -0
  10. data/lib/write_xlsx/chart/column.rb +2 -2
  11. data/lib/write_xlsx/chart/d_pt_point_writer.rb +14 -0
  12. data/lib/write_xlsx/chart/doughnut.rb +0 -3
  13. data/lib/write_xlsx/chart/formatting_writer.rb +652 -0
  14. data/lib/write_xlsx/chart/initialization.rb +100 -0
  15. data/lib/write_xlsx/chart/line.rb +4 -3
  16. data/lib/write_xlsx/chart/pie.rb +6 -2
  17. data/lib/write_xlsx/chart/radar.rb +2 -2
  18. data/lib/write_xlsx/chart/scatter.rb +4 -3
  19. data/lib/write_xlsx/chart/series.rb +35 -15
  20. data/lib/write_xlsx/chart/series_data.rb +132 -0
  21. data/lib/write_xlsx/chart/series_writer.rb +318 -0
  22. data/lib/write_xlsx/chart/settings.rb +226 -0
  23. data/lib/write_xlsx/chart/stock.rb +2 -2
  24. data/lib/write_xlsx/chart/table.rb +50 -0
  25. data/lib/write_xlsx/chart/xml_writer.rb +305 -0
  26. data/lib/write_xlsx/chart.rb +286 -2477
  27. data/lib/write_xlsx/chartsheet.rb +31 -82
  28. data/lib/write_xlsx/constants.rb +11 -0
  29. data/lib/write_xlsx/drawing.rb +5 -3
  30. data/lib/write_xlsx/format/alignment_state.rb +39 -0
  31. data/lib/write_xlsx/format/alignment_style.rb +92 -0
  32. data/lib/write_xlsx/format/border_state.rb +47 -0
  33. data/lib/write_xlsx/format/border_style.rb +116 -0
  34. data/lib/write_xlsx/format/fill_state.rb +26 -0
  35. data/lib/write_xlsx/format/fill_style.rb +52 -0
  36. data/lib/write_xlsx/format/font_state.rb +74 -0
  37. data/lib/write_xlsx/format/font_style.rb +172 -0
  38. data/lib/write_xlsx/format/format_state.rb +65 -0
  39. data/lib/write_xlsx/format/number_format_state.rb +20 -0
  40. data/lib/write_xlsx/format/number_format_style.rb +28 -0
  41. data/lib/write_xlsx/format/protection_state.rb +20 -0
  42. data/lib/write_xlsx/format/protection_style.rb +28 -0
  43. data/lib/write_xlsx/format.rb +1093 -426
  44. data/lib/write_xlsx/formats.rb +0 -2
  45. data/lib/write_xlsx/image_property.rb +4 -1
  46. data/lib/write_xlsx/inserted_chart.rb +1 -1
  47. data/lib/write_xlsx/object_positioning.rb +15 -1
  48. data/lib/write_xlsx/package/app.rb +2 -2
  49. data/lib/write_xlsx/package/button.rb +6 -2
  50. data/lib/write_xlsx/package/comments.rb +11 -3
  51. data/lib/write_xlsx/package/conditional_format.rb +7 -3
  52. data/lib/write_xlsx/package/content_types.rb +2 -2
  53. data/lib/write_xlsx/package/core.rb +2 -2
  54. data/lib/write_xlsx/package/custom.rb +3 -2
  55. data/lib/write_xlsx/package/metadata.rb +2 -2
  56. data/lib/write_xlsx/package/packager.rb +0 -3
  57. data/lib/write_xlsx/package/relationships.rb +2 -2
  58. data/lib/write_xlsx/package/rich_value.rb +4 -2
  59. data/lib/write_xlsx/package/rich_value_rel.rb +2 -2
  60. data/lib/write_xlsx/package/rich_value_structure.rb +2 -2
  61. data/lib/write_xlsx/package/rich_value_types.rb +3 -3
  62. data/lib/write_xlsx/package/shared_strings.rb +2 -2
  63. data/lib/write_xlsx/package/styles.rb +13 -9
  64. data/lib/write_xlsx/package/table.rb +8 -2
  65. data/lib/write_xlsx/package/theme.rb +0 -3
  66. data/lib/write_xlsx/package/vml.rb +2 -2
  67. data/lib/write_xlsx/page_setup.rb +3 -1
  68. data/lib/write_xlsx/shape.rb +97 -100
  69. data/lib/write_xlsx/sheets.rb +6 -1
  70. data/lib/write_xlsx/sparkline.rb +2 -2
  71. data/lib/write_xlsx/utility/cell_reference.rb +124 -0
  72. data/lib/write_xlsx/utility/chart_formatting.rb +262 -0
  73. data/lib/write_xlsx/utility/common.rb +44 -0
  74. data/lib/write_xlsx/utility/date_time.rb +113 -0
  75. data/lib/write_xlsx/utility/dimensions.rb +40 -0
  76. data/lib/write_xlsx/utility/drawing.rb +136 -0
  77. data/lib/write_xlsx/utility/rich_text.rb +184 -0
  78. data/lib/write_xlsx/utility/sheetname_quoting.rb +73 -0
  79. data/lib/write_xlsx/utility/string_width.rb +45 -0
  80. data/lib/write_xlsx/utility/url.rb +27 -0
  81. data/lib/write_xlsx/utility/xml_primitives.rb +32 -0
  82. data/lib/write_xlsx/version.rb +1 -1
  83. data/lib/write_xlsx/workbook/chart_data.rb +188 -0
  84. data/lib/write_xlsx/workbook/format_preparation.rb +199 -0
  85. data/lib/write_xlsx/workbook/initialization.rb +223 -0
  86. data/lib/write_xlsx/workbook/package_preparation.rb +231 -0
  87. data/lib/write_xlsx/workbook/workbook_writer.rb +164 -0
  88. data/lib/write_xlsx/workbook.rb +143 -981
  89. data/lib/write_xlsx/worksheet/autofilter.rb +3 -1
  90. data/lib/write_xlsx/worksheet/cell_data.rb +5 -1
  91. data/lib/write_xlsx/worksheet/columns.rb +8 -3
  92. data/lib/write_xlsx/worksheet/data_validation.rb +9 -1
  93. data/lib/write_xlsx/worksheet/data_writing.rb +42 -15
  94. data/lib/write_xlsx/worksheet/drawing_preparation.rb +1 -1
  95. data/lib/write_xlsx/worksheet/formatting.rb +3 -1
  96. data/lib/write_xlsx/worksheet/hyperlink.rb +9 -1
  97. data/lib/write_xlsx/worksheet/row_col_sizing.rb +3 -1
  98. data/lib/write_xlsx/worksheet/xml_writer.rb +9 -4
  99. data/lib/write_xlsx/worksheet.rb +19 -2
  100. metadata +42 -3
  101. data/lib/write_xlsx/utility.rb +0 -1034
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module DateTime
7
+ #
8
+ # convert_date_time(date_time_string)
9
+ #
10
+ # The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format
11
+ # and converts it to a decimal number representing a valid Excel date.
12
+ #
13
+ def convert_date_time(date_time_string) # :nodoc:
14
+ date_time = date_time_string.to_s.sub(/^\s+/, '').sub(/\s+$/, '').sub(/Z$/, '')
15
+
16
+ # Check for invalid date char.
17
+ return nil if date_time =~ /[^0-9T:\-.Z]/
18
+
19
+ # Check for "T" after date or before time.
20
+ return nil unless date_time =~ /\dT|T\d/
21
+
22
+ days = 0 # Number of days since epoch
23
+ seconds = 0 # Time expressed as fraction of 24h hours in seconds
24
+
25
+ # Split into date and time.
26
+ date, time = date_time.split("T")
27
+
28
+ # We allow the time portion of the input DateTime to be optional.
29
+ if time
30
+ # Match hh:mm:ss.sss+ where the seconds are optional
31
+ if time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/
32
+ hour = ::Regexp.last_match(1).to_i
33
+ min = ::Regexp.last_match(2).to_i
34
+ sec = ::Regexp.last_match(4).to_f
35
+ else
36
+ return nil # Not a valid time format.
37
+ end
38
+
39
+ # Some boundary checks
40
+ return nil if hour >= 24
41
+ return nil if min >= 60
42
+ return nil if sec >= 60
43
+
44
+ # Excel expresses seconds as a fraction of the number in 24 hours.
45
+ seconds = ((hour * 60 * 60) + (min * 60) + sec) / (24.0 * 60 * 60)
46
+ end
47
+
48
+ # We allow the date portion of the input DateTime to be optional.
49
+ return seconds if date == ''
50
+
51
+ # Match date as yyyy-mm-dd.
52
+ if date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/
53
+ year = ::Regexp.last_match(1).to_i
54
+ month = ::Regexp.last_match(2).to_i
55
+ day = ::Regexp.last_match(3).to_i
56
+ else
57
+ return nil # Not a valid date format.
58
+ end
59
+
60
+ # Set the epoch as 1900 or 1904. Defaults to 1900.
61
+ # Special cases for Excel.
62
+ unless date_1904?
63
+ return seconds if date == '1899-12-31' # Excel 1900 epoch
64
+ return seconds if date == '1900-01-00' # Excel 1900 epoch
65
+ return 60 + seconds if date == '1900-02-29' # Excel false leapday
66
+ end
67
+
68
+ # We calculate the date by calculating the number of days since the epoch
69
+ # and adjust for the number of leap days. We calculate the number of leap
70
+ # days by normalising the year in relation to the epoch. Thus the year 2000
71
+ # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
72
+ #
73
+ epoch = date_1904? ? 1904 : 1900
74
+ offset = date_1904? ? 4 : 0
75
+ norm = 300
76
+ range = year - epoch
77
+
78
+ # Set month days and check for leap year.
79
+ mdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
80
+ leap = 0
81
+ leap = 1 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
82
+ mdays[1] = 29 if leap != 0
83
+
84
+ # Some boundary checks
85
+ return nil if year < epoch || year > 9999
86
+ return nil if month < 1 || month > 12
87
+ return nil if day < 1 || day > mdays[month - 1]
88
+
89
+ # Accumulate the number of days since the epoch.
90
+ days = day # Add days for current month
91
+ (0..(month - 2)).each do |m|
92
+ days += mdays[m] # Add days for past months
93
+ end
94
+ days += range * 365 # Add days for past years
95
+ days += (range / 4) # Add leapdays
96
+ days -= ((range + offset) / 100) # Subtract 100 year leapdays
97
+ days += ((range + offset + norm) / 400) # Add 400 year leapdays
98
+ days -= leap # Already counted above
99
+
100
+ # Adjust for Excel erroneously treating 1900 as a leap year.
101
+ days += 1 if !date_1904? && days > 59
102
+
103
+ date_time = sprintf("%0.10f", days + seconds)
104
+ date_time = date_time.sub(/\.?0+$/, '') if date_time =~ /\./
105
+ if date_time =~ /\./
106
+ date_time.to_f
107
+ else
108
+ date_time.to_i
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'write_xlsx/constants'
5
+
6
+ module Writexlsx
7
+ module Utility
8
+ module Dimensions
9
+ include Constants
10
+
11
+ def check_dimensions(row, col)
12
+ raise WriteXLSXDimensionError if !row || row >= ROW_MAX || !col || col >= COL_MAX
13
+
14
+ 0
15
+ end
16
+
17
+ #
18
+ # Check that row and col are valid and store max and min values for use in
19
+ # other methods/elements.
20
+ #
21
+ def check_dimensions_and_update_max_min_values(row, col, ignore_row = 0, ignore_col = 0) # :nodoc:
22
+ check_dimensions(row, col)
23
+ store_row_max_min_values(row) if ignore_row == 0
24
+ store_col_max_min_values(col) if ignore_col == 0
25
+
26
+ 0
27
+ end
28
+
29
+ def store_row_max_min_values(row)
30
+ @dim_rowmin = row if !@dim_rowmin || (row < @dim_rowmin)
31
+ @dim_rowmax = row if !@dim_rowmax || (row > @dim_rowmax)
32
+ end
33
+
34
+ def store_col_max_min_values(col)
35
+ @dim_colmin = col if !@dim_colmin || (col < @dim_colmin)
36
+ @dim_colmax = col if !@dim_colmax || (col > @dim_colmax)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,136 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Writexlsx
5
+ module Utility
6
+ module Drawing
7
+ #
8
+ # Convert vertices from pixels to points.
9
+ #
10
+ def pixels_to_points(vertices)
11
+ _col_start, _row_start, _x1, _y1,
12
+ _col_end, _row_end, _x2, _y2,
13
+ left, top, width, height = vertices.flatten
14
+
15
+ left *= 0.75
16
+ top *= 0.75
17
+ width *= 0.75
18
+ height *= 0.75
19
+
20
+ [left, top, width, height]
21
+ end
22
+
23
+ def v_shape_attributes_base(id)
24
+ [
25
+ ['id', "_x0000_s#{id}"],
26
+ ['type', type]
27
+ ]
28
+ end
29
+
30
+ def v_shape_style_base(z_index, vertices)
31
+ left, top, width, height = pixels_to_points(vertices)
32
+
33
+ left_str = float_to_str(left)
34
+ top_str = float_to_str(top)
35
+ width_str = float_to_str(width)
36
+ height_str = float_to_str(height)
37
+ z_index_str = float_to_str(z_index)
38
+
39
+ shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
40
+ end
41
+
42
+ def shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
43
+ [
44
+ 'position:absolute;',
45
+ 'margin-left:',
46
+ left_str, 'pt;',
47
+ 'margin-top:',
48
+ top_str, 'pt;',
49
+ 'width:',
50
+ width_str, 'pt;',
51
+ 'height:',
52
+ height_str, 'pt;',
53
+ 'z-index:',
54
+ z_index_str, ';'
55
+ ]
56
+ end
57
+
58
+ #
59
+ # Write the <v:fill> element.
60
+ #
61
+ def write_fill
62
+ @writer.empty_tag('v:fill', fill_attributes)
63
+ end
64
+
65
+ #
66
+ # Write the <v:path> element.
67
+ #
68
+ def write_comment_path(gradientshapeok, connecttype)
69
+ attributes = []
70
+
71
+ attributes << %w[gradientshapeok t] if gradientshapeok
72
+ attributes << ['o:connecttype', connecttype]
73
+
74
+ @writer.empty_tag('v:path', attributes)
75
+ end
76
+
77
+ #
78
+ # Write the <x:Anchor> element.
79
+ #
80
+ def write_anchor
81
+ col_start, row_start, x1, y1, col_end, row_end, x2, y2 = @vertices
82
+ data = [col_start, x1, row_start, y1, col_end, x2, row_end, y2].join(', ')
83
+
84
+ @writer.data_element('x:Anchor', data)
85
+ end
86
+
87
+ #
88
+ # Write the <x:AutoFill> element.
89
+ #
90
+ def write_auto_fill
91
+ @writer.data_element('x:AutoFill', 'False')
92
+ end
93
+
94
+ #
95
+ # Write the <div> element.
96
+ #
97
+ def write_div(align, font = nil)
98
+ style = "text-align:#{align}"
99
+ attributes = [['style', style]]
100
+
101
+ @writer.tag_elements('div', attributes) do
102
+ if font
103
+ # Write the font element.
104
+ write_font(font)
105
+ end
106
+ end
107
+ end
108
+
109
+ #
110
+ # Write the <font> element.
111
+ #
112
+ def write_font(font)
113
+ caption = font[:_caption]
114
+ face = 'Calibri'
115
+ size = 220
116
+ color = '#000000'
117
+
118
+ attributes = [
119
+ ['face', face],
120
+ ['size', size],
121
+ ['color', color]
122
+ ]
123
+ @writer.data_element('font', caption, attributes)
124
+ end
125
+
126
+ #
127
+ # Write the <v:stroke> element.
128
+ #
129
+ def write_stroke
130
+ attributes = [%w[joinstyle miter]]
131
+
132
+ @writer.empty_tag('v:stroke', attributes)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -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.13.0"
3
+ WriteXLSX_VERSION = "1.15.1"