write_xlsx 1.10.1 → 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.
@@ -15,6 +15,7 @@ require 'write_xlsx/worksheet/data_validation'
15
15
  require 'write_xlsx/worksheet/hyperlink'
16
16
  require 'write_xlsx/worksheet/page_setup'
17
17
  require 'tempfile'
18
+ require 'date'
18
19
 
19
20
  module Writexlsx
20
21
  class Worksheet
@@ -30,20 +31,25 @@ module Writexlsx
30
31
  attr_reader :vml_data_id # :nodoc:
31
32
  attr_reader :vml_header_id # :nodoc:
32
33
  attr_reader :autofilter_area # :nodoc:
33
- attr_reader :writer, :set_rows, :col_formats # :nodoc:
34
+ attr_reader :writer, :set_rows, :col_info # :nodoc:
34
35
  attr_reader :vml_shape_id # :nodoc:
35
36
  attr_reader :comments, :comments_author # :nodoc:
36
37
  attr_accessor :data_bars_2010, :dxf_priority # :nodoc:
37
38
  attr_reader :vba_codename # :nodoc:
38
- attr_writer :excel_version
39
+ attr_writer :excel_version # :nodoc:
40
+ attr_reader :filter_cells # :nodoc:
39
41
 
40
42
  def initialize(workbook, index, name) # :nodoc:
43
+ rowmax = 1_048_576
44
+ colmax = 16_384
45
+ strmax = 32_767
46
+
41
47
  @writer = Package::XMLWriterSimple.new
42
48
 
43
49
  @workbook = workbook
44
50
  @index = index
45
51
  @name = name
46
- @colinfo = {}
52
+ @col_info = {}
47
53
  @cell_data_table = []
48
54
  @excel_version = 2007
49
55
  @palette = workbook.palette
@@ -54,6 +60,10 @@ module Writexlsx
54
60
 
55
61
  @screen_gridlines = true
56
62
  @show_zeros = true
63
+
64
+ @xls_rowmax = rowmax
65
+ @xls_colmax = colmax
66
+ @xls_strmax = strmax
57
67
  @dim_rowmin = nil
58
68
  @dim_rowmax = nil
59
69
  @dim_colmin = nil
@@ -76,11 +86,10 @@ module Writexlsx
76
86
  @filter_on = false
77
87
  @filter_range = []
78
88
  @filter_cols = {}
89
+ @filter_cells = {}
79
90
  @filter_type = {}
80
91
 
81
- @col_sizes = {}
82
92
  @row_sizes = {}
83
- @col_formats = {}
84
93
 
85
94
  @last_shape_id = 1
86
95
  @rel_count = 0
@@ -89,8 +98,8 @@ module Writexlsx
89
98
  @external_drawing_links = []
90
99
  @external_comment_links = []
91
100
  @external_vml_links = []
92
- @external_table_links = []
93
101
  @external_background_links = []
102
+ @external_table_links = []
94
103
  @drawing_links = []
95
104
  @vml_drawing_links = []
96
105
  @charts = []
@@ -120,6 +129,7 @@ module Writexlsx
120
129
  @default_col_width = 8.43
121
130
  @default_col_pixels = 64
122
131
  @default_row_rezoed = 0
132
+ @default_date_pixels = 68
123
133
 
124
134
  @merge = []
125
135
 
@@ -304,7 +314,8 @@ module Writexlsx
304
314
  #
305
315
  def set_column(*args)
306
316
  # Check for a cell reference in A1 notation and substitute row and column
307
- 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/
308
319
  _row1, firstcol, _row2, lastcol, *data = substitute_cellref(*args)
309
320
  else
310
321
  firstcol, lastcol, *data = args
@@ -320,6 +331,7 @@ module Writexlsx
320
331
  firstcol, lastcol = lastcol, firstcol if firstcol > lastcol
321
332
 
322
333
  width, format, hidden, level, collapsed = data
334
+ autofit = 0
323
335
 
324
336
  # Check that cols are valid and store max and min values with default row.
325
337
  # NOTE: The check shouldn't modify the row dimensions and should only modify
@@ -337,22 +349,20 @@ module Writexlsx
337
349
  level = 0 if level < 0
338
350
  level = 7 if level > 7
339
351
 
352
+ # Excel has a maximum column width of 255 characters.
353
+ width = 255.0 if width && width > 255.0
354
+
340
355
  @outline_col_level = level if level > @outline_col_level
341
356
 
342
357
  # Store the column data based on the first column. Padded for sorting.
343
- @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
344
363
 
345
364
  # Store the column change to allow optimisations.
346
365
  @col_size_changed = 1
347
-
348
- # Store the col sizes for use when calculating image vertices taking
349
- # hidden columns into account. Also store the column formats.
350
- width ||= @default_col_width
351
-
352
- (firstcol..lastcol).each do |col|
353
- @col_sizes[col] = [width, hidden]
354
- @col_formats[col] = format if format
355
- end
356
366
  end
357
367
 
358
368
  #
@@ -386,6 +396,113 @@ module Writexlsx
386
396
  set_column(first_col, last_col, width, format, hidden, level)
387
397
  end
388
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
+
389
506
  #
390
507
  # :call-seq:
391
508
  # set_selection(cell_or_cell_range)
@@ -395,7 +512,12 @@ module Writexlsx
395
512
  def set_selection(*args)
396
513
  return if args.empty?
397
514
 
398
- row_first, col_first, row_last, col_last = row_col_notation(args)
515
+ if (row_col_array = row_col_notation(args.first))
516
+ row_first, col_first, row_last, col_last = row_col_array
517
+ else
518
+ row_first, col_first, row_last, col_last = args
519
+ end
520
+
399
521
  active_cell = xl_rowcol_to_cell(row_first, col_first)
400
522
 
401
523
  if row_last # Range selection.
@@ -420,10 +542,15 @@ module Writexlsx
420
542
  #
421
543
  # Set the first visible cell at the top left of the worksheet.
422
544
  #
423
- def set_top_left_cell(*args)
424
- row, col = row_col_notation(args)
545
+ def set_top_left_cell(row, col = nil)
546
+ if (row_col_array = row_col_notation(row))
547
+ _row, _col = row_col_array
548
+ else
549
+ _row = row
550
+ _col = col
551
+ end
425
552
 
426
- @top_left_cell = xl_rowcol_to_cell(row, col)
553
+ @top_left_cell = xl_rowcol_to_cell(_row, _col)
427
554
  end
428
555
 
429
556
  #
@@ -439,7 +566,12 @@ module Writexlsx
439
566
  return if args.empty?
440
567
 
441
568
  # Check for a cell reference in A1 notation and substitute row and column.
442
- row, col, top_row, left_col, type = row_col_notation(args)
569
+ if (row_col_array = row_col_notation(args.first))
570
+ row, col, top_row, left_col = row_col_array
571
+ type = args[1]
572
+ else
573
+ row, col, top_row, left_col, type = args
574
+ end
443
575
 
444
576
  col ||= 0
445
577
  top_row ||= row
@@ -772,7 +904,12 @@ module Writexlsx
772
904
  def print_area(*args)
773
905
  return @page_setup.print_area.dup if args.empty?
774
906
 
