simpleexpression 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ == 0.0.1 2007-10-12
2
+
3
+ * 1 major enhancement:
4
+ * Initial public release
5
+ * Much unit testing
6
+ * Test expression parsing but only a tiny bit of optimization
@@ -0,0 +1,2 @@
1
+ This software is released into the public domain in 2007 by Noah Gibbs.
2
+ It may be used, modified or redistributed in any way whatsoever.
@@ -0,0 +1,28 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/expressionparser.rb
9
+ lib/simpleexpression.rb
10
+ lib/simpleexpression/version.rb
11
+ log/debug.log
12
+ script/destroy
13
+ script/destroy.cmd
14
+ script/generate
15
+ script/generate.cmd
16
+ script/txt2html
17
+ script/txt2html.cmd
18
+ setup.rb
19
+ tasks/deployment.rake
20
+ tasks/environment.rake
21
+ tasks/website.rake
22
+ test/test_helper.rb
23
+ test/test_simpleexpression.rb
24
+ website/index.html
25
+ website/index.txt
26
+ website/javascripts/rounded_corners_lite.inc.js
27
+ website/stylesheets/screen.css
28
+ website/template.rhtml
@@ -0,0 +1,7 @@
1
+ The SimpleExpression library is designed to allow simple manipulations of
2
+ algebraic quantities like "3x + 4" or "7xy * sin(z)". In its initial form
3
+ it can parse and evaluate such expressions, and may eventually become more
4
+ capable.
5
+
6
+ It's designed to be hooked into a numeric integrator, though that will
7
+ be through a different gem rather than the SimpleExpression gem.
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,71 @@
1
+ require 'simpleexpression/version'
2
+
3
+ AUTHOR = 'Noah Gibbs' # can also be an array of Authors
4
+ EMAIL = "angelbob@users.sourceforge.net"
5
+ DESCRIPTION = "simple algebraic expressions library"
6
+ GEM_NAME = 'simpleexpression' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'diffeq' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Simpleexpression::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'simpleexpression documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/test_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\\n\\n")
62
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+
64
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
65
+
66
+ end
67
+
68
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
69
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
70
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
71
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'simpleexpression'
@@ -0,0 +1,407 @@
1
+ #
2
+ # SimpleExpression library
3
+ # Copyright (C) 2007 Noah Gibbs
4
+ #
5
+ # This library is in the public domain, and may be redistributed in any
6
+ # way.
7
+ #
8
+
9
+ class EPNode < Array
10
+ private
11
+
12
+ def self.prveval(obj, vars, do_raise=true)
13
+ return :NoInfo if obj == :NoInfo
14
+ if SimpleExpression.number?(obj)
15
+ return obj
16
+ end
17
+ if obj.kind_of?(String)
18
+ return vars[obj] if vars[obj]
19
+ raise "Unknown variable '#{obj}'!" if do_raise
20
+ return :NoInfo
21
+ end
22
+ obj.eval(vars)
23
+ end
24
+
25
+ public
26
+
27
+ def eval(vars = {})
28
+ args = self[1..-1].collect { |arg| EPNode.prveval(arg, vars) }
29
+ op = self[0]
30
+
31
+ op = "**" if op == "^" # exponentiation uses different op in ruby
32
+
33
+ if SimpleExpression.operator?(op)
34
+ if self.length == 2
35
+ return (op == "-" ? -args[0] : args[0])
36
+ elsif self.length == 3
37
+ return (args[0]).send(op, args[1])
38
+ else
39
+ raise "Unexpected number of arguments to #{op}!"
40
+ end
41
+ end
42
+
43
+ if SimpleExpression.function?(op)
44
+ return Math.send(op, args[0])
45
+ end
46
+
47
+ return vars[op] if vars[op] and self.length == 1
48
+
49
+ raise "Unknown expression in EPNode##eval!"
50
+ end
51
+
52
+ # For now, just do constant folding
53
+ def optimize(vars = {})
54
+ args = self[1..-1].collect { |arg| EPNode.prveval(arg, vars, false) }
55
+ if args.any? { |arg| arg == :NoInfo }
56
+ opt = [ nil ] * args.length
57
+ args.each_with_index do |arg, index|
58
+ opt[index] = args[index]
59
+ opt[index] = self[index + 1] if args[index] == :NoInfo
60
+ end
61
+ return EPNode.new(opt)
62
+ end
63
+
64
+ self.eval(vars)
65
+ end
66
+ end
67
+
68
+ class SimpleExpression
69
+
70
+ NUMBER_REGEXP = /^([-+0-9.]+(e[-+]?[0-9]+)?)(.*)$/
71
+ OPERATORS = "-+/*()[]{}^"
72
+ OPERATOR_REGEXP = Regexp.new("[#{Regexp.escape(OPERATORS)}]")
73
+ FUNCLIST = ["sin", "cos", "tan"]
74
+ PAREN_LIST = ["(", "[", "{"]
75
+
76
+ def self.number?(quantity)
77
+ return true if quantity.kind_of?(Float)
78
+ return true if quantity.kind_of?(Fixnum)
79
+ return true if quantity.kind_of?(Bignum)
80
+ false
81
+ end
82
+
83
+ def self.operator?(string)
84
+ (string =~ OPERATOR_REGEXP) ? true : false
85
+ end
86
+
87
+ def self.function?(string)
88
+ FUNCLIST.include?(string)
89
+ end
90
+
91
+ def self.vars_from_parse_tree(tree)
92
+ var_table = {}
93
+
94
+ token_iter(tree) do |item|
95
+ var_table[item] = 1 if SimpleExpression.legal_variable_name?(item) and
96
+ not SimpleExpression.function?(item)
97
+ end
98
+
99
+ return var_table.keys
100
+ end
101
+
102
+ def expression_from_string(string)
103
+ raise "Not a string!" unless string.kind_of?(String)
104
+
105
+ tokens = SimpleExpression.tokenize(string)
106
+
107
+ tokens = SimpleExpression.group_parens(tokens, "(", ")")
108
+ tokens = SimpleExpression.group_parens(tokens, "[", "]")
109
+ tokens = SimpleExpression.group_parens(tokens, "{", "}")
110
+
111
+ tokens = SimpleExpression.group_functions(tokens, FUNCLIST,
112
+ PAREN_LIST)
113
+ tokens = SimpleExpression.group_right_left(tokens, ["^"])
114
+ tokens = SimpleExpression.group_unary(tokens, ["-", "+"])
115
+ tokens = SimpleExpression.group_operands(tokens)
116
+ tokens = SimpleExpression.group_left_right(tokens, ["*", "/"])
117
+ tokens = SimpleExpression.group_left_right(tokens, ["+", "-"])
118
+ tokens = SimpleExpression.unwrap_parens(tokens, PAREN_LIST)
119
+
120
+ # If the outer layer is a single element array around an array,
121
+ # upwrap it.
122
+ if tokens.kind_of?(Array) and tokens.length == 1 and
123
+ tokens[0].kind_of?(Array)
124
+ tokens = tokens[0]
125
+ end
126
+
127
+ #print "+++ Parsed: #{pretty_print_parse_tree(tokens)}\n"
128
+
129
+ raise "Can't completely parse expression '#{string}'!" unless
130
+ SimpleExpression.fully_parsed?(tokens)
131
+
132
+ tokens
133
+ end
134
+
135
+ def self.tokenize(expression)
136
+ tokenlist = []
137
+ current = ""
138
+
139
+ raise "Not a string!" unless expression.kind_of?(String)
140
+
141
+ if not expression or expression == ""
142
+ return []
143
+ end
144
+
145
+ while expression != ""
146
+ # Cut out whitespace
147
+ if /\s/.match(expression[0..0])
148
+ expression.sub!(/^\s+/, "")
149
+ next
150
+ end
151
+
152
+ # Grab a variable name
153
+ if expression[0..0] =~ /[A-Za-z]/
154
+ matchobj = /^([A-Za-z][-_A-Za-z0-9]*)(.*)$/.match(expression)
155
+
156
+ raise "Can't parse variable name in expression!" unless matchobj
157
+ unless SimpleExpression.legal_variable_name?(matchobj[1])
158
+ raise "Illegal var name"
159
+ end
160
+
161
+ tokenlist += [ matchobj[1] ]
162
+ expression = matchobj[2]
163
+ next
164
+ end
165
+
166
+ # Grab an operator symbol
167
+ if expression[0..0] =~ OPERATOR_REGEXP
168
+ tokenlist += [ expression[0..0] ]
169
+ expression = expression [1, expression.size]
170
+ next
171
+ end
172
+
173
+ # Grab a number
174
+ if expression[0..0] =~ /[-+0-9.]/
175
+ matchobj = NUMBER_REGEXP.match(expression)
176
+ unless matchobj
177
+ raise "Can't parse number in expression!"
178
+ end
179
+ num = matchobj[1].to_f()
180
+ tokenlist += [ num ]
181
+ expression = matchobj[3]
182
+ next
183
+ end
184
+
185
+ raise "Untokenizable expression '#{expression}'!\n"
186
+ end
187
+
188
+ tokenlist
189
+ end
190
+
191
+ # If the argument is an array containing arrays, this will call the
192
+ # function in question (which takes and returns an array) on each
193
+ # subarray, then on the top-level array with the replaced subarrays.
194
+ #
195
+ def self.grouping_iter(tokens, &myproc)
196
+ return [] if tokens.nil? or tokens.empty?
197
+ tokenclass = tokens.class
198
+
199
+ newtokens = tokens.collect do |token|
200
+ token.kind_of?(Array) ? grouping_iter(token, &myproc) : token
201
+ end
202
+
203
+ # Preserve EPNode-ness
204
+ tokenclass.new(myproc.call(tokenclass.new(newtokens)))
205
+ end
206
+
207
+ # Iterate over every token, changing nothing and returning nil.
208
+ #
209
+ def self.token_iter(tokens)
210
+ return if tokens.nil? or tokens.empty?
211
+
212
+ grouping_iter(tokens) do |tokens|
213
+ tokens.each do |token|
214
+ yield(token)
215
+ end
216
+ end
217
+
218
+ nil
219
+ end
220
+
221
+ # The full_grouping_pass function iterates over the entire parse
222
+ # tree, examining unparsed sections and testing (with grouptestproc)
223
+ # to see if they can be partially parsed. If so, tokenchangeproc is
224
+ # used to determine the new token string after replacement.
225
+ #
226
+ def self.full_grouping_pass(tokens, grouptestproc, tokenchangeproc,
227
+ indexchangeproc = lambda { |tokens, index| index + 1 } )
228
+ return [] if tokens.nil? or tokens.empty?
229
+
230
+ finaltokens = grouping_iter(tokens, &lambda { |tokens|
231
+ return [] if tokens == []
232
+ return tokens if parsed?(tokens)
233
+
234
+ newtokens = [ ]
235
+ index = 0
236
+
237
+ while index < tokens.length
238
+ if grouptestproc.call(tokens, index)
239
+ newtokens = tokenchangeproc.call(tokens, newtokens, index)
240
+ index = indexchangeproc.call(tokens, index)
241
+ else
242
+ newtokens += [ tokens[index] ]
243
+ end
244
+
245
+ index += 1
246
+ end
247
+
248
+ newtokens
249
+ }) # end lambda and function call
250
+
251
+ finaltokens
252
+ end
253
+
254
+ def self.group_parens(tokens, open_paren, close_paren)
255
+ group_stack = []
256
+ savedindex = -1
257
+ savedtokens = nil
258
+
259
+ newtokens = full_grouping_pass(tokens,
260
+ proc { |tokens, index|
261
+ if savedindex >= index and group_stack != []
262
+ raise "Unmatched '#{open_paren}' in '#{tokens}' " +
263
+ "in group_parens!"
264
+ end
265
+ savedindex = index
266
+
267
+ return true if tokens[index] == open_paren
268
+ return true if tokens[index] == close_paren
269
+ false
270
+ },
271
+ proc { |tokens, newtokens, index|
272
+ if tokens[index] == open_paren
273
+ group_stack += [ newtokens ]
274
+ return []
275
+ end
276
+ if tokens[index] == close_paren
277
+ if group_stack == []
278
+ raise "Unmatched '#{close_paren}' parsing " +
279
+ "'#{tokens}' in group_parens!"
280
+ end
281
+ rettokens = group_stack[-1] +
282
+ [ EPNode.new([open_paren] + [ newtokens ]) ]
283
+ group_stack = group_stack[0..-2]
284
+
285
+ rettokens
286
+ end
287
+ },
288
+ proc { |tokens, index| index }
289
+ ) # end full_grouping_pass()
290
+
291
+ if group_stack != []
292
+ raise "Unmatched '#{open_paren}' in '#{tokens}' in group_parens!\n"
293
+ end
294
+
295
+ newtokens
296
+ end
297
+
298
+ def self.unwrap_parens(tokens, paren_list)
299
+ grouping_iter(tokens) do |tokens|
300
+ if tokens.length > 1 and paren_list.include?(tokens[0])
301
+ [ tokens[0] ] + tokens[1]
302
+ else
303
+ tokens
304
+ end
305
+ end
306
+ end
307
+
308
+ def self.group_functions(tokens, funclist, parenlist)
309
+ full_grouping_pass(tokens,
310
+ # test if we should group as function
311
+ proc { |tokens, index|
312
+ return false if index == (tokens.length - 1)
313
+ function?(tokens[index]) and
314
+ tokens[index+1].kind_of?(Array) and
315
+ parenlist.include?(tokens[index + 1][0])
316
+ },
317
+ proc { |tokens, newtokens, index|
318
+ newtokens + [ EPNode.new([ tokens[index],
319
+ tokens[index + 1] ]) ]
320
+ })
321
+ end
322
+
323
+ def self.group_unary(tokens, oplist)
324
+ full_grouping_pass(tokens,
325
+ # test if we should group unary minus or plus
326
+ proc { |tokens, index|
327
+ return false if index == (tokens.length - 1)
328
+ oplist.include?(tokens[index]) and
329
+ operand?(tokens[index + 1]) and
330
+ (index == 0 or not operand?(tokens[index - 1]))
331
+ },
332
+ proc { |tokens, newtokens, index|
333
+ newtokens + [ EPNode.new([ tokens[index],
334
+ tokens[index + 1] ]) ]
335
+ })
336
+ end
337
+
338
+ def self.group_operands(tokens)
339
+ full_grouping_pass(tokens,
340
+ # test if we should group two terms adjacent
341
+ proc { |tokens, index|
342
+ return false if index == (tokens.length - 1)
343
+ operand?(tokens[index]) and
344
+ operand?(tokens[index + 1])
345
+ },
346
+ proc { |tokens, newtokens, index|
347
+ newtokens + [ EPNode.new([ "*", tokens[index],
348
+ tokens[index + 1] ]) ]
349
+ })
350
+ end
351
+
352
+ def self.parse_tree_reverse(tokens, oplist)
353
+ grouping_iter(tokens) do |list|
354
+ operator?(list[0]) ? [ list[0] ] + list[1..-1].reverse : list.reverse()
355
+ end
356
+ end
357
+
358
+ def self.group_right_left(tokens, oplist)
359
+ return [] if tokens.nil? or tokens.empty?
360
+
361
+ newtokens = parse_tree_reverse(tokens, oplist)
362
+
363
+ grouped_tokens = group_left_right(newtokens, oplist)
364
+
365
+ finaltokens = parse_tree_reverse(grouped_tokens, oplist)
366
+ end
367
+
368
+ def self.operand?(token)
369
+ token.kind_of?(Array) or
370
+ SimpleExpression.number?(token) or
371
+ (SimpleExpression.legal_variable_name?(token) and
372
+ not SimpleExpression.function?(token))
373
+ end
374
+
375
+ def self.group_left_right(tokens, oplist)
376
+ full_grouping_pass(tokens,
377
+ # test if we should group around a binary op
378
+ proc { |tokens, index|
379
+ return false if index == 0
380
+ return false if index == (tokens.length - 1)
381
+ oplist.include?(tokens[index]) and
382
+ operand?(tokens[index-1]) and
383
+ operand?(tokens[index + 1])
384
+ },
385
+ proc { |tokens, newtokens, index|
386
+ popped_token = newtokens[-1]
387
+ newtokens = newtokens[0..-2]
388
+ newtokens + [ EPNode.new([ tokens[index],
389
+ popped_token,
390
+ tokens[index+1] ]) ]
391
+ })
392
+ end
393
+
394
+ def self.parsed?(tokens)
395
+ return true if tokens.nil? or tokens.empty?
396
+ return true if tokens.length == 1
397
+ return true if tokens.kind_of?(EPNode)
398
+
399
+ false
400
+ end
401
+
402
+ def self.fully_parsed?(tree)
403
+ parsed?(tree) and
404
+ tree.select{|item| item.kind_of?(Array) }.all?{|arr| fully_parsed?(arr)}
405
+ end
406
+
407
+ end # class SimpleExpression