Shunt 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,61 @@
1
+ module Shunt
2
+
3
+ def self.convert(infix_expression, context = NumericContext.instance)
4
+ output, stack = [], []
5
+
6
+ infix_expression.each do |token|
7
+ if context.opening_bracket == token || context.function?(token)
8
+ stack << token
9
+ elsif context.closing_bracket == token
10
+ until context.opening_bracket == stack.last || stack.empty?
11
+ output << stack.pop
12
+ end
13
+ if context.opening_bracket == stack.last
14
+ stack.pop
15
+ else
16
+ raise ConversionError, "missing %p" % context.opening_bracket
17
+ end
18
+ if !stack.empty? && context.function?(stack.last)
19
+ output << stack.pop
20
+ end
21
+ elsif context.function_seperator == token
22
+ until context.opening_bracket == stack.last || stack.empty?
23
+ output << stack.pop
24
+ end
25
+ unless context.opening_bracket == stack.last
26
+ raise ConversionError, "missing %p or misplaced %p" % [
27
+ context.opening_bracket, context.function_seperator
28
+ ]
29
+ end
30
+ elsif context.operator?(token)
31
+ unless stack.empty?
32
+ until stack.empty?
33
+ if context.operator?(stack.last) && context.precedence(token) <= context.precedence(stack.last)
34
+ output << stack.pop
35
+ else
36
+ break
37
+ end
38
+ end
39
+ end
40
+ stack << token
41
+ else
42
+ output << token
43
+ end
44
+ end
45
+
46
+ return output + stack.reverse
47
+ end
48
+
49
+ def self.eval(infix_expression, context = NumericContext.instance)
50
+ convert(infix_expression, context).inject([]) do |stack, token|
51
+ if context.operator?(token)
52
+ context.call_operator(token, stack)
53
+ elsif context.function?(token)
54
+ context.call_function(token, stack)
55
+ else
56
+ stack << token
57
+ end
58
+ end.first
59
+ end
60
+
61
+ end
@@ -0,0 +1,80 @@
1
+ module Shunt
2
+ class Context
3
+
4
+ include Singleton
5
+
6
+ class <<self
7
+ def opening_bracket(token)
8
+ @opening_bracket = token
9
+ end
10
+ def closing_bracket(token)
11
+ @closing_bracket = token
12
+ end
13
+ def function_seperator(token)
14
+ @function_seperator = token
15
+ end
16
+ def operator(name, equivalent = nil)
17
+ operators << name
18
+ operator_equivalents << (equivalent || name)
19
+ end
20
+ end
21
+
22
+ # Duck-type from here to private
23
+
24
+ def opening_bracket
25
+ self.class.instance_variable_get('@opening_bracket') || '('
26
+ end
27
+
28
+ def closing_bracket
29
+ self.class.instance_variable_get('@closing_bracket') || ')'
30
+ end
31
+
32
+ def function_seperator
33
+ self.class.instance_variable_get('@function_seperator') || ','
34
+ end
35
+
36
+ def operator?(token)
37
+ self.class.operators.include? token
38
+ end
39
+
40
+ def function?(token)
41
+ token.is_a?(String) && respond_to?(token)
42
+ end
43
+
44
+ def precedence(operator_token)
45
+ self.class.operators.size - self.class.operators.index(operator_token)
46
+ end
47
+
48
+ def call_operator(operator_token, stack)
49
+ stack << stack.delete_at(-2).send(ruby_equivalent_of(operator_token), stack.pop)
50
+ end
51
+
52
+ def call_function(function_token, stack)
53
+ stack << case arity = method(function_token).arity
54
+ when 0
55
+ send function_token
56
+ when -1
57
+ send function_token, *stack.last!(stack.size)
58
+ else
59
+ send function_token, *stack.last!(arity)
60
+ end
61
+ end
62
+
63
+
64
+ private
65
+
66
+ class <<self
67
+ def operators
68
+ @operators ||= []
69
+ end
70
+ def operator_equivalents
71
+ @operator_equivalents ||= []
72
+ end
73
+ end
74
+
75
+ def ruby_equivalent_of(operator_token)
76
+ self.class.operator_equivalents[self.class.operators.index(operator_token)]
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,31 @@
1
+ module Shunt
2
+ class NumericContext < Context
3
+
4
+ operator '^', :**
5
+
6
+ operator '*'
7
+
8
+ operator '/'
9
+
10
+ operator '+'
11
+
12
+ operator '-'
13
+
14
+ def abs(number)
15
+ number.abs
16
+ end
17
+
18
+ def mean(*numbers)
19
+ numbers.inject { |sum, n| sum + n } / numbers.size
20
+ end
21
+
22
+ def mod(a, b)
23
+ a % b
24
+ end
25
+
26
+ def pi
27
+ Math::PI
28
+ end
29
+
30
+ end
31
+ end
data/lib/shunt.rb ADDED
@@ -0,0 +1,33 @@
1
+ # Shunt - A Ruby implementation of Dijkstra's Shunting Yard Algorithm.
2
+ #
3
+ # Copyright (c) 2006 Tim Fletcher <twoggle@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ # of the Software, and to permit persons to whom the Software is furnished to
10
+ # do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
18
+ # OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
20
+ # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'singleton'
23
+ require 'shunt/context'
24
+ require 'shunt/numeric'
25
+ require 'shunt/algorithm'
26
+
27
+ class Array
28
+ def last!(n = 1); slice!(size - n, n) end
29
+ end
30
+
31
+ module Shunt
32
+ class ConversionError < StandardError; end
33
+ end
data/rakefile.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'rake/gempackagetask'
6
+
7
+ require 'spec'
8
+ require 'spec/rake/spectask'
9
+
10
+ gemspec = Gem::Specification.new do |s|
11
+ s.name = 'Shunt'
12
+ s.version = '0.1'
13
+ s.summary = "A Ruby implementation of Dijkstra's Shunting Yard Algorithm"
14
+ s.files = FileList['{lib,spec}/**/*.rb', '*.txt', 'rakefile.rb']
15
+ s.require_path = 'lib'
16
+ s.autorequire = 'shunt'
17
+ s.has_rdoc = false
18
+ s.rubyforge_project = 'shunt'
19
+ s.homepage = 'http://shunt.rubyforge.org/'
20
+ s.author = 'Tim Fletcher'
21
+ s.email = 'twoggle@gmail.com'
22
+ end
23
+
24
+ Rake::GemPackageTask.new(gemspec) { |t| t.package_dir = 'gems' }
25
+
26
+ Spec::Rake::SpecTask.new# { |t| t.spec_opts = ['-f s'] }
27
+
28
+ task :default => :spec
data/readme.txt ADDED
@@ -0,0 +1,61 @@
1
+ = Shunt
2
+
3
+ A Ruby implementation of Dijkstra's "Shunting Yard Algorithm", for converting
4
+ tokens from infix notation to postfix notation (aka Reverse Polish Notation).
5
+
6
+ Find out more at [Wikipedia](http://en.wikipedia.org/wiki/Reverse_Polish_notation).
7
+
8
+ == Usage
9
+
10
+ An expression (array of tokens) can be converted from infix to postfix like so:
11
+
12
+ Shunt.convert([3,'+',4,'*',2,'/','(',1,'-',5,')','^',2]) # => [3,4,2,'*',1,5,'-',2,'^','/','+']
13
+
14
+ The expression can be evaluated (with implicit conversion) like so:
15
+
16
+ Shunt.eval([3,'+',4,'*',2,'/','(',1,'-',5,')','^',2]) # => 7/2
17
+
18
+ Some (not all) invalid expressions can be detected:
19
+
20
+ Shunt.eval([1,'+',2,'*',3,')']) # raises Shunt::ConversionError
21
+
22
+ Remember to require 'mathn' if you want precise answers.
23
+
24
+ == Contexts
25
+
26
+ The default context (used in the examples above) is numeric i.e. it expects an
27
+ array of Numeric objects, brackets/parentheses (as Strings), binary operators
28
+ (as Strings), and function symbols (as Strings or Symbols).
29
+
30
+ Other contexts can be defined by subclassing Shunt::Context, e.g.
31
+
32
+ class MyContext < Shunt::Context
33
+
34
+ # Declare a binary operator, '&',
35
+ # with a natural equivalent Ruby method:
36
+
37
+ operator '&'
38
+
39
+ # Declare a binary operator, 'or',
40
+ # with :| as the equivalent Ruby method:
41
+
42
+ operator 'or', :|
43
+
44
+ # Operators are given precedence in the order that they are declared,
45
+ # so in this example '&' has higher precedence than 'or'.
46
+
47
+ # Functions can be defined, as ordinary Ruby methods:
48
+
49
+ def foo; end
50
+
51
+ def bar(role); end
52
+
53
+ def blah(*roles); end
54
+
55
+ end
56
+
57
+ This can then be used to evaluate an appropriate expression, e.g.
58
+
59
+ permissions = Shunt.eval([User,'&','(',Admin,'or',Moderator,')'], MyContext.instance)
60
+
61
+ Context's are singletons (remember .instance), and are easily duck-typed.
@@ -0,0 +1,19 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'shunt'
4
+ require 'mathn'
5
+
6
+ context 'Algorithm' do
7
+
8
+ specify 'should correctly accept Wikipedia example' do
9
+ expression = [3,'+',4,'*',2,'/','(',1,'-',5,')','^',2]
10
+ Shunt.convert(expression).should_equal [3,4,2,'*',1,5,'-',2,'^','/','+']
11
+ Shunt.eval(expression).should_equal 7/2
12
+ end
13
+
14
+ specify 'should correctly fail on invalid expressions' do
15
+ lambda { Shunt.eval([1,'+',2,'*',3,')']) }.should_raise Shunt::ConversionError
16
+ lambda { Shunt.eval([1,',',2]) }.should_raise Shunt::ConversionError
17
+ end
18
+
19
+ end
@@ -0,0 +1,72 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'shunt'
4
+
5
+ context 'A (Shunt) Context' do
6
+
7
+ setup do
8
+ @class = Class.new(Shunt::Context)
9
+ @instance = @class.instance
10
+ end
11
+
12
+ specify 'should allow overriding of function_seperator' do
13
+ @instance.function_seperator.should_equal ','
14
+ lambda { @class.function_seperator ';' }.should_not_raise
15
+ @instance.function_seperator.should_equal ';'
16
+ end
17
+
18
+ specify 'should allow overriding of opening_bracket' do
19
+ @instance.opening_bracket.should_equal '('
20
+ lambda { @class.opening_bracket '[' }.should_not_raise
21
+ @instance.opening_bracket.should_equal '['
22
+ end
23
+
24
+ specify 'should allow overriding of closing_bracket' do
25
+ @instance.closing_bracket.should_equal ')'
26
+ lambda { @class.closing_bracket ']' }.should_not_raise
27
+ @instance.closing_bracket.should_equal ']'
28
+ end
29
+
30
+ specify 'should allow operators that are also ruby methods' do
31
+ @instance.operator?('+').should_be false
32
+ lambda { @class.operator '+' }.should_not_raise
33
+ @instance.operator?('+').should_be true
34
+ @instance.call_operator('+', [1, 2]).should_equal [3]
35
+ end
36
+
37
+ specify 'should allow operators that are aliases of ruby methods' do
38
+ @instance.operator?('plus').should_be false
39
+ lambda { @class.operator 'plus', :+ }.should_not_raise
40
+ @instance.operator?('plus').should_be true
41
+ @instance.call_operator('plus', [1, 2]).should_equal [3]
42
+ end
43
+
44
+ specify 'should allow definition and call of a function with arity 0' do
45
+ @instance.function?('ans').should_be false
46
+ lambda { @class.class_eval { define_method(:ans) { 42 } } }.should_not_raise
47
+ @instance.call_function('ans', []).should_equal [42]
48
+ @instance.function?('ans').should_be true
49
+ end
50
+
51
+ specify 'should allow definition and call of a function with arity 1' do
52
+ @instance.function?('inc').should_be false
53
+ lambda { @class.class_eval { define_method(:inc) { |x| x + 1 } } }.should_not_raise
54
+ @instance.call_function('inc', [0]).should_equal [1]
55
+ @instance.function?('inc').should_be true
56
+ end
57
+
58
+ specify 'should allow definition and call of a function with arity 2' do
59
+ @instance.function?('add').should_be false
60
+ lambda { @class.class_eval { define_method(:add) { |one, two| one + two } } }.should_not_raise
61
+ @instance.call_function('add', [1, 2]).should_equal [3]
62
+ @instance.function?('add').should_be true
63
+ end
64
+
65
+ specify 'should allow definition and call of a function with arity -1' do
66
+ @instance.function?('min').should_be false
67
+ lambda { @class.class_eval { define_method(:min) { |*args| args.min } } }.should_not_raise
68
+ @instance.call_function('min', [10, 1, 2]).should_equal [1]
69
+ @instance.function?('min').should_be true
70
+ end
71
+
72
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: Shunt
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.1"
7
+ date: 2006-05-29 00:00:00 +01:00
8
+ summary: A Ruby implementation of Dijkstra's Shunting Yard Algorithm
9
+ require_paths:
10
+ - lib
11
+ email: twoggle@gmail.com
12
+ homepage: http://shunt.rubyforge.org/
13
+ rubyforge_project: shunt
14
+ description:
15
+ autorequire: shunt
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Tim Fletcher
30
+ files:
31
+ - lib/shunt.rb
32
+ - lib/shunt/algorithm.rb
33
+ - lib/shunt/context.rb
34
+ - lib/shunt/numeric.rb
35
+ - spec/algorithm_spec.rb
36
+ - spec/context_spec.rb
37
+ - readme.txt
38
+ - rakefile.rb
39
+ test_files: []
40
+
41
+ rdoc_options: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ executables: []
46
+
47
+ extensions: []
48
+
49
+ requirements: []
50
+
51
+ dependencies: []
52
+