775
- row1, col1, row2, col2 = row_col_notation(args)
907
+ if (row_col_array = row_col_notation(args.first))
908
+ row1, col1, row2, col2 = row_col_array
909
+ else
910
+ row1, col1, row2, col2 = args
911
+ end
912
+
776
913
  return if [row1, col1, row2, col2].include?(nil)
777
914
 
778
915
  # Ignore max print area since this is the same as no print area for Excel.
@@ -889,50 +1026,66 @@ module Writexlsx
889
1026
  # data the {#write()}[#method-i-write] method acts as a general alias for several more
890
1027
  # specific methods:
891
1028
  #
892
- def write(*args)
1029
+ def write(row, col, token = nil, format = nil, value1 = nil, value2 = nil)
893
1030
  # Check for a cell reference in A1 notation and substitute row and column
894
- row_col_args = row_col_notation(args)
895
- token = row_col_args[2] || ''
896
- token = token.to_s if token.instance_of?(Time)
1031
+ if (row_col_array = row_col_notation(row))
1032
+ _row, _col = row_col_array
1033
+ _token = col
1034
+ _format = token
1035
+ _value1 = format
1036
+ _value2 = value1
1037
+ else
1038
+ _row = row
1039
+ _col = col
1040
+ _token = token
1041
+ _format = format
1042
+ _value1 = value1
1043
+ _value2 = value2
1044
+ end
1045
+ _token ||= ''
1046
+ _token = _token.to_s if token.instance_of?(Time) || token.instance_of?(Date)
897
1047
 
898
- fmt = row_col_args[3]
899
- if fmt.respond_to?(:force_text_format?) && fmt.force_text_format?
900
- write_string(*args) # Force text format
1048
+ if _format.respond_to?(:force_text_format?) && _format.force_text_format?
1049
+ write_string(_row, _col, _token, _format) # Force text format
901
1050
  # Match an array ref.
902
- elsif token.respond_to?(:to_ary)
903
- write_row(*args)
904
- elsif token.respond_to?(:coerce) # Numeric
905
- write_number(*args)
906
- # Match integer with leading zero(s)
907
- elsif @leading_zeros && token =~ /^0\d*$/
908
- write_string(*args)
909
- elsif token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
910
- write_number(*args)
911
- # Match formula
912
- elsif token =~ /^=/
913
- write_formula(*args)
914
- # Match array formula
915
- elsif token =~ /^\{=.*\}$/
916
- write_formula(*args)
917
- # Match blank
918
- elsif token == ''
919
- row_col_args.delete_at(2) # remove the empty string from the parameter list
920
- write_blank(*row_col_args)
921
- elsif @workbook.strings_to_urls
922
- # Match http, https or ftp URL
923
- if token =~ %r{\A[fh]tt?ps?://}
924
- write_url(*args)
925
- # Match mailto:
926
- elsif token =~ /\Amailto:/
927
- write_url(*args)
928
- # Match internal or external sheet link
929
- elsif token =~ /\A(?:in|ex)ternal:/
930
- write_url(*args)
1051
+ elsif _token.respond_to?(:to_ary)
1052
+ write_row(_row, _col, _token, _format, _value1, _value2)
1053
+ elsif _token.respond_to?(:coerce) # Numeric
1054
+ write_number(_row, _col, _token, _format)
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
931
1084
  else
932
- write_string(*args)
1085
+ write_string(_row, _col, _token, _format)
933
1086
  end
934
1087
  else
935
- write_string(*args)
1088
+ write_string(_row, _col, _token, _format)
936
1089
  end
937
1090
  end
938
1091
 
@@ -944,19 +1097,28 @@ module Writexlsx
944
1097
  # the elements of the array are in turn array. This allows the writing
945
1098
  # of 1D or 2D arrays of data in one go.
946
1099
  #
947
- def write_row(*args)
1100
+ def write_row(row, col, tokens = nil, *options)
948
1101
  # Check for a cell reference in A1 notation and substitute row and column
949
- row, col, tokens, *options = row_col_notation(args)
950
- raise "Not an array ref in call to write_row()$!" unless tokens.respond_to?(:to_ary)
1102
+ if (row_col_array = row_col_notation(row))
1103
+ _row, _col = row_col_array
1104
+ _tokens = col
1105
+ _options = [tokens] + options
1106
+ else
1107
+ _row = row
1108
+ _col = col
1109
+ _tokens = tokens
1110
+ _options = options
1111
+ end
1112
+ raise "Not an array ref in call to write_row()$!" unless _tokens.respond_to?(:to_ary)
951
1113
 
952
- tokens.each do |token|
1114
+ _tokens.each do |_token|
953
1115
  # Check for nested arrays
954
- if token.respond_to?(:to_ary)
955
- write_col(row, col, token, *options)
1116
+ if _token.respond_to?(:to_ary)
1117
+ write_col(_row, _col, _token, *_options)
956
1118
  else
957
- write(row, col, token, *options)
1119
+ write(_row, _col, _token, *_options)
958
1120
  end
959
- col += 1
1121
+ _col += 1
960
1122
  end
961
1123
  end
962
1124
 
@@ -968,13 +1130,22 @@ module Writexlsx
968
1130
  # the elements of the array are in turn array. This allows the writing
969
1131
  # of 1D or 2D arrays of data in one go.
970
1132
  #
971
- def write_col(*args)
972
- row, col, tokens, *options = row_col_notation(args)
1133
+ def write_col(row, col, tokens = nil, *options)
1134
+ if (row_col_array = row_col_notation(row))
1135
+ _row, _col = row_col_array
1136
+ _tokens = col
1137
+ _options = [tokens] + options if options
1138
+ else
1139
+ _row = row
1140
+ _col = col
1141
+ _tokens = tokens
1142
+ _options = options
1143
+ end
973
1144
 
974
- tokens.each do |token|
1145
+ _tokens.each do |_token|
975
1146
  # write() will deal with any nested arrays
976
- write(row, col, token, *options)
977
- row += 1
1147
+ write(_row, _col, _token, *_options)
1148
+ _row += 1
978
1149
  end
979
1150
  end
980
1151
 
@@ -984,19 +1155,28 @@ module Writexlsx
984
1155
  #
985
1156
  # Write a comment to the specified row and column (zero indexed).
986
1157
  #
987
- def write_comment(*args)
1158
+ def write_comment(row, col, string = nil, options = nil)
988
1159
  # Check for a cell reference in A1 notation and substitute row and column
989
- row, col, string, options = row_col_notation(args)
990
- raise WriteXLSXInsufficientArgumentError if [row, col, string].include?(nil)
1160
+ if (row_col_array = row_col_notation(row))
1161
+ _row, _col = row_col_array
1162
+ _string = col
1163
+ _options = string
1164
+ else
1165
+ _row = row
1166
+ _col = col
1167
+ _string = string
1168
+ _options = options
1169
+ end
1170
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _string].include?(nil)
991
1171
 
992
1172
  # Check that row and col are valid and store max and min values
993
- check_dimensions(row, col)
994
- store_row_col_max_min_values(row, col)
1173
+ check_dimensions(_row, _col)
1174
+ store_row_col_max_min_values(_row, _col)
995
1175
 
996
1176
  @has_vml = true
997
1177
 
998
1178
  # Process the properties of the cell comment.
