WriteExcel 0.2.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.
Files changed (80) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +17 -0
  5. data/Rakefile +47 -0
  6. data/VERSION +1 -0
  7. data/examples/a_simple.rb +42 -0
  8. data/examples/autofilters.rb +266 -0
  9. data/examples/bigfile.rb +30 -0
  10. data/examples/copyformat.rb +51 -0
  11. data/examples/data_validate.rb +278 -0
  12. data/examples/date_time.rb +86 -0
  13. data/examples/demo.rb +118 -0
  14. data/examples/diag_border.rb +35 -0
  15. data/examples/formats.rb +489 -0
  16. data/examples/header.rb +136 -0
  17. data/examples/hidden.rb +28 -0
  18. data/examples/hyperlink.rb +42 -0
  19. data/examples/images.rb +52 -0
  20. data/examples/merge1.rb +39 -0
  21. data/examples/merge2.rb +44 -0
  22. data/examples/merge3.rb +65 -0
  23. data/examples/merge4.rb +82 -0
  24. data/examples/merge5.rb +79 -0
  25. data/examples/protection.rb +46 -0
  26. data/examples/regions.rb +52 -0
  27. data/examples/repeat.rb +42 -0
  28. data/examples/stats.rb +75 -0
  29. data/examples/stocks.rb +80 -0
  30. data/examples/tab_colors.rb +30 -0
  31. data/lib/WriteExcel.rb +30 -0
  32. data/lib/WriteExcel/biffwriter.rb +259 -0
  33. data/lib/WriteExcel/chart.rb +217 -0
  34. data/lib/WriteExcel/excelformula.y +138 -0
  35. data/lib/WriteExcel/excelformulaparser.rb +573 -0
  36. data/lib/WriteExcel/format.rb +1108 -0
  37. data/lib/WriteExcel/formula.rb +986 -0
  38. data/lib/WriteExcel/olewriter.rb +322 -0
  39. data/lib/WriteExcel/properties.rb +250 -0
  40. data/lib/WriteExcel/storage_lite.rb +590 -0
  41. data/lib/WriteExcel/workbook.rb +2602 -0
  42. data/lib/WriteExcel/worksheet.rb +6378 -0
  43. data/spec/WriteExcel_spec.rb +7 -0
  44. data/spec/spec.opts +1 -0
  45. data/spec/spec_helper.rb +9 -0
  46. data/test/tc_all.rb +31 -0
  47. data/test/tc_biff.rb +104 -0
  48. data/test/tc_chart.rb +22 -0
  49. data/test/tc_example_match.rb +1280 -0
  50. data/test/tc_format.rb +1264 -0
  51. data/test/tc_formula.rb +63 -0
  52. data/test/tc_ole.rb +110 -0
  53. data/test/tc_storage_lite.rb +102 -0
  54. data/test/tc_workbook.rb +115 -0
  55. data/test/tc_worksheet.rb +115 -0
  56. data/test/test_00_IEEE_double.rb +14 -0
  57. data/test/test_01_add_worksheet.rb +12 -0
  58. data/test/test_02_merge_formats.rb +58 -0
  59. data/test/test_04_dimensions.rb +397 -0
  60. data/test/test_05_rows.rb +182 -0
  61. data/test/test_06_extsst.rb +80 -0
  62. data/test/test_11_date_time.rb +484 -0
  63. data/test/test_12_date_only.rb +506 -0
  64. data/test/test_13_date_seconds.rb +486 -0
  65. data/test/test_21_escher.rb +629 -0
  66. data/test/test_22_mso_drawing_group.rb +739 -0
  67. data/test/test_23_note.rb +78 -0
  68. data/test/test_24_txo.rb +80 -0
  69. data/test/test_26_autofilter.rb +327 -0
  70. data/test/test_27_autofilter.rb +144 -0
  71. data/test/test_28_autofilter.rb +174 -0
  72. data/test/test_29_process_jpg.rb +131 -0
  73. data/test/test_30_validation_dval.rb +82 -0
  74. data/test/test_31_validation_dv_strings.rb +131 -0
  75. data/test/test_32_validation_dv_formula.rb +211 -0
  76. data/test/test_40_property_types.rb +191 -0
  77. data/test/test_41_properties.rb +238 -0
  78. data/test/test_42_set_properties.rb +430 -0
  79. data/test/ts_all.rb +34 -0
  80. metadata +154 -0
