lxl 0.3.1 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +12 -0
- data/VERSION +1 -1
- data/lib/lxl.rb +102 -32
- data/lxl.gemspec +1 -0
- data/test/lxl_test.rb +35 -6
- metadata +5 -4
- data/lxl.rdoc +0 -114
data/CHANGES
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
0.3.4
|
2
|
+
|
3
|
+
- Now recognizes complex Range formats
|
4
|
+
|
5
|
+
B3:D5
|
6
|
+
Sheet1!B3:D5
|
7
|
+
[Book1.xls]Sheet1!B3:D5
|
8
|
+
|
9
|
+
LXL::Range has attr_accessors for :book and :sheet
|
10
|
+
|
11
|
+
- Documentation
|
12
|
+
|
1
13
|
0.3.1
|
2
14
|
|
3
15
|
- LXL::LittleLexer => LXL::Lexer, types is a string again.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.3.
|
1
|
+
0.3.4
|
data/lib/lxl.rb
CHANGED
@@ -20,12 +20,17 @@
|
|
20
20
|
#
|
21
21
|
# =Ranges
|
22
22
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
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
|
27
30
|
#
|
28
|
-
#
|
31
|
+
# <b>Collection</b>
|
32
|
+
#
|
33
|
+
# # Ruby Range
|
29
34
|
# Range.new("B3", "D5").collect
|
30
35
|
# # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
31
36
|
# "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
|
@@ -33,7 +38,7 @@
|
|
33
38
|
#
|
34
39
|
# # LXL Range
|
35
40
|
# LXL::Range.new("B3", "D5").collect
|
36
|
-
#
|
41
|
+
# # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
|
37
42
|
#
|
38
43
|
# =Percentages
|
39
44
|
#
|
@@ -71,9 +76,9 @@
|
|
71
76
|
# a > b # Greater than
|
72
77
|
# a <= b # Less than or equal to
|
73
78
|
# a >= b # Greater than or equal to
|
74
|
-
# a &
|
75
|
-
#
|
76
|
-
#
|
79
|
+
# a & b # String concentation
|
80
|
+
# a ^ b # Exponential
|
81
|
+
#
|
77
82
|
# =Constants
|
78
83
|
#
|
79
84
|
# TRUE # true
|
@@ -98,7 +103,7 @@
|
|
98
103
|
# =List Functions
|
99
104
|
#
|
100
105
|
# LIST (a,b,..) # Variable length list
|
101
|
-
# IN (find,list) # True if find is
|
106
|
+
# IN (find,list) # True if find is in the given list (or string)
|
102
107
|
#
|
103
108
|
# =Notes
|
104
109
|
#
|
@@ -196,12 +201,13 @@ class LXL::Parser
|
|
196
201
|
def initialize(namespace=self.class.namespace_class.new)
|
197
202
|
@namespace = namespace
|
198
203
|
ops = EXCEL_OPERATORS.collect { |v| Regexp.quote(v) }.join('|')
|
204
|
+
xlr = Regexp.new('\A'+LXL::Range::EXCEL_RANGE.source, true)
|
199
205
|
@lexer = self.class.lexer_class.new([
|
200
206
|
[/\A[\s,]+/, ?w], # Whitespace (includes Commas)
|
201
207
|
[/\A;/, ?;], # Semi-Colon (statement separator)
|
202
208
|
[/\A(#{ops})/, ?o], # Operator
|
203
209
|
[/\A("([^"]|"")*")/m, ?s], # String
|
204
|
-
[
|
210
|
+
[xlr, ?r], # Range
|
205
211
|
[/\A\d+(\.\d+)?%/, ?p], # Percentage
|
206
212
|
[/\A\d+\.\d+/, ?f], # Float
|
207
213
|
[/\A\d+/, ?i], # Integer
|
@@ -273,9 +279,9 @@ class LXL::Parser
|
|
273
279
|
when ?f then token.to_f
|
274
280
|
when ?i then token.to_i
|
275
281
|
when ?t then
|
276
|
-
upper = token.to_s.upcase
|
277
|
-
lower = token.to_s.downcase
|
278
|
-
raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(
|
282
|
+
upper = token.to_s.upcase
|
283
|
+
lower = token.to_s.downcase
|
284
|
+
raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
|
279
285
|
if @namespace.respond_to?(lower)
|
280
286
|
if tokens[i+1] != '('
|
281
287
|
raise ArgumentError, "wrong number of arguments for #{token}"
|
@@ -303,31 +309,91 @@ class LXL::Parser
|
|
303
309
|
|
304
310
|
end
|
305
311
|
|
306
|
-
# Excel-style ranges.
|
312
|
+
# Excel-style ranges.
|
313
|
+
#
|
314
|
+
# =Valid formats
|
315
|
+
#
|
316
|
+
# B3:D5
|
317
|
+
# Sheet1!B3:D5
|
318
|
+
# [Book1.xls]Sheet1!B3:D5
|
319
|
+
#
|
320
|
+
# =Collection
|
307
321
|
#
|
322
|
+
# # Ruby Range
|
308
323
|
# Range.new("B3", "D5").collect
|
309
324
|
# # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
310
325
|
# "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
|
311
326
|
# "D0", "D1", "D2", "D3", "D4", "D5"]
|
312
|
-
#
|
327
|
+
#
|
328
|
+
# # LXL Range
|
313
329
|
# LXL::Range.new("B3", "D5").collect
|
314
|
-
#
|
315
|
-
#
|
330
|
+
# # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
|
331
|
+
#
|
316
332
|
class LXL::Range < Range
|
317
333
|
|
318
|
-
|
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
|
339
|
+
|
340
|
+
# True if this is an Excel range.
|
341
|
+
attr_accessor :excel
|
342
|
+
|
343
|
+
# Given workbook name.
|
344
|
+
attr_accessor :book
|
345
|
+
|
346
|
+
# Given worksheet name.
|
347
|
+
attr_accessor :sheet
|
348
|
+
|
349
|
+
def self.new(first, last)
|
350
|
+
excel = (first =~ EXCEL_VECTOR && last =~ EXCEL_VECTOR)
|
351
|
+
if excel
|
352
|
+
first =~ EXCEL_VECTOR
|
353
|
+
book,sheet,first = $2,$4,$5
|
354
|
+
obj = super(*[first.upcase,last.upcase].sort)
|
355
|
+
obj.excel = true
|
356
|
+
obj.book = book
|
357
|
+
obj.sheet = sheet
|
358
|
+
else
|
359
|
+
obj = super
|
360
|
+
obj.excel = false
|
361
|
+
end
|
362
|
+
obj
|
363
|
+
end
|
364
|
+
|
365
|
+
def excel?
|
366
|
+
@excel ? true : false
|
367
|
+
end
|
368
|
+
|
369
|
+
def first_column
|
370
|
+
first.to_s.upcase.gsub(/[^A-Z]/, '')
|
371
|
+
end
|
372
|
+
|
373
|
+
def last_column
|
374
|
+
last.to_s.upcase.gsub(/[^A-Z]/, '')
|
375
|
+
end
|
376
|
+
|
377
|
+
def first_cell
|
378
|
+
first.to_s.gsub(/[^1-9]/, '')
|
379
|
+
end
|
380
|
+
|
381
|
+
def last_cell
|
382
|
+
last.to_s.gsub(/[^1-9]/, '')
|
383
|
+
end
|
319
384
|
|
320
385
|
def each
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
386
|
+
if excel?
|
387
|
+
Range.new(first_column, last_column).each do |column|
|
388
|
+
Range.new(first_cell, last_cell).each do |cell|
|
389
|
+
yield column+cell
|
390
|
+
end
|
391
|
+
end
|
326
392
|
else
|
327
393
|
super
|
328
394
|
end
|
329
395
|
end
|
330
|
-
|
396
|
+
|
331
397
|
end
|
332
398
|
|
333
399
|
# Snapshots the symbol/arguments of a function call for later use.
|
@@ -353,9 +419,19 @@ end
|
|
353
419
|
#
|
354
420
|
class LXL::EmptyNamespace
|
355
421
|
|
356
|
-
#
|
422
|
+
# Ensure all constants are included (inherited, extended, included, etc).
|
423
|
+
#
|
424
|
+
def self.const_defined?(symbol) #:nodoc:
|
425
|
+
constants.include?(symbol.to_s)
|
426
|
+
end
|
427
|
+
|
428
|
+
# Create instance methods for constant-related methods
|
357
429
|
['constants', 'const_defined?', 'const_get', 'const_set'].each { |m| define_method(m) { |s| self.class.send(m,s) } }
|
430
|
+
|
431
|
+
# Preserved methods
|
358
432
|
METHODS = ['__id__', '__send__', 'constants', 'const_defined?', 'const_get', 'const_set', 'class', 'instance_methods', 'method', 'methods', 'respond_to?', 'to_s']
|
433
|
+
|
434
|
+
# Clear all unessecary methods
|
359
435
|
(instance_methods-METHODS).sort.each { |m| undef_method m }
|
360
436
|
|
361
437
|
# Default deferred class.
|
@@ -364,12 +440,6 @@ class LXL::EmptyNamespace
|
|
364
440
|
LXL::Deferred
|
365
441
|
end
|
366
442
|
|
367
|
-
# Ensure all constants are included (inherited, extended, included, etc).
|
368
|
-
#
|
369
|
-
def self.const_defined?(symbol) #:nodoc:
|
370
|
-
constants.include?(symbol.to_s)
|
371
|
-
end
|
372
|
-
|
373
443
|
# Register lowercase methods which return a Deferred function call.
|
374
444
|
#
|
375
445
|
# class MyNamespace < LXL::Namespace
|
data/lxl.gemspec
CHANGED
@@ -14,6 +14,7 @@ spec = Gem::Specification.new do |s|
|
|
14
14
|
s.files = Dir['**/*'].delete_if { |f| f =~ /(cvs|gem|svn)$/i }
|
15
15
|
s.require_path = 'lib'
|
16
16
|
s.test_file = "test/lxl_test.rb"
|
17
|
+
s.rdoc_options << '--all' << '--inline-source'
|
17
18
|
s.has_rdoc = true
|
18
19
|
s.rubyforge_project = "lxl"
|
19
20
|
end
|
data/test/lxl_test.rb
CHANGED
@@ -51,10 +51,15 @@ class LXLTest < Test::Unit::TestCase
|
|
51
51
|
def test_range
|
52
52
|
lxl = LXL::Parser.new(MyNamespace.new)
|
53
53
|
res = lxl.eval('=LIST(d5:b3, "x")')
|
54
|
-
|
55
|
-
assert_equal(
|
56
|
-
assert_equal("
|
57
|
-
assert_equal(
|
54
|
+
range = res.first
|
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')
|
58
63
|
end
|
59
64
|
|
60
65
|
def test_deferred
|
@@ -65,6 +70,26 @@ class LXLTest < Test::Unit::TestCase
|
|
65
70
|
assert_equal(:testing, res.symbol)
|
66
71
|
assert_equal(3, res.args.last)
|
67
72
|
end
|
73
|
+
|
74
|
+
def test_range_naming
|
75
|
+
lxl = LXL::Parser.new(MyNamespace.new)
|
76
|
+
range = lxl.eval('=B1:D2')
|
77
|
+
assert_equal('B', range.first_column)
|
78
|
+
range = lxl.eval('=Sheet1!B3:D5')
|
79
|
+
assert_equal('B', range.first_column)
|
80
|
+
assert_equal('5', range.last_cell)
|
81
|
+
assert_equal('Sheet1', range.sheet)
|
82
|
+
range = lxl.eval('=[Book.xls]Sheet2!B5:D6')
|
83
|
+
assert_equal('B', range.first_column)
|
84
|
+
assert_equal('6', range.last_cell)
|
85
|
+
assert_equal('Book.xls', range.book)
|
86
|
+
assert_equal('Sheet2', range.sheet)
|
87
|
+
range = lxl.eval('=[Book1]Sheet3!B7:D8')
|
88
|
+
assert_equal('B', range.first_column)
|
89
|
+
assert_equal('8', range.last_cell)
|
90
|
+
assert_equal('Book1', range.book)
|
91
|
+
assert_equal('Sheet3', range.sheet)
|
92
|
+
end
|
68
93
|
|
69
94
|
def test_ns_methods
|
70
95
|
ns = MyNamespace.new
|
@@ -91,8 +116,12 @@ class LXLTest < Test::Unit::TestCase
|
|
91
116
|
end
|
92
117
|
|
93
118
|
def test_ns_access
|
94
|
-
|
95
|
-
|
119
|
+
res = begin
|
120
|
+
LXL.eval('=CLASS()')
|
121
|
+
rescue NoMethodError => e
|
122
|
+
:failed
|
123
|
+
end
|
124
|
+
assert_equal(:failed, res)
|
96
125
|
end
|
97
126
|
|
98
127
|
class MyLXLNamespace < LXL::Namespace
|
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.4
|
7
|
+
date: 2005-02-14
|
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
|
@@ -34,12 +34,13 @@ files:
|
|
34
34
|
- lxl.gemspec
|
35
35
|
- setup.rb
|
36
36
|
- README.en
|
37
|
-
- lxl.rdoc
|
38
37
|
- lib/lxl.rb
|
39
38
|
- test/lxl_test.rb
|
40
39
|
test_files:
|
41
40
|
- test/lxl_test.rb
|
42
|
-
rdoc_options:
|
41
|
+
rdoc_options:
|
42
|
+
- "--all"
|
43
|
+
- "--inline-source"
|
43
44
|
extra_rdoc_files: []
|
44
45
|
executables: []
|
45
46
|
extensions: []
|
data/lxl.rdoc
DELETED
@@ -1,114 +0,0 @@
|
|
1
|
-
LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas
|
2
|
-
and is easily extended with new constants and functions.
|
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
|
-
B3:D5 => LXL::Range.new("B3","D5")
|
24
|
-
|
25
|
-
LXL::Range is a subclass of Range which overrides
|
26
|
-
the #each method to return Excel-style ranges.
|
27
|
-
|
28
|
-
# Standard Ruby Range
|
29
|
-
Range.new("B3", "D5").collect
|
30
|
-
# => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
31
|
-
"C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
|
32
|
-
"D0", "D1", "D2", "D3", "D4", "D5"]
|
33
|
-
|
34
|
-
# LXL Range
|
35
|
-
LXL::Range.new("B3", "D5").collect
|
36
|
-
# => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
|
37
|
-
|
38
|
-
=Percentages
|
39
|
-
|
40
|
-
25% => .25
|
41
|
-
|
42
|
-
=Deferred function calls
|
43
|
-
|
44
|
-
LXL::Deferred snapshots the symbol/arguments of a function call for later use.
|
45
|
-
|
46
|
-
class MyNamespace < LXL::Namespace
|
47
|
-
register_deferred :foo
|
48
|
-
end
|
49
|
-
LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)').inspect
|
50
|
-
# => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
|
51
|
-
|
52
|
-
=Symbol registration
|
53
|
-
|
54
|
-
Register uppercase constants of the same name and value.
|
55
|
-
|
56
|
-
class MyNamespace < LXL::Namespace
|
57
|
-
register_symbols :foo, :bar
|
58
|
-
end
|
59
|
-
LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)').inspect
|
60
|
-
# => [:FOO, :BAR]
|
61
|
-
|
62
|
-
=Operators
|
63
|
-
|
64
|
-
a + b # Add
|
65
|
-
a - b # Subtract
|
66
|
-
a * b # Multiply
|
67
|
-
a / b # Divide
|
68
|
-
a = b # Equal to
|
69
|
-
a <> b # Not equal to
|
70
|
-
a < b # Less than
|
71
|
-
a > b # Greater than
|
72
|
-
a <= b # Less than or equal to
|
73
|
-
a >= b # Greater than or equal to
|
74
|
-
a & b # String concentation
|
75
|
-
n ^ n # Exponential
|
76
|
-
|
77
|
-
=Constants
|
78
|
-
|
79
|
-
TRUE # true
|
80
|
-
FALSE # false
|
81
|
-
NULL # nil
|
82
|
-
|
83
|
-
=Logical Functions
|
84
|
-
|
85
|
-
AND (a,b)
|
86
|
-
OR (a,b)
|
87
|
-
NOT (cond)
|
88
|
-
IF (cond,true,false)
|
89
|
-
|
90
|
-
=Date/Time Functions
|
91
|
-
|
92
|
-
TODAY () # current date value
|
93
|
-
NOW () # current date/time value
|
94
|
-
DATE (y,m,d) # date value
|
95
|
-
TIME (h,m,s) # time value
|
96
|
-
DATETIME (text) # convert a date/time string into a date/time value
|
97
|
-
|
98
|
-
=List Functions
|
99
|
-
|
100
|
-
LIST (a,b,..) # Variable length list
|
101
|
-
IN (find,list) # True if find is found in the given list/string
|
102
|
-
|
103
|
-
=Notes
|
104
|
-
|
105
|
-
* Values prefixed with = are formulas, anything else is assumed to be text.
|
106
|
-
|
107
|
-
LXL.eval("5+5") # => '5+5'
|
108
|
-
LXL.eval("=5+5") # => 10
|
109
|
-
|
110
|
-
* Constant and Function names are case-insensitive.
|
111
|
-
|
112
|
-
* The number zero is interpereted as FALSE.
|
113
|
-
|
114
|
-
* Separating formulas with a semi-colon returns an Array of results.
|