write_xlsx 1.10.2 → 1.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74c1fe583292a7a5318db09651b114e58358646a8706b5b4d35fc01c1850ec6c
4
- data.tar.gz: 424d3f3a1983e4de16de9a943c67718874e7ef7e4a78c440767ecf8c68f0ef88
3
+ metadata.gz: 14a7593970ecc4db14f2accbdc1634033ac9042cdd7680a20d4400ea1eda741f
4
+ data.tar.gz: 2ec671ba25dc571f612dc5b9d4a9c4ede2e147203d2e085313ea8b4a5f774377
5
5
  SHA512:
6
- metadata.gz: 4603c1400dd60ec345b8cf4db50ae1c03e87716192a074d2a767cebc7cfc085439c733ea11167cf900efc3aae3fe555a32639731d36338100bf9fbb170f28fd5
7
- data.tar.gz: 3ccbb69460382ae8074cc62fc896daa1aa608dbbb6ff894bb1fc425643c0ac3b9e91cc55b0c8e2de05a50113b179621db83fcea033b9b201bf56957f833624e3
6
+ metadata.gz: 255b8f5d60623d6c9966ba1209773129d3312238df315a16564d8c950fa39ac738296a9c788d4cb707abad7e659e9eaef528749fa7f1de7f93a2152e0534fbb4
7
+ data.tar.gz: c161bd9e8cfe47754f763a874ab83b183f76d7d290ecc53db1fa7a1ea1ec4433ee1d80efda0c139417321f02069db690fc113ab54104b5922b9c942210949e60
data/.rubocop.yml CHANGED
@@ -8,6 +8,9 @@ AllCops:
8
8
  TargetRubyVersion: 2.6
9
9
  NewCops: enable
10
10
 
11
+ Gemspec/DevelopmentDependencies:
12
+ Enabled: false
13
+
11
14
  Gemspec/RequiredRubyVersion:
12
15
  Enabled: false
13
16
 
@@ -33,6 +36,9 @@ Layout/HeredocIndentation:
33
36
  Layout/LineLength:
34
37
  Max: 7000
35
38
 
39
+ Layout/MultilineMethodCallIndentation:
40
+ Enabled: false
41
+
36
42
  Lint/DuplicateBranch:
37
43
  IgnoreLiteralBranches: true
38
44
  Exclude:
@@ -60,6 +66,9 @@ Metrics/CyclomaticComplexity:
60
66
  Metrics/MethodLength:
61
67
  Max: 400
62
68
 
69
+ Metrics/ModuleLength:
70
+ Max: 1000
71
+
63
72
  Metrics/ParameterLists:
64
73
  Max: 12
65
74
  MaxOptionalParameters: 6
data/Changes CHANGED
@@ -1,5 +1,20 @@
1
1
  Change history of write_xlsx rubygem.
2
2
 
3
+ 2023-05-06 v1.11.0
4
+ Added support for simulated worksheet `autofit()`.
5
+
6
+ Refactored internal column property handling to allow column ranges
7
+ to be overridden (a common UX expectation).
8
+
9
+ Add `quote_prefix` format property.
10
+
11
+ Fix for duplicate number formats. Issue #283(excel-write-xlsx)
12
+
13
+ Add fix for worksheets with tables and background images.
14
+
15
+ Replace/fix the worksheet protection password algorithm
16
+ so that is works correctly for strings over 24 chars.
17
+
3
18
  2023-02-16 v1.10.2
4
19
  Fixed issue #104. Worksheet#write Ruby 3.2 removed Object#=~
5
20
  making it impossible to write Date objects
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ #
5
+ # An example of using simulated autofit to automatically adjust the width of
6
+ # worksheet columns based on the data in the cells.
7
+ #
8
+ # Copyright 2000-2023, John McNamara, jmcnamara@cpan.org
9
+ #
10
+ # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
11
+ #
12
+ # convert to Ruby by Hideo NAKAMURA, nakamura.hideo@gmail.com
13
+ #
14
+ require 'write_xlsx'
15
+
16
+ workbook = WriteXLSX.new('autofit.xlsx')
17
+ worksheet = workbook.add_worksheet
18
+
19
+ # Write some worksheet data to demonstrate autofitting.
20
+ worksheet.write(0, 0, "Foo")
21
+ worksheet.write(1, 0, "Food")
22
+ worksheet.write(2, 0, "Foody")
23
+ worksheet.write(3, 0, "Froody")
24
+
25
+ worksheet.write(0, 1, 12345)
26
+ worksheet.write(1, 1, 12345678)
27
+ worksheet.write(2, 1, 12345)
28
+
29
+ worksheet.write(0, 2, "Some longer text")
30
+
31
+ worksheet.write(0, 3, 'http://www.google.com')
32
+ worksheet.write(1, 3, 'https://github.com')
33
+
34
+ # Autofit the worksheet
35
+ worksheet.autofit
36
+
37
+ workbook.close
@@ -44,10 +44,8 @@ chart1.add_series(
44
44
  values: ['Sheet1', 1, 6, 2, 2]
45
45
  )
46
46
 
47
- # Add a chart title and some axis labels.
47
+ # Add a chart title.
48
48
  chart1.set_title(name: 'Results of sample analysis')
49
- chart1.set_x_axis(name: 'Test number')
50
- chart1.set_y_axis(name: 'Sample length (mm)')
51
49
 
52
50
  # Set an Excel chart style. Blue colors with white outline and shadow.
53
51
  chart1.set_style(11)
@@ -81,10 +79,8 @@ chart2.add_series(
81
79
  values: ['Sheet1', 1, 6, 2, 2]
82
80
  )
83
81
 
84
- # Add a chart title and some axis labels.
82
+ # Add a chart title.
85
83
  chart2.set_title(name: 'Stacked Chart')
86
- chart2.set_x_axis(name: 'Test number')
87
- chart2.set_y_axis(name: 'Sample length (mm)')
88
84
 
89
85
  # Set an Excel chart style. Blue colors with white outline and shadow.
90
86
  chart2.set_style(12)
@@ -118,10 +114,8 @@ chart3.add_series(
118
114
  values: ['Sheet1', 1, 6, 2, 2]
119
115
  )
120
116
 
121
- # Add a chart title and some axis labels.
117
+ # Add a chart title.
122
118
  chart3.set_title(name: 'Percent Stacked Chart')
123
- chart3.set_x_axis(name: 'Test number')
124
- chart3.set_y_axis(name: 'Sample length (mm)')
125
119
 
126
120
  # Set an Excel chart style. Blue colors with white outline and shadow.
127
121
  chart3.set_style(13)
@@ -371,7 +371,7 @@ module Writexlsx
371
371
  # Set on of the 42 built-in Excel chart styles. The default style is 2.
372
372
  #
373
373
  def set_style(style_id = 2)
374
- style_id = 2 if style_id < 0 || style_id > 48
374
+ style_id = 2 if style_id < 1 || style_id > 48
375
375
  @style_id = style_id
376
376
  end
377
377
 
@@ -12,7 +12,7 @@ module Writexlsx
12
12
  attr_reader :diag_type, :diag_color, :font_only, :color_indexed # :nodoc:
13
13
  attr_reader :left, :left_color, :right, :right_color, :top, :top_color, :bottom, :bottom_color # :nodoc:
14
14
  attr_reader :font_scheme # :nodoc:
15
- attr_accessor :num_format_index, :border_index, :font_index # :nodoc:
15
+ attr_accessor :quote_prefix, :num_format_index, :border_index, :font_index # :nodoc:
16
16
  attr_accessor :fill_index, :font_condense, :font_extend, :diag_border # :nodoc:
17
17
  attr_accessor :bg_color, :fg_color, :pattern # :nodoc:
18
18
 
@@ -84,6 +84,7 @@ module Writexlsx
84
84
  @just_distrib = 0
85
85
  @color_indexed = 0
86
86
  @font_only = 0
87
+ @quote_prefix = 0
87
88
 
88
89
  set_format_properties(params) unless params.empty?
89
90
  end
@@ -213,7 +214,7 @@ module Writexlsx
213
214
  # Returns a unique hash key for the Format object.
214
215
  #
215
216
  def get_format_key
216
- [get_font_key, get_border_key, get_fill_key, get_alignment_key, @num_format, @locked, @hidden].join(':')
217
+ [get_font_key, get_border_key, get_fill_key, get_alignment_key, @num_format, @locked, @hidden, @quote_prefix].join(':')
217
218
  end
218
219
 
219
220
  #
@@ -642,6 +643,7 @@ module Writexlsx
642
643
  ['borderId', border_index],
643
644
  ['xfId', xf_id]
644
645
  ]
646
+ attributes << ['quotePrefix', 1] if ptrue?(quote_prefix)
645
647
  attributes << ['applyNumberFormat', 1] if num_format_index > 0
646
648
  # Add applyFont attribute if XF format uses a font element.
647
649
  attributes << ['applyFont', 1] if font_index > 0 && !ptrue?(@hyperlink)
@@ -11,11 +11,14 @@ module Writexlsx
11
11
 
12
12
  PRESERVE_SPACE_ATTRIBUTES = ['xml:space', 'preserve'].freeze
13
13
 
14
+ attr_reader :strings
15
+
14
16
  def initialize
15
17
  @writer = Package::XMLWriterSimple.new
16
18
  @strings = [] # string table
17
19
  @strings_index = {} # string table index
18
20
  @count = 0 # count
21
+ @str_unique = 0
19
22
  end
20
23
 
21
24
  def index(string, params = {})
@@ -25,10 +28,9 @@ module Writexlsx
25
28
 
26
29
  def add(string)
27
30
  unless @strings_index[string]
28
- # Only first time the string will be append to list
29
- # next time we only check and not #dup it
30
31
  str = string.frozen? ? string : string.freeze
31
32
  @strings << str
33
+ @str_unique += 1
32
34
  @strings_index[str] = @strings.size - 1
33
35
  end
34
36
  @count += 1
@@ -128,7 +130,7 @@ module Writexlsx
128
130
  end
129
131
 
130
132
  def unique_count
131
- @strings.size
133
+ @str_unique
132
134
  end
133
135
  end
134
136
  end
@@ -14,7 +14,7 @@ module Writexlsx
14
14
  @xf_formats = nil
15
15
  @palette = []
16
16
  @font_count = 0
17
- @num_format_count = 0
17
+ @num_formats = []
18
18
  @border_count = 0
19
19
  @fill_count = 0
20
20
  @custom_colors = []
@@ -38,18 +38,18 @@ module Writexlsx
38
38
  # Pass in the Format objects and other properties used to set the styles.
39
39
  #
40
40
  def set_style_properties(
41
- xf_formats, palette, font_count, num_format_count, border_count,
41
+ xf_formats, palette, font_count, num_formats, border_count,
42
42
  fill_count, custom_colors, dxf_formats, has_comments
43
43
  )
44
- @xf_formats = xf_formats
45
- @palette = palette
46
- @font_count = font_count
47
- @num_format_count = num_format_count
48
- @border_count = border_count
49
- @fill_count = fill_count
50
- @custom_colors = custom_colors
51
- @dxf_formats = dxf_formats
52
- @has_comments = has_comments
44
+ @xf_formats = xf_formats
45
+ @palette = palette
46
+ @font_count = font_count
47
+ @num_formats = num_formats
48
+ @border_count = border_count
49
+ @fill_count = fill_count
50
+ @custom_colors = custom_colors
51
+ @dxf_formats = dxf_formats
52
+ @has_comments = has_comments
53
53
  end
54
54
 
55
55
  #
@@ -77,7 +77,7 @@ module Writexlsx
77
77
  # Write the <numFmts> element.
78
78
  #
79
79
  def write_num_fmts
80
- count = @num_format_count
80
+ count = @num_formats.size
81
81
 
82
82
  return if count == 0
83
83
 
@@ -85,11 +85,10 @@ module Writexlsx
85
85
 
86
86
  @writer.tag_elements('numFmts', attributes) do
87
87
  # Write the numFmts elements.
88
- @xf_formats.each do |format|
89
- # Ignore built-in number formats, i.e., < 164.
90
- next unless format.num_format_index >= 164
91
-
92
- write_num_fmt(format.num_format_index, format.num_format)
88
+ index = 164
89
+ @num_formats.each do |num_format|
90
+ write_num_fmt(index, num_format)
91
+ index += 1
93
92
  end
94
93
  end
95
94
  end
@@ -51,6 +51,7 @@ module Writexlsx
51
51
 
52
52
  add_the_table_columns
53
53
  write_the_cell_data_if_supplied
54
+ store_filter_cell_positions
54
55
  end
55
56
 
56
57
  def set_xml_writer(filename)
@@ -155,6 +156,14 @@ module Writexlsx
155
156
  end
156
157
  end
157
158
 
159
+ def store_filter_cell_positions
160
+ if ptrue?(@param[:autofilter])
161
+ (@col1..@col2).each do |col|
162
+ @worksheet.filter_cells["#{@row1}:#{col}"] = 1
163
+ end
164
+ end
165
+ end
166
+
158
167
  def prepare(id)
159
168
  @id = id
160
169
  @name ||= "Table#{id}"
@@ -5,10 +5,28 @@ require 'write_xlsx/col_name'
5
5
 
6
6
  module Writexlsx
7
7
  module Utility
8
- ROW_MAX = 1048576 # :nodoc:
9
- COL_MAX = 16384 # :nodoc:
10
- STR_MAX = 32767 # :nodoc:
8
+ ROW_MAX = 1048576 # :nodoc:
9
+ COL_MAX = 16384 # :nodoc:
10
+ STR_MAX = 32767 # :nodoc:
11
11
  SHEETNAME_MAX = 31 # :nodoc:
12
+ CHAR_WIDTHS = {
13
+ ' ' => 3, '!' => 5, '"' => 6, '#' => 7, '$' => 7, '%' => 11,
14
+ '&' => 10, "'" => 3, '(' => 5, ')' => 5, '*' => 7, '+' => 7,
15
+ ',' => 4, '-' => 5, '.' => 4, '/' => 6, '0' => 7, '1' => 7,
16
+ '2' => 7, '3' => 7, '4' => 7, '5' => 7, '6' => 7, '7' => 7,
17
+ '8' => 7, '9' => 7, ':' => 4, ';' => 4, '<' => 7, '=' => 7,
18
+ '>' => 7, '?' => 7, '@' => 13, 'A' => 9, 'B' => 8, 'C' => 8,
19
+ 'D' => 9, 'E' => 7, 'F' => 7, 'G' => 9, 'H' => 9, 'I' => 4,
20
+ 'J' => 5, 'K' => 8, 'L' => 6, 'M' => 12, 'N' => 10, 'O' => 10,
21
+ 'P' => 8, 'Q' => 10, 'R' => 8, 'S' => 7, 'T' => 7, 'U' => 9,
22
+ 'V' => 9, 'W' => 13, 'X' => 8, 'Y' => 7, 'Z' => 7, '[' => 5,
23
+ '\\' => 6, ']' => 5, '^' => 7, '_' => 7, '`' => 4, 'a' => 7,
24
+ 'b' => 8, 'c' => 6, 'd' => 8, 'e' => 8, 'f' => 5, 'g' => 7,
25
+ 'h' => 8, 'i' => 4, 'j' => 4, 'k' => 7, 'l' => 4, 'm' => 12,
26
+ 'n' => 8, 'o' => 8, 'p' => 8, 'q' => 8, 'r' => 5, 's' => 6,
27
+ 't' => 5, 'u' => 8, 'v' => 7, 'w' => 11, 'x' => 7, 'y' => 7,
28
+ 'z' => 6, '{' => 5, '|' => 7, '}' => 5, '~' => 7
29
+ }.freeze
12
30
 
13
31
  #
14
32
  # xl_rowcol_to_cell($row, col, row_absolute, col_absolute)
@@ -84,6 +102,21 @@ module Writexlsx
84
102
  "=#{sheetname}!#{range1}:#{range2}"
85
103
  end
86
104
 
