write_xlsx 1.10.2 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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.