lxl 0.3.8 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +8 -0
- data/VERSION +1 -1
- data/lib/lxl.rb +114 -97
- data/test/lxl_test.rb +6 -6
- metadata +3 -3
data/CHANGES
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
0.4.0
|
2
|
+
|
3
|
+
- Tokenizing greatly refactored
|
4
|
+
- All tokenizing code moved into tokenize (out of eval)
|
5
|
+
- Token class added
|
6
|
+
- Fixes bug with text (non-formula) strings and constant parsing
|
7
|
+
- Removed unnessecary Lexer code
|
8
|
+
|
1
9
|
0.3.8
|
2
10
|
|
3
11
|
- Range#first_colnum and Range#last_colnum added (both use LXL#xlcolnum)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/lxl.rb
CHANGED
@@ -35,7 +35,7 @@
|
|
35
35
|
# NOW () # current date/time value
|
36
36
|
# DATE (y,m,d) # date value
|
37
37
|
# TIME (h,m,s) # time value
|
38
|
-
# DATETIME (
|
38
|
+
# DATETIME (string) # convert a date/time string into a date/time value
|
39
39
|
#
|
40
40
|
# =List Functions
|
41
41
|
#
|
@@ -76,7 +76,7 @@
|
|
76
76
|
#
|
77
77
|
# class MyLXLNamespace < LXL::Namespace
|
78
78
|
# NAME = 'John Doe'
|
79
|
-
# def upper(
|
79
|
+
# def upper(string) string.to_s.upcase end
|
80
80
|
# end
|
81
81
|
#
|
82
82
|
# class MyLXL < LXL::Parser
|
@@ -86,7 +86,7 @@
|
|
86
86
|
# MyLXL.eval('=UPPER(NAME)')
|
87
87
|
# # => JOHN DOE
|
88
88
|
#
|
89
|
-
# =Symbol
|
89
|
+
# =Symbol Registration
|
90
90
|
#
|
91
91
|
# Register uppercase constants of the same name and value.
|
92
92
|
#
|
@@ -96,7 +96,7 @@
|
|
96
96
|
# LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)')
|
97
97
|
# # => [:FOO, :BAR]
|
98
98
|
#
|
99
|
-
# =Deferred
|
99
|
+
# =Deferred Calls
|
100
100
|
#
|
101
101
|
# LXL::Deferred snapshots the symbol/arguments of a function call for later use.
|
102
102
|
#
|
@@ -154,7 +154,7 @@ module LXL
|
|
154
154
|
def xlcolnum(colname)
|
155
155
|
count = 0
|
156
156
|
letters = colname.to_s.split(//)
|
157
|
-
map =
|
157
|
+
map = proc { |c| ('A'..'Z').collect.index(c)+1 }
|
158
158
|
letters[0..-2].each { |l| count += 26*map.call(l) }
|
159
159
|
count += map.call(letters.last)
|
160
160
|
count
|
@@ -166,6 +166,7 @@ end
|
|
166
166
|
#
|
167
167
|
# ; Statement separator
|
168
168
|
# , Argument separator
|
169
|
+
# = Formula prefix
|
169
170
|
# ( Tuple open
|
170
171
|
# ) Tuple close
|
171
172
|
# w Whitespace
|
@@ -175,13 +176,21 @@ end
|
|
175
176
|
# p Percentage
|
176
177
|
# f Float
|
177
178
|
# i Integer
|
178
|
-
#
|
179
|
+
# u Unknown
|
179
180
|
#
|
180
181
|
# F Function
|
181
182
|
# C Constant
|
182
183
|
#
|
183
184
|
class LXL::Parser
|
184
185
|
|
186
|
+
class Token
|
187
|
+
attr_accessor :type, :value
|
188
|
+
def initialize(type, value) @type = type; @value = value end
|
189
|
+
def whitespace?() value.to_s.strip.empty? end
|
190
|
+
def to_s() "T:#{@type.chr}:#{@value}" end
|
191
|
+
alias inspect to_s
|
192
|
+
end
|
193
|
+
|
185
194
|
RUBY_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '==', '!=', '<', '>', '+', '**']
|
186
195
|
EXCEL_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '=', '<>', '<', '>', '&', '^' ]
|
187
196
|
|
@@ -218,6 +227,7 @@ class LXL::Parser
|
|
218
227
|
@lexer = self.class.lexer_class.new([
|
219
228
|
[/\A;+/, ?;], # Statement separator
|
220
229
|
[/\A,+/, ?,], # Argument separator
|
230
|
+
[/\A`=/, ?=], # Formula prefix
|
221
231
|
[/\A\(/, ?(], # Tuple open
|
222
232
|
[/\A\)/, ?)], # Tuple close
|
223
233
|
[/\A\s+/, ?w], # Whitespace
|
@@ -227,29 +237,15 @@ class LXL::Parser
|
|
227
237
|
[/\A\d+(\.\d+)?%/, ?p], # Percentage
|
228
238
|
[/\A\d+\.\d+/, ?f], # Float
|
229
239
|
[/\A\d+/, ?i], # Integer
|
230
|
-
[/\A\w+/, ?
|
231
|
-
]
|
240
|
+
[/\A\w+/, ?u], # Unknown
|
241
|
+
])
|
232
242
|
end
|
233
243
|
|
234
244
|
# Evaluate a formula.
|
235
245
|
#
|
236
|
-
def eval(
|
237
|
-
formulas =
|
238
|
-
|
239
|
-
types,tokens = tokenize(formula)
|
240
|
-
tokens.each_index { |i| tokens[i] == ';' ? formulas << Array.new : formulas.last << [types[i], tokens[i]] }
|
241
|
-
formulas.collect! { |f|
|
242
|
-
types = f.collect { |t| t.first }
|
243
|
-
tokens = f.collect { |t| t.last }
|
244
|
-
token = tokens.join.strip
|
245
|
-
if token =~ /\A=/
|
246
|
-
tokens.each_index { |i| tokens[i] = translate_quotes(tokens[i]) if types[i] == ?s }
|
247
|
-
token = tokens.join.strip.gsub(/\A=+/,'')
|
248
|
-
else
|
249
|
-
token = translate_quotes(quote(tokens.join.strip))
|
250
|
-
end
|
251
|
-
}
|
252
|
-
formulas.delete_if { |f| f == '""' }
|
246
|
+
def eval(string)
|
247
|
+
formulas = tokenize(string)
|
248
|
+
formulas.collect! { |f| f.collect { |t| t.value }.join }
|
253
249
|
formulas.collect! { |f| Kernel.eval(f, binding) }
|
254
250
|
formulas.size == 1 ? formulas.first : formulas
|
255
251
|
end
|
@@ -261,8 +257,8 @@ class LXL::Parser
|
|
261
257
|
# quote('a "quoted" value.')
|
262
258
|
# # => '"a ""quoted"" value."'
|
263
259
|
#
|
264
|
-
def quote(
|
265
|
-
'"'+
|
260
|
+
def quote(string)
|
261
|
+
'"'+string.to_s.gsub('"','""')+'"'
|
266
262
|
end
|
267
263
|
|
268
264
|
# Translate "" to \" in quoted strings.
|
@@ -270,58 +266,91 @@ class LXL::Parser
|
|
270
266
|
# translate_quotes('"a ""quoted"" value."')
|
271
267
|
# # => '"a \"quoted\" value."'
|
272
268
|
#
|
273
|
-
def translate_quotes(
|
274
|
-
|
269
|
+
def translate_quotes(string)
|
270
|
+
string.to_s.gsub(/([^\\])""/,'\1\"')
|
275
271
|
end
|
276
272
|
|
277
|
-
# Tokenize a
|
273
|
+
# Tokenize a string into an array of formulas (Array of Token).
|
278
274
|
#--
|
279
275
|
# const_missing is defined by Ruby, raises a NameError.
|
280
276
|
# method_missing is not, Ruby uses user-defined if present, NoMethodError otherwise.
|
281
277
|
#++
|
282
|
-
def tokenize(
|
278
|
+
def tokenize(string)
|
279
|
+
formulas = [[]]
|
280
|
+
separators = [?=, ?;]
|
283
281
|
ops = Hash[*EXCEL_OPERATORS.zip(RUBY_OPERATORS).flatten]
|
284
282
|
|
285
|
-
#
|
286
|
-
|
283
|
+
# Translate formula prefixes to `= to avoid clashing with the = operator
|
284
|
+
# For lines that start with = (optional leading whitespace)
|
285
|
+
string.gsub!(/^\s*=/, '`=')
|
286
|
+
|
287
|
+
# Parse string
|
288
|
+
types,tokens = @lexer.scan(string.to_s)
|
287
289
|
raise SyntaxError, 'unbalanced parentheses' unless balanced?(types)
|
288
290
|
|
291
|
+
# Translate formula prefixes to `= to avoid clashing with the = operator
|
292
|
+
# For single line separations via semi-colon: semi-colon (optional whitespace) =
|
293
|
+
types.each_index do |i|
|
294
|
+
type,token = types[i], tokens[i]
|
295
|
+
if token == '='
|
296
|
+
last = types[0..i-1].reverse.find { |t| t != ?w }
|
297
|
+
types[i] = ?= if last == ?;
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
289
301
|
# Parse tokens
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
302
|
+
last_type = ??
|
303
|
+
while tokens.size > 0
|
304
|
+
type,token = types.shift, tokens.shift
|
305
|
+
# Text
|
306
|
+
if formulas.last.size == 0 and last_type != ?= and type != ?=
|
307
|
+
text = []
|
308
|
+
while tokens.size > 0 and ! separators.include?(types.first)
|
309
|
+
types.shift
|
310
|
+
text << tokens.shift
|
311
|
+
end
|
312
|
+
text = text.join.chomp
|
313
|
+
formulas.last << Token.new(type, translate_quotes(quote(text))) unless text.empty?
|
314
|
+
# Formula
|
315
|
+
else
|
316
|
+
t = Token.new(type, token)
|
317
|
+
case type
|
318
|
+
when ?; then formulas << []
|
319
|
+
when ?= then formulas << []
|
320
|
+
when ?o then formulas.last << Token.new(type, ops[token])
|
321
|
+
when ?s then formulas.last << Token.new(type, translate_quotes(token))
|
322
|
+
when ?p then formulas.last << Token.new(type, token.to_f/100)
|
323
|
+
when ?f then formulas.last << Token.new(type, token.to_f)
|
324
|
+
when ?i then formulas.last << Token.new(type, token.to_i)
|
325
|
+
when ?r then
|
326
|
+
range = token.split(/:/)
|
327
|
+
range[1] = range[0] if range.size == 1
|
328
|
+
formulas.last << Token.new(type, "self.class.range_class.new(*#{range.inspect})")
|
329
|
+
when ?u then
|
330
|
+
upper = token.to_s.upcase
|
331
|
+
lower = token.to_s.downcase
|
332
|
+
raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
|
333
|
+
custom_const_missing = begin [@namespace.const_get(:ConstMissing),true].last rescue false end
|
334
|
+
# Constants
|
335
|
+
if tokens.first != '('
|
336
|
+
if @namespace.const_defined?(upper) or custom_const_missing
|
337
|
+
formulas.last << Token.new(?C, "@namespace.const_get(:#{upper})")
|
338
|
+
else formulas.last << t end
|
339
|
+
# Functions
|
340
|
+
else
|
341
|
+
if @namespace.respond_to?(lower) or @namespace.respond_to?(:method_missing)
|
342
|
+
formulas.last << Token.new(?F, "@namespace.#{lower}")
|
343
|
+
else formulas.last << t end
|
344
|
+
end
|
345
|
+
else formulas.last << t end
|
346
|
+
end
|
347
|
+
last_type = type
|
320
348
|
end
|
321
349
|
|
322
|
-
|
350
|
+
formulas.reject! { |f| f.size == 0 }
|
351
|
+
formulas
|
323
352
|
end
|
324
|
-
|
353
|
+
|
325
354
|
# Check that parentheses are balanced.
|
326
355
|
#
|
327
356
|
def balanced?(list)
|
@@ -352,7 +381,6 @@ end
|
|
352
381
|
#
|
353
382
|
class LXL::Range < Range
|
354
383
|
|
355
|
-
# B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
|
356
384
|
EXCEL_RANGE = /\A(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+)(:([A-Z]+[1-9]+)?)?/i
|
357
385
|
|
358
386
|
# Workbook name.
|
@@ -410,8 +438,8 @@ class LXL::Range < Range
|
|
410
438
|
|
411
439
|
def each
|
412
440
|
if excel?
|
413
|
-
|
414
|
-
|
441
|
+
(first_column..last_column).each do |column|
|
442
|
+
(first_cell..last_cell).each do |cell|
|
415
443
|
yield column+cell.to_s
|
416
444
|
end
|
417
445
|
end
|
@@ -498,7 +526,7 @@ end
|
|
498
526
|
#
|
499
527
|
# class MyLXLNamespace < LXL::Namespace
|
500
528
|
# NAME = 'John Doe'
|
501
|
-
# def upper(
|
529
|
+
# def upper(string) string.to_s.upcase end
|
502
530
|
# end
|
503
531
|
#
|
504
532
|
# class MyLXL < LXL::Parser
|
@@ -550,8 +578,8 @@ class LXL::Namespace < LXL::EmptyNamespace
|
|
550
578
|
Date.new(y,m,d).ajd.to_f
|
551
579
|
end
|
552
580
|
|
553
|
-
def datetime(
|
554
|
-
DateTime.parse(
|
581
|
+
def datetime(string)
|
582
|
+
DateTime.parse(string.to_s).ajd.to_f
|
555
583
|
end
|
556
584
|
|
557
585
|
def time(h,m,s)
|
@@ -562,35 +590,24 @@ end
|
|
562
590
|
|
563
591
|
# Based on John Carter's LittleLexer (http://littlelexer.rubyforge.org).
|
564
592
|
#
|
565
|
-
class LXL::Lexer
|
593
|
+
class LXL::Lexer
|
566
594
|
|
567
595
|
class LexerJammed < Exception; end
|
568
596
|
|
569
|
-
def initialize(re_to_chr
|
597
|
+
def initialize(re_to_chr)
|
570
598
|
@re_to_chr = re_to_chr
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
next_token(string) do |t,token,tail|
|
579
|
-
types << t
|
580
|
-
tokens << [string_tokens[0...tail], string[0...tail]]
|
581
|
-
string = string[tail..-1]
|
582
|
-
string_tokens = string_tokens[tail..-1]
|
583
|
-
end
|
584
|
-
else
|
585
|
-
next_token(string) do |t,token,tail|
|
586
|
-
types << t
|
587
|
-
tokens << token
|
588
|
-
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def scan(string)
|
602
|
+
types,tokens = [],[]
|
603
|
+
next_token(string) do |type,token|
|
604
|
+
types << type
|
605
|
+
tokens << token
|
589
606
|
end
|
590
|
-
|
607
|
+
[types,tokens]
|
591
608
|
end
|
592
609
|
|
593
|
-
|
610
|
+
protected
|
594
611
|
|
595
612
|
def next_token(string)
|
596
613
|
match_data = nil
|
@@ -600,7 +617,7 @@ class LXL::Lexer #:nodoc: all
|
|
600
617
|
match_data = re.match(string)
|
601
618
|
next unless match_data
|
602
619
|
token = match_data[0]
|
603
|
-
yield chr,token
|
620
|
+
yield chr, token
|
604
621
|
string = match_data.post_match
|
605
622
|
failed = false
|
606
623
|
break
|
@@ -618,8 +635,8 @@ end
|
|
618
635
|
if $0 == __FILE__
|
619
636
|
|
620
637
|
formulas = %{
|
621
|
-
This is
|
622
|
-
="This is
|
638
|
+
This is a string;
|
639
|
+
="This is a ""quoted"" string"
|
623
640
|
=((1+2)*(10-6))/2
|
624
641
|
=datetime("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00)
|
625
642
|
=IN(" is ", "this is a string")
|
@@ -638,6 +655,6 @@ if $0 == __FILE__
|
|
638
655
|
|
639
656
|
# multiple formulas separated by semi-colon
|
640
657
|
puts LXL.eval(formulas).inspect
|
641
|
-
# => ["This is
|
658
|
+
# => ["This is a string", "This is a \"quoted\" string", 6, true, true, [1, "two", 3.0], true, false, true, "yes", "this and that", 8, 0.502]
|
642
659
|
|
643
660
|
end
|
data/test/lxl_test.rb
CHANGED
@@ -5,7 +5,7 @@ require 'lxl'
|
|
5
5
|
class MyNamespace < LXL::Namespace
|
6
6
|
register_symbols :foo
|
7
7
|
register_deferred :bar
|
8
|
-
def capitalize(
|
8
|
+
def capitalize(string) string.to_s.capitalize end
|
9
9
|
end
|
10
10
|
|
11
11
|
class LXLTest < Test::Unit::TestCase
|
@@ -16,9 +16,9 @@ class LXLTest < Test::Unit::TestCase
|
|
16
16
|
|
17
17
|
def test_multiple_formula
|
18
18
|
formulas = %{
|
19
|
-
This is
|
20
|
-
"This is
|
21
|
-
="This is
|
19
|
+
This is a string;
|
20
|
+
"This is a ""quoted"" string"
|
21
|
+
="This is a ""quoted"" string"
|
22
22
|
=";"=";"
|
23
23
|
=";"=":"
|
24
24
|
=A
|
@@ -39,7 +39,7 @@ class LXLTest < Test::Unit::TestCase
|
|
39
39
|
=0.2589=25.89%
|
40
40
|
="embedded percentages 25% and semi-colons ; are working properly"
|
41
41
|
}
|
42
|
-
expected = ["This is
|
42
|
+
expected = ["This is a string", "\"This is a \"quoted\" string\"", "This is a \"quoted\" string"]
|
43
43
|
expected += [true, false, :A, :B, false, 6, true, true, [1, 'two', 3.0], true, false, true, 'yes', 'this and that']
|
44
44
|
expected += [8, 0.502, true, true, 'embedded percentages 25% and semi-colons ; are working properly']
|
45
45
|
MyNamespace.register_symbols(:A, :B)
|
@@ -132,7 +132,7 @@ class LXLTest < Test::Unit::TestCase
|
|
132
132
|
|
133
133
|
class MyLXLNamespace < LXL::Namespace
|
134
134
|
NAME = 'John Doe'
|
135
|
-
def upper(
|
135
|
+
def upper(string) string.to_s.upcase end
|
136
136
|
end
|
137
137
|
|
138
138
|
class MyLXL < LXL::Parser
|
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.
|
7
|
-
date: 2005-02-
|
6
|
+
version: 0.4.0
|
7
|
+
date: 2005-02-16
|
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
|
@@ -36,8 +36,8 @@ files:
|
|
36
36
|
- README.en
|
37
37
|
- lib/lxl.rb
|
38
38
|
- test/spreadsheet.rb
|
39
|
-
- test/lxl_test.rb
|
40
39
|
- test/lxl_spreadsheet_test.rb
|
40
|
+
- test/lxl_test.rb
|
41
41
|
test_files:
|
42
42
|
- test/lxl_test.rb
|
43
43
|
rdoc_options:
|