lxl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README +46 -0
  2. data/VERSION +1 -0
  3. data/lib/lxl.rb +309 -0
  4. data/test/lxl_test.rb +27 -0
  5. metadata +42 -0
data/README ADDED
@@ -0,0 +1,46 @@
1
+ LXL README
2
+ ============
3
+
4
+ LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas.
5
+ Easily extended with new constants and functions.
6
+
7
+ Usage
8
+ -----
9
+
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");
19
+ }
20
+
21
+ # single formula
22
+ puts LXL.eval('5+5').inspect # => 10
23
+
24
+ # multiple formulas separated by semi-colon
25
+ puts LXL.eval(formulas).inspect # => [6, true, true, [1, "two", 3.0], true, false, true, "yes"]
26
+
27
+ See API docs for more information.
28
+
29
+ Requirements
30
+ ------------
31
+
32
+ * Ruby 1.8.2
33
+
34
+ Install
35
+ -------
36
+
37
+ gem install lxl
38
+
39
+ License
40
+ -------
41
+
42
+ LXL uses John Carter's LittleLexer for parsing.
43
+ Distributes under the same terms as LittleLexer.
44
+ http://www.rubyforge.org/projects/littlelexer/
45
+
46
+ Kevin Howe <kh@newclear.ca>
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/lxl.rb ADDED
@@ -0,0 +1,309 @@
1
+ # LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas.
2
+ # Easily extended with new constants and functions.
3
+ #
4
+ # =Constants
5
+ #
6
+ # TRUE # true
7
+ # FALSE # false
8
+ # NULL # nil
9
+ #
10
+ # =Operators
11
+ #
12
+ # a + b # Add
13
+ # a - b # Subtract
14
+ # a * b # Multiply
15
+ # a / b # Divide
16
+ # a = b # Equal to
17
+ # a <> b # Not equal to
18
+ # a < b # Less than
19
+ # a > b # Greater than
20
+ # a <= b # Less than or equal to
21
+ # a >= b # Greater than or equal to
22
+ #
23
+ # =Logical Functions
24
+ #
25
+ # AND (a,b)
26
+ # OR (a,b)
27
+ # IF (cond,true,false)
28
+ #
29
+ # =Date/Time Functions
30
+ #
31
+ # TODAY () # current date value
32
+ # NOW () # current date/time value
33
+ # DATE (y,m,d) # date value
34
+ # TIME (h,m,s) # time value
35
+ # DATETIME (value) # convert a date/time string into a date/time value
36
+ #
37
+ # =List Functions
38
+ #
39
+ # LIST (a,b,..) # Variable length list
40
+ # IN (find,list) # True if find value is found in the given list (works on strings too)
41
+ #
42
+ # =Notes
43
+ #
44
+ # * The number zero is interpereted as FALSE
45
+ # * Return multiple results in an array by separating formulas with a semi-colon (;)
46
+ #
47
+ # =Lexical Types
48
+ #
49
+ # w Whitespace (includes Commas)
50
+ # ; Semi-Colon (statement separator)
51
+ # o Operator
52
+ # i Integer
53
+ # f Float
54
+ # t Token
55
+ # s String (single quoted)
56
+ # S String (double quoted)
57
+ # ( Open (
58
+ # ) Close )
59
+ # C Constant
60
+ # F Function
61
+ #
62
+ module LXL
63
+
64
+ module_function
65
+
66
+ # Evaluate a formula
67
+ #
68
+ def eval(formula)
69
+ LXL::Parser.eval(formula)
70
+ end
71
+
72
+ end
73
+
74
+ class LXL::Parser
75
+
76
+ attr_reader :constants, :functions, :lexer, :tokens, :types
77
+
78
+ RUBY_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '==', '!=', '<', '>']
79
+ EXCEL_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '=', '<>', '<', '>']
80
+
81
+ # Evaluate a formula
82
+ #
83
+ def self.eval(formula)
84
+ self.new.eval(formula)
85
+ end
86
+
87
+ # Initialize parser
88
+ #
89
+ def initialize
90
+
91
+ # Constants
92
+ @constants = {
93
+ :TRUE => true,
94
+ :FALSE => false,
95
+ :NULL => nil,
96
+ }
97
+
98
+ # Functions
99
+ @functions = Hash.new
100
+ register(:AND) { |a,b| to_b(a) && to_b(b) }
101
+ register(:OR) { |a,b| to_b(a) || to_b(b) }
102
+ register(:IF) { |v,a,b| to_b(v) ? a : b }
103
+ register(:LIST) { |*args| args }
104
+ register(:IN) { |n,h| h.respond_to?(:include?) ? h.include?(n) : false }
105
+ register(:TODAY) { Date.today.ajd.to_f }
106
+ register(:NOW) { DateTime.now.ajd.to_f }
107
+ register(:DATE) { |y,m,d| Date.new(y,m,d).ajd.to_f }
108
+ register(:TIME) { |h,m,s|
109
+ DateTime.valid_time?(h,m,s) ? DateTime.valid_time?(h,m,s).to_f : raise(ArgumentError, 'invalid time')
110
+ }
111
+ register(:DATETIME) { |value| DateTime.parse(value).ajd.to_f }
112
+
113
+ # Lexer
114
+ ops = EXCEL_OPERATORS.collect { |v| Regexp.quote(v) }.join('|')
115
+ #
116
+ @lexer = LXL::LittleLexer.new([
117
+ [/\A[\s,]+/,?w] , # Whitespace (includes Commas)
118
+ [/\A;+/, ?;], # Semi-Colon (statement separator)
119
+ [/\A(#{ops})/,?o], # Operator
120
+ [/\A[0-9]+\.[0-9]+/,?f], # Float
121
+ [/\A[0-9]+/,?i], # Integer
122
+ [/\A("[^\"]*")/m,?s], # String (single quoted)
123
+ [/\A('[^\']*')/m,?S], # String (double quoted)
124
+ [/\A\w+/,?t], # Token
125
+ [/\A\(/,?(], # Open (
126
+ [/\A\)/,?)], # Close )
127
+ ], false)
128
+
129
+ # Other
130
+ @tokens = Array.new
131
+ @types = Array.new
132
+ end
133
+
134
+ # Evaluate formula
135
+ #
136
+ def eval(formula)
137
+ tokenize(formula.to_s.strip)
138
+ @tokens.pop if @tokens.last == ';'
139
+ if @tokens.include?(';')
140
+ expr = [ [] ]
141
+ @tokens.each do |token|
142
+ if token == ';'
143
+ expr << []
144
+ else
145
+ expr.last << token
146
+ end
147
+ end
148
+ expr.collect { |e| Kernel.eval(e.join, binding) }
149
+ else
150
+ Kernel.eval(@tokens.join, binding)
151
+ end
152
+ end
153
+
154
+ protected
155
+
156
+ # Register a function
157
+ #
158
+ # * Converts name to symbol
159
+ # * Wraps function with a debugging procedure
160
+ #
161
+ def register(name, &block)
162
+ @functions[name.to_sym] = debug(name.to_sym, &block)
163
+ end
164
+
165
+ # Wrap a procedure in a debugging procedure
166
+ #
167
+ # * Raise an error unless given the correct number of arguments
168
+ #
169
+ def debug(name, &func)
170
+ raise ArgumentError, 'block not given to debug' unless block_given?
171
+ Proc.new { |*args|
172
+ if func.arity >= 0 and func.arity != args.size
173
+ raise ArgumentError, "wrong number of arguments (#{args.size} for #{func.arity}) for #{name}"
174
+ end
175
+ func.call(*args)
176
+ }
177
+ end
178
+
179
+ # Tokenize formula (String => Array)
180
+ #
181
+ def tokenize(formula)
182
+ ops = Hash[*EXCEL_OPERATORS.zip(RUBY_OPERATORS).flatten]
183
+
184
+ # Parse formula
185
+ types, @tokens = @lexer.scan(formula)
186
+ @types = types.split(//)
187
+ raise SyntaxError, 'unbalanced parentheses' unless balanced?
188
+
189
+ # Parse tokens
190
+ @tokens.each_index do |i|
191
+ type, token = @types[i], @tokens[i]
192
+ token = token.to_i if type == 'i'
193
+ token = token.to_f if type == 'f'
194
+ if type == 't'
195
+ token = token.to_sym
196
+ if @functions.key?(token)
197
+ if @tokens[i+1] != '('
198
+ raise ArgumentError, "wrong number of arguments for #{token}"
199
+ else
200
+ @types[i] = 'F'
201
+ token = '@functions['+token.inspect+'].call'
202
+ end
203
+ elsif @constants.key?(token)
204
+ @types[i] = 'C'
205
+ token = '@constants['+token.inspect+']'
206
+ else
207
+ raise NameError, "unknown constant #{token}"
208
+ end
209
+ end
210
+ token = ops[token] if EXCEL_OPERATORS.include?(token)
211
+ @tokens[i] = token
212
+ end
213
+
214
+ @tokens
215
+ end
216
+
217
+ # Check that parentheses are balanced
218
+ #
219
+ def balanced?
220
+ @tokens.find_all { |t| ['(', ')'].include?(t) }.size % 2 == 0
221
+ end
222
+
223
+ # False if nil, false or zero
224
+ #
225
+ def to_b(value)
226
+ [nil,false,0].include?(value) ? false : true
227
+ end
228
+
229
+ end
230
+
231
+ # John Carter's LittleLexer
232
+ #
233
+ # http://www.rubyforge.org/projects/littlelexer
234
+ #
235
+ class LXL::LittleLexer #:nodoc: all
236
+
237
+ class LexerJammed < Exception; end
238
+
239
+ def initialize(regex_to_char,skip_white_space = true)
240
+ @skip_white_space = skip_white_space
241
+ @regex_to_char = regex_to_char
242
+ end
243
+
244
+ def scan(string,string_token_list=nil)
245
+ result_string = ''
246
+ token_list = []
247
+
248
+ if string_token_list
249
+ next_token(string) do |t,token, tail|
250
+ result_string << t
251
+ token_list << [string_token_list[0...tail], string[0...tail]]
252
+ string = string[tail..-1]
253
+ string_token_list = string_token_list[tail..-1]
254
+ end
255
+ else
256
+ next_token(string) do |t,token, tail|
257
+ result_string << t
258
+ token_list << token
259
+ end
260
+ end
261
+ return result_string, token_list
262
+ end
263
+
264
+ private
265
+
266
+ def next_token( string)
267
+ match_data = nil
268
+ while string != ''
269
+ failed = true
270
+ @regex_to_char.each do |regex,char|
271
+ match_data = regex.match(string)
272
+ next unless match_data
273
+ token = match_data[0]
274
+ yield char,token, match_data.end(0) unless char == ?\s && @skip_white_space
275
+ string = match_data.post_match
276
+ failed = false
277
+ break
278
+ end
279
+ raise LexerJammed, string if failed
280
+ end
281
+
282
+ rescue RegexpError => details
283
+ raise RegexpError, "Choked while '#{@scanner}' was trying to match '#{string}' : #{details}"
284
+ end
285
+
286
+ end
287
+
288
+ # Test
289
+ #
290
+ if $0 == __FILE__
291
+
292
+ formulas = %{
293
+ ((1+2)*(10-6))/2;
294
+ DATETIME("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00);
295
+ IN(" is ", "this is a string");
296
+ LIST(1, "two", 3.0);
297
+ IN("b", LIST("a", "b", "c"));
298
+ AND(TRUE, NULL);
299
+ OR(TRUE, FALSE);
300
+ IF(1+1=2, "yes", "no");
301
+ }
302
+
303
+ # single formula
304
+ puts LXL.eval('5+5').inspect # => 10
305
+
306
+ # multiple formulas separated by semi-colon
307
+ puts LXL.eval(formulas).inspect # => [6, true, true, [1, "two", 3.0], true, false, true, "yes"]
308
+
309
+ end
data/test/lxl_test.rb ADDED
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.unshift('../lib')
2
+ require 'test/unit'
3
+ require 'lxl'
4
+
5
+ class LXLTest < Test::Unit::TestCase
6
+
7
+ def test_single_formula
8
+ assert_equal(10, LXL.eval('5+5'))
9
+ end
10
+
11
+ def test_multiple_formula
12
+ formulas = %{
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");
21
+ }
22
+ expected = [6, true, true, [1, "two", 3.0], true, false, true, "yes"]
23
+ results = LXL.eval(formulas)
24
+ expected.each_index { |i| assert_equal(expected[i], results[i]) }
25
+ end
26
+
27
+ end
metadata ADDED
@@ -0,0 +1,42 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.3
3
+ specification_version: 1
4
+ name: lxl
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2005-02-08
8
+ summary: LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas. Easily extended with new constants and functions.
9
+ require_paths:
10
+ - lib
11
+ email: kh@newclear.ca
12
+ homepage: lxl.rubyforge.org
13
+ rubyforge_project: lxl
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Kevin Howe
29
+ files:
30
+ - lib
31
+ - test
32
+ - README
33
+ - VERSION
34
+ - lib/lxl.rb
35
+ - test/lxl_test.rb
36
+ test_files: []
37
+ rdoc_options: []
38
+ extra_rdoc_files: []
39
+ executables: []
40
+ extensions: []
41
+ requirements: []
42
+ dependencies: []