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