lxl 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/CHANGES +10 -0
  2. data/README +13 -11
  3. data/VERSION +1 -1
  4. data/lib/lxl.rb +70 -69
  5. data/test/lxl_test.rb +19 -12
  6. 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
- ((1+2)*(10-6))/2;
12
- DATETIME("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
13
- IN(" is ", "this is a string");
14
- LIST(1, "two", 3.0);
15
- IN("b", LIST("a", "b", "c"));
16
- AND(TRUE, NULL);
17
- OR(TRUE, FALSE);
18
- IF(1+1=2, "yes", "no");
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.1
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
- # ' String (single quoted)
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, :tokens, :types
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] , # Whitespace (includes Commas)
119
- [/\A;+/, ?;], # Semi-Colon (statement separator)
120
- [/\A(#{ops})/,?o], # Operator
121
- [/\A[0-9]+\.[0-9]+/,?f], # Float
122
- [/\A[0-9]+/,?i], # Integer
123
- [/\A("[^\"]*")/m,?'], # String (single quoted)
124
- [/\A('[^\']*')/m,?"], # String (double quoted)
125
- [/\A:\w+/,?s], # Symbol
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
- tokenize(formula.to_s.strip)
140
- @tokens.pop if @tokens.last == ';'
141
- if @tokens.include?(';')
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
- protected
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
- @constants[name.to_sym] = value
152
+ name = name(name)
153
+ @constants[name] = value
164
154
  end
165
155
 
166
- # Register a function
156
+ # Registers constant for each symbol, with the same name and value
167
157
  #
168
- # * Converts name to symbol
169
- # * Wraps function with a debugging procedure
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 register_function(name, &block)
172
- @functions[name.to_sym] = debug(name.to_sym, &block)
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
- types, @tokens = @lexer.scan(formula)
196
- @types = types.split(//)
197
- raise SyntaxError, 'unbalanced parentheses' unless balanced?
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
- @tokens.each_index do |i|
201
- type, token = @types[i], @tokens[i]
202
- token = token.to_i if type == 'i'
203
- token = token.to_f if type == 'f'
204
- token = token.to_sym if type == 's'
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.to_sym
205
+ token = name(token)
207
206
  if @functions.key?(token)
208
- if @tokens[i+1] != '('
207
+ if tokens[i+1] != '('
209
208
  raise ArgumentError, "wrong number of arguments for #{token}"
210
209
  else
211
- @types[i] = 'F'
210
+ types[i] = 'F'
212
211
  token = '@functions['+token.inspect+'].call'
213
212
  end
214
213
  elsif @constants.key?(token)
215
- @types[i] = 'C'
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
- @tokens[i] = token
221
+ tokens[i] = token
223
222
  end
224
223
 
225
- @tokens
224
+ tokens
226
225
  end
227
226
 
228
227
  # Check that parentheses are balanced
229
228
  #
230
- def balanced?
231
- @tokens.find_all { |t| ['(', ')'].include?(t) }.size % 2 == 0
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
- # Test
298
+ # Demo
300
299
  #
301
300
  if $0 == __FILE__
302
-
301
+
303
302
  formulas = %{
304
- ((1+2)*(10-6))/2;
305
- DATETIME("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
306
- IN(" is ", "this is a string");
307
- LIST(1, "two", 3.0);
308
- IN("b", LIST("a", "b", "c"));
309
- AND(TRUE, NULL);
310
- OR(TRUE, FALSE);
311
- IF(1+1=2, "yes", "no");
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
- # multiple formulas separated by semi-colon
317
- puts LXL.eval(formulas).inspect # => [6, true, true, [1, "two", 3.0], true, false, true, "yes"]
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
@@ -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
- :SYMBOL;
14
- ((1+2)*(10-6))/2;
15
- DATETIME("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
16
- IN(" is ", "this is a string");
17
- LIST(1, "two", 3.0);
18
- IN("b", LIST("a", "b", "c"));
19
- AND(TRUE, NULL);
20
- OR(TRUE, FALSE);
21
- IF(1+1=2, "yes", "no");
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 = [:SYMBOL, 6, true, true, [1, "two", 3.0], true, false, true, "yes"]
24
- results = LXL.eval(formulas)
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.1.1
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: