lxl 0.3.1 → 0.3.4
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 +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.
|