lxl 0.1.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 (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: []