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 +25 -0
- data/VERSION +1 -1
- data/lib/lxl.rb +170 -144
- data/test/lxl_spreadsheet_test.rb +45 -0
- data/test/lxl_test.rb +21 -15
- data/test/spreadsheet.rb +85 -0
- metadata +4 -2
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.
|
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
|
-
#
|
154
|
-
#
|
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 =
|
217
|
+
xlr = LXL::Range::EXCEL_RANGE
|
205
218
|
@lexer = self.class.lexer_class.new([
|
206
|
-
[/\A
|
207
|
-
[/\A
|
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*=)/,
|
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 <<
|
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?(
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
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?(
|
307
|
-
|
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
|
-
# =
|
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
|
335
|
-
|
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
|
-
#
|
341
|
-
attr_accessor :excel
|
342
|
-
|
343
|
-
# Given workbook name.
|
358
|
+
# Workbook name.
|
344
359
|
attr_accessor :book
|
345
360
|
|
346
|
-
#
|
361
|
+
# Worksheet name.
|
347
362
|
attr_accessor :sheet
|
348
363
|
|
349
364
|
def self.new(first, last)
|
350
|
-
excel = (first =~
|
365
|
+
excel = (first =~ EXCEL_RANGE && last =~ EXCEL_RANGE)
|
351
366
|
if excel
|
352
|
-
first =~
|
367
|
+
first =~ EXCEL_RANGE
|
353
368
|
book,sheet,first = $2,$4,$5
|
354
369
|
obj = super(*[first.upcase,last.upcase].sort)
|
355
|
-
obj.excel
|
370
|
+
obj.excel!
|
356
371
|
obj.book = book
|
357
372
|
obj.sheet = sheet
|
373
|
+
obj
|
358
374
|
else
|
359
|
-
|
360
|
-
obj.excel = false
|
375
|
+
super
|
361
376
|
end
|
362
|
-
|
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)')
|
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)')
|
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)')
|
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(
|
544
|
-
@
|
545
|
-
@
|
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,
|
549
|
-
|
550
|
-
|
551
|
-
if
|
552
|
-
next_token(string) do |t,token,
|
553
|
-
|
554
|
-
|
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
|
-
|
582
|
+
string_tokens = string_tokens[tail..-1]
|
557
583
|
end
|
558
584
|
else
|
559
|
-
next_token(string) do |t,token,
|
560
|
-
|
561
|
-
|
585
|
+
next_token(string) do |t,token,tail|
|
586
|
+
types << t
|
587
|
+
tokens << token
|
562
588
|
end
|
563
589
|
end
|
564
|
-
return
|
590
|
+
return types,tokens
|
565
591
|
end
|
566
592
|
|
567
593
|
private
|
568
594
|
|
569
|
-
def next_token(
|
595
|
+
def next_token(string)
|
570
596
|
match_data = nil
|
571
597
|
while ! string.empty?
|
572
598
|
failed = true
|
573
|
-
@
|
574
|
-
match_data =
|
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
|
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
|
data/test/lxl_test.rb
CHANGED
@@ -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,
|
44
|
-
expected += [8, 0.502, true, true,
|
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(
|
57
|
-
assert_equal(
|
58
|
-
assert_equal(
|
59
|
-
assert_equal(range.first_column
|
60
|
-
assert_equal(range.last_column
|
61
|
-
assert_equal(range.first_cell
|
62
|
-
assert_equal(range.last_cell
|
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(
|
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(
|
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(
|
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(
|
106
|
-
assert_equal(false, ns.in(
|
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)
|
data/test/spreadsheet.rb
ADDED
@@ -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.
|
7
|
-
date: 2005-02-
|
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:
|