lxl 0.3.4 → 0.3.8

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.
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: