writeexcel 0.6.9 → 0.6.10

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