@@ -0,0 +1,986 @@
1
+ ###############################################################################
2
+ #
3
+ # Formula - A class for generating Excel formulas.
4
+ #
5
+ #
6
+ # Used in conjunction with Spreadsheet::WriteExcel
7
+ #
8
+ # Copyright 2000-2008, John McNamara, jmcnamara@cpan.org
9
+ #
10
+ # original written in Perl by John McNamara
11
+ # converted to Ruby by Hideo Nakamura, cxn03651@msj.biglobe.ne.jp
12
+ #
13
+ #require 'nkf'
14
+ #require 'strscan'
15
+ #require 'WriteExcel/excelformulaparser'
16
+
17
+ class Formula < ExcelFormulaParser
18
+
19
+ NonAscii = /[^!"#\$%&'\(\)\*\+,\-\.\/\:\;<=>\?@0-9A-Za-z_\[\\\]^` ~\0\n]/
20
+
21
+ attr_accessor :byte_order, :workbook, :ext_sheets, :ext_refs, :ext_ref_count
22
+
23
+ def initialize(byte_order)
24
+ @byte_order = byte_order
25
+ @workbook = ""
26
+ @ext_sheets = {}
27
+ @ext_refs = {}
28
+ @ext_ref_count = 0
29
+ initialize_hashes
30
+ end
31
+
32
+ ###############################################################################
33
+ #
34
+ # parse_formula()
35
+ #
36
+ # Takes a textual description of a formula and returns a RPN encoded byte
37
+ # string.
38
+ #
39
+ def parse_formula(formula, byte_stream = false)
40
+ # Build the parse tree for the formula
41
+ tokens = reverse(parse(formula))
42
+
43
+ # Add a volatile token if the formula contains a volatile function.
44
+ # This must be the first token in the list
45
+ #
46
+ tokens.unshift('_vol') if check_volatile(tokens) != 0
47
+
48
+ # The return value depends on which Worksheet.pm method is the caller
49
+ unless byte_stream
50
+ # Parse formula to see if it throws any errors and then
51
+ # return raw tokens to Worksheet::store_formula()
52
+ #
53
+ tokens
54
+ else
55
+ # Return byte stream to Worksheet::write_formula()
56
+ parse_tokens(tokens)
57
+ end
58
+ end
59
+
60
+ ###############################################################################
61
+ #
62
+ # parse_tokens()
63
+ #
64
+ # Convert each token or token pair to its Excel 'ptg' equivalent.
65
+ #
66
+ def parse_tokens(tokens)
67
+ parse_str = ''
68
+ last_type = ''
69
+ modifier = ''
70
+ num_args = 0
71
+ _class = 0
72
+ _classary = [1]
73
+ args = tokens.dup
74
+ # A note about the class modifiers used below. In general the class,
75
+ # "reference" or "value", of a function is applied to all of its operands.
76
+ # However, in certain circumstances the operands can have mixed classes,
77
+ # e.g. =VLOOKUP with external references. These will eventually be dealt
78
+ # with by the parser. However, as a workaround the class type of a token
79
+ # can be changed via the repeat_formula interface. Thus, a _ref2d token can
80
+ # be changed by the user to _ref2dA or _ref2dR to change its token class.
81
+ #
82
+ while (!args.empty?)
83
+ token = args.shift
84
+
85
+ if (token == '_arg')
86
+ num_args = args.shift
87
+ elsif (token == '_class')
88
+ token = args.shift
89
+ _class = @functions[token][2]
90
+ # If _class is undef then it means that the function isn't valid.
91
+ exit "Unknown function #{token}() in formula\n" if _class.nil?
92
+ _classary.push(_class)
93
+ elsif (token == '_vol')
94
+ parse_str = parse_str + convert_volatile()
95
+ elsif (token == 'ptgBool')
96
+ token = args.shift
97
+ parse_str = parse_str + convert_bool(token)
98
+ elsif (token == '_num')
99
+ token = args.shift
100
+ parse_str = parse_str + convert_number(token)
101
+ elsif (token == '_str')
102
+ token = args.shift
103
+ parse_str = parse_str + convert_string(token)
104
+ elsif (token =~ /^_ref2d/)
105
+ modifier = token.sub(/_ref2d/, '')
106
+ _class = _classary[-1]
107
+ _class = 0 if modifier == 'R'
108
+ _class = 1 if modifier == 'V'
109
+ token = args.shift
110
+ parse_str = parse_str + convert_ref2d(token, _class)
111
+ elsif (token =~ /^_ref3d/)
112
+ modifier = token.sub(/_ref3d/,'')
113
+ _class = _classary[-1]
114
+ _class = 0 if modifier == 'R'
115
+ _class = 1 if modifier == 'V'
116
+ token = args.shift
117
+ parse_str = parse_str + convert_ref3d(token, _class)
118
+ elsif (token =~ /^_range2d/)
119
+ modifier = token.sub(/_range2d/,'')
120
+ _class = _classary[-1]
121
+ _class = 0 if modifier == 'R'
122
+ _class = 1 if modifier == 'V'
123
+ token = args.shift
124
+ parse_str = parse_str + convert_range2d(token, _class)
125
+ elsif (token =~ /^_range3d/)
126
+ modifier = token.sub(/_range3d/,'')
127
+ _class = _classary[-1]
128
+ _class = 0 if modifier == 'R'
129
+ _class = 1 if modifier == 'V'
130
+ token = args.shift
131
+ parse_str = parse_str + convert_range3d(token, _class)
132
+ elsif (token == '_func')
133
+ token = args.shift
134
+ parse_str = parse_str + convert_function(token, num_args.to_i)
135
+ _classary.pop
136
+ num_args = 0 # Reset after use
137
+ elsif @ptg[token]
138
+ parse_str = parse_str + [@ptg[token]].pack("C")
139
+ else
140
+ # Unrecognised token
141
+ return nil
142
+ end
143
+ end
144
+
145
+
146
+ return parse_str
147
+ end
148
+
149
+ def scan(formula)
150
+ s = StringScanner.new(formula)
151
+ q = []
152
+ until s.eos?
153
+ # order is important.
154
+ if s.scan(/(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?/)
155
+ q.push [:NUMBER, s.matched]
156
+ elsif s.scan(/"([^"]|"")*"/)
157
+ q.push [:STRING, s.matched]
158
+ elsif s.scan(/\$?[A-I]?[A-Z]\$?(\d+)?:\$?[A-I]?[A-Z]\$?(\d+)?/)
159
+ q.push [:RANGE2D , s.matched]
160
+ elsif s.scan(/[^!(,]+!\$?[A-I]?[A-Z]\$?(\d+)?:\$?[A-I]?[A-Z]\$?(\d+)?/)
161
+ q.push [:RANGE3D , s.matched]
162
+ elsif s.scan(/\$?[A-I]?[A-Z]\$?\d+/)
163
+ q.push [:REF2D, s.matched]
164
+ elsif s.scan(/[^!(,]+!\$?[A-I]?[A-Z]\$?\d+/)
165
+ q.push [:REF3D , s.matched]
166
+ elsif s.scan(/'[^']+'!\$?[A-I]?[A-Z]\$?\d+/)
167
+ q.push [:REF3D , s.matched]
168
+ elsif s.scan(/<=/)
169
+ q.push [:LE , s.matched]
170
+ elsif s.scan(/>=/)
171
+ q.push [:GE , s.matched]
172
+ elsif s.scan(/<>/)
173
+ q.push [:NE , s.matched]
174
+ elsif s.scan(/</)
175
+ q.push [:LT , s.matched]
176
+ elsif s.scan(/>/)
177
+ q.push [:GT , s.matched]
178
+ elsif s.scan(/TRUE/)
179
+ q.push [:TRUE, s.matched]
180
+ elsif s.scan(/FALSE/)
181
+ q.push [:FALSE, s.matched]
182
+ elsif s.scan(/[A-Z0-9_.]+/)
183
+ q.push [:FUNC, s.matched]
184
+ elsif s.scan(/\s+/)
185
+ ;
186
+ elsif s.scan(/./)
187
+ q.push [s.matched, s.matched]
188
+ end
189
+ end
190
+ q.push [:EOL, nil]
191
+ end
192
+
193
+ def parse(formula)
194
+ @q = scan(formula)
195
+ @q.push [false, nil]
196
+ @yydebug=true
197
+ do_parse
198
+ end
199
+
200
+ def next_token
201
+ @q.shift
202
+ end
203
+
204
+ def reverse(expression)
205
+ expression.flatten
206
+ end
207
+
208
+ ###############################################################################
209
+ #
210
+ # get_ext_sheets()
211
+ #
212
+ # This semi-public method is used to update the hash of sheet names. It is
213
+ # updated by the add_worksheet() method of the Workbook class.
214
+ #
215
+ # TODO
216
+ #
217
+ def get_ext_sheets
218
+ # TODO
219
+ refs = @ext_refs
220
+ return refs
221
+
222
+ #my @refs = sort {$refs{$a} <=> $refs{$b}} keys %refs;
223
+
224
+ #foreach my $ref (@refs) {
225
+ # $ref = [split /:/, $ref];
226
+ #}
227
+
228
+ #return @refs;
229
+ end
230
+
231
+
232
+ ###############################################################################
233
+
234
+ private
235
+
236
+ ###############################################################################
237
+
238
+
239
+ ###############################################################################
240
+ #
241
+ # _check_volatile()
242
+ #
243
+ # Check if the formula contains a volatile function, i.e. a function that must
244
+ # be recalculated each time a cell is updated. These formulas require a ptgAttr
245
+ # with the volatile flag set as the first token in the parsed expression.
246
+ #
247
+ # Examples of volatile functions: RAND(), NOW(), TODAY()
248
+ #
249
+ def check_volatile(tokens)
250
+ volatile = 0
251
+
252
+ (0..tokens.size-1).each do |i|
253
+ # If the next token is a function check if it is volatile.
254
+ if tokens[i] == '_func' and @functions[tokens[i+1]][3] != 0
255
+ volatile = 1
256
+ break
257
+ end
258
+ end
259
+
260
+ return volatile
261
+ end
262
+
263
+ ###############################################################################
264
+ #
265
+ # _convert_volatile()
266
+ #
267
+ # Convert _vol to a ptgAttr tag formatted to indicate that the formula contains
268
+ # a volatile function. See _check_volatile()
269
+ #
270
+ def convert_volatile
271
+ # Set bitFattrSemi flag to indicate volatile function, "w" is set to zero.
272
+ return [@ptg['ptgAttr'], 0x1, 0x0].pack("CCv")
273
+ end
274
+
275
+ ###############################################################################
276
+ #
277
+ # _convert_bool()
278
+ #
279
+ # Convert a boolean token to ptgBool
280
+ #
281
+ def convert_bool(bool)
282
+ return [@ptg['ptgBool'], bool.to_i].pack("CC")
283
+ end
284
+
285
+
286
+ ###############################################################################
287
+ #
288
+ # _convert_number()
289
+ #
290
+ # Convert a number token to ptgInt or ptgNum
291
+ #
292
+ def convert_number(num)
293
+ # Integer in the range 0..2**16-1
294
+ if ((num =~ /^\d+$/) && (num.to_i <= 65535))
295
+ return [@ptg['ptgInt'], num.to_i].pack("Cv")
296
+ else # A float
297
+ num = [num].pack("d")
298
+ num.reverse! if @byte_order != 0 && @byte_order != ''
299
+ return [@ptg['ptgNum']].pack("C") + num
300
+ end
301
+ end
302
+
303
+ ###############################################################################
304
+ #
305
+ # _convert_string()
306
+ #
307
+ # Convert a string to a ptg Str.
308
+ #
309
+ def convert_string(str)
310
+ encoding = 0
311
+
312
+ str.sub!(/^"/,'') # Remove leading "
313
+ str.sub!(/"$/,'') # Remove trailing "
314
+ str.gsub!(/""/,'"') # Substitute Excel's escaped double quote "" for "
315
+
316
+ length = str.length
317
+
318
+ # Handle utf8 strings
319
+ if str =~ NonAscii
320
+ str = NKF.nkf('-w16L0 -m0 -W', str)
321
+ encoding = 1
322
+ end
323
+
324
+ exit "String in formula has more than 255 chars\n" if length > 255
325
+
326
+ return [@ptg['ptgStr'], length, encoding].pack("CCC") + str
327
+ end
328
+
329
+ ###############################################################################
330
+ #
331
+ # _convert_ref2d()
332
+ #
333
+ # Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
334
+ #
335
+ def convert_ref2d(cell, _class)
336
+ # Convert the cell reference
337
+ row, col = cell_to_packed_rowcol(cell)
338
+
339
+ # The ptg value depends on the class of the ptg.
340
+ if (_class == 0)
341
+ ptgref = [@ptg['ptgRef']].pack("C")
342
+ elsif (_class == 1)
343
+ ptgref = [@ptg['ptgRefV']].pack("C")
344
+ elsif (_class == 2)
345
+ ptgref = [@ptg['ptgRefA']].pack("C")
346
+ else
347
+ exit "Unknown function class in formula\n"
348
+ end
349
+
350
+ return ptgref + row + col
351
+ end
352
+
353
+ ###############################################################################
354
+ #
355
+ # _convert_ref3d
356
+ #
357
+ # Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
358
+ # ptgRef3dV.
359
+ #
360
+ def convert_ref3d(token, _class)
361
+ # Split the ref at the ! symbol
362
+ ext_ref, cell = token.split('!')
363
+
364
+ # Convert the external reference part
365
+ ext_ref = pack_ext_ref(ext_ref)
366
+
367
+ # Convert the cell reference part
368
+ row, col = cell_to_packed_rowcol(cell)
369
+
370
+ # The ptg value depends on the class of the ptg.
371
+ if (_class == 0)
372
+ ptgref = [@ptg['ptgRef3d']].pack("C")
373
+ elsif (_class == 1)
374
+ ptgref = [@ptg['ptgRef3dV']].pack("C")
375
+ elsif (_class == 2)
376
+ ptgref = [@ptg['ptgRef3dA']].pack("C")
377
+ else
378
+ exit "Unknown function class in formula\n"
379
+ end
380
+
381
+ return ptgref + ext_ref + row + col
382
+ end
383
+
384
+ ###############################################################################
385
+ #
386
+ # _convert_range2d()
387
+ #
388
+ # Convert an Excel range such as A1:D4 or A:D to a ptgRefV.
389
+ #
390
+ def convert_range2d(range, _class)
391
+ # Split the range into 2 cell refs
392
+ cell1, cell2 = range.split(':')
393
+
394
+ # A range such as A:D is equivalent to A1:D65536, so add rows as required
395
+ cell1 = cell1 + '1' unless cell1 =~ /\d/
396
+ cell2 = cell2 + '65536' unless cell2 =~ /\d/
397
+
398
+ # Convert the cell references
399
+ row1, col1 = cell_to_packed_rowcol(cell1)
400
+ row2, col2 = cell_to_packed_rowcol(cell2)
401
+
402
+ # The ptg value depends on the class of the ptg.
403
+ if (_class == 0)
404
+ ptgarea = [@ptg['ptgArea']].pack("C")
405
+ elsif (_class == 1)
406
+ ptgarea = [@ptg['ptgAreaV']].pack("C")
407
+ elsif (_class == 2)
408
+ ptgarea = [@ptg['ptgAreaA']].pack("C")
409
+ else
410
+ exit "Unknown function class in formula\n"
411
+ end
412
+
413
+ return ptgarea + row1 + row2 + col1 + col2
414
+ end
415
+
416
+ ###############################################################################
417
+ #
418
+ # _convert_range3d
419
+ #
420
+ # Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
421
+ # a ptgArea3dV.
422
+ #
423
+ def convert_range3d(token, _class)
424
+ # Split the ref at the ! symbol
425
+ ext_ref, range = token.split('!')
426
+
427
+ # Convert the external reference part
428
+ ext_ref = pack_ext_ref(ext_ref)
429
+
430
+ # Split the range into 2 cell refs
431
+ cell1, cell2 = range.split(':')
432
+
433
+ # A range such as A:D is equivalent to A1:D65536, so add rows as required
434
+ cell1 = cell1 + '1' unless cell1 =~ /\d/
435
+ cell2 = cell2 + '65536' unless cell2 =~ /\d/
436
+
437
+ # Convert the cell references
438
+ row1, col1 = cell_to_packed_rowcol(cell1)
439
+ row2, col2 = cell_to_packed_rowcol(cell2)
440
+
441
+ # The ptg value depends on the class of the ptg.
442
+ if (_class == 0)
443
+ ptgarea = [@ptg['ptgArea3d']].pack("C")
444
+ elsif (_class == 1)
445
+ ptgarea = [@ptg['ptgArea3dV']].pack("C")
446
+ elsif (_class == 2)
447
+ ptgarea = [@ptg['ptgArea3dA']].pack("C")
448
+ else
449
+ exit "Unknown function class in formula\n"
450
+ end
451
+
452
+ return ptgarea + ext_ref + row1 + row2 + col1+ col2
453
+ end
454
+
455
+ ###############################################################################
456
+ #
457
+ # _pack_ext_ref()
458
+ #
459
+ # Convert the sheet name part of an external reference, for example "Sheet1" or
460
+ # "Sheet1:Sheet2", to a packed structure.
461
+ #
462
+ def pack_ext_ref(ext_ref)
463
+ ext_ref.sub!(/^'/,'') # Remove leading ' if any.
464
+ ext_ref.sub!(/'$/,'') # Remove trailing ' if any.
465
+
466
+ # Check if there is a sheet range eg., Sheet1:Sheet2.
467
+ if (ext_ref =~ /:/)
468
+ sheet1, sheet2 = ext_ref.split(':')
469
+
470
+ sheet1 = get_sheet_index(sheet1)
471
+ sheet2 = get_sheet_index(sheet2)
472
+
473
+ # Reverse max and min sheet numbers if necessary
474
+ if (sheet1 > sheet2)
475
+ sheet1, sheet2 = [sheet2, sheet1]
476
+ end
477
+ else
478
+ # Single sheet name only.
479
+ sheet1, sheet2 = [ext_ref, ext_ref]
480
+
481
+ sheet1 = get_sheet_index(sheet1)
482
+ sheet2 = sheet1
483
+ end
484
+
485
+ key = "#{sheet1}:#{sheet2}"
486
+
487
+ unless @ext_refs[key]
488
+ index = @ext_refs[key]
489
+ else
490
+ index = @ext_ref_count
491
+ @ext_refs[key] = index
492
+ @ext_ref_count += 1
493
+ end
494
+
495
+ return [index].pack("v")
496
+ end
497
+
498
+ ###############################################################################
499
+ #
500
+ # _get_sheet_index()
501
+ #
502
+ # Look up the index that corresponds to an external sheet name. The hash of
503
+ # sheet names is updated by the add_worksheet() method of the Workbook class.
504
+ #
505
+ def get_sheet_index(sheet_name)
506
+ # Handle utf8 sheetnames
507
+ if sheet_name =~ NonAscii
508
+ sheet_name = NKF.nkf('-w16B0 -m0 -W', sheet_name)
509
+ end
510
+
511
+ if @ext_sheets[sheet_name].nil?
512
+ exit "Unknown sheet name #{sheet_name} in formula\n"
513
+ else
514
+ return @ext_sheets[sheet_name]
515
+ end
516
+ end
517
+
518
+ ###############################################################################
519
+ #
520
+ # get_ext_ref_count()
521
+ #
522
+ # TODO This semi-public method is used to update the hash of sheet names. It is
523
+ # updated by the add_worksheet() method of the Workbook class.
524
+ #
525
+ def get_ext_ref_count
526
+ return @ext_ref_count
527
+ end
528
+
529
+ ###############################################################################
530
+ #
531
+ # _convert_function()
532
+ #
533
+ # Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
534
+ # args that it takes.
535
+ #
536
+ def convert_function(token, num_args)
537
+ exit "Unknown function #{token}() in formula\n" if @functions[token][0].nil?
538
+
539
+ args = @functions[token][1]
540
+
541
+ # Fixed number of args eg. TIME($i,$j,$k).
542
+ if (args >= 0)
543
+ # Check that the number of args is valid.
544
+ if (args != num_args)
545
+ raise "Incorrect number of arguments for #{token}() in formula\n";
546
+ else
547
+ return [@ptg['ptgFuncV'], @functions[token][0]].pack("Cv")
548
+ end
549
+ end
550
+
551
+ # Variable number of args eg. SUM(i,j,k, ..).
552
+ if (args == -1)
553
+ return [@ptg['ptgFuncVarV'], num_args, @functions[token][0]].pack("CCv")
554
+ end
555
+ end
556
+
557
+ ###############################################################################
558
+ #
559
+ # _cell_to_rowcol($cell_ref)
560
+ #
561
+ # Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
562
+ # indexed row and column number. Also returns two boolean values to indicate
563
+ # whether the row or column are relative references.
564
+ # TODO use function in Utility.pm
565
+ #
566
+ def cell_to_rowcol(cell)
567
+ cell =~ /(\$?)([A-I]?[A-Z])(\$?)(\d+)/
568
+
569
+ col_rel = $1 == "" ? 1 : 0
570
+ col = $2
571
+ row_rel = $3 == "" ? 1 : 0
572
+ row = $4.to_i
573
+
574
+ # Convert base26 column string to a number.
575
+ # All your Base are belong to us.
576
+ chars = col.split(//)
577
+ expn = 0
578
+ col = 0
579
+
580
+ while (!chars.empty?)
581
+ char = chars.pop # LS char first
582
+ col = col + (char[0] - "A"[0] + 1) * (26**expn)
583
+ expn += 1
584
+ end
585
+ # Convert 1-index to zero-index
586
+ row -= 1
587
+ col -= 1
588
+
589
+ return [row, col, row_rel, col_rel]
590
+ end
591
+
592
+ ###############################################################################
593
+ #
594
+ # _cell_to_packed_rowcol($row, $col, $row_rel, $col_rel)
595
+ #
596
+ # pack() row and column into the required 3 byte format.
597
+ #
598
+ def cell_to_packed_rowcol(cell)
599
+ row, col, row_rel, col_rel = cell_to_rowcol(cell)
600
+
601
+ exit "Column #{cell} greater than IV in formula\n" if col >= 256
602
+ exit "Row #{cell} greater than 65536 in formula\n" if row >= 65536
603
+
604
+ # Set the high bits to indicate if row or col are relative.
605
+ col |= col_rel << 14
606
+ col |= row_rel << 15
607
+
608
+ row = [row].pack('v')
609
+ col = [col].pack('v')
610
+
611
+ return [row, col]
612
+ end
613
+
614
+ ###############################################################################
615
+ #
616
+ # _initialize_hashes()
617
+ #
618
+ def initialize_hashes
619
+
620
+ # The Excel ptg indices
621
+ @ptg = {
622
+ 'ptgExp' => 0x01,
623
+ 'ptgTbl' => 0x02,
624
+ 'ptgAdd' => 0x03,
625
+ 'ptgSub' => 0x04,
626
+ 'ptgMul' => 0x05,
627
+ 'ptgDiv' => 0x06,
628
+ 'ptgPower' => 0x07,
629
+ 'ptgConcat' => 0x08,
630
+ 'ptgLT' => 0x09,
631
+ 'ptgLE' => 0x0A,
632
+ 'ptgEQ' => 0x0B,
633
+ 'ptgGE' => 0x0C,
634
+ 'ptgGT' => 0x0D,
635
+ 'ptgNE' => 0x0E,
636
+ 'ptgIsect' => 0x0F,
637
+ 'ptgUnion' => 0x10,
638
+ 'ptgRange' => 0x11,
639
+ 'ptgUplus' => 0x12,
640
+ 'ptgUminus' => 0x13,
641
+ 'ptgPercent' => 0x14,
642
+ 'ptgParen' => 0x15,
643
+ 'ptgMissArg' => 0x16,
644
+ 'ptgStr' => 0x17,
645
+ 'ptgAttr' => 0x19,
646
+ 'ptgSheet' => 0x1A,
647
+ 'ptgEndSheet' => 0x1B,
648
+ 'ptgErr' => 0x1C,
649
+ 'ptgBool' => 0x1D,
650
+ 'ptgInt' => 0x1E,
651
+ 'ptgNum' => 0x1F,
652
+ 'ptgArray' => 0x20,
653
+ 'ptgFunc' => 0x21,
654
+ 'ptgFuncVar' => 0x22,
655
+ 'ptgName' => 0x23,
656
+ 'ptgRef' => 0x24,
657
+ 'ptgArea' => 0x25,
658
+ 'ptgMemArea' => 0x26,
659
+ 'ptgMemErr' => 0x27,
660
+ 'ptgMemNoMem' => 0x28,
661
+ 'ptgMemFunc' => 0x29,
662
+ 'ptgRefErr' => 0x2A,
663
+ 'ptgAreaErr' => 0x2B,
664
+ 'ptgRefN' => 0x2C,
665
+ 'ptgAreaN' => 0x2D,
666
+ 'ptgMemAreaN' => 0x2E,
667
+ 'ptgMemNoMemN' => 0x2F,
668
+ 'ptgNameX' => 0x39,
669
+ 'ptgRef3d' => 0x3A,
670
+ 'ptgArea3d' => 0x3B,
671
+ 'ptgRefErr3d' => 0x3C,
672
+ 'ptgAreaErr3d' => 0x3D,
673
+ 'ptgArrayV' => 0x40,
674
+ 'ptgFuncV' => 0x41,
675
+ 'ptgFuncVarV' => 0x42,
676
+ 'ptgNameV' => 0x43,
677
+ 'ptgRefV' => 0x44,
678
+ 'ptgAreaV' => 0x45,
679
+ 'ptgMemAreaV' => 0x46,
680
+ 'ptgMemErrV' => 0x47,
681
+ 'ptgMemNoMemV' => 0x48,
682
+ 'ptgMemFuncV' => 0x49,
683
+ 'ptgRefErrV' => 0x4A,
684
+ 'ptgAreaErrV' => 0x4B,
685
+ 'ptgRefNV' => 0x4C,
686
+ 'ptgAreaNV' => 0x4D,
687
+ 'ptgMemAreaNV' => 0x4E,
688
+ 'ptgMemNoMemN' => 0x4F,
689
+ 'ptgFuncCEV' => 0x58,
690
+ 'ptgNameXV' => 0x59,
691
+ 'ptgRef3dV' => 0x5A,
692
+ 'ptgArea3dV' => 0x5B,
693
+ 'ptgRefErr3dV' => 0x5C,
694
+ 'ptgAreaErr3d' => 0x5D,
695
+ 'ptgArrayA' => 0x60,
696
+ 'ptgFuncA' => 0x61,
697
+ 'ptgFuncVarA' => 0x62,
698
+ 'ptgNameA' => 0x63,
699
+ 'ptgRefA' => 0x64,
700
+ 'ptgAreaA' => 0x65,
701
+ 'ptgMemAreaA' => 0x66,
702
+ 'ptgMemErrA' => 0x67,
703
+ 'ptgMemNoMemA' => 0x68,
704
+ 'ptgMemFuncA' => 0x69,
705
+ 'ptgRefErrA' => 0x6A,
706
+ 'ptgAreaErrA' => 0x6B,
707
+ 'ptgRefNA' => 0x6C,
708
+ 'ptgAreaNA' => 0x6D,
709
+ 'ptgMemAreaNA' => 0x6E,
710
+ 'ptgMemNoMemN' => 0x6F,
711
+ 'ptgFuncCEA' => 0x78,
712
+ 'ptgNameXA' => 0x79,
713
+ 'ptgRef3dA' => 0x7A,
714
+ 'ptgArea3dA' => 0x7B,
715
+ 'ptgRefErr3dA' => 0x7C,
716
+ 'ptgAreaErr3d' => 0x7D
717
+ };
718
+
719
+ # Thanks to Michael Meeks and Gnumeric for the initial arg values.
720
+ #
721
+ # The following hash was generated by "function_locale.pl" in the distro.
722
+ # Refer to function_locale.pl for non-English function names.
723
+ #
724
+ # The array elements are as follow:
725
+ # ptg: The Excel function ptg code.
726
+ # args: The number of arguments that the function takes:
727
+ # >=0 is a fixed number of arguments.
728
+ # -1 is a variable number of arguments.
729
+ # class: The reference, value or array class of the function args.
730
+ # vol: The function is volatile.
731
+ #
732
+ @functions = {
733
+ # ptg args class vol
734
+ 'COUNT' => [ 0, -1, 0, 0 ],
735
+ 'IF' => [ 1, -1, 1, 0 ],
736
+ 'ISNA' => [ 2, 1, 1, 0 ],
737
+ 'ISERROR' => [ 3, 1, 1, 0 ],
738
+ 'SUM' => [ 4, -1, 0, 0 ],
739
+ 'AVERAGE' => [ 5, -1, 0, 0 ],
740
+ 'MIN' => [ 6, -1, 0, 0 ],
741
+ 'MAX' => [ 7, -1, 0, 0 ],
742
+ 'ROW' => [ 8, -1, 0, 0 ],
743
+ 'COLUMN' => [ 9, -1, 0, 0 ],
744
+ 'NA' => [ 10, 0, 0, 0 ],
745
+ 'NPV' => [ 11, -1, 1, 0 ],
746
+ 'STDEV' => [ 12, -1, 0, 0 ],
747
+ 'DOLLAR' => [ 13, -1, 1, 0 ],
748
+ 'FIXED' => [ 14, -1, 1, 0 ],
749
+ 'SIN' => [ 15, 1, 1, 0 ],
750
+ 'COS' => [ 16, 1, 1, 0 ],
751
+ 'TAN' => [ 17, 1, 1, 0 ],
752
+ 'ATAN' => [ 18, 1, 1, 0 ],
753
+ 'PI' => [ 19, 0, 1, 0 ],
754
+ 'SQRT' => [ 20, 1, 1, 0 ],
755
+ 'EXP' => [ 21, 1, 1, 0 ],
756
+ 'LN' => [ 22, 1, 1, 0 ],
757
+ 'LOG10' => [ 23, 1, 1, 0 ],
758
+ 'ABS' => [ 24, 1, 1, 0 ],
759
+ 'INT' => [ 25, 1, 1, 0 ],
760
+ 'SIGN' => [ 26, 1, 1, 0 ],
761
+ 'ROUND' => [ 27, 2, 1, 0 ],
762
+ 'LOOKUP' => [ 28, -1, 0, 0 ],
763
+ 'INDEX' => [ 29, -1, 0, 1 ],
764
+ 'REPT' => [ 30, 2, 1, 0 ],
765
+ 'MID' => [ 31, 3, 1, 0 ],
766
+ 'LEN' => [ 32, 1, 1, 0 ],
767
+ 'VALUE' => [ 33, 1, 1, 0 ],
768
+ 'TRUE' => [ 34, 0, 1, 0 ],
769
+ 'FALSE' => [ 35, 0, 1, 0 ],
770
+ 'AND' => [ 36, -1, 1, 0 ],
771
+ 'OR' => [ 37, -1, 1, 0 ],
772
+ 'NOT' => [ 38, 1, 1, 0 ],
773
+ 'MOD' => [ 39, 2, 1, 0 ],
774
+ 'DCOUNT' => [ 40, 3, 0, 0 ],
775
+ 'DSUM' => [ 41, 3, 0, 0 ],
776
+ 'DAVERAGE' => [ 42, 3, 0, 0 ],
777
+ 'DMIN' => [ 43, 3, 0, 0 ],
778
+ 'DMAX' => [ 44, 3, 0, 0 ],
779
+ 'DSTDEV' => [ 45, 3, 0, 0 ],
780
+ 'VAR' => [ 46, -1, 0, 0 ],
781
+ 'DVAR' => [ 47, 3, 0, 0 ],
782
+ 'TEXT' => [ 48, 2, 1, 0 ],
783
+ 'LINEST' => [ 49, -1, 0, 0 ],
784
+ 'TREND' => [ 50, -1, 0, 0 ],
785
+ 'LOGEST' => [ 51, -1, 0, 0 ],
786
+ 'GROWTH' => [ 52, -1, 0, 0 ],
787
+ 'PV' => [ 56, -1, 1, 0 ],
788
+ 'FV' => [ 57, -1, 1, 0 ],
789
+ 'NPER' => [ 58, -1, 1, 0 ],
790
+ 'PMT' => [ 59, -1, 1, 0 ],
791
+ 'RATE' => [ 60, -1, 1, 0 ],
792
+ 'MIRR' => [ 61, 3, 0, 0 ],
793
+ 'IRR' => [ 62, -1, 0, 0 ],
794
+ 'RAND' => [ 63, 0, 1, 1 ],
795
+ 'MATCH' => [ 64, -1, 0, 0 ],
796
+ 'DATE' => [ 65, 3, 1, 0 ],
797
+ 'TIME' => [ 66, 3, 1, 0 ],
798
+ 'DAY' => [ 67, 1, 1, 0 ],
799
+ 'MONTH' => [ 68, 1, 1, 0 ],
800
+ 'YEAR' => [ 69, 1, 1, 0 ],
801
+ 'WEEKDAY' => [ 70, -1, 1, 0 ],
802
+ 'HOUR' => [ 71, 1, 1, 0 ],
803
+ 'MINUTE' => [ 72, 1, 1, 0 ],
804
+ 'SECOND' => [ 73, 1, 1, 0 ],
805
+ 'NOW' => [ 74, 0, 1, 1 ],
806
+ 'AREAS' => [ 75, 1, 0, 1 ],
807
+ 'ROWS' => [ 76, 1, 0, 1 ],
808
+ 'COLUMNS' => [ 77, 1, 0, 1 ],
809
+ 'OFFSET' => [ 78, -1, 0, 1 ],
810
+ 'SEARCH' => [ 82, -1, 1, 0 ],
811
+ 'TRANSPOSE' => [ 83, 1, 1, 0 ],
812
+ 'TYPE' => [ 86, 1, 1, 0 ],
813
+ 'ATAN2' => [ 97, 2, 1, 0 ],
814
+ 'ASIN' => [ 98, 1, 1, 0 ],
815
+ 'ACOS' => [ 99, 1, 1, 0 ],
816
+ 'CHOOSE' => [ 100, -1, 1, 0 ],
817
+ 'HLOOKUP' => [ 101, -1, 0, 0 ],
818
+ 'VLOOKUP' => [ 102, -1, 0, 0 ],
819
+ 'ISREF' => [ 105, 1, 0, 0 ],
820
+ 'LOG' => [ 109, -1, 1, 0 ],
821
+ 'CHAR' => [ 111, 1, 1, 0 ],
822
+ 'LOWER' => [ 112, 1, 1, 0 ],
823
+ 'UPPER' => [ 113, 1, 1, 0 ],
824
+ 'PROPER' => [ 114, 1, 1, 0 ],
825
+ 'LEFT' => [ 115, -1, 1, 0 ],
826
+ 'RIGHT' => [ 116, -1, 1, 0 ],
827
+ 'EXACT' => [ 117, 2, 1, 0 ],
828
+ 'TRIM' => [ 118, 1, 1, 0 ],
829
+ 'REPLACE' => [ 119, 4, 1, 0 ],
830
+ 'SUBSTITUTE' => [ 120, -1, 1, 0 ],
831
+ 'CODE' => [ 121, 1, 1, 0 ],
832
+ 'FIND' => [ 124, -1, 1, 0 ],
833
+ 'CELL' => [ 125, -1, 0, 1 ],
834
+ 'ISERR' => [ 126, 1, 1, 0 ],
835
+ 'ISTEXT' => [ 127, 1, 1, 0 ],
836
+ 'ISNUMBER' => [ 128, 1, 1, 0 ],
837
+ 'ISBLANK' => [ 129, 1, 1, 0 ],
838
+ 'T' => [ 130, 1, 0, 0 ],
839
+ 'N' => [ 131, 1, 0, 0 ],
840
+ 'DATEVALUE' => [ 140, 1, 1, 0 ],
841
+ 'TIMEVALUE' => [ 141, 1, 1, 0 ],
842
+ 'SLN' => [ 142, 3, 1, 0 ],
843
+ 'SYD' => [ 143, 4, 1, 0 ],
844
+ 'DDB' => [ 144, -1, 1, 0 ],
845
+ 'INDIRECT' => [ 148, -1, 1, 1 ],
846
+ 'CALL' => [ 150, -1, 1, 0 ],
847
+ 'CLEAN' => [ 162, 1, 1, 0 ],
848
+ 'MDETERM' => [ 163, 1, 2, 0 ],
849
+ 'MINVERSE' => [ 164, 1, 2, 0 ],
850
+ 'MMULT' => [ 165, 2, 2, 0 ],
851
+ 'IPMT' => [ 167, -1, 1, 0 ],
852
+ 'PPMT' => [ 168, -1, 1, 0 ],
853
+ 'COUNTA' => [ 169, -1, 0, 0 ],
854
+ 'PRODUCT' => [ 183, -1, 0, 0 ],
855
+ 'FACT' => [ 184, 1, 1, 0 ],
856
+ 'DPRODUCT' => [ 189, 3, 0, 0 ],
857
+ 'ISNONTEXT' => [ 190, 1, 1, 0 ],
858
+ 'STDEVP' => [ 193, -1, 0, 0 ],
859
+ 'VARP' => [ 194, -1, 0, 0 ],
860
+ 'DSTDEVP' => [ 195, 3, 0, 0 ],
861
+ 'DVARP' => [ 196, 3, 0, 0 ],
862
+ 'TRUNC' => [ 197, -1, 1, 0 ],
863
+ 'ISLOGICAL' => [ 198, 1, 1, 0 ],
864
+ 'DCOUNTA' => [ 199, 3, 0, 0 ],
865
+ 'ROUNDUP' => [ 212, 2, 1, 0 ],
866
+ 'ROUNDDOWN' => [ 213, 2, 1, 0 ],
867
+ 'RANK' => [ 216, -1, 0, 0 ],
868
+ 'ADDRESS' => [ 219, -1, 1, 0 ],
869
+ 'DAYS360' => [ 220, -1, 1, 0 ],
870
+ 'TODAY' => [ 221, 0, 1, 1 ],
871
+ 'VDB' => [ 222, -1, 1, 0 ],
872
+ 'MEDIAN' => [ 227, -1, 0, 0 ],
873
+ 'SUMPRODUCT' => [ 228, -1, 2, 0 ],
874
+ 'SINH' => [ 229, 1, 1, 0 ],
875
+ 'COSH' => [ 230, 1, 1, 0 ],
876
+ 'TANH' => [ 231, 1, 1, 0 ],
877
+ 'ASINH' => [ 232, 1, 1, 0 ],
878
+ 'ACOSH' => [ 233, 1, 1, 0 ],
879
+ 'ATANH' => [ 234, 1, 1, 0 ],
880
+ 'DGET' => [ 235, 3, 0, 0 ],
881
+ 'INFO' => [ 244, 1, 1, 1 ],
882
+ 'DB' => [ 247, -1, 1, 0 ],
883
+ 'FREQUENCY' => [ 252, 2, 0, 0 ],
884
+ 'ERROR.TYPE' => [ 261, 1, 1, 0 ],
885
+ 'REGISTER.ID' => [ 267, -1, 1, 0 ],
886
+ 'AVEDEV' => [ 269, -1, 0, 0 ],
887
+ 'BETADIST' => [ 270, -1, 1, 0 ],
888
+ 'GAMMALN' => [ 271, 1, 1, 0 ],
889
+ 'BETAINV' => [ 272, -1, 1, 0 ],
890
+ 'BINOMDIST' => [ 273, 4, 1, 0 ],
891
+ 'CHIDIST' => [ 274, 2, 1, 0 ],
892
+ 'CHIINV' => [ 275, 2, 1, 0 ],
893
+ 'COMBIN' => [ 276, 2, 1, 0 ],
894
+ 'CONFIDENCE' => [ 277, 3, 1, 0 ],
895
+ 'CRITBINOM' => [ 278, 3, 1, 0 ],
896
+ 'EVEN' => [ 279, 1, 1, 0 ],
897
+ 'EXPONDIST' => [ 280, 3, 1, 0 ],
898
+ 'FDIST' => [ 281, 3, 1, 0 ],
899
+ 'FINV' => [ 282, 3, 1, 0 ],
900
+ 'FISHER' => [ 283, 1, 1, 0 ],
901
+ 'FISHERINV' => [ 284, 1, 1, 0 ],
902
+ 'FLOOR' => [ 285, 2, 1, 0 ],
903
+ 'GAMMADIST' => [ 286, 4, 1, 0 ],
904
+ 'GAMMAINV' => [ 287, 3, 1, 0 ],
905
+ 'CEILING' => [ 288, 2, 1, 0 ],
906
+ 'HYPGEOMDIST' => [ 289, 4, 1, 0 ],
907
+ 'LOGNORMDIST' => [ 290, 3, 1, 0 ],
908
+ 'LOGINV' => [ 291, 3, 1, 0 ],
909
+ 'NEGBINOMDIST' => [ 292, 3, 1, 0 ],
910
+ 'NORMDIST' => [ 293, 4, 1, 0 ],
911
+ 'NORMSDIST' => [ 294, 1, 1, 0 ],
912
+ 'NORMINV' => [ 295, 3, 1, 0 ],
913
+ 'NORMSINV' => [ 296, 1, 1, 0 ],
914
+ 'STANDARDIZE' => [ 297, 3, 1, 0 ],
915
+ 'ODD' => [ 298, 1, 1, 0 ],
916
+ 'PERMUT' => [ 299, 2, 1, 0 ],
917
+ 'POISSON' => [ 300, 3, 1, 0 ],
918
+ 'TDIST' => [ 301, 3, 1, 0 ],
919
+ 'WEIBULL' => [ 302, 4, 1, 0 ],
920
+ 'SUMXMY2' => [ 303, 2, 2, 0 ],
921
+ 'SUMX2MY2' => [ 304, 2, 2, 0 ],
922
+ 'SUMX2PY2' => [ 305, 2, 2, 0 ],
923
+ 'CHITEST' => [ 306, 2, 2, 0 ],
924
+ 'CORREL' => [ 307, 2, 2, 0 ],
925
+ 'COVAR' => [ 308, 2, 2, 0 ],
926
+ 'FORECAST' => [ 309, 3, 2, 0 ],
927
+ 'FTEST' => [ 310, 2, 2, 0 ],
928
+ 'INTERCEPT' => [ 311, 2, 2, 0 ],
929
+ 'PEARSON' => [ 312, 2, 2, 0 ],
930
+ 'RSQ' => [ 313, 2, 2, 0 ],
931
+ 'STEYX' => [ 314, 2, 2, 0 ],
932
+ 'SLOPE' => [ 315, 2, 2, 0 ],
933
+ 'TTEST' => [ 316, 4, 2, 0 ],
934
+ 'PROB' => [ 317, -1, 2, 0 ],
935
+ 'DEVSQ' => [ 318, -1, 0, 0 ],
936
+ 'GEOMEAN' => [ 319, -1, 0, 0 ],
937
+ 'HARMEAN' => [ 320, -1, 0, 0 ],
938
+ 'SUMSQ' => [ 321, -1, 0, 0 ],
939
+ 'KURT' => [ 322, -1, 0, 0 ],
940
+ 'SKEW' => [ 323, -1, 0, 0 ],
941
+ 'ZTEST' => [ 324, -1, 0, 0 ],
942
+ 'LARGE' => [ 325, 2, 0, 0 ],
943
+ 'SMALL' => [ 326, 2, 0, 0 ],
944
+ 'QUARTILE' => [ 327, 2, 0, 0 ],
945
+ 'PERCENTILE' => [ 328, 2, 0, 0 ],
946
+ 'PERCENTRANK' => [ 329, -1, 0, 0 ],
947
+ 'MODE' => [ 330, -1, 2, 0 ],
948
+ 'TRIMMEAN' => [ 331, 2, 0, 0 ],
949
+ 'TINV' => [ 332, 2, 1, 0 ],
950
+ 'CONCATENATE' => [ 336, -1, 1, 0 ],
951
+ 'POWER' => [ 337, 2, 1, 0 ],
952
+ 'RADIANS' => [ 342, 1, 1, 0 ],
953
+ 'DEGREES' => [ 343, 1, 1, 0 ],
954
+ 'SUBTOTAL' => [ 344, -1, 0, 0 ],
955
+ 'SUMIF' => [ 345, -1, 0, 0 ],
956
+ 'COUNTIF' => [ 346, 2, 0, 0 ],
957
+ 'COUNTBLANK' => [ 347, 1, 0, 0 ],
958
+ 'ROMAN' => [ 354, -1, 1, 0 ]
959
+ }
960
+
961
+ end
962
+
963
+
964
+ end
965
+
966
+ if $0 ==__FILE__
967
+
968
+
969
+ parser = Formula.new
970
+ puts
971
+ puts 'type "Q" to quit.'
972
+ puts
973
+ while true
974
+ puts
975
+ print '? '
976
+ str = gets.chop!
977
+ break if /q/i =~ str
978
+ begin
979
+ e = parser.parse(str)
980
+ p parser.reverse(e)
981
+ rescue ParseError
982
+ puts $!
983
+ end
984
+ end
985
+
986
+ end