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.
- data/lib/shunt/algorithm.rb +61 -0
- data/lib/shunt/context.rb +80 -0
- data/lib/shunt/numeric.rb +31 -0
- data/lib/shunt.rb +33 -0
- data/rakefile.rb +28 -0
- data/readme.txt +61 -0
- data/spec/algorithm_spec.rb +19 -0
- data/spec/context_spec.rb +72 -0
- metadata +52 -0
@@ -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
|
+
|