lxl 0.3.4 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,28 @@
1
+ 0.3.8
2
+
3
+ - Range#first_colnum and Range#last_colnum added (both use LXL#xlcolnum)
4
+
5
+ - LXL#xlcolnum (maps excel column names to numeric equivalents)
6
+
7
+ - New range formats
8
+ B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
9
+ (the first two become B3:B3)
10
+
11
+ - Demo LXL interface to Jim Weirich�s SpreadSheet object.
12
+ http://onestepback.org/index.cgi/Tech/Ruby/SlowingDownCalculations.rdoc
13
+ http://onestepback.org/cgi-bin/osbwiki.pl?SpreadSheetCode
14
+ => test/lxl_spreadsheet_test.rb
15
+
16
+ - Namespaces mow play nicely with const_missing and method_missing
17
+
18
+ - Range#x_cell methods return to_i
19
+
20
+ - balanced? works on both tokens and types
21
+
22
+ - Argument separator ?, added
23
+
24
+ - General refactoring
25
+
1
26
  0.3.4
2
27
 
3
28
  - Now recognizes complex Range formats
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.4
1
+ 0.3.8
data/lib/lxl.rb CHANGED
@@ -1,69 +1,6 @@
1
1
  # LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas
2
2
  # and is easily extended with new constants and functions.
3
3
  #
4
- # =Namespaces
5
- #
6
- # Extending the language is as easy as defining new constants
7
- # and methods on an LXL::Namespace class.
8
- #
9
- # class MyLXLNamespace < LXL::Namespace
10
- # NAME = 'John Doe'
11
- # def upper(text) text.to_s.upcase end
12
- # end
13
- #
14
- # class MyLXL < LXL::Parser
15
- # def self.namespace_class() MyLXLNamespace end
16
- # end
17
- #
18
- # MyLXL.eval('=UPPER(NAME)')
19
- # # => JOHN DOE
20
- #
21
- # =Ranges
22
- #
23
- # LXL::Range provides Excel-style ranges.
24
- #
25
- # <b>Valid Formats</b>
26
- #
27
- # B3:D5
28
- # Sheet1!B3:D5
29
- # [Book1.xls]Sheet1!B3:D5
30
- #
31
- # <b>Collection</b>
32
- #
33
- # # Ruby Range
34
- # Range.new("B3", "D5").collect
35
- # # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
36
- # "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
37
- # "D0", "D1", "D2", "D3", "D4", "D5"]
38
- #
39
- # # LXL Range
40
- # LXL::Range.new("B3", "D5").collect
41
- # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
42
- #
43
- # =Percentages
44
- #
45
- # 25% => .25
46
- #
47
- # =Deferred function calls
48
- #
49
- # LXL::Deferred snapshots the symbol/arguments of a function call for later use.
50
- #
51
- # class MyNamespace < LXL::Namespace
52
- # register_deferred :foo
53
- # end
54
- # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)').inspect
55
- # # => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
56
- #
57
- # =Symbol registration
58
- #
59
- # Register uppercase constants of the same name and value.
60
- #
61
- # class MyNamespace < LXL::Namespace
62
- # register_symbols :foo, :bar
63
- # end
64
- # LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)').inspect
65
- # # => [:FOO, :BAR]
66
- #
67
4
  # =Operators
68
5
  #
69
6
  # a + b # Add
@@ -105,6 +42,70 @@
105
42
  # LIST (a,b,..) # Variable length list
106
43
  # IN (find,list) # True if find is in the given list (or string)
107
44
  #