105
+ #
106
+ # xl_string_pixel_width($string)
107
+ #
108
+ # Get the pixel width of a string based on individual character widths taken
109
+ # from Excel. UTF8 characters are given a default width of 8.
110
+ #
111
+ # Note, Excel adds an additional 7 pixels padding to a cell.
112
+ #
113
+ def xl_string_pixel_width(string)
114
+ length = 0
115
+ string.to_s.split(//).each { |char| length += CHAR_WIDTHS[char] || 8 }
116
+
117
+ length
118
+ end
119
+
87
120
  #
88
121
  # Sheetnames used in references should be quoted if they contain any spaces,
89
122
  # special characters or if the look like something that isn't a sheet name.
@@ -258,7 +291,7 @@ module Writexlsx
258
291
 
259
292
  # Check for a cell reference in A1 notation and substitute row and column
260
293
  def row_col_notation(row_or_a1) # :nodoc:
261
- substitute_cellref(row_or_a1) if row_or_a1.to_s =~ /^\D/
294
+ substitute_cellref(row_or_a1) if row_or_a1.respond_to?(:match) && row_or_a1.to_s =~ /^\D/
262
295
  end
263
296
 
264
297
  #
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WriteXLSX_VERSION = "1.10.2"
3
+ WriteXLSX_VERSION = "1.11.0"
@@ -58,7 +58,7 @@ module Writexlsx
58
58
  @xf_formats = []
59
59
  @dxf_formats = []
60
60
  @font_count = 0
61
- @num_format_count = 0
61
+ @num_formats = []
62
62
  @defined_names = []
63
63
  @named_ranges = []
64
64
  @custom_colors = []
@@ -560,7 +560,7 @@ module Writexlsx
560
560
  @xf_formats,
561
561
  @palette,
562
562
  @font_count,
563
- @num_format_count,
563
+ @num_formats,
564
564
  @border_count,
565
565
  @fill_count,
566
566
  @custom_colors,
@@ -869,6 +869,9 @@ module Writexlsx
869
869
  @activesheet = @worksheets.visible_first.index if @activesheet == 0
870
870
  @worksheets[@activesheet].activate
871
871
 
872
+ # Convert the SST strings data structure.
873
+ prepare_sst_string_data
874
+
872
875
  # Prepare the worksheet VML elements such as comments and buttons.
873
876
  prepare_vml_objects
874
877
  # Set the defined names for the worksheets such as Print Titles.
@@ -917,6 +920,11 @@ module Writexlsx
917
920
  Dir.glob(File.join(tempdir, "**", "*"), File::FNM_DOTMATCH).select { |f| File.file?(f) }
918
921
  end
919
922
 
923
+ #
924
+ # prepare_sst_string_data
925
+ #
926
+ def prepare_sst_string_data; end
927
+
920
928
  #
921
929
  # Prepare all of the format properties prior to passing them to Styles.rb.
922
930
  #
@@ -977,9 +985,10 @@ module Writexlsx
977
985
  # User defined records start from index 0xA4.
978
986
  #
979
987
  def prepare_num_formats # :nodoc:
980
- num_formats = {}
981
- index = 164
982
- num_format_count = 0
988
+ num_formats = []
989
+ unique_num_formats = {}
990
+ index = 164
991
+ num_format_count = 0
983
992
 
984
993
  (@xf_formats + @dxf_formats).each do |format|
985
994
  num_format = format.num_format
@@ -1000,21 +1009,22 @@ module Writexlsx
1000
1009
  next
1001
1010
  end
1002
1011
 
1003
- if num_formats[num_format]
1012
+ if unique_num_formats[num_format]
1004
1013
  # Number format has already been used.
1005
- format.num_format_index = num_formats[num_format]
1014
+ format.num_format_index = unique_num_formats[num_format]
1006
1015
  else
1007
1016
  # Add a new number format.
1008
- num_formats[num_format] = index
1017
+ unique_num_formats[num_format] = index
1009
1018
  format.num_format_index = index
1010
1019
  index += 1
1011
1020
 
1012
- # Only increase font count for XF formats (not for DXF formats).
1013
- num_format_count += 1 if ptrue?(format.xf_index)
1021
+ # Only store/increase number format count for XF formats
1022
+ # (not for DXF formats).
1023
+ num_formats << num_format if ptrue?(format.xf_index)
1014
1024
  end
1015
1025
  end
1016
1026
 
1017
- @num_format_count = num_format_count
1027
+ @num_formats = num_formats
1018
1028
  end
1019
1029
 
1020
1030
  #
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # frozen_string_literal: true
2
+
3
+ # frozen__literal: true
3
4
 
4
5
  module Writexlsx
5
6
  class Worksheet
@@ -24,8 +25,8 @@ module Writexlsx
24
25
  elsif worksheet.set_rows[row] && worksheet.set_rows[row][1]
25
26
  row_xf = worksheet.set_rows[row][1]
26
27
  attributes << ['s', row_xf.get_xf_index]
27
- elsif worksheet.col_formats[col]
28
- col_xf = worksheet.col_formats[col]
28
+ elsif worksheet.col_info[col] && worksheet.col_info[col].format
29
+ col_xf = worksheet.col_info[col].format
29
30
  attributes << ['s', col_xf.get_xf_index]
30
31
  end
31
32
  attributes
@@ -56,11 +57,12 @@ module Writexlsx
56
57
  end
57
58
 
58
59
  class StringCellData < CellData # :nodoc:
59
- attr_reader :token
60
+ attr_reader :token, :raw_string
60
61
 
61
- def initialize(index, xf)
62
+ def initialize(index, xf, raw_string)
62
63
  @token = index
63
64
  @xf = xf
65
+ @raw_string = raw_string
64
66
  end
65
67
 
66
68
  def data
@@ -81,6 +83,12 @@ module Writexlsx
81
83
  end
82
84
  end
83
85
 
86
+ class RichStringCellData < StringCellData # :nodoc:
87
+ end
88
+
89
+ class DateTimeCellData < NumberCellData # :nodoc:
90
+ end
91
+
84
92
  class FormulaCellData < CellData # :nodoc:
85
93
  attr_reader :token, :result, :range, :link_type, :url
86
94
 
@@ -31,20 +31,25 @@ module Writexlsx
31
31
  attr_reader :vml_data_id # :nodoc:
32
32
  attr_reader :vml_header_id # :nodoc:
33
33
  attr_reader :autofilter_area # :nodoc:
34
- attr_reader :writer, :set_rows, :col_formats # :nodoc:
34
+ attr_reader :writer, :set_rows, :col_info # :nodoc:
35
35
  attr_reader :vml_shape_id # :nodoc:
36
36
  attr_reader :comments, :comments_author # :nodoc:
37
37
  attr_accessor :data_bars_2010, :dxf_priority # :nodoc:
38
38
  attr_reader :vba_codename # :nodoc:
39
- attr_writer :excel_version
39
+ attr_writer :excel_version # :nodoc:
40
+ attr_reader :filter_cells # :nodoc:
40
41
 
41
42
  def initialize(workbook, index, name) # :nodoc:
43
+ rowmax = 1_048_576
44
+ colmax = 16_384
45
+ strmax = 32_767
46
+
42
47
  @writer = Package::XMLWriterSimple.new
43
48
 
44
49
  @workbook = workbook
45
50
  @index = index
46
51
  @name = name
47
- @colinfo = {}
52
+ @col_info = {}
48
53
  @cell_data_table = []
49
54
  @excel_version = 2007
50
55
  @palette = workbook.palette
@@ -55,6 +60,10 @@ module Writexlsx
55
60
 
56
61
  @screen_gridlines = true
57
62
  @show_zeros = true
63
+
64
+ @xls_rowmax = rowmax
65
+ @xls_colmax = colmax
66
+ @xls_strmax = strmax
58
67
  @dim_rowmin = nil
59
68
  @dim_rowmax = nil
60
69
  @dim_colmin = nil
@@ -77,11 +86,10 @@ module Writexlsx
77
86
  @filter_on = false
78
87
  @filter_range = []
79
88
  @filter_cols = {}
89
+ @filter_cells = {}
80
90
  @filter_type = {}
81
91
 
82
- @col_sizes = {}
83
92
  @row_sizes = {}
84
- @col_formats = {}
85
93
 
86
94
  @last_shape_id = 1
87
95
  @rel_count = 0
@@ -90,8 +98,8 @@ module Writexlsx
90
98
  @external_drawing_links = []
91
99
  @external_comment_links = []
92
100
  @external_vml_links = []
93
- @external_table_links = []
94
101
  @external_background_links = []
102
+ @external_table_links = []
95
103
  @drawing_links = []
96
104
  @vml_drawing_links = []
97
105
  @charts = []
@@ -121,6 +129,7 @@ module Writexlsx
121
129
  @default_col_width = 8.43
122
130
  @default_col_pixels = 64
123
131
  @default_row_rezoed = 0
132
+ @default_date_pixels = 68
124
133
 
125
134
  @merge = []
126
135
 
@@ -305,7 +314,8 @@ module Writexlsx
305
314
  #
306
315
  def set_column(*args)
307
316
  # Check for a cell reference in A1 notation and substitute row and column
308
- if args[0].to_s =~ /^\D/
317
+ # ruby 3.2 no longer handles =~ for various types
318
+ if args[0].respond_to?(:=~) && args[0].to_s =~ /^\D/
309
319
  _row1, firstcol, _row2, lastcol, *data = substitute_cellref(*args)
310
320
  else
311
321
  firstcol, lastcol, *data = args
@@ -321,6 +331,7 @@ module Writexlsx
321
331
  firstcol, lastcol = lastcol, firstcol if firstcol > lastcol
322
332
 
323
333
  width, format, hidden, level, collapsed = data
334
+ autofit = 0
324
335
 
325
336
  # Check that cols are valid and store max and min values with default row.
326
337
  # NOTE: The check shouldn't modify the row dimensions and should only modify
@@ -338,22 +349,20 @@ module Writexlsx
338
349
  level = 0 if level < 0
339
350
  level = 7 if level > 7
340
351
 
352
+ # Excel has a maximum column width of 255 characters.
353
+ width = 255.0 if width && width > 255.0
354
+
341
355
  @outline_col_level = level if level > @outline_col_level
342
356
 
343
357
  # Store the column data based on the first column. Padded for sorting.
344
- @colinfo[sprintf("%05d", firstcol)] = [firstcol, lastcol, width, format, hidden, level, collapsed]
358
+ (firstcol..lastcol).each do |col|
359
+ @col_info[col] =
360
+ Struct.new('ColInfo', :width, :format, :hidden, :level, :collapsed, :autofit)
361
+ .new(width, format, hidden, level, collapsed, autofit)
362
+ end
345
363
 
346
364
  # Store the column change to allow optimisations.
347
365
  @col_size_changed = 1
348
-
349
- # Store the col sizes for use when calculating image vertices taking
350
- # hidden columns into account. Also store the column formats.
351
- width ||= @default_col_width
352
-
353
- (firstcol..lastcol).each do |col|
354
- @col_sizes[col] = [width, hidden]
355
- @col_formats[col] = format if format
356
- end
357
366
  end
358
367
 
359
368
  #
@@ -387,6 +396,113 @@ module Writexlsx
387
396
  set_column(first_col, last_col, width, format, hidden, level)
388
397
  end
389
398
 
399
+ #
400
+ # autofit()
401
+ #
402
+ # Simulate autofit based on the data, and datatypes in each column. We do this
403
+ # by estimating a pixel width for each cell data.
404
+ #
405
+ def autofit
406
+ col_width = {}
407
+
408
+ # Iterate through all the data in the worksheet.
409
+ (@dim_rowmin..@dim_rowmax).each do |row_num|
410
+ # Skip row if it doesn't contain cell data.
411
+ next unless @cell_data_table[row_num]
412
+
413
+ (@dim_colmin..@dim_colmax).each do |col_num|
414
+ length = 0
415
+ case (cell_data = @cell_data_table[row_num][col_num])
416
+ when StringCellData, RichStringCellData
417
+ # Handle strings and rich strings.
418
+ #
419
+ # For standard shared strings we do a reverse lookup
420
+ # from the shared string id to the actual string. For
421
+ # rich strings we use the unformatted string. We also
422
+ # split multiline strings and handle each part
423
+ # separately.
424
+ string = cell_data.raw_string
425
+
426
+ if string =~ /\n/
427
+ # Handle multiline strings.
428
+ length = max = string.split("\n").collect do |str|
429
+ xl_string_pixel_width(str)
430
+ end.max
431
+ else
432
+ length = xl_string_pixel_width(string)
433
+ end
434
+ when DateTimeCellData
435
+
436
+ # Handle dates.
437
+ #
438
+ # The following uses the default width for mm/dd/yyyy
439
+ # dates. It isn't feasible to parse the number format
440
+ # to get the actual string width for all format types.
441
+ length = @default_date_pixels
442
+ when NumberCellData
443
+
444
+ # Handle numbers.
445
+ #
446
+ # We use a workaround/optimization for numbers since
447
+ # digits all have a pixel width of 7. This gives a
448
+ # slightly greater width for the decimal place and
449
+ # minus sign but only by a few pixels and
450
+ # over-estimation is okay.
451
+ length = 7 * cell_data.token.to_s.length
452
+ when BooleanCellData
453
+
454
+ # Handle boolean values.
455
+ #
456
+ # Use the Excel standard widths for TRUE and FALSE.
457
+ if ptrue?(cell_data.token)
458
+ length = 31
459
+ else
460
+ length = 36
461
+ end
462
+ when FormulaCellData, FormulaArrayCellData, DynamicFormulaArrayCellData
463
+ # Handle formulas.
464
+ #
465
+ # We only try to autofit a formula if it has a
466
+ # non-zero value.
467
+ if ptrue?(cell_data.data)
468
+ length = xl_string_pixel_width(cell_data.data)
469
+ end
470
+ end
471
+
472
+ # If the cell is in an autofilter header we add an
473
+ # additional 16 pixels for the dropdown arrow.
474
+ if length > 0 &&
475
+ @filter_cells["#{row_num}:#{col_num}"]
476
+ length += 16
477
+ end
478
+
479
+ # Add the string lenght to the lookup hash.
480
+ max = col_width[col_num] || 0
481
+ col_width[col_num] = length if length > max
482
+ end
483
+ end
484
+
485
+ # Apply the width to the column.
486
+ col_width.each do |col_num, pixel_width|
487
+ # Convert the string pixel width to a character width using an
488
+ # additional padding of 7 pixels, like Excel.
489
+ width = pixels_to_width(pixel_width + 7)
490
+
491
+ # The max column character width in Excel is 255.
492
+ width = 255.0 if width > 255.0
493
+
494
+ # Add the width to an existing col info structure or add a new one.
495
+ if @col_info[col_num]
496
+ @col_info[col_num].width = width
497
+ @col_info[col_num].autofit = 1
498
+ else
499
+ @col_info[col_num] =
500
+ Struct.new('ColInfo', :width, :format, :hidden, :level, :collapsed, :autofit)
501
+ .new(width, nil, 0, 0, 0, 1)
502
+ end
503
+ end
504
+ end
505
+
390
506
  #
391
507
  # :call-seq:
392
508
  # set_selection(cell_or_cell_range)
@@ -936,31 +1052,35 @@ module Writexlsx
936
1052
  write_row(_row, _col, _token, _format, _value1, _value2)
937
1053
  elsif _token.respond_to?(:coerce) # Numeric
938
1054
  write_number(_row, _col, _token, _format)
939
- # Match integer with leading zero(s)
940
- elsif @leading_zeros && _token =~ /^0\d*$/
941
- write_string(_row, _col, _token, _format)
942
- elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
943
- write_number(_row, _col, _token, _format)
944
- # Match formula
945
- elsif _token =~ /^=/
946
- write_formula(_row, _col, _token, _format, _value1)
947
- # Match array formula
948
- elsif _token =~ /^\{=.*\}$/
949
- write_formula(_row, _col, _token, _format, _value1)
950
- # Match blank
951
- elsif _token == ''
952
- # row_col_args.delete_at(2) # remove the empty string from the parameter list
953
- write_blank(_row, _col, _format)
954
- elsif @workbook.strings_to_urls
955
- # Match http, https or ftp URL
956
- if _token =~ %r{\A[fh]tt?ps?://}
957
- write_url(_row, _col, _token, _format, _value1, _value2)
958
- # Match mailto:
959
- elsif _token =~ /\Amailto:/
960
- write_url(_row, _col, _token, _format, _value1, _value2)
961
- # Match internal or external sheet link
962
- elsif _token =~ /\A(?:in|ex)ternal:/
963
- write_url(_row, _col, _token, _format, _value1, _value2)
1055
+ elsif _token.respond_to?(:=~) # String
1056
+ # Match integer with leading zero(s)
1057
+ if @leading_zeros && _token =~ /^0\d*$/
1058
+ write_string(_row, _col, _token, _format)
1059
+ elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
1060
+ write_number(_row, _col, _token, _format)
1061
+ # Match formula
1062
+ elsif _token =~ /^=/
1063
+ write_formula(_row, _col, _token, _format, _value1)
1064
+ # Match array formula
1065
+ elsif _token =~ /^\{=.*\}$/
1066
+ write_formula(_row, _col, _token, _format, _value1)
1067
+ # Match blank
1068
+ elsif _token == ''
1069
+ # row_col_args.delete_at(2) # remove the empty string from the parameter list
1070
+ write_blank(_row, _col, _format)
1071
+ elsif @workbook.strings_to_urls
1072
+ # Match http, https or ftp URL
1073
+ if _token =~ %r{\A[fh]tt?ps?://}
1074
+ write_url(_row, _col, _token, _format, _value1, _value2)
1075
+ # Match mailto:
1076
+ elsif _token =~ /\Amailto:/
1077
+ write_url(_row, _col, _token, _format, _value1, _value2)
1078
+ # Match internal or external sheet link
1079
+ elsif _token =~ /\A(?:in|ex)ternal:/
1080
+ write_url(_row, _col, _token, _format, _value1, _value2)
1081
+ else
1082
+ write_string(_row, _col, _token, _format)
1083
+ end
964
1084
  else
965
1085
  write_string(_row, _col, _token, _format)
966
1086
  end
@@ -1114,7 +1234,7 @@ module Writexlsx
1114
1234
 
1115
1235
  index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)
1116
1236
 
1117
- store_data_to_table(StringCellData.new(index, _format), _row, _col)
1237
+ store_data_to_table(StringCellData.new(index, _format, _string), _row, _col)
1118
1238
  end
1119
1239
 
1120
1240
  #
@@ -1143,13 +1263,16 @@ module Writexlsx
1143
1263
  check_dimensions(_row, _col)
1144
1264
  store_row_col_max_min_values(_row, _col)
1145
1265
 
1146
- _fragments, _length = rich_strings_fragments(_rich_strings)
1266
+ _fragments, _raw_string = rich_strings_fragments(_rich_strings)
1147
1267
  # can't allow 2 formats in a row
1148
1268
  return -4 unless _fragments
1149
1269
 
1270
+ # Check that the string si < 32767 chars.
1271
+ return 3 if _raw_string.size > @xls_strmax
1272
+
1150
1273
  index = shared_string_index(xml_str_of_rich_string(_fragments))
1151
1274
 
1152
- store_data_to_table(StringCellData.new(index, _xf), _row, _col)
1275
+ store_data_to_table(RichStringCellData.new(index, _xf, _raw_string), _row, _col)
1153
1276
  end
1154
1277
 
1155
1278
  #
@@ -1679,7 +1802,7 @@ module Writexlsx
1679
1802
  date_time = convert_date_time(_str)
1680
1803
 
1681
1804
  if date_time
1682
- store_data_to_table(NumberCellData.new(date_time, _format), _row, _col)
1805
+ store_data_to_table(DateTimeCellData.new(date_time, _format), _row, _col)
1683
1806
  else
1684
1807
  # If the date isn't valid then write it as a string.
1685
1808
  write_string(_row, _col, _str, _format)
@@ -2164,6 +2287,11 @@ module Writexlsx
2164
2287
  @autofilter_area = convert_name_area(_row1, _col1, _row2, _col2)
2165
2288
  @autofilter_ref = xl_range(_row1, _row2, _col1, _col2)
2166
2289
  @filter_range = [_col1, _col2]
2290
+
2291
+ # Store the filter cell positions for use in the autofit calculation.
2292
+ (_col1.._col2).each do |col|
2293
+ @filter_cells["#{_row1}:#{col}"] = 1
2294
+ end
2167
2295
  end
2168
2296
 
2169
2297
  #
@@ -2506,8 +2634,8 @@ module Writexlsx
2506
2634
  @external_hyper_links,
2507
2635
  @external_drawing_links,
2508
2636
  @external_vml_links,
2509
- @external_table_links,
2510
2637
  @external_background_links,
2638
+ @external_table_links,
2511
2639
  @external_comment_links
2512
2640
  ].reject { |a| a.empty? }
2513
2641
  end
@@ -2634,6 +2762,33 @@ module Writexlsx
2634
2762
 
2635
2763
  private
2636
2764
 
2765
+ #
2766
+ # Compare adjacent column information structures.
2767
+ #
2768
+ def compare_col_info(col_options, previous_options)
2769
+ if !col_options.width.nil? != !previous_options.width.nil?
2770
+ return nil
2771
+ end
2772
+ if col_options.width && previous_options.width &&
2773
+ col_options.width != previous_options.width
2774
+ return nil
2775
+ end
2776
+
2777
+ if !col_options.format.nil? != !previous_options.format.nil?
2778
+ return nil
2779
+ end
2780
+ if col_options.format && previous_options.format &&
2781
+ col_options.format != previous_options.format
2782
+ return nil
2783
+ end
2784
+
2785
+ return nil if col_options.hidden != previous_options.hidden
2786
+ return nil if col_options.level != previous_options.level
2787
+ return nil if col_options.collapsed != previous_options.collapsed
2788
+
2789
+ true
2790
+ end
2791
+
2637
2792
  #
2638
2793
  # Get the index used to address a drawing rel link.
2639
2794
  #
@@ -2685,9 +2840,9 @@ module Writexlsx
2685
2840
  # Create a temp format with the default font for unformatted fragments.
2686
2841
  default = Format.new(0)
2687
2842
 
2688
- length = 0 # String length.
2689
2843
  last = 'format'
2690
2844
  pos = 0
2845
+ raw_string = ''
2691
2846
 
2692
2847
  fragments = []
2693
2848
  rich_strings.each do |token|
@@ -2708,12 +2863,12 @@ module Writexlsx
2708
2863
  fragments << default << token
2709
2864
  end
2710
2865
 
2711
- length += token.size # Keep track of actual string length.
2866
+ raw_string += token # Keep track of actual string length.
2712
2867
  last = 'string'
2713
2868
  end
2714
2869
  pos += 1
2715
2870
  end
2716
- [fragments, length]
2871
+ [fragments, raw_string]
2717
2872
  end
2718
2873
 
2719
2874
  def xml_str_of_rich_string(fragments)
@@ -2944,8 +3099,9 @@ module Writexlsx
2944
3099
  #
2945
3100
  def size_col(col, anchor = 0) # :nodoc:
2946
3101
  # Look up the cell value to see if it has been changed.
2947
- if @col_sizes[col]
2948
- width, hidden = @col_sizes[col]
3102
+ if @col_info[col]
3103
+ width = @col_info[col].width || @default_col_width
3104
+ hidden = @col_info[col].hidden
2949
3105
 
2950
3106
  # Convert to pixels.
2951
3107
  pixels = if hidden == 1 && anchor != 4
@@ -3276,28 +3432,22 @@ EOS
3276
3432
  end
3277
3433
 
3278
3434
  #
3279
- # Based on the algorithm provided by Daniel Rentz of OpenOffice.
3280
- #
3435
+ # Hash a worksheet password. Based on the algorithm in ECMA-376-4:2016,
3436
+ # Office Open XML File Foemats -- Transitional Migration Features,
3437
+ # Additional attributes for workbookProtection element (Part 1, §18.2.29). #
3281
3438
  def encode_password(password) # :nodoc:
3282
- i = 0
3283
- chars = password.split(//)
3284
- count = chars.size
3439
+ hash = 0
3285
3440
 
3286
- chars.collect! do |char|
3287
- i += 1
3288
- char = char.ord << i
3289
- low_15 = char & 0x7fff
3290
- high_15 = char & (0x7fff << 15)
3291
- high_15 = high_15 >> 15
3292
- char = low_15 | high_15
3441
+ password.reverse.split(//).each do |char|
3442
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3443
+ hash ^= char.ord
3293
3444
  end
3294
3445
 
3295
- encoded_password = 0x0000
3296
- chars.each { |c| encoded_password ^= c }
3297
- encoded_password ^= count
3298
- encoded_password ^= 0xCE4B
3446
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3447
+ hash ^= password.length
3448
+ hash ^= 0xCE4B
3299
3449
 
3300
- sprintf("%X", encoded_password)
3450
+ sprintf("%X", hash)
3301
3451
  end
3302
3452
 
3303
3453
  #
@@ -3475,10 +3625,42 @@ EOS
3475
3625
  #
3476
3626
  def write_cols # :nodoc:
3477
3627
  # Exit unless some column have been formatted.
3478
- return if @colinfo.empty?
3628
+ return if @col_info.empty?
3479
3629
 
3480
3630
  @writer.tag_elements('cols') do
3481
- @colinfo.keys.sort.each { |col| write_col_info(@colinfo[col]) }
3631
+ # Use the first element of the column informatin structure to set
3632
+ # the initial/previous properties.
3633
+ first_col = @col_info.keys.min
3634
+ last_col = first_col
3635
+ previous_options = @col_info[first_col]
3636
+ deleted_col = first_col
3637
+ deleted_col_options = previous_options
3638
+
3639
+ @col_info.delete(first_col)
3640
+
3641
+ @col_info.keys.sort.each do |col|
3642
+ col_options = @col_info[col]
3643
+
3644
+ # Check if the column number is contiguous with the previous
3645
+ # column and if the properties are the same.
3646
+ if (col == last_col + 1) &&
3647
+ compare_col_info(col_options, previous_options)
3648
+ last_col = col
3649
+ else
3650
+ # If not contiguous/equal then we write out the current range
3651
+ # of columns and start again.
3652
+ write_col_info([first_col, last_col, previous_options])
3653
+ first_col = col
3654
+ last_col = first_col
3655
+ previous_options = col_options
3656
+ end
3657
+ end
3658
+
3659
+ # We will exit the previous loop with one unhandled column range.
3660
+ write_col_info([first_col, last_col, previous_options])
3661
+
3662
+ # Put back the deleted first column information structure:
3663
+ @col_info[deleted_col] = deleted_col_options
3482
3664
  end
3483
3665
  end
3484
3666
 
@@ -3490,13 +3672,14 @@ EOS
3490
3672
  end
3491
3673
 
3492
3674
  def col_info_attributes(args)
3493
- min = args[0] || 0 # First formatted column.
3494
- max = args[1] || 0 # Last formatted column.
3495
- width = args[2] # Col width in user units.
3496
- format = args[3] # Format index.
3497
- hidden = args[4] || 0 # Hidden flag.
3498
- level = args[5] || 0 # Outline level.
3499
- collapsed = args[6] || 0 # Outline level.
3675
+ min = args[0] || 0 # First formatted column.
3676
+ max = args[1] || 0 # Last formatted column.
3677
+ width = args[2].width # Col width in user units.
3678
+ format = args[2].format # Format index.
3679
+ hidden = args[2].hidden || 0 # Hidden flag.
3680
+ level = args[2].level || 0 # Outline level.
3681
+ collapsed = args[2].collapsed || 0 # Outline Collapsed
3682
+ autofit = args[2].autofit || 0 # Best fit for autofit numbers.
3500
3683
  xf_index = format ? format.get_xf_index : 0
3501
3684
 
3502
3685
  custom_width = true
@@ -3519,10 +3702,11 @@ EOS
3519
3702
  ['width', width]
3520
3703
  ]
3521
3704
 
3522
- attributes << ['style', xf_index] if xf_index != 0
3523
- attributes << ['hidden', 1] if hidden != 0
3705
+ attributes << ['style', xf_index] if xf_index != 0
3706
+ attributes << ['hidden', 1] if hidden != 0
3707
+ attributes << ['bestFit', 1] if autofit != 0
3524
3708
  attributes << ['customWidth', 1] if custom_width
3525
- attributes << ['outlineLevel', level] if level != 0
3709
+ attributes << ['outlineLevel', level] if level != 0
3526
3710
  attributes << ['collapsed', 1] if collapsed != 0
3527
3711
  attributes
3528
3712
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: write_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.2
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hideo NAKAMURA
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-16 00:00:00.000000000 Z
11
+ date: 2023-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -130,6 +130,7 @@ files:
130
130
  - examples/add_vba_project.rb
131
131
  - examples/array_formula.rb
132
132
  - examples/autofilter.rb
133
+ - examples/autofit.rb
133
134
  - examples/background.rb
134
135
  - examples/chart_area.rb
135
136
  - examples/chart_bar.rb
@@ -275,7 +276,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
275
276
  - !ruby/object:Gem::Version
276
277
  version: '0'
277
278
  requirements: []
278
- rubygems_version: 3.4.5
279
+ rubygems_version: 3.4.10
279
280
  signing_key:
280
281
  specification_version: 4
281
282
  summary: write_xlsx is a gem to create a new file in the Excel 2007+ XLSX format.