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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -0
- data/Changes +21 -0
- data/README.md +1 -1
- data/examples/autofit.rb +37 -0
- data/examples/chart_radar.rb +3 -9
- data/lib/write_xlsx/chart.rb +1 -1
- data/lib/write_xlsx/format.rb +4 -2
- data/lib/write_xlsx/package/conditional_format.rb +15 -4
- data/lib/write_xlsx/package/shared_strings.rb +5 -3
- data/lib/write_xlsx/package/styles.rb +16 -17
- data/lib/write_xlsx/package/table.rb +34 -16
- data/lib/write_xlsx/utility.rb +45 -11
- data/lib/write_xlsx/version.rb +1 -1
- data/lib/write_xlsx/workbook.rb +21 -11
- data/lib/write_xlsx/worksheet/cell_data.rb +13 -5
- data/lib/write_xlsx/worksheet/data_validation.rb +13 -1
- data/lib/write_xlsx/worksheet.rb +702 -278
- metadata +4 -3
data/lib/write_xlsx/worksheet.rb
CHANGED
@@ -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, :
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
424
|
-
|
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(
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
895
|
-
|
896
|
-
|
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
|
-
|
899
|
-
|
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
|
903
|
-
write_row(
|
904
|
-
elsif
|
905
|
-
write_number(
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
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(
|
1085
|
+
write_string(_row, _col, _token, _format)
|
933
1086
|
end
|
934
1087
|
else
|
935
|
-
write_string(
|
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(*
|
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
|
-
|
950
|
-
|
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
|
-
|
1114
|
+
_tokens.each do |_token|
|
953
1115
|
# Check for nested arrays
|
954
|
-
if
|
955
|
-
write_col(
|
1116
|
+
if _token.respond_to?(:to_ary)
|
1117
|
+
write_col(_row, _col, _token, *_options)
|
956
1118
|
else
|
957
|
-
write(
|
1119
|
+
write(_row, _col, _token, *_options)
|
958
1120
|
end
|
959
|
-
|
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(*
|
972
|
-
|
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
|
-
|
1145
|
+
_tokens.each do |_token|
|
975
1146
|
# write() will deal with any nested arrays
|
976
|
-
write(
|
977
|
-
|
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(
|
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
|
-
|
990
|
-
|
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(
|
994
|
-
store_row_col_max_min_values(
|
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,
|
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(
|
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
|
-
|
1011
|
-
|
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(
|
1015
|
-
store_row_col_max_min_values(
|
1203
|
+
check_dimensions(_row, _col)
|
1204
|
+
store_row_col_max_min_values(_row, _col)
|
1016
1205
|
|
1017
|
-
store_data_to_table(NumberCellData.new(
|
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(
|
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
|
-
|
1030
|
-
|
1031
|
-
|
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(
|
1035
|
-
store_row_col_max_min_values(
|
1232
|
+
check_dimensions(_row, _col)
|
1233
|
+
store_row_col_max_min_values(_row, _col)
|
1036
1234
|
|
1037
|
-
index = shared_string_index(
|
1235
|
+
index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)
|
1038
1236
|
|
1039
|
-
store_data_to_table(StringCellData.new(index,
|
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(*
|
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
|
-
|
1053
|
-
|
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
|
-
|
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(
|
1059
|
-
store_row_col_max_min_values(
|
1263
|
+
check_dimensions(_row, _col)
|
1264
|
+
store_row_col_max_min_values(_row, _col)
|
1060
1265
|
|
1061
|
-
|
1266
|
+
_fragments, _raw_string = rich_strings_fragments(_rich_strings)
|
1062
1267
|
# can't allow 2 formats in a row
|
1063
|
-
return -4 unless
|
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(
|
1273
|
+
index = shared_string_index(xml_str_of_rich_string(_fragments))
|
1066
1274
|
|
1067
|
-
store_data_to_table(
|
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(
|
1286
|
+
def write_blank(row, col, format = nil)
|
1079
1287
|
# Check for a cell reference in A1 notation and substitute row and column
|
1080
|
-
|
1081
|
-
|
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
|
1299
|
+
return unless _format
|
1085
1300
|
|
1086
1301
|
# Check that row and col are valid and store max and min values
|
1087
|
-
check_dimensions(
|
1088
|
-
store_row_col_max_min_values(
|
1302
|
+
check_dimensions(_row, _col)
|
1303
|
+
store_row_col_max_min_values(_row, _col)
|
1089
1304
|
|
1090
|
-
store_data_to_table(BlankCellData.new(
|
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(
|
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
|
-
|
1267
|
-
|
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
|
1497
|
+
if _formula =~ regex
|
1272
1498
|
return write_dynamic_array_formula(
|
1273
|
-
|
1499
|
+
_row, _col, _row, _col, _formula, _format, _value
|
1274
1500
|
)
|
1275
1501
|
end
|
1276
1502
|
|
1277
1503
|
# Hand off array formulas.
|
1278
|
-
if
|
1279
|
-
write_array_formula(
|
1504
|
+
if _formula =~ /^\{=.*\}$/
|
1505
|
+
write_array_formula(_row, _col, _row, _col, _formula, _format, _value)
|
1280
1506
|
else
|
1281
|
-
check_dimensions(
|
1282
|
-
store_row_col_max_min_values(
|
1283
|
-
|
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(
|
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
|
-
|
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(
|
1353
|
-
write_array_formula_base('a',
|
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(
|
1362
|
-
write_array_formula_base('d',
|
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(
|
1372
|
-
|
1373
|
-
|
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
|
-
|
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(
|
1380
|
-
store_row_col_max_min_values(
|
1619
|
+
check_dimensions(_row, _col)
|
1620
|
+
store_row_col_max_min_values(_row, _col)
|
1381
1621
|
|
1382
|
-
store_data_to_table(BooleanCellData.new(
|
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(
|
1392
|
-
|
1393
|
-
|
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(
|
1397
|
-
store_row_col_max_min_values(
|
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[
|
1402
|
-
format = @workbook.add_format(
|
1403
|
-
write_blank(
|
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[
|
1406
|
-
format = @workbook.add_format(
|
1407
|
-
cell_data = @cell_data_table[
|
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[
|
1657
|
+
cell_data = @cell_data_table[_row][_col]
|
1411
1658
|
format.copy(cell_data.xf)
|
1412
|
-
format.set_format_properties(
|
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(
|
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(
|
1435
|
-
|
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 [
|
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
|
-
|
1441
|
-
|
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(
|
1445
|
-
store_row_col_max_min_values(
|
1700
|
+
check_dimensions(_row_last, _col_last)
|
1701
|
+
store_row_col_max_min_values(_row_last, _col_last)
|
1446
1702
|
|
1447
|
-
(
|
1448
|
-
(
|
1449
|
-
update_format_with_params(row, col,
|
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(
|
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
|
-
|
1489
|
-
|
1490
|
-
|
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(
|
1494
|
-
store_row_col_max_min_values(
|
1762
|
+
check_dimensions(_row, _col)
|
1763
|
+
store_row_col_max_min_values(_row, _col)
|
1495
1764
|
|
1496
|
-
hyperlink = Hyperlink.factory(
|
1497
|
-
store_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
|
-
|
1771
|
+
_format ||= @default_url_format
|
1503
1772
|
|
1504
1773
|
# Write the hyperlink string.
|
1505
|
-
write_string(
|
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(
|
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
|
-
|
1518
|
-
|
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(
|
1522
|
-
store_row_col_max_min_values(
|
1799
|
+
check_dimensions(_row, _col)
|
1800
|
+
store_row_col_max_min_values(_row, _col)
|
1523
1801
|
|
1524
|
-
date_time = convert_date_time(
|
1802
|
+
date_time = convert_date_time(_str)
|
1525
1803
|
|
1526
1804
|
if date_time
|
1527
|
-
store_data_to_table(
|
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(
|
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(*
|
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
|
-
|
1545
|
-
|
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
|
1548
|
-
params =
|
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 =
|
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
|
1566
|
-
raise "Not a embedded style Chart object in insert_chart()" if
|
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
|
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
|
-
|
1572
|
-
|
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 =
|
1577
|
-
y_scale =
|
1578
|
-
x_offset =
|
1579
|
-
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
|
-
|
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(*
|
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
|
-
|
1594
|
-
|
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
|
1892
|
+
if _options.first.instance_of?(Hash)
|
1597
1893
|
# Newer hash bashed options
|
1598
|
-
params =
|
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 =
|
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
|
-
|
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(*
|
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
|
-
|
1633
|
-
|
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
|
1636
|
-
raise "Not a valid formula" unless
|
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 =
|
1945
|
+
tokens = _formula.join("\t").split("\t")
|
1639
1946
|
raise "No tokens in formula" if tokens.empty?
|
1640
1947
|
|
1641
|
-
|
1642
|
-
if
|
1643
|
-
|
1644
|
-
|
1948
|
+
_value = nil
|
1949
|
+
if _pairs[-2] == 'result'
|
1950
|
+
_value = _pairs.pop
|
1951
|
+
_pairs.pop
|
1645
1952
|
end
|
1646
|
-
until
|
1647
|
-
pattern =
|
1648
|
-
replace =
|
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
|
-
|
1655
|
-
write_formula(
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
1874
|
-
|
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(
|
1941
|
-
|
1942
|
-
|
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
|
-
|
1946
|
-
|
2284
|
+
_row1, _row2 = _row2, _row1 if _row2 < _row1
|
2285
|
+
_col1, _col2 = _col2, _col1 if _col2 < _col1
|
1947
2286
|
|
1948
|
-
@autofilter_area = convert_name_area(
|
1949
|
-
@autofilter_ref = xl_range(
|
1950
|
-
@filter_range = [
|
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
|
2575
|
+
return write_cell_formula('=NA()') if value.is_a?(Float) && value.nan?
|
2232
2576
|
|
2233
2577
|
value ||= ''
|
2234
|
-
|
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
|
-
|
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,
|
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 @
|
2730
|
-
width
|
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(
|
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
|
-
|
2908
|
-
|
2909
|
-
|
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
|
-
|
2912
|
-
|
2913
|
-
|
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 =
|
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
|
-
|
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[
|
3323
|
+
@shape_hash[_shape.id] = _shape.element = @shapes.size
|
2931
3324
|
|
2932
|
-
insert = if ptrue?(
|
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
|
-
|
3329
|
+
_shape.dup
|
2937
3330
|
else
|
2938
|
-
|
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
|
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
|
-
|
3046
|
-
chars = password.split(//)
|
3047
|
-
count = chars.size
|
3439
|
+
hash = 0
|
3048
3440
|
|
3049
|
-
|
3050
|
-
|
3051
|
-
|
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
|
-
|
3059
|
-
|
3060
|
-
|
3061
|
-
encoded_password ^= 0xCE4B
|
3446
|
+
hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
|
3447
|
+
hash ^= password.length
|
3448
|
+
hash ^= 0xCE4B
|
3062
3449
|
|
3063
|
-
sprintf("%X",
|
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 @
|
3628
|
+
return if @col_info.empty?
|
3242
3629
|
|
3243
3630
|
@writer.tag_elements('cols') do
|
3244
|
-
|
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
|
3257
|
-
max
|
3258
|
-
width
|
3259
|
-
format
|
3260
|
-
hidden
|
3261
|
-
level
|
3262
|
-
collapsed = args[
|
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
|
3286
|
-
attributes << ['hidden', 1] if hidden
|
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
|
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
|
-
|
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
|
|