45
+ # =Percentages
46
+ #
47
+ # 25% => .25
48
+ # 25.2% => .252
49
+ #
50
+ # =Ranges
51
+ #
52
+ # LXL::Range provides Excel-style ranges.
53
+ #
54
+ # <b>Recognized formats</b>
55
+ #
56
+ # B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
57
+ #
58
+ # (the first two become B3:B3)
59
+ #
60
+ # <b>Collection</b>
61
+ #
62
+ # # Ruby Range
63
+ # Range.new("B3", "D5").collect
64
+ # # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
65
+ # "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
66
+ # "D0", "D1", "D2", "D3", "D4", "D5"]
67
+ #
68
+ # # LXL Range
69
+ # LXL::Range.new("B3", "D5").collect
70
+ # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
71
+ #
72
+ # =Namespaces
73
+ #
74
+ # Extending the language is as easy as defining new constants
75
+ # and methods on an LXL::Namespace.
76
+ #
77
+ # class MyLXLNamespace < LXL::Namespace
78
+ # NAME = 'John Doe'
79
+ # def upper(text) text.to_s.upcase end
80
+ # end
81
+ #
82
+ # class MyLXL < LXL::Parser
83
+ # def self.namespace_class() MyLXLNamespace end
84
+ # end
85
+ #
86
+ # MyLXL.eval('=UPPER(NAME)')
87
+ # # => JOHN DOE
88
+ #
89
+ # =Symbol registration
90
+ #
91
+ # Register uppercase constants of the same name and value.
92
+ #
93
+ # class MyNamespace < LXL::Namespace
94
+ # register_symbols :foo, :bar
95
+ # end
96
+ # LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)')
97
+ # # => [:FOO, :BAR]
98
+ #
99
+ # =Deferred function calls
100
+ #
101
+ # LXL::Deferred snapshots the symbol/arguments of a function call for later use.
102
+ #
103
+ # class MyNamespace < LXL::Namespace
104
+ # register_deferred :foo
105
+ # end
106
+ # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)')
107
+ # # => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
108
+ #
108
109
  # =Notes
109
110
  #
110
111
  # * Values prefixed with = are formulas, anything else is assumed to be text.
@@ -146,12 +147,28 @@ module LXL
146
147
  [nil,false,0].include?(obj) ? false : true
147
148
  end
148
149
 
150
+ # Convert an Excel column name to it's numeric equivalent.
151
+ #
152
+ # ['A', 'F', 'AD', 'BK'].collect { |c| xlcolnum(c) } # => [1, 6, 30, 63]
153
+ #
154
+ def xlcolnum(colname)
155
+ count = 0
156
+ letters = colname.to_s.split(//)
157
+ map = Proc.new { |c| ::Range.new('A','Z').collect.index(c)+1 }
158
+ letters[0..-2].each { |l| count += 26*map.call(l) }
159
+ count += map.call(letters.last)
160
+ count
161
+ end
162
+
149
163
  end
150
164
 
151
165
  # =Lexical Types
152
166
  #
153
- # w Whitespace (includes commas)
154
- # ; Semi-Colon (statement separator)
167
+ # ; Statement separator
168
+ # , Argument separator
169
+ # ( Tuple open
170
+ # ) Tuple close
171
+ # w Whitespace
155
172
  # o Operator
156
173
  # s String
157
174
  # r Range
@@ -159,16 +176,12 @@ end
159
176
  # f Float
160
177
  # i Integer
161
178
  # t Token
162
- # ( Open (
163
- # ) Close )
164
179
  #
165
180
  # F Function
166
181
  # C Constant
167
182
  #
168
183
  class LXL::Parser
169
184
 
170
- attr_reader :namespace, :lexer
171
-
172
185
  RUBY_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '==', '!=', '<', '>', '+', '**']
173
186
  EXCEL_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '=', '<>', '<', '>', '&', '^' ]
174
187
 
@@ -201,10 +214,13 @@ class LXL::Parser
201
214
  def initialize(namespace=self.class.namespace_class.new)
202
215
  @namespace = namespace
203
216
  ops = EXCEL_OPERATORS.collect { |v| Regexp.quote(v) }.join('|')