999
- @comments.add(@workbook, self, row, col, string, options)
1179
+ @comments.add(@workbook, self, _row, _col, _string, _options)
1000
1180
  end
1001
1181
 
1002
1182
  #
@@ -1005,16 +1185,25 @@ module Writexlsx
1005
1185
  #
1006
1186
  # Write an integer or a float to the cell specified by row and column:
1007
1187
  #
1008
- def write_number(*args)
1188
+ def write_number(row, col, number, format = nil)
1009
1189
  # Check for a cell reference in A1 notation and substitute row and column
1010
- row, col, num, xf = row_col_notation(args)
1011
- raise WriteXLSXInsufficientArgumentError if row.nil? || col.nil? || num.nil?
1190
+ if (row_col_array = row_col_notation(row))
1191
+ _row, _col = row_col_array
1192
+ _number = col
1193
+ _format = number
1194
+ else
1195
+ _row = row
1196
+ _col = col
1197
+ _number = number
1198
+ _format = format
1199
+ end
1200
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _number.nil?
1012
1201
 
1013
1202
  # Check that row and col are valid and store max and min values
1014
- check_dimensions(row, col)
1015
- store_row_col_max_min_values(row, col)
1203
+ check_dimensions(_row, _col)
1204
+ store_row_col_max_min_values(_row, _col)
1016
1205
 
1017
- store_data_to_table(NumberCellData.new(num, xf), row, col)
1206
+ store_data_to_table(NumberCellData.new(_number, _format), _row, _col)
1018
1207
  end
1019
1208
 
1020
1209
  #
@@ -1024,19 +1213,28 @@ module Writexlsx
1024
1213
  # Write a string to the specified row and column (zero indexed).
1025
1214
  # +format+ is optional.
1026
1215
  #
1027
- def write_string(*args)
1216
+ def write_string(row, col, string = nil, format = nil)
1028
1217
  # Check for a cell reference in A1 notation and substitute row and column
1029
- row, col, str, xf = row_col_notation(args)
1030
- str &&= str.to_s
1031
- raise WriteXLSXInsufficientArgumentError if row.nil? || col.nil? || str.nil?
1218
+ if (row_col_array = row_col_notation(row))
1219
+ _row, _col = row_col_array
1220
+ _string = col
1221
+ _format = string
1222
+ else
1223
+ _row = row
1224
+ _col = col
1225
+ _string = string
1226
+ _format = format
1227
+ end
1228
+ _string &&= _string.to_s
1229
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _string.nil?
1032
1230
 
1033
1231
  # Check that row and col are valid and store max and min values
1034
- check_dimensions(row, col)
1035
- store_row_col_max_min_values(row, col)
1232
+ check_dimensions(_row, _col)
1233
+ store_row_col_max_min_values(_row, _col)
1036
1234
 
1037
- index = shared_string_index(str.length > STR_MAX ? str[0, STR_MAX] : str)
1235
+ index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)
1038
1236
 
1039
- store_data_to_table(StringCellData.new(index, xf), row, col)
1237
+ store_data_to_table(StringCellData.new(index, _format, _string), _row, _col)
1040
1238
  end
1041
1239
 
1042
1240
  #
@@ -1047,24 +1245,34 @@ module Writexlsx
1047
1245
  # The method receives string fragments prefixed by format objects. The final
1048
1246
  # format object is used as the cell format.
1049
1247
  #
1050
- def write_rich_string(*args)
1248
+ def write_rich_string(row, col, *rich_strings)
1051
1249
  # Check for a cell reference in A1 notation and substitute row and column
1052
- row, col, *rich_strings = row_col_notation(args)
1053
- raise WriteXLSXInsufficientArgumentError if [row, col, rich_strings[0]].include?(nil)
1250
+ if (row_col_array = row_col_notation(row))
1251
+ _row, _col = row_col_array
1252
+ _rich_strings = [col] + rich_strings
1253
+ else
1254
+ _row = row
1255
+ _col = col
1256
+ _rich_strings = rich_strings
1257
+ end
1258
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _rich_strings[0]].include?(nil)
1054
1259
 
1055
- xf = cell_format_of_rich_string(rich_strings)
1260
+ _xf = cell_format_of_rich_string(_rich_strings)
1056
1261
 
1057
1262
  # Check that row and col are valid and store max and min values
1058
- check_dimensions(row, col)
1059
- store_row_col_max_min_values(row, col)
1263
+ check_dimensions(_row, _col)
1264
+ store_row_col_max_min_values(_row, _col)
1060
1265
 
1061
- fragments, _length = rich_strings_fragments(rich_strings)
1266
+ _fragments, _raw_string = rich_strings_fragments(_rich_strings)
1062
1267
  # can't allow 2 formats in a row
1063
- return -4 unless fragments
1268
+ return -4 unless _fragments
1269
+
1270
+ # Check that the string si < 32767 chars.
1271
+ return 3 if _raw_string.size > @xls_strmax
1064
1272
 
1065
- index = shared_string_index(xml_str_of_rich_string(fragments))
1273
+ index = shared_string_index(xml_str_of_rich_string(_fragments))
1066
1274
 
1067
- store_data_to_table(StringCellData.new(index, xf), row, col)
1275
+ store_data_to_table(RichStringCellData.new(index, _xf, _raw_string), _row, _col)
1068
1276
  end
1069
1277
 
1070
1278
  #
@@ -1075,19 +1283,26 @@ module Writexlsx
1075
1283
  # A blank cell is used to specify formatting without adding a string
1076
1284
  # or a number.
1077
1285
  #
1078
- def write_blank(*args)
1286
+ def write_blank(row, col, format = nil)
1079
1287
  # Check for a cell reference in A1 notation and substitute row and column
1080
- row, col, xf = row_col_notation(args)
1081
- raise WriteXLSXInsufficientArgumentError if [row, col].include?(nil)
1288
+ if (row_col_array = row_col_notation(row))
1289
+ _row, _col = row_col_array
1290
+ _format = col
1291
+ else
1292
+ _row = row
1293
+ _col = col
1294
+ _format = format
1295
+ end
1296
+ raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)
1082
1297
 
1083
1298
  # Don't write a blank cell unless it has a format
1084
- return unless xf
1299
+ return unless _format
1085
1300
 
1086
1301
  # Check that row and col are valid and store max and min values
1087
- check_dimensions(row, col)
1088
- store_row_col_max_min_values(row, col)
1302
+ check_dimensions(_row, _col)
1303
+ store_row_col_max_min_values(_row, _col)
1089
1304
 
1090
- store_data_to_table(BlankCellData.new(xf), row, col)
1305
+ store_data_to_table(BlankCellData.new(_format), _row, _col)
1091
1306
  end
1092
1307
 
1093
1308
  def expand_formula(formula, function, addition = '')
@@ -1261,28 +1476,39 @@ module Writexlsx
1261
1476
  #
1262
1477
  # Write a formula or function to the cell specified by +row+ and +column+:
1263
1478
  #
1264
- def write_formula(*args)
1479
+ def write_formula(row, col, formula = nil, format = nil, value = nil)
1265
1480
  # Check for a cell reference in A1 notation and substitute row and column
