lxl 0.1.1 → 0.2.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 +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:
|