lxl 0.3.8 → 0.4.0
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 +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:
|