writeexcel 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. data/README.rdoc +2 -0
  2. data/VERSION +1 -1
  3. data/lib/writeexcel/biffwriter.rb +29 -43
  4. data/lib/writeexcel/cell_range.rb +332 -0
  5. data/lib/writeexcel/chart.rb +50 -51
  6. data/lib/writeexcel/col_info.rb +87 -0
  7. data/lib/writeexcel/comments.rb +456 -0
  8. data/lib/writeexcel/convert_date_time.rb +117 -0
  9. data/lib/writeexcel/data_validations.rb +370 -0
  10. data/lib/writeexcel/debug_info.rb +5 -1
  11. data/lib/writeexcel/embedded_chart.rb +35 -0
  12. data/lib/writeexcel/format.rb +1 -1
  13. data/lib/writeexcel/formula.rb +3 -3
  14. data/lib/writeexcel/helper.rb +3 -0
  15. data/lib/writeexcel/image.rb +61 -1
  16. data/lib/writeexcel/olewriter.rb +2 -8
  17. data/lib/writeexcel/outline.rb +24 -0
  18. data/lib/writeexcel/shared_string_table.rb +153 -0
  19. data/lib/writeexcel/workbook.rb +86 -444
  20. data/lib/writeexcel/worksheet.rb +693 -2029
  21. data/lib/writeexcel/worksheets.rb +25 -0
  22. data/lib/writeexcel/write_file.rb +34 -15
  23. data/test/test_02_merge_formats.rb +0 -4
  24. data/test/test_04_dimensions.rb +0 -4
  25. data/test/test_05_rows.rb +0 -4
  26. data/test/test_06_extsst.rb +3 -6
  27. data/test/test_11_date_time.rb +0 -4
  28. data/test/test_12_date_only.rb +262 -231
  29. data/test/test_13_date_seconds.rb +0 -4
  30. data/test/test_21_escher.rb +71 -84
  31. data/test/test_22_mso_drawing_group.rb +0 -4
  32. data/test/test_23_note.rb +5 -21
  33. data/test/test_24_txo.rb +6 -7
  34. data/test/test_25_position_object.rb +0 -4
  35. data/test/test_26_autofilter.rb +0 -5
  36. data/test/test_27_autofilter.rb +0 -5
  37. data/test/test_28_autofilter.rb +0 -5
  38. data/test/test_29_process_jpg.rb +1 -1
  39. data/test/test_30_validation_dval.rb +4 -7
  40. data/test/test_31_validation_dv_strings.rb +9 -12
  41. data/test/test_32_validation_dv_formula.rb +11 -14
  42. data/test/test_42_set_properties.rb +0 -3
  43. data/test/test_50_name_stored.rb +0 -4
  44. data/test/test_51_name_print_area.rb +0 -4
  45. data/test/test_52_name_print_titles.rb +0 -4
  46. data/test/test_53_autofilter.rb +0 -4
  47. data/test/test_60_chart_generic.rb +42 -46
  48. data/test/test_61_chart_subclasses.rb +7 -11
  49. data/test/test_62_chart_formats.rb +12 -16
  50. data/test/test_63_chart_area_formats.rb +3 -7
  51. data/test/test_biff.rb +0 -4
  52. data/test/test_big_workbook.rb +17 -0
  53. data/test/test_format.rb +0 -3
  54. data/test/test_ole.rb +0 -4
  55. data/test/test_storage_lite.rb +0 -9
  56. data/test/test_workbook.rb +0 -4
  57. data/test/test_worksheet.rb +3 -7
  58. data/writeexcel.gemspec +12 -2
  59. metadata +12 -2