204
- xlr = Regexp.new('\A'+LXL::Range::EXCEL_RANGE.source, true)
217
+ xlr = LXL::Range::EXCEL_RANGE
205
218
  @lexer = self.class.lexer_class.new([
206
- [/\A[\s,]+/, ?w], # Whitespace (includes Commas)
207
- [/\A;/, ?;], # Semi-Colon (statement separator)
219
+ [/\A;+/, ?;], # Statement separator
220
+ [/\A,+/, ?,], # Argument separator
221
+ [/\A\(/, ?(], # Tuple open
222
+ [/\A\)/, ?)], # Tuple close
223
+ [/\A\s+/, ?w], # Whitespace
208
224
  [/\A(#{ops})/, ?o], # Operator
209
225
  [/\A("([^"]|"")*")/m, ?s], # String
210
226
  [xlr, ?r], # Range
@@ -212,25 +228,23 @@ class LXL::Parser
212
228
  [/\A\d+\.\d+/, ?f], # Float
213
229
  [/\A\d+/, ?i], # Integer
214
230
  [/\A\w+/, ?t], # Token
215
- [/\A\(/, ?(], # Open (
216
- [/\A\)/, ?)], # Close )
217
231
  ], false)
218
232
  end
219
233
 
220
234
  # Evaluate a formula.
221
235
  #
222
236
  def eval(formula)
223
- formulas = [[]]
224
- formula = formula.to_s.gsub(/(\n)(\s*=)/, '\1;\2') # infer statement separators
237
+ formulas = Array.new << Array.new
238
+ formula = formula.to_s.gsub(/(\n)(\s*=)/,'\1;\2') # infer statement separators
225
239
  types,tokens = tokenize(formula)
226
- tokens.each_index { |i| tokens[i] == ';' ? formulas << [] : formulas.last << [types[i], tokens[i]] }
240
+ tokens.each_index { |i| tokens[i] == ';' ? formulas << Array.new : formulas.last << [types[i], tokens[i]] }
227
241
  formulas.collect! { |f|
228
242
  types = f.collect { |t| t.first }
229
243
  tokens = f.collect { |t| t.last }
230
244
  token = tokens.join.strip
231
- if token =~ /^=/
245
+ if token =~ /\A=/
232
246
  tokens.each_index { |i| tokens[i] = translate_quotes(tokens[i]) if types[i] == ?s }
233
- token = tokens.join.strip.gsub(/^=+/, '')
247
+ token = tokens.join.strip.gsub(/\A=+/,'')
234
248
  else
235
249
  token = translate_quotes(quote(tokens.join.strip))
236
250
  end
@@ -261,41 +275,48 @@ class LXL::Parser
261
275
  end
262
276
 
263
277
  # Tokenize a formula into <tt>[TypesString, TokensArray]</tt>.
264
- #
278
+ #--
279
+ # const_missing is defined by Ruby, raises a NameError.
280
+ # method_missing is not, Ruby uses user-defined if present, NoMethodError otherwise.
281
+ #++
265
282
  def tokenize(formula)
266
283
  ops = Hash[*EXCEL_OPERATORS.zip(RUBY_OPERATORS).flatten]
267
284
 
268
285
  # Parse formula
269
286
  types,tokens = @lexer.scan(formula.to_s)
270
- raise SyntaxError, 'unbalanced parentheses' unless balanced?(tokens)
287
+ raise SyntaxError, 'unbalanced parentheses' unless balanced?(types)
271
288
 
272
289
  # Parse tokens
273
290
  tokens.each_index do |i|
274
291
  type, token = types[i], tokens[i]
275
292
  tokens[i] = case type
276
293
  when ?o then ops[token]
277
- when ?r then "self.class.range_class.new(*#{token.split(':').inspect})"
278
294
  when ?p then token.to_f/100
279
295
  when ?f then token.to_f
280
296
  when ?i then token.to_i
297
+ when ?r then
298
+ range = token.split(/:/)
299
+ range[1] = range[0] if range.size == 1
300
+ "self.class.range_class.new(*#{range.inspect})"
281
301
  when ?t then
282
302
  upper = token.to_s.upcase
283
303
  lower = token.to_s.downcase
284
304
  raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
285
- if @namespace.respond_to?(lower)
286
- if tokens[i+1] != '('
287
- raise ArgumentError, "wrong number of arguments for #{token}"
288
- else
305
+ custom_const_missing = begin [@namespace.const_get(:ConstMissing),true].last rescue false end
306
+ # Constants
307
+ if tokens[i+1] != '('
308
+ if @namespace.const_defined?(upper) or custom_const_missing
309
+ types[i] = ?C
310
+ "@namespace.const_get(:#{upper})"
311
+ else token; end
312
+ # Functions
313
+ else
314
+ if @namespace.respond_to?(lower) or @namespace.respond_to?(:method_missing)
289
315
  types[i] = ?F
290
- "@namespace.method(:#{lower}).call"
291
- end
292
- elsif @namespace.const_defined?(upper)
293
- types[i] = ?C
294
- "@namespace.const_get(:#{upper})"
295
- else token
316
+ "@namespace.#{lower}"
317
+ else token; end
296
318
  end
297
- else token
298
- end
319
+ else token; end
299
320
  end
300
321
 
301
322
  [types,tokens]
@@ -303,20 +324,20 @@ class LXL::Parser
303
324
 
304
325
  # Check that parentheses are balanced.
305
326
  #
306
- def balanced?(tokens)
307
- tokens.find_all { |t| ['(', ')'].include?(t) }.size % 2 == 0
327
+ def balanced?(list)
328
+ list.to_s.gsub(/[^()]/,'').size % 2 == 0
308
329
  end
309
330
 
310
331
  end
311
332
 
312
333
  # Excel-style ranges.
313
334
  #
314
- # =Valid formats
335
+ # =Recognized formats
336
+ #
337
+ # B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
338
+ #
339
+ # (the first two become B3:B3)
315
340
  #
316
- # B3:D5
317
- # Sheet1!B3:D5
318
- # [Book1.xls]Sheet1!B3:D5
319
- #
320
341
  # =Collection
321
342
  #
322
343
  # # Ruby Range
@@ -331,35 +352,32 @@ end
331
352
  #
332
353
  class LXL::Range < Range
333
354
 
334
- # B3, Sheet1!B3, [Book1.xls]Sheet1!B3
335
- EXCEL_VECTOR = /(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+)/i
336
-
337
- # B3:D5, Sheet1!B3:D5, [Book1.xls]Sheet1!B3:D5
338
- EXCEL_RANGE = /(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+):([A-Z]+[1-9]+)/i
355
+ # B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
356
+ EXCEL_RANGE = /\A(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+)(:([A-Z]+[1-9]+)?)?/i
339
357
 
340
- # True if this is an Excel range.
341
- attr_accessor :excel
342
-
343
- # Given workbook name.
358
+ # Workbook name.
344
359
  attr_accessor :book
345
360
 
346
- # Given worksheet name.
361
+ # Worksheet name.
347
362
  attr_accessor :sheet
348
363
 
349
364
  def self.new(first, last)
350
- excel = (first =~ EXCEL_VECTOR && last =~ EXCEL_VECTOR)
365
+ excel = (first =~ EXCEL_RANGE && last =~ EXCEL_RANGE)
351
366
  if excel
352
- first =~ EXCEL_VECTOR
367
+ first =~ EXCEL_RANGE
353
368
  book,sheet,first = $2,$4,$5
354
369
  obj = super(*[first.upcase,last.upcase].sort)
355
- obj.excel = true
370
+ obj.excel!
356
371
  obj.book = book
357
372
  obj.sheet = sheet
373
+ obj
358
374
  else
359
- obj = super
360
- obj.excel = false
375
+ super
361
376
  end
362
- obj
377
+ end
378
+
379
+ def excel!
380
+ @excel = true
363
381
  end
364
382
 
365
383
  def excel?
@@ -367,26 +385,34 @@ class LXL::Range < Range
367
385
  end
368
386
 
369
387
  def first_column
370
- first.to_s.upcase.gsub(/[^A-Z]/, '')
388
+ first.to_s.upcase.gsub(/[^A-Z]/,'')
371
389
  end
372
390
 
373
391
  def last_column
374
- last.to_s.upcase.gsub(/[^A-Z]/, '')
392
+ last.to_s.upcase.gsub(/[^A-Z]/,'')
375
393
  end
376
394
 
395
+ def first_colnum
396
+ LXL.xlcolnum(first_column)
397
+ end
398
+
399
+ def last_colnum
400
+ LXL.xlcolnum(last_column)
401
+ end
402
+
377
403
  def first_cell
378
- first.to_s.gsub(/[^1-9]/, '')
404
+ first.to_s.gsub(/[^1-9]/,'').to_i
379
405
  end
380
406
 
381
407
  def last_cell
382
- last.to_s.gsub(/[^1-9]/, '')
408
+ last.to_s.gsub(/[^1-9]/,'').to_i
383
409
  end
384
410
 
385
411
  def each
386
412
  if excel?
387
413
  Range.new(first_column, last_column).each do |column|
388
414
  Range.new(first_cell, last_cell).each do |cell|
389
- yield column+cell
415
+ yield column+cell.to_s
390
416
  end
391
417
  end
392
418
  else
@@ -401,7 +427,7 @@ end
401
427
  # class MyNamespace < LXL::Namespace
402
428
  # register_deferred :foo
403
429
  # end
404
- # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)').inspect
430
+ # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)')
405
431
  # # => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
406
432
  #
407
433
  class LXL::Deferred
@@ -445,7 +471,7 @@ class LXL::EmptyNamespace
445
471
  # class MyNamespace < LXL::Namespace
446
472
  # register_deferred :foo
447
473
  # end
448
- # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)').inspect
474
+ # LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)')
449
475
  # # => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
450
476
  #
451
477
  def self.register_deferred(*symbols)
@@ -458,7 +484,7 @@ class LXL::EmptyNamespace
458
484
  # class MyNamespace < LXL::Namespace
459
485
  # register_symbols :foo, :bar
460
486
  # end
461
- # LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)').inspect
487
+ # LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)')
462
488
  # # => [:FOO, :BAR]
463
489
  #
464
490
  def self.register_symbols(*symbols)
@@ -540,41 +566,41 @@ class LXL::Lexer #:nodoc: all
540
566
 
541
567
  class LexerJammed < Exception; end
542
568
 
543
- def initialize(regex_to_char,skip_white_space=true)
544
- @skip_white_space = skip_white_space
545
- @regex_to_char = regex_to_char
569
+ def initialize(re_to_chr, skip_whitespace=true)
570
+ @re_to_chr = re_to_chr
571
+ @skip_whitespace = skip_whitespace
546
572
  end
547
573
 
548
- def scan(string,string_token_list=nil)
549
- result_string = String.new
550
- token_list = Array.new
551
- if string_token_list
552
- next_token(string) do |t,token, tail|
553
- result_string << t
554
- token_list << [string_token_list[0...tail], string[0...tail]]
574
+ def scan(string, string_tokens=nil)
575
+ types = String.new
576
+ tokens = Array.new
577
+ if string_tokens
578
+ next_token(string) do |t,token,tail|
579
+ types << t
580
+ tokens << [string_tokens[0...tail], string[0...tail]]
555
581
  string = string[tail..-1]
556
- string_token_list = string_token_list[tail..-1]
582
+ string_tokens = string_tokens[tail..-1]
557
583
  end
558
584
  else
559
- next_token(string) do |t,token, tail|
560
- result_string << t
561
- token_list << token
585
+ next_token(string) do |t,token,tail|
586
+ types << t
587
+ tokens << token
562
588
  end
563
589
  end
564
- return result_string, token_list
590
+ return types,tokens
565
591
  end
566
592
 
567
593
  private
568
594
 
569
- def next_token( string)
595
+ def next_token(string)
570
596
  match_data = nil
571
597
  while ! string.empty?
572
598
  failed = true
573
- @regex_to_char.each do |regex,char|
574
- match_data = regex.match(string)
599
+ @re_to_chr.each do |re,chr|
600
+ match_data = re.match(string)
575
601
  next unless match_data
576
602
  token = match_data[0]
577
- yield char,token, match_data.end(0) unless char == ?\s && @skip_white_space
603
+ yield chr,token, match_data.end(0) unless chr == ?\s && @skip_whitespace
578
604
  string = match_data.post_match
579
605
  failed = false
580
606
  break
@@ -0,0 +1,45 @@
1
+ #--
2
+ # LXL interface to Jim Weirich�s SpreadSheet object.
3
+ # http://onestepback.org/index.cgi/Tech/Ruby/SlowingDownCalculations.rdoc
4
+ # http://onestepback.org/cgi-bin/osbwiki.pl?SpreadSheetCode
5
+ #
6
+ # NOTE: Spreadsheet#[a,b] does single lookups only.
7
+ #++
8
+
9
+ $LOAD_PATH.unshift('../lib')
10
+ require 'test/unit'
11
+ require 'spreadsheet'
12
+ require 'lxl'
13
+
14
+ #--
15
+ # Spreadsheet
16
+ #++
17
+
18
+ $ss = SpreadSheet.new
19
+ $ss[1,1].value = 5
20
+ $ss[1,2].value = $ss[1,1] * 6
21
+ $ss[1,3].value = $ss[1,2] * 7
22
+
23
+ #--
24
+ # LXL Interface
25
+ #++
26
+
27
+ class MyNamespace < LXL::EmptyNamespace
28
+ def lookup(range)
29
+ $ss[range.first_colnum, range.first_cell].value
30
+ end
31
+ end
32
+
33
+ class MyLXL < LXL::Parser
34
+ def self.namespace_class() MyNamespace end
35
+ end
36
+
37
+ #--
38
+ # Test
39
+ #++
40
+
41
+ class SpreadsheetTest < Test::Unit::TestCase
42
+ def test_lookup
43
+ assert_equal($ss[1,3].value, MyLXL.eval('=LOOKUP(A3)'))
44
+ end
45
+ end
@@ -17,7 +17,7 @@ class LXLTest < Test::Unit::TestCase
17
17
  def test_multiple_formula
18
18
  formulas = %{
19
19
  This is some text;
20
- "This is some ""quoted"" text";
20
+ "This is some ""quoted"" text"
21
21
  ="This is some ""quoted"" text"
22
22
  =";"=";"
23
23
  =";"=":"
@@ -40,8 +40,8 @@ class LXLTest < Test::Unit::TestCase
40
40
  ="embedded percentages 25% and semi-colons ; are working properly"
41
41
  }
42
42
  expected = ["This is some text", "\"This is some \"quoted\" text\"", "This is some \"quoted\" text"]
43
- expected += [true, false, :A, :B, false, 6, true, true, [1, "two", 3.0], true, false, true, "yes", "this and that"]
44
- expected += [8, 0.502, true, true, "embedded percentages 25% and semi-colons ; are working properly"]
43
+ expected += [true, false, :A, :B, false, 6, true, true, [1, 'two', 3.0], true, false, true, 'yes', 'this and that']
44
+ expected += [8, 0.502, true, true, 'embedded percentages 25% and semi-colons ; are working properly']
45
45
  MyNamespace.register_symbols(:A, :B)
46
46
  lxl = LXL::Parser.new(MyNamespace.new)
47
47
  results = lxl.eval(formulas)
@@ -53,13 +53,19 @@ class LXLTest < Test::Unit::TestCase
53
53
  res = lxl.eval('=LIST(d5:b3, "x")')
54
54
  range = res.first
55
55
  assert_equal(LXL::Range, range.class)
56
- assert_equal("B3", range.min)
57
- assert_equal("D5", range.max)
58
- assert_equal(range.collect.join(','), "B3,B4,B5,C3,C4,C5,D3,D4,D5")
59
- assert_equal(range.first_column, 'B')
60
- assert_equal(range.last_column, 'D')
61
- assert_equal(range.first_cell, '3')
62
- assert_equal(range.last_cell, '5')
56
+ assert_equal('B3', range.min)
57
+ assert_equal('D5', range.max)
58
+ assert_equal('B3,B4,B5,C3,C4,C5,D3,D4,D5', range.collect.join(','))
59
+ assert_equal('B', range.first_column)
60
+ assert_equal('D', range.last_column)
61
+ assert_equal(3, range.first_cell)
62
+ assert_equal(5, range.last_cell)
63
+ res = lxl.eval('=B3; =B3:; =B3:D5; =Sheet1!B3:D5; =[Book1]Sheet1!B3:D5; =[file.xls]Sheet1!B3:D5')
64
+ assert_equal(3, res[0].last_cell)
65
+ assert_equal(3, res[1].last_cell)
66
+ assert_equal(nil, res[2].sheet)
67
+ assert_equal('file.xls', res[5].book)
68
+ assert_equal('Sheet1', res[5].sheet)
63
69
  end
64
70
 
65
71
  def test_deferred
@@ -77,16 +83,16 @@ class LXLTest < Test::Unit::TestCase
77
83
  assert_equal('B', range.first_column)
78
84
  range = lxl.eval('=Sheet1!B3:D5')
79
85
  assert_equal('B', range.first_column)
80
- assert_equal('5', range.last_cell)
86
+ assert_equal(5, range.last_cell)
81
87
  assert_equal('Sheet1', range.sheet)
82
88
  range = lxl.eval('=[Book.xls]Sheet2!B5:D6')
83
89
  assert_equal('B', range.first_column)
84
- assert_equal('6', range.last_cell)
90
+ assert_equal(6, range.last_cell)
85
91
  assert_equal('Book.xls', range.book)
86
92
  assert_equal('Sheet2', range.sheet)
87
93
  range = lxl.eval('=[Book1]Sheet3!B7:D8')
88
94
  assert_equal('B', range.first_column)
89
- assert_equal('8', range.last_cell)
95
+ assert_equal(8, range.last_cell)
90
96
  assert_equal('Book1', range.book)
91
97
  assert_equal('Sheet3', range.sheet)
92
98
  end
@@ -102,8 +108,8 @@ class LXLTest < Test::Unit::TestCase
102
108
  assert_equal('no', ns.if(false, 'yes', 'no'))
103
109
  assert_equal(true, ns.in(1, [1,2]))
104
110
  assert_equal(false, ns.in(1, [3,4]))
105
- assert_equal(true, ns.in("a", "ab"))
106
- assert_equal(false, ns.in("a", "cd"))
111
+ assert_equal(true, ns.in('a', 'ab'))
112
+ assert_equal(false, ns.in('a', 'cd'))
107
113
  assert_equal(true, ns.datetime('Jan 1, 2005') == ns.date(2005,1,1))
108
114
  assert_equal(false, ns.datetime('Jan 1, 2005 12:30') == ns.date(2005,1,1)+ns.time(12,30,01))
109
115
  assert_equal('[1, "two", 3]', ns.list(1,'two',3).inspect)
@@ -0,0 +1,85 @@
1
+ #--
2
+ # From Jim Weirich�s blog { | one, step, back | }
3
+ # http://onestepback.org/index.cgi/Tech/Ruby/SlowingDownCalculations.rdoc
4
+ # http://onestepback.org/cgi-bin/osbwiki.pl?SpreadSheetCode
5
+ #++
6
+
7
+ class BlankSlate
8
+ safe_methods = ['__id__', '__send__']
9
+ (instance_methods-safe_methods).each { |m| undef_method m }
10
+ end
11
+
12
+ module Kernel
13
+ def formula_value
14
+ self
15
+ end
16
+ end
17
+
18
+ class SpreadSheet
19
+ def initialize
20
+ @hash = Hash.new
21
+ end
22
+
23
+ def [](r,c)
24
+ @hash["#{r}@#{c}"] ||= Cell.new
25
+ end
26
+ end
27
+
28
+ class Formula < BlankSlate
29
+ def method_missing(sym, *args, &block)
30
+ Deferred.new(self, sym, args, block)
31
+ end
32
+
33
+ def coerce(other)
34
+ [Const.new(other), self]
35
+ end
36
+
37
+ def formula_value
38
+ fail "Subclass Responsibility"
39
+ end
40
+ end
41
+
42
+ class Deferred < Formula
43
+ attr_reader :operation, :args, :target
44
+ def initialize(target, operation, args, block)
45
+ @target = target
46
+ @operation = operation
47
+ @args = args
48
+ @block = block
49
+ end
50
+
51
+ def formula_value
52
+ @target.formula_value.send(@operation, *eval_args, &@block)
53
+ end
54
+
55
+ private
56
+
57
+ def eval_args
58
+ @args.collect { |a| a.formula_value }
59
+ end
60
+ end
61
+
62
+ class Const < Formula
63
+ attr_reader :formula_value
64
+ def initialize(value)
65
+ @formula_value = value
66
+ end
67
+ end
68
+
69
+ class Cell < Formula
70
+ def initialize
71
+ @formula = 0
72
+ end
73
+
74
+ def value
75
+ @formula.formula_value
76
+ end
77
+
78
+ def value=(new_value)
79
+ @formula = new_value
80
+ end
81
+
82
+ def formula_value
83
+ @formula.formula_value
84
+ end
85
+ end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.3
3
3
  specification_version: 1
4
4
  name: lxl
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.4
7
- date: 2005-02-14
6
+ version: 0.3.8
7
+ date: 2005-02-15
8
8
  summary: LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas. Easily extended with new constants and functions.
9
9
  require_paths:
10
10
  - lib
@@ -35,7 +35,9 @@ files:
35
35
  - setup.rb
36
36
  - README.en
37
37
  - lib/lxl.rb
38
+ - test/spreadsheet.rb
38
39
  - test/lxl_test.rb
40
+ - test/lxl_spreadsheet_test.rb
39
41
  test_files:
40
42
  - test/lxl_test.rb
41
43
  rdoc_options: