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.
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: