lxl 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +10 -0
- data/README +13 -11
- data/VERSION +1 -1
- data/lib/lxl.rb +70 -69
- data/test/lxl_test.rb +19 -12
- metadata +1 -1
data/CHANGES
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
0.2.0
|
2
|
+
|
3
|
+
- Double quotes only used to define strings.
|
4
|
+
- Embedded quote escaping by doubling them up: ="This is a ""quoted"" string."
|
5
|
+
- Text/Formula split. Formulas start with =, anything else is seen as a string
|
6
|
+
- :SYMBOL parsing removed. register_symbols added to enable symbols as constants
|
7
|
+
- Case insensitive function and constant names
|
8
|
+
- Semi-Colons no longer parsed as a token (still used as statement separator)
|
9
|
+
- General refactoring (code/doc cleanup)
|
10
|
+
|
1
11
|
0.1.1
|
2
12
|
|
3
13
|
- String tokens s/S => '/"
|
data/README
CHANGED
@@ -8,21 +8,23 @@ Usage
|
|
8
8
|
-----
|
9
9
|
|
10
10
|
formulas = %{
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
IN("
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
This is some text;
|
12
|
+
="This is some ""quoted"" text";
|
13
|
+
=((1+2)*(10-6))/2;
|
14
|
+
=datetime("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
|
15
|
+
=IN(" is ", "this is a string");
|
16
|
+
=LIST(1, "two", 3.0);
|
17
|
+
=IN("b", LIST("a", "b", "c"));
|
18
|
+
=AND(TRUE, NULL);
|
19
|
+
=OR(TRUE, FALSE);
|
20
|
+
=IF(1+1=2, "yes", "no");
|
19
21
|
}
|
20
|
-
|
22
|
+
|
21
23
|
# single formula
|
22
|
-
puts LXL.eval('5+5').inspect # => 10
|
24
|
+
puts LXL.eval('=5+5').inspect # => 10
|
23
25
|
|
24
26
|
# multiple formulas separated by semi-colon
|
25
|
-
puts LXL.eval(formulas).inspect # => [6, true, true, [1, "two", 3.0], true, false, true, "yes"]
|
27
|
+
puts LXL.eval(formulas).inspect # => ["This is some text", "This is some \"quoted\" text", 6, true, true, [1, "two", 3.0], true, false, true, "yes"]
|
26
28
|
|
27
29
|
See API docs for more information.
|
28
30
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/lxl.rb
CHANGED
@@ -43,18 +43,14 @@
|
|
43
43
|
#
|
44
44
|
# * The number zero is interpereted as FALSE
|
45
45
|
# * Return multiple results in an array by separating formulas with a semi-colon (;)
|
46
|
-
# * A :SYMBOL token is recognized as a ruby symbol
|
47
46
|
#
|
48
47
|
# =Lexical Types
|
49
48
|
#
|
50
49
|
# w Whitespace (includes Commas)
|
51
|
-
# ; Semi-Colon (statement separator)
|
52
50
|
# o Operator
|
53
51
|
# f Float
|
54
52
|
# i Integer
|
55
|
-
#
|
56
|
-
# " String (double quoted)
|
57
|
-
# s Symbol
|
53
|
+
# s String
|
58
54
|
# t Token
|
59
55
|
# ( Open (
|
60
56
|
# ) Close )
|
@@ -75,7 +71,7 @@ end
|
|
75
71
|
|
76
72
|
class LXL::Parser
|
77
73
|
|
78
|
-
attr_reader :constants, :functions, :lexer
|
74
|
+
attr_reader :constants, :functions, :lexer
|
79
75
|
|
80
76
|
RUBY_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '==', '!=', '<', '>']
|
81
77
|
EXCEL_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '=', '<>', '<', '>']
|
@@ -115,17 +111,14 @@ class LXL::Parser
|
|
115
111
|
ops = EXCEL_OPERATORS.collect { |v| Regexp.quote(v) }.join('|')
|
116
112
|
#
|
117
113
|
@lexer = LXL::LittleLexer.new([
|
118
|
-
[/\A[\s,]+/,?w]
|
119
|
-
[/\A
|
120
|
-
[/\A
|
121
|
-
[/\A[0-9]
|
122
|
-
[/\A[
|
123
|
-
[/\A
|
124
|
-
[/\A(
|
125
|
-
[/\A
|
126
|
-
[/\A\w+/,?t], # Token
|
127
|
-
[/\A\(/,?(], # Open (
|
128
|
-
[/\A\)/,?)], # Close )
|
114
|
+
[/\A[\s,]+/,?w], # Whitespace (includes Commas)
|
115
|
+
[/\A(#{ops})/,?o], # Operator
|
116
|
+
[/\A[0-9]+\.[0-9]+/,?f], # Float
|
117
|
+
[/\A[0-9]+/,?i], # Integer
|
118
|
+
[/\A("([^"]|"")*")/m,?s], # String
|
119
|
+
[/\A\w+/,?t], # Token
|
120
|
+
[/\A\(/,?(], # Open (
|
121
|
+
[/\A\)/,?)], # Close )
|
129
122
|
], false)
|
130
123
|
|
131
124
|
# Other
|
@@ -136,40 +129,42 @@ class LXL::Parser
|
|
136
129
|
# Evaluate formula
|
137
130
|
#
|
138
131
|
def eval(formula)
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
expr = [ [] ]
|
143
|
-
@tokens.each do |token|
|
144
|
-
if token == ';'
|
145
|
-
expr << []
|
146
|
-
else
|
147
|
-
expr.last << token
|
148
|
-
end
|
149
|
-
end
|
150
|
-
expr.collect { |e| Kernel.eval(e.join, binding) }
|
151
|
-
else
|
152
|
-
Kernel.eval(@tokens.join, binding)
|
153
|
-
end
|
132
|
+
formulas = formula.to_s.split(';').collect { |f| f.strip }.find_all { |f| ! f.empty? }
|
133
|
+
formulas.collect! { |f| Kernel.eval(tokenize(f).join, binding) }
|
134
|
+
formulas.size == 1 ? formulas.first : formulas
|
154
135
|
end
|
155
136
|
|
156
|
-
|
137
|
+
# Register a function
|
138
|
+
#
|
139
|
+
# * Converts name to symbol
|
140
|
+
# * Wraps function with a debugging procedure
|
141
|
+
#
|
142
|
+
def register_function(name, &block)
|
143
|
+
name = name(name)
|
144
|
+
@functions[name] = debug(name, &block)
|
145
|
+
end
|
157
146
|
|
158
147
|
# Register a constant
|
159
148
|
#
|
160
149
|
# * Converts name to symbol
|
161
150
|
#
|
162
151
|
def register_constant(name, value)
|
163
|
-
|
152
|
+
name = name(name)
|
153
|
+
@constants[name] = value
|
164
154
|
end
|
165
155
|
|
166
|
-
#
|
156
|
+
# Registers constant for each symbol, with the same name and value
|
167
157
|
#
|
168
|
-
|
169
|
-
|
158
|
+
def register_symbols(*symbols)
|
159
|
+
symbols.each { |s| register_constant(s, s) }
|
160
|
+
end
|
161
|
+
|
162
|
+
protected
|
163
|
+
|
164
|
+
# Translate to uppercase symbol
|
170
165
|
#
|
171
|
-
def
|
172
|
-
|
166
|
+
def name(obj)
|
167
|
+
obj.to_s.upcase.to_sym
|
173
168
|
end
|
174
169
|
|
175
170
|
# Wrap a procedure in a debugging procedure
|
@@ -192,43 +187,47 @@ class LXL::Parser
|
|
192
187
|
ops = Hash[*EXCEL_OPERATORS.zip(RUBY_OPERATORS).flatten]
|
193
188
|
|
194
189
|
# Parse formula
|
195
|
-
|
196
|
-
|
197
|
-
|
190
|
+
formula = '="'+formula.gsub('"','""')+'"' unless formula =~ /^=/ # text to formula (text to ="quoted-text")
|
191
|
+
types, tokens = @lexer.scan(formula.gsub(/^=/,''))
|
192
|
+
types = types.split(//)
|
193
|
+
raise SyntaxError, 'unbalanced parentheses' unless balanced?(tokens)
|
198
194
|
|
199
195
|
# Parse tokens
|
200
|
-
|
201
|
-
type, token =
|
202
|
-
token =
|
203
|
-
|
204
|
-
|
196
|
+
tokens.each_index do |i|
|
197
|
+
type, token = types[i], tokens[i]
|
198
|
+
token = case type
|
199
|
+
when 'i': token.to_i
|
200
|
+
when 'f': token.to_f
|
201
|
+
when 's': token.gsub(/([^\\])""/,'\1\"') # "" to \"
|
202
|
+
else token
|
203
|
+
end
|
205
204
|
if type == 't'
|
206
|
-
token = token
|
205
|
+
token = name(token)
|
207
206
|
if @functions.key?(token)
|
208
|
-
if
|
207
|
+
if tokens[i+1] != '('
|
209
208
|
raise ArgumentError, "wrong number of arguments for #{token}"
|
210
209
|
else
|
211
|
-
|
210
|
+
types[i] = 'F'
|
212
211
|
token = '@functions['+token.inspect+'].call'
|
213
212
|
end
|
214
213
|
elsif @constants.key?(token)
|
215
|
-
|
214
|
+
types[i] = 'C'
|
216
215
|
token = '@constants['+token.inspect+']'
|
217
216
|
else
|
218
217
|
raise NameError, "unknown constant #{token}"
|
219
218
|
end
|
220
219
|
end
|
221
220
|
token = ops[token] if EXCEL_OPERATORS.include?(token)
|
222
|
-
|
221
|
+
tokens[i] = token
|
223
222
|
end
|
224
223
|
|
225
|
-
|
224
|
+
tokens
|
226
225
|
end
|
227
226
|
|
228
227
|
# Check that parentheses are balanced
|
229
228
|
#
|
230
|
-
def balanced?
|
231
|
-
|
229
|
+
def balanced?(tokens)
|
230
|
+
tokens.find_all { |t| ['(', ')'].include?(t) }.size % 2 == 0
|
232
231
|
end
|
233
232
|
|
234
233
|
# False if nil, false or zero
|
@@ -296,24 +295,26 @@ class LXL::LittleLexer #:nodoc: all
|
|
296
295
|
|
297
296
|
end
|
298
297
|
|
299
|
-
#
|
298
|
+
# Demo
|
300
299
|
#
|
301
300
|
if $0 == __FILE__
|
302
|
-
|
301
|
+
|
303
302
|
formulas = %{
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
IN("
|
309
|
-
|
310
|
-
|
311
|
-
|
303
|
+
This is some text;
|
304
|
+
="This is some ""quoted"" text";
|
305
|
+
=((1+2)*(10-6))/2;
|
306
|
+
=datetime("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
|
307
|
+
=IN(" is ", "this is a string");
|
308
|
+
=LIST(1, "two", 3.0);
|
309
|
+
=IN("b", LIST("a", "b", "c"));
|
310
|
+
=AND(TRUE, NULL);
|
311
|
+
=OR(TRUE, FALSE);
|
312
|
+
=IF(1+1=2, "yes", "no");
|
312
313
|
}
|
313
|
-
|
314
|
-
puts LXL.eval('5+5').inspect # => 10
|
315
314
|
|
316
|
-
#
|
317
|
-
puts LXL.eval(
|
315
|
+
# single formula
|
316
|
+
puts LXL.eval('=5+5').inspect # => 10
|
318
317
|
|
318
|
+
# multiple formulas separated by semi-colon
|
319
|
+
puts LXL.eval(formulas).inspect # => ["This is some text", "This is some \"quoted\" text", 6, true, true, [1, "two", 3.0], true, false, true, "yes"]
|
319
320
|
end
|
data/test/lxl_test.rb
CHANGED
@@ -5,23 +5,30 @@ require 'lxl'
|
|
5
5
|
class LXLTest < Test::Unit::TestCase
|
6
6
|
|
7
7
|
def test_single_formula
|
8
|
-
assert_equal(10, LXL.eval('5+5'))
|
8
|
+
assert_equal(10, LXL.eval('=5+5'))
|
9
9
|
end
|
10
10
|
|
11
11
|
def test_multiple_formula
|
12
12
|
formulas = %{
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
This is some text;
|
14
|
+
"This is some ""quoted"" text";
|
15
|
+
="This is some ""quoted"" text";
|
16
|
+
=FOO;
|
17
|
+
=bar;
|
18
|
+
=((1+2)*(10-6))/2;
|
19
|
+
=datetime("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
|
20
|
+
=IN(" is ", "this is a string");
|
21
|
+
=LIST(1, "two", 3.0);
|
22
|
+
=IN("b", LIST("a", "b", "c"));
|
23
|
+
=AND(TRUE, NULL);
|
24
|
+
=OR(TRUE, FALSE);
|
25
|
+
=IF(1+1=2, "yes", "no");
|
22
26
|
}
|
23
|
-
expected = [
|
24
|
-
|
27
|
+
expected = ["This is some text", "\"This is some \"quoted\" text\"", "This is some \"quoted\" text"]
|
28
|
+
expected += [:FOO, :BAR, 6, true, true, [1, "two", 3.0], true, false, true, "yes"]
|
29
|
+
lxl = LXL::Parser.new
|
30
|
+
lxl.register_symbols(:FOO, :BAR)
|
31
|
+
results = lxl.eval(formulas)
|
25
32
|
expected.each_index { |i| assert_equal(expected[i], results[i]) }
|
26
33
|
end
|
27
34
|
|
metadata
CHANGED
@@ -3,7 +3,7 @@ rubygems_version: 0.8.3
|
|
3
3
|
specification_version: 1
|
4
4
|
name: lxl
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
6
|
+
version: 0.2.0
|
7
7
|
date: 2005-02-08
|
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:
|