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.
- data/README +46 -0
- data/VERSION +1 -0
- data/lib/lxl.rb +309 -0
- data/test/lxl_test.rb +27 -0
- 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: []
|