simpleexpression 0.0.1

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