1266
- row, col, formula, format, value = row_col_notation(args)
1267
- raise WriteXLSXInsufficientArgumentError if [row, col, formula].include?(nil)
1481
+ if (row_col_array = row_col_notation(row))
1482
+ _row, _col = row_col_array
1483
+ _formula = col
1484
+ _format = formula
1485
+ _value = format
1486
+ else
1487
+ _row = row
1488
+ _col = col
1489
+ _formula = formula
1490
+ _format = format
1491
+ _value = value
1492
+ end
1493
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _formula].include?(nil)
1268
1494
 
1269
1495
  # Check for dynamic array functions.
1270
1496
  regex = /\bLET\(|\bSORT\(|\bLAMBDA\(|\bSINGLE\(|\bSORTBY\(|\bUNIQUE\(|\bXMATCH\(|\bFILTER\(|\bXLOOKUP\(|\bSEQUENCE\(|\bRANDARRAY\(|\bANCHORARRAY\(/
1271
- if formula =~ regex
1497
+ if _formula =~ regex
1272
1498
  return write_dynamic_array_formula(
1273
- row, col, row, col, formula, format, value
1499
+ _row, _col, _row, _col, _formula, _format, _value
1274
1500
  )
1275
1501
  end
1276
1502
 
1277
1503
  # Hand off array formulas.
1278
- if formula =~ /^\{=.*\}$/
1279
- write_array_formula(row, col, row, col, formula, format, value)
1504
+ if _formula =~ /^\{=.*\}$/
1505
+ write_array_formula(_row, _col, _row, _col, _formula, _format, _value)
1280
1506
  else
1281
- check_dimensions(row, col)
1282
- store_row_col_max_min_values(row, col)
1283
- formula = formula.sub(/^=/, '')
1507
+ check_dimensions(_row, _col)
1508
+ store_row_col_max_min_values(_row, _col)
1509
+ _formula = _formula.sub(/^=/, '')
1284
1510
 
1285
- store_data_to_table(FormulaCellData.new(formula, format, value), row, col)
1511
+ store_data_to_table(FormulaCellData.new(_formula, _format, _value), _row, _col)
1286
1512
  end
1287
1513
  end
1288
1514
 
@@ -1300,7 +1526,12 @@ module Writexlsx
1300
1526
  params = args
1301
1527
  end
1302
1528
 
1303
- row1, col1, row2, col2, formula, xf, value = row_col_notation(params)
1529
+ if (row_col_array = row_col_notation(params.first))
1530
+ row1, col1, row2, col2 = row_col_array
1531
+ formula, xf, value = params[1..-1]
1532
+ else
1533
+ row1, col1, row2, col2, formula, xf, value = params
1534
+ end
1304
1535
  raise WriteXLSXInsufficientArgumentError if [row1, col1, row2, col2, formula].include?(nil)
1305
1536
 
1306
1537
  # Swap last row/col with first row/col as necessary
@@ -1349,8 +1580,8 @@ module Writexlsx
1349
1580
  #
1350
1581
  # Write an array formula to the specified row and column (zero indexed).
1351
1582
  #
1352
- def write_array_formula(*args)
1353
- write_array_formula_base('a', *args)
1583
+ def write_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
1584
+ write_array_formula_base('a', row1, col1, row2, col2, formula, format, value)
1354
1585
  end
1355
1586
 
1356
1587
  #
@@ -1358,8 +1589,8 @@ module Writexlsx
1358
1589
  #
1359
1590
  # Write a dynamic formula to the specified row and column (zero indexed).
1360
1591
  #
1361
- def write_dynamic_array_formula(*args)
1362
- write_array_formula_base('d', *args)
1592
+ def write_dynamic_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
1593
+ write_array_formula_base('d', row1, col1, row2, col2, formula, format, value)
1363
1594
  @has_dynamic_arrays = true
1364
1595
  end
1365
1596
 
@@ -1368,18 +1599,27 @@ module Writexlsx
1368
1599
  #
1369
1600
  # Write a boolean value to the specified row and column (zero indexed).
1370
1601
  #
1371
- def write_boolean(*args)
1372
- row, col, val, xf = row_col_notation(args)
1373
- raise WriteXLSXInsufficientArgumentError if row.nil? || col.nil?
1602
+ def write_boolean(row, col, val = nil, format = nil)
1603
+ if (row_col_array = row_col_notation(row))
1604
+ _row, _col = row_col_array
1605
+ _val = col
1606
+ _format = val
1607
+ else
1608
+ _row = row
1609
+ _col = col
1610
+ _val = val
1611
+ _format = format
1612
+ end
1613
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil?
1374
1614
 
1375
- val = val ? 1 : 0 # Boolean value.
1615
+ _val = _val ? 1 : 0 # Boolean value.
1376
1616
  # xf : cell format.
1377
1617
 
1378
1618
  # Check that row and col are valid and store max and min values
1379
- check_dimensions(row, col)
1380
- store_row_col_max_min_values(row, col)
1619
+ check_dimensions(_row, _col)
1620
+ store_row_col_max_min_values(_row, _col)
1381
1621
 
1382
- store_data_to_table(BooleanCellData.new(val, xf), row, col)
1622
+ store_data_to_table(BooleanCellData.new(_val, _format), _row, _col)
1383
1623
  end
1384
1624
 
1385
1625
  #
@@ -1388,28 +1628,35 @@ module Writexlsx
1388
1628
  #
1389
1629
  # Update formatting of the cell to the specified row and column (zero indexed).
1390
1630
  #
1391
- def update_format_with_params(*args)
1392
- row, col, params = row_col_notation(args)
1393
- raise WriteXLSXInsufficientArgumentError if row.nil? || col.nil? || params.nil?
1631
+ def update_format_with_params(row, col, params = nil)
1632
+ if (row_col_array = row_col_notation(row))
1633
+ _row, _col = row_col_array
1634
+ _params = args[1]
1635
+ else
1636
+ _row = row
1637
+ _col = col
1638
+ _params = params
1639
+ end
1640
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _params.nil?
1394
1641
 
1395
1642
  # Check that row and col are valid and store max and min values
1396
- check_dimensions(row, col)
1397
- store_row_col_max_min_values(row, col)
1643
+ check_dimensions(_row, _col)
1644
+ store_row_col_max_min_values(_row, _col)
1398
1645
 
1399
1646
  format = nil
1400
1647
  cell_data = nil
1401
- if @cell_data_table[row].nil? || @cell_data_table[row][col].nil?
1402
- format = @workbook.add_format(params)
1403
- write_blank(row, col, format)
1648
+ if @cell_data_table[_row].nil? || @cell_data_table[_row][_col].nil?
1649
+ format = @workbook.add_format(_params)
1650
+ write_blank(_row, _col, format)
1404
1651
  else
1405
- if @cell_data_table[row][col].xf.nil?
1406
- format = @workbook.add_format(params)
1407
- cell_data = @cell_data_table[row][col]
1652
+ if @cell_data_table[_row][_col].xf.nil?
1653
+ format = @workbook.add_format(_params)
1654
+ cell_data = @cell_data_table[_row][_col]
1408
1655
  else
1409
1656
  format = @workbook.add_format
1410
- cell_data = @cell_data_table[row][col]
1657
+ cell_data = @cell_data_table[_row][_col]
1411
1658
  format.copy(cell_data.xf)
1412
- format.set_format_properties(params)
1659
+ format.set_format_properties(_params)
1413
1660
  end
1414
1661
  # keep original value of cell
1415
1662
  value = if cell_data.is_a? FormulaCellData
@@ -1421,7 +1668,7 @@ module Writexlsx
1421
1668
  else
1422
1669
  cell_data.data
1423
1670
  end
1424
- write(row, col, value, format)
1671
+ write(_row, _col, value, format)
1425
1672
  end
1426
1673
  end
1427
1674
 
@@ -1431,22 +1678,31 @@ module Writexlsx
1431
1678
  #
1432
1679
  # Update formatting of cells in range to the specified row and column (zero indexed).
1433
1680
  #
1434
- def update_range_format_with_params(*args)
1435
- row_first, col_first, row_last, col_last, params = row_col_notation(args)
1681
+ def update_range_format_with_params(row_first, col_first, row_last = nil, col_last = nil, params = nil)
1682
+ if (row_col_array = row_col_notation(row_first))
1683
+ _row_first, _col_first, _row_last, _col_last = row_col_array
1684
+ params = args[1..-1]
1685
+ else
1686
+ _row_first = row_first
1687
+ _col_first = col_first
1688
+ _row_last = row_last
1689
+ _col_last = col_last
1690
+ _params = params
1691
+ end
1436
1692
 
1437
- raise WriteXLSXInsufficientArgumentError if [row_first, col_first, row_last, col_last, params].include?(nil)
1693
+ raise WriteXLSXInsufficientArgumentError if [_row_first, _col_first, _row_last, _col_last, _params].include?(nil)
1438
1694
 
1439
1695
  # Swap last row/col with first row/col as necessary
1440
- row_first, row_last = row_last, row_first if row_first > row_last
1441
- col_first, col_last = col_last, col_first if col_first > col_last
1696
+ _row_first, _row_last = _row_last, _row_first if _row_first > _row_last
1697
+ _col_first, _col_last = _col_last, _col_first if _col_first > _col_last
1442
1698
 
1443
1699
  # Check that column number is valid and store the max value
1444
- check_dimensions(row_last, col_last)
1445
- store_row_col_max_min_values(row_last, col_last)
1700
+ check_dimensions(_row_last, _col_last)
1701
+ store_row_col_max_min_values(_row_last, _col_last)
1446
1702
 
1447
- (row_first..row_last).each do |row|
1448
- (col_first..col_last).each do |col|
1449
- update_format_with_params(row, col, params)
1703
+ (_row_first.._row_last).each do |row|
1704
+ (_col_first.._col_last).each do |col|
1705
+ update_format_with_params(row, col, _params)
1450
1706
  end
1451
1707
  end
1452
1708
  end
@@ -1483,26 +1739,39 @@ module Writexlsx
1483
1739
  # The label is written using the {#write()}[#method-i-write] method. Therefore it is
1484
1740
  # possible to write strings, numbers or formulas as labels.
1485
1741
  #
1486
- def write_url(*args)
1742
+ def write_url(row, col, url = nil, format = nil, str = nil, tip = nil)
1487
1743
  # Check for a cell reference in A1 notation and substitute row and column
1488
- row, col, url, xf, str, tip = row_col_notation(args)
1489
- xf, str = str, xf if str.respond_to?(:xf_index) || !xf.respond_to?(:xf_index)
1490
- raise WriteXLSXInsufficientArgumentError if [row, col, url].include?(nil)
1744
+ if (row_col_array = row_col_notation(row))
1745
+ _row, _col = row_col_array
1746
+ _url = col
1747
+ _format = url
1748
+ _str = format
1749
+ _tip = str
1750
+ else
1751
+ _row = row
1752
+ _col = col
1753
+ _url = url
1754
+ _format = format
1755
+ _str = str
1756
+ _tip = tip
1757
+ end
1758
+ _format, _str = _str, _format if _str.respond_to?(:xf_index) || !_format.respond_to?(:xf_index)
1759
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _url].include?(nil)
1491
1760
 
1492
1761
  # Check that row and col are valid and store max and min values
1493
- check_dimensions(row, col)
1494
- store_row_col_max_min_values(row, col)
1762
+ check_dimensions(_row, _col)
1763
+ store_row_col_max_min_values(_row, _col)
1495
1764
 
1496
- hyperlink = Hyperlink.factory(url, str, tip)
1497
- store_hyperlink(row, col, hyperlink)
1765
+ hyperlink = Hyperlink.factory(_url, _str, _tip)
1766
+ store_hyperlink(_row, _col, hyperlink)
1498
1767
 
1499
1768
  raise "URL '#{url}' added but URL exceeds Excel's limit of 65,530 URLs per worksheet." if hyperlinks_count > 65_530
1500
1769
 
1501
1770
  # Add the default URL format.
1502
- xf ||= @default_url_format
1771
+ _format ||= @default_url_format
1503
1772
 
1504
1773
  # Write the hyperlink string.
1505
- write_string(row, col, hyperlink.str, xf)
1774
+ write_string(_row, _col, hyperlink.str, _format)
1506
1775
  end
1507
1776
 
1508
1777
  #
@@ -1512,22 +1781,31 @@ module Writexlsx
1512
1781
  # Write a datetime string in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format as a
1513
1782
  # number representing an Excel date. format is optional.
1514
1783
  #
1515
- def write_date_time(*args)
1784
+ def write_date_time(row, col, str, format = nil)
1516
1785
  # Check for a cell reference in A1 notation and substitute row and column
1517
- row, col, str, xf = row_col_notation(args)
1518
- raise WriteXLSXInsufficientArgumentError if [row, col, str].include?(nil)
1786
+ if (row_col_array = row_col_notation(row))
1787
+ _row, _col = row_col_array
1788
+ _str = col
1789
+ _format = str
1790
+ else
1791
+ _row = row
1792
+ _col = col
1793
+ _str = str
1794
+ _format = format
1795
+ end
1796
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _str].include?(nil)
1519
1797
 
1520
1798
  # Check that row and col are valid and store max and min values
1521
- check_dimensions(row, col)
1522
- store_row_col_max_min_values(row, col)
1799
+ check_dimensions(_row, _col)
1800
+ store_row_col_max_min_values(_row, _col)
1523
1801
 
1524
- date_time = convert_date_time(str)
1802
+ date_time = convert_date_time(_str)
1525
1803
 
1526
1804
  if date_time
1527
- store_data_to_table(NumberCellData.new(date_time, xf), row, col)
1805
+ store_data_to_table(DateTimeCellData.new(date_time, _format), _row, _col)
1528
1806
  else
1529
1807
  # If the date isn't valid then write it as a string.
1530
- write_string(*args)
1808
+ write_string(_row, _col, _str, _format)
1531
1809
  end
1532
1810
  end
1533
1811
 
@@ -1539,13 +1817,22 @@ module Writexlsx
1539
1817
  # The Chart must be created by the add_chart() Workbook method and
1540
1818
  # it must have the embedded option set.
1541
1819
  #
1542
- def insert_chart(*args)
1820
+ def insert_chart(row, col, chart = nil, *options)
1543
1821
  # Check for a cell reference in A1 notation and substitute row and column.
1544
- row, col, chart, *options = row_col_notation(args)
1545
- raise WriteXLSXInsufficientArgumentError if [row, col, chart].include?(nil)
1822
+ if (row_col_array = row_col_notation(row))
1823
+ _row, _col = row_col_array
1824
+ _chart = col
1825
+ _options = [chart] + options
1826
+ else
1827
+ _row = row
1828
+ _col = col
1829
+ _chart = chart
1830
+ _options = options
1831
+ end
1832
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _chart].include?(nil)
1546
1833
 
1547
- if options.first.instance_of?(Hash)
1548
- params = options.first
1834
+ if _options.first.instance_of?(Hash)
1835
+ params = _options.first
1549
1836
  x_offset = params[:x_offset]
1550
1837
  y_offset = params[:y_offset]
1551
1838
  x_scale = params[:x_scale]
@@ -1554,7 +1841,7 @@ module Writexlsx
1554
1841
  description = params[:description]
1555
1842
  decorative = params[:decorative]
1556
1843
  else
1557
- x_offset, y_offset, x_scale, y_scale, anchor = options
1844
+ x_offset, y_offset, x_scale, y_scale, anchor = _options
1558
1845
  end
1559
1846
  x_offset ||= 0
1560
1847
  y_offset ||= 0
@@ -1562,24 +1849,24 @@ module Writexlsx
1562
1849
  y_scale ||= 1
1563
1850
  anchor ||= 1
1564
1851
 
1565
- raise "Not a Chart object in insert_chart()" unless chart.is_a?(Chart) || chart.is_a?(Chartsheet)
1566
- raise "Not a embedded style Chart object in insert_chart()" if chart.respond_to?(:embedded) && chart.embedded == 0
1852
+ raise "Not a Chart object in insert_chart()" unless _chart.is_a?(Chart) || _chart.is_a?(Chartsheet)
1853
+ raise "Not a embedded style Chart object in insert_chart()" if _chart.respond_to?(:embedded) && _chart.embedded == 0
1567
1854
 
1568
- if chart.already_inserted? || (chart.combined && chart.combined.already_inserted?)
1855
+ if _chart.already_inserted? || (_chart.combined && _chart.combined.already_inserted?)
1569
1856
  raise "Chart cannot be inserted in a worksheet more than once"
1570
1857
  else
1571
- chart.already_inserted = true
1572
- chart.combined.already_inserted = true if chart.combined
1858
+ _chart.already_inserted = true
1859
+ _chart.combined.already_inserted = true if _chart.combined
1573
1860
  end
1574
1861
 
1575
1862
  # Use the values set with chart.set_size, if any.
1576
- x_scale = chart.x_scale if chart.x_scale != 1
1577
- y_scale = chart.y_scale if chart.y_scale != 1
1578
- x_offset = chart.x_offset if ptrue?(chart.x_offset)
1579
- y_offset = chart.y_offset if ptrue?(chart.y_offset)
1863
+ x_scale = _chart.x_scale if _chart.x_scale != 1
1864
+ y_scale = _chart.y_scale if _chart.y_scale != 1
1865
+ x_offset = _chart.x_offset if ptrue?(_chart.x_offset)
1866
+ y_offset = _chart.y_offset if ptrue?(_chart.y_offset)
1580
1867
 
1581
1868
  @charts << [
1582
- row, col, chart, x_offset, y_offset,
1869
+ _row, _col, _chart, x_offset, y_offset,
1583
1870
  x_scale, y_scale, anchor, description, decorative
1584
1871
  ]
1585
1872
  end
@@ -1588,14 +1875,23 @@ module Writexlsx
1588
1875
  # :call-seq:
1589
1876
  # insert_image(row, column, filename, options)
1590
1877
  #
1591
- def insert_image(*args)
1878
+ def insert_image(row, col, image = nil, *options)
1592
1879
  # Check for a cell reference in A1 notation and substitute row and column.
1593
- row, col, image, *options = row_col_notation(args)
1594
- raise WriteXLSXInsufficientArgumentError if [row, col, image].include?(nil)
1880
+ if (row_col_array = row_col_notation(row))
1881
+ _row, _col = row_col_array
1882
+ _image = col
1883
+ _options = [image] + options
1884
+ else
1885
+ _row = row
1886
+ _col = col
1887
+ _image = image
1888
+ _options = options
1889
+ end
1890
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _image].include?(nil)
1595
1891
 
1596
- if options.first.instance_of?(Hash)
1892
+ if _options.first.instance_of?(Hash)
1597
1893
  # Newer hash bashed options
1598
- params = options.first
1894
+ params = _options.first
1599
1895
  x_offset = params[:x_offset]
1600
1896
  y_offset = params[:y_offset]
1601
1897
  x_scale = params[:x_scale]
@@ -1606,7 +1902,7 @@ module Writexlsx
1606
1902
  description = params[:description]
1607
1903
  decorative = params[:decorative]
1608
1904
  else
1609
- x_offset, y_offset, x_scale, y_scale, anchor = options
1905
+ x_offset, y_offset, x_scale, y_scale, anchor = _options
1610
1906
  end
1611
1907
  x_offset ||= 0
1612
1908
  y_offset ||= 0
@@ -1615,7 +1911,7 @@ module Writexlsx
1615
1911
  anchor ||= 2
1616
1912
 
1617
1913
  @images << [
1618
- row, col, image, x_offset, y_offset,
1914
+ _row, _col, _image, x_offset, y_offset,
1619
1915
  x_scale, y_scale, url, tip, anchor, description, decorative
1620
1916
  ]
1621
1917
  end
@@ -1627,32 +1923,43 @@ module Writexlsx
1627
1923
  # Deprecated. This is a writeexcel gem's method that is no longer
1628
1924
  # required by WriteXLSX.
1629
1925
  #
1630
- def repeat_formula(*args)
1926
+ def repeat_formula(row, col, formula, format, *pairs)
1631
1927
  # Check for a cell reference in A1 notation and substitute row and column.
1632
- row, col, formula, format, *pairs = row_col_notation(args)
1633
- raise WriteXLSXInsufficientArgumentError if [row, col].include?(nil)
1928
+ if (row_col_array = row_col_notation(row))
1929
+ _row, _col = row_col_array
1930
+ _formula = col
1931
+ _format = formula
1932
+ _pairs = [format] + pairs
1933
+ else
1934
+ _row = row
1935
+ _col = col
1936
+ _formula = formula
1937
+ _format = format
1938
+ _pairs = pairs
1939
+ end
1940
+ raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)
1634
1941
 
1635
- raise "Odd number of elements in pattern/replacement list" unless pairs.size.even?
1636
- raise "Not a valid formula" unless formula.respond_to?(:to_ary)
1942
+ raise "Odd number of elements in pattern/replacement list" unless _pairs.size.even?
1943
+ raise "Not a valid formula" unless _formula.respond_to?(:to_ary)
1637
1944
 
1638
- tokens = formula.join("\t").split("\t")
1945
+ tokens = _formula.join("\t").split("\t")
1639
1946
  raise "No tokens in formula" if tokens.empty?
1640
1947
 
1641
- value = nil
1642
- if pairs[-2] == 'result'
1643
- value = pairs.pop
1644
- pairs.pop
1948
+ _value = nil
1949
+ if _pairs[-2] == 'result'
1950
+ _value = _pairs.pop
1951
+ _pairs.pop
1645
1952
  end
1646
- until pairs.empty?
1647
- pattern = pairs.shift
1648
- replace = pairs.shift
1953
+ until _pairs.empty?
1954
+ pattern = _pairs.shift
1955
+ replace = _pairs.shift
1649
1956
 
1650
1957
  tokens.each do |token|
1651
1958
  break if token.sub!(pattern, replace)
1652
1959
  end
1653
1960
  end
1654
- formula = tokens.join('')
1655
- write_formula(row, col, formula, format, value)
1961
+ _formula = tokens.join('')
1962
+ write_formula(_row, _col, _formula, _format, _value)
1656
1963
  end
1657
1964
 
1658
1965
  #
@@ -1741,7 +2048,13 @@ module Writexlsx
1741
2048
  # others should be blank. All cells should contain the same format.
1742
2049
  #
1743
2050
  def merge_range(*args)
1744
- row_first, col_first, row_last, col_last, string, format, *extra_args = row_col_notation(args)
2051
+ if (row_col_array = row_col_notation(args.first))
2052
+ row_first, col_first, row_last, col_last = row_col_array
2053
+ string, format, *extra_args = args[1..-1]
2054
+ else
2055
+ row_first, col_first, row_last, col_last,
2056
+ string, format, *extra_args = args
2057
+ end
1745
2058
 
1746
2059
  raise "Incorrect number of arguments" if [row_first, col_first, row_last, col_last, format].include?(nil)
1747
2060
  raise "Fifth parameter must be a format object" unless format.respond_to?(:xf_index)
@@ -1774,10 +2087,21 @@ module Writexlsx
1774
2087
  def merge_range_type(type, *args)
1775
2088
  case type
1776
2089
  when 'array_formula', 'blank', 'rich_string'
1777
- row_first, col_first, row_last, col_last, *others = row_col_notation(args)
2090
+ if (row_col_array = row_col_notation(args.first))
2091
+ row_first, col_first, row_last, col_last = row_col_array
2092
+ *others = args[1..-1]
2093
+ else
2094
+ row_first, col_first, row_last, col_last, *others = args
2095
+ end
1778
2096
  format = others.pop
1779
2097
  else
1780
- row_first, col_first, row_last, col_last, token, format, *others = row_col_notation(args)
2098
+ if (row_col_array = row_col_notation(args.first))
2099
+ row_first, col_first, row_last, col_last = row_col_array
2100
+ token, format, *others = args[1..-1]
2101
+ else
2102
+ row_first, col_first, row_last, col_last,
2103
+ token, format, *others = args
2104
+ end
1781
2105
  end
1782
2106
 
1783
2107
  raise "Format object missing or in an incorrect position" unless format.respond_to?(:xf_index)
@@ -1870,8 +2194,16 @@ module Writexlsx
1870
2194
  # The insert_button() method can be used to insert an Excel form button
1871
2195
  # into a worksheet.
1872
2196
  #
1873
- def insert_button(*args)
1874
- @buttons_array << button_params(*row_col_notation(args))
2197
+ def insert_button(row, col, properties = nil)
2198
+ if (row_col_array = row_col_notation(row))
2199
+ _row, _col = row_col_array
2200
+ _properties = col
2201
+ else
2202
+ _row = row
2203
+ _col = col
2204
+ _properties = properties
2205
+ end
2206
+ @buttons_array << button_params(_row, _col, _properties)
1875
2207
  @has_vml = 1
1876
2208
  end
1877
2209
 
@@ -1937,17 +2269,29 @@ module Writexlsx
1937
2269
  #
1938
2270
  # Set the autofilter area in the worksheet.
1939
2271
  #
1940
- def autofilter(*args)
1941
- row1, col1, row2, col2 = row_col_notation(args)
1942
- return if [row1, col1, row2, col2].include?(nil)
2272
+ def autofilter(row1, col1 = nil, row2 = nil, col2 = nil)
2273
+ if (row_col_array = row_col_notation(row1))
2274
+ _row1, _col1, _row2, _col2 = row_col_array
2275
+ else
2276
+ _row1 = row1
2277
+ _col1 = col1
2278
+ _row2 = row2
2279
+ _col2 = col2
2280
+ end
2281
+ return if [_row1, _col1, _row2, _col2].include?(nil)
1943
2282
 
1944
2283
  # Reverse max and min values if necessary.
1945
- row1, row2 = row2, row1 if row2 < row1
1946
- col1, col2 = col2, col1 if col2 < col1
2284
+ _row1, _row2 = _row2, _row1 if _row2 < _row1
2285
+ _col1, _col2 = _col2, _col1 if _col2 < _col1
1947
2286
 
1948
- @autofilter_area = convert_name_area(row1, col1, row2, col2)
1949
- @autofilter_ref = xl_range(row1, row2, col1, col2)
1950
- @filter_range = [col1, col2]
2287
+ @autofilter_area = convert_name_area(_row1, _col1, _row2, _col2)
2288
+ @autofilter_ref = xl_range(_row1, _row2, _col1, _col2)
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
1951
2295
  end
1952
2296
 
1953
2297
  #
@@ -2228,10 +2572,12 @@ module Writexlsx
2228
2572
  # Write the cell value <v> element.
2229
2573
  #
2230
2574
  def write_cell_value(value = '') # :nodoc:
2231
- return write_cell_formula('=NA()') if !value.nil? && value.is_a?(Float) && value.nan?
2575
+ return write_cell_formula('=NA()') if value.is_a?(Float) && value.nan?
2232
2576
 
2233
2577
  value ||= ''
2234
- value = value.to_i if value == value.to_i
2578
+
2579
+ int_value = value.to_i
2580
+ value = int_value if value == int_value
2235
2581
  @writer.data_element('v', value)
2236
2582
  end
2237
2583
 
@@ -2288,8 +2634,8 @@ module Writexlsx
2288
2634
  @external_hyper_links,
2289
2635
  @external_drawing_links,
2290
2636
  @external_vml_links,
2291
- @external_table_links,
2292
2637
  @external_background_links,
2638
+ @external_table_links,
2293
2639
  @external_comment_links
2294
2640
  ].reject { |a| a.empty? }
2295
2641
  end
@@ -2416,6 +2762,33 @@ module Writexlsx
2416
2762
 
2417
2763
  private
2418
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
+
2419
2792
  #
2420
2793
  # Get the index used to address a drawing rel link.
2421
2794
  #
@@ -2467,9 +2840,9 @@ module Writexlsx
2467
2840
  # Create a temp format with the default font for unformatted fragments.
2468
2841
  default = Format.new(0)
2469
2842
 
2470
- length = 0 # String length.
2471
2843
  last = 'format'
2472
2844
  pos = 0
2845
+ raw_string = ''
2473
2846
 
2474
2847
  fragments = []
2475
2848
  rich_strings.each do |token|
@@ -2490,12 +2863,12 @@ module Writexlsx
2490
2863
  fragments << default << token
2491
2864
  end
2492
2865
 
2493
- length += token.size # Keep track of actual string length.
2866
+ raw_string += token # Keep track of actual string length.
2494
2867
  last = 'string'
2495
2868
  end
2496
2869
  pos += 1
2497
2870
  end
2498
- [fragments, length]
2871
+ [fragments, raw_string]
2499
2872
  end
2500
2873
 
2501
2874
  def xml_str_of_rich_string(fragments)
@@ -2726,8 +3099,9 @@ module Writexlsx
2726
3099
  #
2727
3100
  def size_col(col, anchor = 0) # :nodoc:
2728
3101
  # Look up the cell value to see if it has been changed.
2729
- if @col_sizes[col]
2730
- 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
2731
3105
 
2732
3106
  # Convert to pixels.
2733
3107
  pixels = if hidden == 1 && anchor != 4
@@ -2902,19 +3276,38 @@ EOS
2902
3276
  #
2903
3277
  # Insert a shape into the worksheet.
2904
3278
  #
2905
- def insert_shape(*args)
3279
+ def insert_shape(
3280
+ row_start, column_start, shape = nil, x_offset = nil, y_offset = nil,
3281
+ x_scale = nil, y_scale = nil, anchor = nil
3282
+ )
2906
3283
  # Check for a cell reference in A1 notation and substitute row and column.
2907
- row_start, column_start, shape, x_offset, y_offset, x_scale, y_scale, anchor =
2908
- row_col_notation(args)
2909
- raise "Insufficient arguments in insert_shape()" if [row_start, column_start, shape].include?(nil)
3284
+ if (row_col_array = row_col_notation(row_start))
3285
+ _row_start, _column_start = row_col_array
3286
+ _shape = column_start
3287
+ _x_offset = shape
3288
+ _y_offset = x_offset
3289
+ _x_scale = y_offset
3290
+ _y_scale = x_scale
3291
+ _anchor = y_scale
3292
+ else
3293
+ _row_start = row_start
3294
+ _column_start = column_start
3295
+ _shape = shape
3296
+ _x_offset = x_offset
3297
+ _y_offset = y_offset
3298
+ _x_scale = x_scale
3299
+ _y_scale = y_scale
3300
+ _anchor = anchor
3301
+ end
3302
+ raise "Insufficient arguments in insert_shape()" if [_row_start, _column_start, _shape].include?(nil)
2910
3303
 
2911
- shape.set_position(
2912
- row_start, column_start, x_offset, y_offset,
2913
- x_scale, y_scale, anchor
3304
+ _shape.set_position(
3305
+ _row_start, _column_start, _x_offset, _y_offset,
3306
+ _x_scale, _y_scale, _anchor
2914
3307
  )
2915
3308
  # Assign a shape ID.
2916
3309
  while true
2917
- id = shape.id || 0
3310
+ id = _shape.id || 0
2918
3311
  used = @shape_hash[id]
2919
3312
 
2920
3313
  # Test if shape ID is already used. Otherwise assign a new one.
@@ -2922,20 +3315,20 @@ EOS
2922
3315
  break
2923
3316
  else
2924
3317
  @last_shape_id += 1
2925
- shape.id = @last_shape_id
3318
+ _shape.id = @last_shape_id
2926
3319
  end
2927
3320
  end
2928
3321
 
2929
3322
  # Allow lookup of entry into shape array by shape ID.
2930
- @shape_hash[shape.id] = shape.element = @shapes.size
3323
+ @shape_hash[_shape.id] = _shape.element = @shapes.size
2931
3324
 
2932
- insert = if ptrue?(shape.stencil)
3325
+ insert = if ptrue?(_shape.stencil)
2933
3326
  # Insert a copy of the shape, not a reference so that the shape is
2934
3327
  # used as a stencil. Previously stamped copies don't get modified
2935
3328
  # if the stencil is modified.
2936
- shape.dup
3329
+ _shape.dup
2937
3330
  else
2938
- shape
3331
+ _shape
2939
3332
  end
2940
3333
 
2941
3334
  # For connectors change x/y coords based on location of connected shapes.
@@ -3039,28 +3432,22 @@ EOS
3039
3432
  end
3040
3433
 
3041
3434
  #
3042
- # Based on the algorithm provided by Daniel Rentz of OpenOffice.
3043
- #
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). #
3044
3438
  def encode_password(password) # :nodoc:
3045
- i = 0
3046
- chars = password.split(//)
3047
- count = chars.size
3439
+ hash = 0
3048
3440
 
3049
- chars.collect! do |char|
3050
- i += 1
3051
- char = char.ord << i
3052
- low_15 = char & 0x7fff
3053
- high_15 = char & (0x7fff << 15)
3054
- high_15 = high_15 >> 15
3055
- char = low_15 | high_15
3441
+ password.reverse.split(//).each do |char|
3442
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3443
+ hash ^= char.ord
3056
3444
  end
3057
3445
 
3058
- encoded_password = 0x0000
3059
- chars.each { |c| encoded_password ^= c }
3060
- encoded_password ^= count
3061
- encoded_password ^= 0xCE4B
3446
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3447
+ hash ^= password.length
3448
+ hash ^= 0xCE4B
3062
3449
 
3063
- sprintf("%X", encoded_password)
3450
+ sprintf("%X", hash)
3064
3451
  end
3065
3452
 
3066
3453
  #
@@ -3238,10 +3625,42 @@ EOS
3238
3625
  #
3239
3626
  def write_cols # :nodoc:
3240
3627
  # Exit unless some column have been formatted.
3241
- return if @colinfo.empty?
3628
+ return if @col_info.empty?
3242
3629
 
3243
3630
  @writer.tag_elements('cols') do
3244
- @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
3245
3664
  end
3246
3665
  end
3247
3666
 
@@ -3253,13 +3672,14 @@ EOS
3253
3672
  end
3254
3673
 
3255
3674
  def col_info_attributes(args)
3256
- min = args[0] || 0 # First formatted column.
3257
- max = args[1] || 0 # Last formatted column.
3258
- width = args[2] # Col width in user units.
3259
- format = args[3] # Format index.
3260
- hidden = args[4] || 0 # Hidden flag.
3261
- level = args[5] || 0 # Outline level.
3262
- 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.
3263
3683
  xf_index = format ? format.get_xf_index : 0
3264
3684
 
3265
3685
  custom_width = true
@@ -3282,10 +3702,11 @@ EOS
3282
3702
  ['width', width]
3283
3703
  ]
3284
3704
 
3285
- attributes << ['style', xf_index] if xf_index != 0
3286
- 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
3287
3708
  attributes << ['customWidth', 1] if custom_width
3288
- attributes << ['outlineLevel', level] if level != 0
3709
+ attributes << ['outlineLevel', level] if level != 0
3289
3710
  attributes << ['collapsed', 1] if collapsed != 0
3290
3711
  attributes
3291
3712
  end
@@ -3333,8 +3754,11 @@ EOS
3333
3754
  end
3334
3755
 
3335
3756
  def write_cell_column_dimension(row_num) # :nodoc:
3757
+ row = @cell_data_table[row_num]
3336
3758
  (@dim_colmin..@dim_colmax).each do |col_num|
3337
- @cell_data_table[row_num][col_num].write_cell(self, row_num, col_num) if @cell_data_table[row_num][col_num]
3759
+ if (cell = row[col_num])
3760
+ cell.write_cell(self, row_num, col_num)
3761
+ end
3338
3762
  end
3339
3763
  end
3340
3764