@@ -0,0 +1,117 @@
1
+ module ConvertDateTime
2
+ #
3
+ # The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format
4
+ # and converts it to a decimal number representing a valid Excel date.
5
+ #
6
+ # Dates and times in Excel are represented by real numbers. The integer part of
7
+ # the number stores the number of days since the epoch and the fractional part
8
+ # stores the percentage of the day in seconds. The epoch can be either 1900 or
9
+ # 1904.
10
+ #
11
+ # Parameter: Date and time string in one of the following formats:
12
+ # yyyy-mm-ddThh:mm:ss.ss # Standard
13
+ # yyyy-mm-ddT # Date only
14
+ # Thh:mm:ss.ss # Time only
15
+ #
16
+ # Returns:
17
+ # A decimal number representing a valid Excel date, or
18
+ # undef if the date is invalid.
19
+ #
20
+ def convert_date_time(date_time_string, date_1904 = false) #:nodoc:
21
+ date_time = date_time_string
22
+
23
+ days = 0 # Number of days since epoch
24
+ seconds = 0 # Time expressed as fraction of 24h hours in seconds
25
+
26
+ # Strip leading and trailing whitespace.
27
+ date_time.sub!(/^\s+/, '')
28
+ date_time.sub!(/\s+$/, '')
29
+
30
+ # Check for invalid date char.
31
+ return nil if date_time =~ /[^0-9T:\-\.Z]/
32
+
33
+ # Check for "T" after date or before time.
34
+ return nil unless date_time =~ /\dT|T\d/
35
+
36
+ # Strip trailing Z in ISO8601 date.
37
+ date_time.sub!(/Z$/, '')
38
+
39
+ # Split into date and time.
40
+ date, time = date_time.split(/T/)
41
+
42
+ # We allow the time portion of the input DateTime to be optional.
43
+ if time
44
+ # Match hh:mm:ss.sss+ where the seconds are optional
45
+ if time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/
46
+ hour = $1.to_i
47
+ min = $2.to_i
48
+ sec = $4.to_f || 0
49
+ else
50
+ return nil # Not a valid time format.
51
+ end
52
+
53
+ # Some boundary checks
54
+ return nil if hour >= 24
55
+ return nil if min >= 60
56
+ return nil if sec >= 60
57
+
58
+ # Excel expresses seconds as a fraction of the number in 24 hours.
59
+ seconds = (hour * 60* 60 + min * 60 + sec) / (24.0 * 60 * 60)
60
+ end
61
+
62
+ # We allow the date portion of the input DateTime to be optional.
63
+ return seconds if date == ''
64
+
65
+ # Match date as yyyy-mm-dd.
66
+ if date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/
67
+ year = $1.to_i
68
+ month = $2.to_i
69
+ day = $3.to_i
70
+ else
71
+ return nil # Not a valid date format.
72
+ end
73
+
74
+ # Set the epoch as 1900 or 1904. Defaults to 1900.
75
+ # Special cases for Excel.
76
+ unless date_1904
77
+ return seconds if date == '1899-12-31' # Excel 1900 epoch
78
+ return seconds if date == '1900-01-00' # Excel 1900 epoch
79
+ return 60 + seconds if date == '1900-02-29' # Excel false leapday
80
+ end
81
+
82
+
83
+ # We calculate the date by calculating the number of days since the epoch
84
+ # and adjust for the number of leap days. We calculate the number of leap
85
+ # days by normalising the year in relation to the epoch. Thus the year 2000
86
+ # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
87
+ #
88
+ epoch = date_1904 ? 1904 : 1900
89
+ offset = date_1904 ? 4 : 0
90
+ norm = 300
91
+ range = year -epoch
92
+
93
+ # Set month days and check for leap year.
94
+ mdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
95
+ leap = 0
96
+ leap = 1 if year % 4 == 0 && year % 100 != 0 || year % 400 == 0
97
+ mdays[1] = 29 if leap != 0
98
+
99
+ # Some boundary checks
100
+ return nil if year < epoch or year > 9999
101
+ return nil if month < 1 or month > 12
102
+ return nil if day < 1 or day > mdays[month -1]
103
+
104
+ # Accumulate the number of days since the epoch.
105
+ days = mdays[0, month - 1].inject(day) {|result, mday| result + mday} # days from 1, Jan
106
+ days += range *365 # Add days for past years
107
+ days += ((range) / 4) # Add leapdays
108
+ days -= ((range + offset) /100) # Subtract 100 year leapdays
109
+ days += ((range + offset + norm)/400) # Add 400 year leapdays
110
+ days -= leap # Already counted above
111
+
112
+ # Adjust for Excel erroneously treating 1900 as a leap year.
113
+ days += 1 if !date_1904 and days > 59
114
+
115
+ days + seconds
116
+ end
117
+ end
@@ -0,0 +1,370 @@
1
+ module Writeexcel
2
+
3
+ class Worksheet < BIFFWriter
4
+ require 'writeexcel/helper'
5
+
6
+ class DataValidations < Array
7
+ #
8
+ # the count of the DV records to follow.
9
+ #
10
+ # Note, this could be wrapped into store_dv() but we may require separate
11
+ # handling of the object id at a later stage.
12
+ #
13
+ def count_dv_record #:nodoc:
14
+ return if empty?
15
+
16
+ dval_record(-1, size) # obj_id = -1
17
+ end
18
+
19
+ private
20
+
21
+ #
22
+ # Store the DV record which contains the number of and information common to
23
+ # all DV structures.
24
+ # obj_id # Object ID number.
25
+ # dv_count # Count of DV structs to follow.
26
+ #
27
+ def dval_record(obj_id, dv_count) #:nodoc:
28
+ record = 0x01B2 # Record identifier
29
+ length = 0x0012 # Bytes to follow
30
+
31
+ flags = 0x0004 # Option flags.
32
+ x_coord = 0x00000000 # X coord of input box.
33
+ y_coord = 0x00000000 # Y coord of input box.
34
+
35
+ # Pack the record.
36
+ header = [record, length].pack('vv')
37
+ data = [flags, x_coord, y_coord, obj_id, dv_count].pack('vVVVV')
38
+
39
+ header + data
40
+ end
41
+ end
42
+
43
+ require 'writeexcel/convert_date_time'
44
+
45
+ class DataValidation
46
+ include ConvertDateTime
47
+
48
+ def initialize(parser = nil, param = {})
49
+ @parser = parser
50
+ @cells = param[:cells]
51
+ @validate = param[:validate]
52
+ @criteria = param[:criteria]
53
+ @value = param[:value]
54
+ @maximum = param[:maximum]
55
+ @input_title = param[:input_title]
56
+ @input_message = param[:input_message]
57
+ @error_title = param[:error_title]
58
+ @error_message = param[:error_message]
59
+ @error_type = param[:error_type]
60
+ @ignore_blank = param[:ignore_blank]
61
+ @dropdown = param[:dropdown]
62
+ @show_input = param[:show_input]
63
+ @show_error = param[:show_error]
64
+ end
65
+
66
+ #
67
+ # Calclate the DV record that specifies the data validation criteria and options
68
+ # for a range of cells..
69
+ # cells # Aref of cells to which DV applies.
70
+ # validate # Type of data validation.
71
+ # criteria # Validation criteria.
72
+ # value # Value/Source/Minimum formula.
73
+ # maximum # Maximum formula.
74
+ # input_title # Title of input message.
75
+ # input_message # Text of input message.
76
+ # error_title # Title of error message.
77
+ # error_message # Text of input message.
78
+ # error_type # Error dialog type.
79
+ # ignore_blank # Ignore blank cells.
80
+ # dropdown # Display dropdown with list.
81
+ # input_box # Display input box.
82
+ # error_box # Display error box.
83
+ #
84
+ def dv_record # :nodoc:
85
+ record = 0x01BE # Record identifier
86
+
87
+ flags = 0x00000000 # DV option flags.
88
+
89
+ ime_mode = 0 # IME input mode for far east fonts.
90
+ str_lookup = 0 # See below.
91
+
92
+ # Set the string lookup flag for 'list' validations with a string array.
93
+ str_lookup = @validate == 3 && @value.respond_to?(:to_ary) ? 1 : 0
94
+
95
+ # The dropdown flag is stored as a negated value.
96
+ no_dropdown = @dropdown ? 0 : 1
97
+
98
+ # Set the required flags.
99
+ flags |= @validate
100
+ flags |= @error_type << 4
101
+ flags |= str_lookup << 7
102
+ flags |= @ignore_blank << 8
103
+ flags |= no_dropdown << 9
104
+ flags |= ime_mode << 10
105
+ flags |= @show_input << 18
106
+ flags |= @show_error << 19
107
+ flags |= @criteria << 20
108
+
109
+ # Pack the DV cell data.
110
+ dv_data = @cells.inject([@cells.size].pack('v')) do |result, range|
111
+ result + [range[0], range[2], range[1], range[3]].pack('vvvv')
112
+ end
113
+
114
+ # Pack the record.
115
+ data = [flags].pack('V') +
116
+ pack_dv_string(@input_title, 32 ) +
117
+ pack_dv_string(@error_title, 32 ) +
118
+ pack_dv_string(@input_message, 255) +
119
+ pack_dv_string(@error_message, 255) +
120
+ pack_dv_formula(@value) +
121
+ pack_dv_formula(@maximum) +
122
+ dv_data
123
+
124
+ header = [record, data.bytesize].pack('vv')
125
+
126
+ header + data
127
+ end
128
+
129
+ def self.factory(parser, date_1904, *args)
130
+ # Check for a valid number of args.
131
+ return -1 if args.size != 5 && args.size != 3
132
+
133
+ # The final hashref contains the validation parameters.
134
+ param = args.pop
135
+
136
+ # 'validate' is a required parameter.
137
+ return -3 unless param.has_key?(:validate)
138
+
139
+ # Make the last row/col the same as the first if not defined.
140
+ row1, col1, row2, col2 = args
141
+ row2, col2 = row1, col1 unless row2
142
+
143
+ # List of valid input parameters.
144
+ obj = DataValidation.new
145
+ valid_parameter = obj.valid_parameter_of_data_validation
146
+
147
+ # Check for valid input parameters.
148
+ param.each_key { |param_key| return -3 unless valid_parameter.has_key?(param_key) }
149
+
150
+ # Map alternative parameter names 'source' or 'minimum' to 'value'.
151
+ param[:value] = param[:source] if param[:source]
152
+ param[:value] = param[:minimum] if param[:minimum]
153
+
154
+ # Check for valid validation types.
155
+ unless obj.valid_validation_type.has_key?(param[:validate].downcase)
156
+ return -3
157
+ else
158
+ param[:validate] = obj.valid_validation_type[param[:validate].downcase]
159
+ end
160
+
161
+ # No action is required for validation type 'any'.
162
+ # TODO: we should perhaps store 'any' for message only validations.
163
+ return 0 if param[:validate] == 0
164
+
165
+ # The list and custom validations don't have a criteria so we use a default
166
+ # of 'between'.
167
+ if param[:validate] == 3 || param[:validate] == 7
168
+ param[:criteria] = 'between'
169
+ param[:maximum] = nil
170
+ end
171
+
172
+ # 'criteria' is a required parameter.
173
+ unless param.has_key?(:criteria)
174
+ # carp "Parameter 'criteria' is required in data_validation()";
175
+ return -3
176
+ end
177
+
178
+ # Check for valid criteria types.
179
+ unless obj.valid_criteria_type.has_key?(param[:criteria].downcase)
180
+ return -3
181
+ else
182
+ param[:criteria] = obj.valid_criteria_type[param[:criteria].downcase]
183
+ end
184
+
185
+ # 'Between' and 'Not between' criteria require 2 values.
186
+ if param[:criteria] == 0 || param[:criteria] == 1
187
+ unless param.has_key?(:maximum)
188
+ return -3
189
+ end
190
+ else
191
+ param[:maximum] = nil
192
+ end
193
+
194
+ # Check for valid error dialog types.
195
+ if not param.has_key?(:error_type)
196
+ param[:error_type] = 0
197
+ elsif not obj.valid_error_type.has_key?(param[:error_type].downcase)
198
+ return -3
199
+ else
200
+ param[:error_type] = obj.valid_error_type[param[:error_type].downcase]
201
+ end
202
+
203
+ # Convert date/times value if required.
204
+ if param[:validate] == 4 || param[:validate] == 5
205
+ if param[:value] =~ /T/
206
+ param[:value] = obj.convert_date_time(param[:value], date_1904) || raise("invalid :value: #{param[:value]}")
207
+ end
208
+ if param[:maximum] && param[:maximum] =~ /T/
209
+ param[:maximum] = obj.convert_date_time(param[:maximum], date_1904) || raise("invalid :maximum: #{param[:maximum]}")
210
+ end
211
+ end
212
+
213
+ # Set some defaults if they haven't been defined by the user.
214
+ param[:ignore_blank] = 1 unless param[:ignore_blank]
215
+ param[:dropdown] = 1 unless param[:dropdown]
216
+ param[:show_input] = 1 unless param[:show_input]
217
+ param[:show_error] = 1 unless param[:show_error]
218
+
219
+ # These are the cells to which the validation is applied.
220
+ param[:cells] = [[row1, col1, row2, col2]]
221
+
222
+ # A (for now) undocumented parameter to pass additional cell ranges.
223
+ if param.has_key?(:other_cells)
224
+ param[:cells].push(param[:other_cells])
225
+ end
226
+
227
+ DataValidation.new(parser, param)
228
+ end
229
+
230
+ #
231
+ # Pack the strings used in the input and error dialog captions and messages.
232
+ # Captions are limited to 32 characters. Messages are limited to 255 chars.
233
+ #
234
+ def pack_dv_string(string, max_length) #:nodoc:
235
+ # The default empty string is "\0".
236
+ string = ruby_18 { "\0" } || ruby_19 { "\0".encode('BINARY') } unless string && string != ''
237
+
238
+ # Excel limits DV captions to 32 chars and messages to 255.
239
+ string = string[0 .. max_length-1] if string.bytesize > max_length
240
+
241
+ ruby_19 { string = convert_to_ascii_if_ascii(string) }
242
+
243
+ # Handle utf8 strings
244
+ if is_utf8?(string)
245
+ str_length = string.gsub(/[^\Wa-zA-Z_\d]/, ' ').bytesize # jlength
246
+ string = utf8_to_16le(string)
247
+ encoding = 1
248
+ else
249
+ str_length = string.bytesize
250
+ encoding = 0
251
+ end
252
+
253
+ ruby_18 { [str_length, encoding].pack('vC') + string } ||
254
+ ruby_19 { [str_length, encoding].pack('vC') + string.force_encoding('BINARY') }
255
+ end
256
+
257
+ #
258
+ # Pack the formula used in the DV record. This is the same as an cell formula
259
+ # with some additional header information. Note, DV formulas in Excel use
260
+ # relative addressing (R1C1 and ptgXxxN) however we use the Formula.pm's
261
+ # default absolute addressing (A1 and ptgXxx).
262
+ #
263
+ def pack_dv_formula(formula) #:nodoc:
264
+ unused = 0x0000
265
+
266
+ # Return a default structure for unused formulas.
267
+ return [0, unused].pack('vv') unless formula && formula != ''
268
+
269
+ # Pack a list array ref as a null separated string.
270
+ formula = %!"#{formula.join("\0")}"! if formula.respond_to?(:to_ary)
271
+
272
+ # Strip the = sign at the beginning of the formula string
273
+ formula = formula.to_s unless formula.respond_to?(:to_str)
274
+ formula.sub!(/^=/, '')
275
+
276
+ # In order to raise formula errors from the point of view of the calling
277
+ # program we use an eval block and re-raise the error from here.
278
+ #
279
+ tokens = @parser.parse_formula(formula) # ????
280
+
281
+ # if ($@) {
282
+ # $@ =~ s/\n$//; # Strip the \n used in the Formula.pm die()
283
+ # croak $@; # Re-raise the error
284
+ # }
285
+ # else {
286
+ # # TODO test for non valid ptgs such as Sheet2!A1
287
+ # }
288
+
289
+ # Force 2d ranges to be a reference class.
290
+ tokens.each do |t|
291
+ t.sub!(/_range2d/, "_range2dR")
292
+ t.sub!(/_name/, "_nameR")
293
+ end
294
+
295
+ # Parse the tokens into a formula string.
296
+ formula = @parser.parse_tokens(tokens)
297
+
298
+ [formula.length, unused].pack('vv') + formula
299
+ end
300
+
301
+ def valid_parameter_of_data_validation
302
+ {
303
+ :validate => 1,
304
+ :criteria => 1,
305
+ :value => 1,
306
+ :source => 1,
307
+ :minimum => 1,
308
+ :maximum => 1,
309
+ :ignore_blank => 1,
310
+ :dropdown => 1,
311
+ :show_input => 1,
312
+ :input_title => 1,
313
+ :input_message => 1,
314
+ :show_error => 1,
315
+ :error_title => 1,
316
+ :error_message => 1,
317
+ :error_type => 1,
318
+ :other_cells => 1
319
+ }
320
+ end
321
+
322
+ def valid_validation_type
323
+ {
324
+ 'any' => 0,
325
+ 'any value' => 0,
326
+ 'whole number' => 1,
327
+ 'whole' => 1,
328
+ 'integer' => 1,
329
+ 'decimal' => 2,
330
+ 'list' => 3,
331
+ 'date' => 4,
332
+ 'time' => 5,
333
+ 'text length' => 6,
334
+ 'length' => 6,
335
+ 'custom' => 7
336
+ }
337
+ end
338
+
339
+ def valid_criteria_type
340
+ {
341
+ 'between' => 0,
342
+ 'not between' => 1,
343
+ 'equal to' => 2,
344
+ '=' => 2,
345
+ '==' => 2,
346
+ 'not equal to' => 3,
347
+ '!=' => 3,
348
+ '<>' => 3,
349
+ 'greater than' => 4,
350
+ '>' => 4,
351
+ 'less than' => 5,
352
+ '<' => 5,
353
+ 'greater than or equal to' => 6,
354
+ '>=' => 6,
355
+ 'less than or equal to' => 7,
356
+ '<=' => 7
357
+ }
358
+ end
359
+
360
+ def valid_error_type
361
+ {
362
+ 'stop' => 0,
363
+ 'warning' => 1,
364
+ 'information' => 2
365
+ }
366
+ end
367
+ end
368
+ end
369
+
370
+ end