exalted_math 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ doc
data/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ = Exalted Math Parser
2
+
3
+ This is a very simple project both to teach myself to use Treetop, and to
4
+ implement simple (or not so simple) parsing of mathematics for Exalted character
5
+ sheets. Although I'm sure it could be used for other simple math situations.
6
+ It consists of two parts; parser and abstract syntax tree. The parser
7
+ constructs the abstract syntax tree as it parses the input string. The AST can
8
+ then be used to compute the value of the expression with a given context. The
9
+ AST also has a simple method to simplify it if possible. The AST is based on an
10
+ array class, which makes it very easy to serialize.
11
+
12
+ == Examples
13
+
14
+ @parser = Exalted::MathsParser.new
15
+
16
+ # simple maths
17
+ # The ast method results a two-tuple.
18
+ # The first value is the success or failure of the parse
19
+ # The second value is the AST, or failure message if it failed
20
+ @parser.ast('3 + 4')
21
+ #=> true, simple_ast
22
+
23
+ # symbolic values
24
+ @parser.ast('Essence * 4')
25
+ #=> true, symbolic_ast
26
+
27
+ # complex maths
28
+ @parser.ast('(Essence * 4) + Willpower + highest[2](Compassion,Conviction,Temperance,Valor)')
29
+ #=> true, complex_ast
30
+
31
+ # evaluate the Ast
32
+ Exalted::Ast.value(simple_ast)
33
+ #=> 7
34
+
35
+ # evaluate a more complex Ast
36
+ Exalted::Ast.value(symbolic_ast, {'essence' => 4})
37
+ #=> 16
38
+
39
+ == Syntax
40
+
41
+ The syntax supported by the parser is pretty simple. In the examples above,
42
+ almost all of it has been demonstrated.
43
+
44
+ [[0-9]+] A number
45
+ [[A-Za-z]+] A stat. This is looked up from the context.
46
+ [spec:"..."] A speciality. This is looked up from the context.
47
+ [highest(stat|number,...)] Has the value of the highest component.
48
+ [highest[n](stat|number,...)] Has the value of the highest n components.
49
+ [lowest(stat|number,...)] Has the value of the lowest component.
50
+ [lowest[2](stat|number,...)] Has the value of the lowest n components.
51
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # Rakefile for phedre
2
+ # Jonathan D. Stott <jonathan.stott@gmail.com>
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:spec) do |t|
6
+ t.libs << 'lib' << 'spec' << 'app'
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ t.verbose = false
9
+ end
10
+
11
+ task :default => :spec
12
+
13
+ begin
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "exalted_math"
17
+ gem.summary = "Parsing and evaluation of simple maths expressions for Exalted"
18
+ gem.description = "Parsing and evaluation of simple maths expressions for Exalted\n\nThis intended to aid in evaluating simple calculations which appear on character sheets, especially for Exalted."
19
+ gem.email = "jonathan.stott@gmail.com"
20
+ gem.homepage = "http://github.com/namelessjon/exalted_math"
21
+ gem.authors = ["Jonathan Stott"]
22
+ gem.add_dependency "treetop", "~> 1.4"
23
+ gem.add_development_dependency "bacon"
24
+ gem.add_development_dependency "yard"
25
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
26
+ end
27
+ Jeweler::GemcutterTasks.new
28
+ rescue LoadError
29
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
30
+ end
31
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{exalted_math}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jonathan Stott"]
12
+ s.date = %q{2010-06-06}
13
+ s.description = %q{Parsing and evaluation of simple maths expressions for Exalted
14
+
15
+ This intended to aid in evaluating simple calculations which appear on character sheets, especially for Exalted.}
16
+ s.email = %q{jonathan.stott@gmail.com}
17
+ s.extra_rdoc_files = [
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "exalted_math.gemspec",
26
+ "lib/exalted_math.rb",
27
+ "lib/exalted_math/ast.rb",
28
+ "lib/exalted_math/math.rb",
29
+ "lib/exalted_math/math.treetop",
30
+ "spec/ast_spec.rb",
31
+ "spec/parser_spec.rb",
32
+ "spec/spec_helper.rb"
33
+ ]
34
+ s.homepage = %q{http://github.com/namelessjon/exalted_math}
35
+ s.rdoc_options = ["--charset=UTF-8"]
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.3.7}
38
+ s.summary = %q{Parsing and evaluation of simple maths expressions for Exalted}
39
+ s.test_files = [
40
+ "spec/ast_spec.rb",
41
+ "spec/spec_helper.rb",
42
+ "spec/parser_spec.rb"
43
+ ]
44
+
45
+ if s.respond_to? :specification_version then
46
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
+ s.add_runtime_dependency(%q<treetop>, ["~> 1.4"])
51
+ s.add_development_dependency(%q<bacon>, [">= 0"])
52
+ s.add_development_dependency(%q<yard>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<treetop>, ["~> 1.4"])
55
+ s.add_dependency(%q<bacon>, [">= 0"])
56
+ s.add_dependency(%q<yard>, [">= 0"])
57
+ end
58
+ else
59
+ s.add_dependency(%q<treetop>, ["~> 1.4"])
60
+ s.add_dependency(%q<bacon>, [">= 0"])
61
+ s.add_dependency(%q<yard>, [">= 0"])
62
+ end
63
+ end
64
+
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/ruby
2
+ # Jonathan D. Stott <jonathan.stott@gmail.com>
3
+ module Exalted
4
+ # Provides a simple AST for Exalted Maths
5
+ class Ast < Array
6
+ class UnknownNodeError < ArgumentError
7
+ def initialize(type)
8
+ @message = "Unknown node type '#{type}'"
9
+ puts @message
10
+ end
11
+ end
12
+
13
+ def initialize(*args)
14
+ super(args)
15
+ end
16
+
17
+ # Is this AST constant?
18
+ #
19
+ # If an AST only has constant child nodes, it is constant.
20
+ # This allows some simplification.
21
+ #
22
+ # @return [Boolean] is the AST constant
23
+ def constant?
24
+ case self[0]
25
+ when 'mul', 'div', 'add', 'sub'
26
+ self[1].constant? and self[2].constant?
27
+ when 'num'
28
+ true
29
+ when 'stat', 'spec'
30
+ false
31
+ when 'max'
32
+ self[2].all? { |ast| ast.constant? }
33
+ else
34
+ raise UnknownNodeError, self[0]
35
+ end
36
+ end
37
+
38
+ # Is the given AST constant?
39
+ #
40
+ # If an AST only has constant child nodes, it is constant.
41
+ # This allows some simplification.
42
+ #
43
+ # @param ast [Ast] An Ast
44
+ #
45
+ # @return [Boolean] is the AST constant
46
+ def self.constant?(ast)
47
+ ast.constant?
48
+ end
49
+
50
+ # Is the given AST valid?
51
+ #
52
+ # Does the context provide all the values the AST is looking for?
53
+ #
54
+ # @param ast [Ast] The Ast to test
55
+ # @param valid_values [Array] Array of valid values, the keys of the context
56
+ # @param errors [Array] Optional array of errors
57
+ #
58
+ # @return [Boolean] True if valid, false if not.
59
+ def self.valid?(ast, valid_values, errors=[])
60
+ case ast[0]
61
+ when 'mul', 'div', 'add', 'sub'
62
+ valid?(ast[1], valid_values, errors) and valid?(ast[2], valid_values, errors)
63
+ when 'num'
64
+ true
65
+ when 'stat', 'spec'
66
+ if valid_values.include? ast[1]
67
+ true
68
+ else
69
+ errors << "Unknown #{ast[0]} '#{ast[1]}'"
70
+ false
71
+ end
72
+ when 'max', 'min'
73
+ ast[2].map { |ast| valid?(ast, valid_values, errors) }.all?
74
+ else
75
+ raise UnknownNodeError, ast[0]
76
+ end
77
+ end
78
+
79
+ # Calculate the value of the AST for the given context
80
+ #
81
+ # This method recursively walks the AST, calculating the final value
82
+ #
83
+ # @param ast [Ast] The AST to compute
84
+ # @param context [#[]] Context used to evaluate the AST
85
+ #
86
+ # @return [Integer] The value of the AST
87
+ def self.value(ast, context={})
88
+ case ast[0]
89
+ when 'mul'
90
+ value(ast[1], context) * value(ast[2], context)
91
+ when 'div'
92
+ value(ast[1], context) / value(ast[2], context)
93
+ when 'add'
94
+ value(ast[1], context) + value(ast[2], context)
95
+ when 'sub'
96
+ value(ast[1], context) - value(ast[2], context)
97
+ when 'num'
98
+ ast[1]
99
+ when 'stat', 'spec'
100
+ context[ast[1]]
101
+ when 'max'
102
+ tmp = ast[2].map { |ast| value(ast, context) }.sort[-ast[1], ast[1]]
103
+ (tmp.size == 1) ? tmp.first : tmp.inject(0) { |fin, val| fin += val }
104
+ when 'min'
105
+ tmp = ast[2].map { |ast| value(ast, context) }.sort[0, ast[1]]
106
+ (tmp.size == 1) ? tmp.first : tmp.inject(0) { |fin, val| fin += val }
107
+ else
108
+ raise UnknownNodeError, self[0]
109
+ end
110
+ end
111
+
112
+ # Simplify the AST
113
+ #
114
+ # This recusively walks the AST, replacing any constant subtrees with their
115
+ # value.
116
+ #
117
+ # @param ast [Ast] The AST to simplify
118
+ # @return [Ast] The simplified AST
119
+ def self.simplify(ast)
120
+ if ast.constant?
121
+ new('num', value(ast))
122
+ else
123
+ case ast[0]
124
+ when 'add', 'sub', 'mul', 'div'
125
+ new(ast[0], simplify(ast[1]), simplify(ast[2]))
126
+ else
127
+ ast
128
+ end
129
+ end
130
+ end
131
+
132
+ # Create a new num node
133
+ #
134
+ # @param value [Integer] The value of the new node
135
+ #
136
+ # @return [Ast] A num Ast Node
137
+ def self.num(value)
138
+ new('num', value)
139
+ end
140
+
141
+ # Create a new stat node
142
+ #
143
+ # @param value [String] The value of the new node
144
+ #
145
+ # @return [Ast] A stat Ast Node
146
+ def self.stat(value)
147
+ new('stat', value)
148
+ end
149
+
150
+ # Create a new spec node
151
+ #
152
+ # @param value [String] The value of the new node
153
+ #
154
+ # @return [Ast] A spec Ast Node
155
+ def self.spec(value)
156
+ new('spec', value)
157
+ end
158
+
159
+ # Create a new add node
160
+ #
161
+ # @param left [Ast] The left node of the add
162
+ # @param right [Ast] The right node of the add
163
+ # @return [Ast] An add Ast Node
164
+ def self.add(left, right)
165
+ new('add', left, right)
166
+ end
167
+
168
+ # Create a new sub node
169
+ #
170
+ # @param left [Ast] The left node of the sub
171
+ # @param right [Ast] The right node of the sub
172
+ # @return [Ast] An sub Ast Node
173
+ def self.sub(left, right)
174
+ new('sub', left, right)
175
+ end
176
+
177
+ # Create a new mul node
178
+ #
179
+ # @param left [Ast] The left node of the mul
180
+ # @param right [Ast] The right node of the mul
181
+ # @return [Ast] An mul Ast Node
182
+ def self.mul(left, right)
183
+ new('mul', left, right)
184
+ end
185
+
186
+ # Create a new div node
187
+ #
188
+ # @param left [Ast] The left node of the div
189
+ # @param right [Ast] The right node of the div
190
+ # @return [Ast] An div Ast Node
191
+ def self.div(left, right)
192
+ new('div', left, right)
193
+ end
194
+
195
+ # Create a new min node
196
+ #
197
+ # @param count [Integer] Number of values to sum for the min
198
+ # @param list [Array] Array of ASTs to calculate the min from
199
+ # @return [Ast] A min Ast Node
200
+ def self.min(count, list)
201
+ new('min', count, list)
202
+ end
203
+
204
+ # Create a new max node
205
+ #
206
+ # @param count [Integer] Number of values to sum for the max
207
+ # @param list [Array] Array of ASTs to calculate the max from
208
+ # @return [Ast] A max Ast Node
209
+ def self.max(count, list)
210
+ new('max', count, list)
211
+ end
212
+
213
+ def self.from_array(array)
214
+ case array[0]
215
+ when 'mul', 'div', 'add', 'sub'
216
+ new(array[0], from_array(array[1]), from_array(array[2]) )
217
+ when 'num', 'stat', 'spec'
218
+ new(array[0], array[1])
219
+ when 'max', 'min'
220
+ new(array[0], array[1], array[2].map! { |ast| new(ast) } )
221
+ else
222
+ raise UnknownNodeError, self[0]
223
+ end
224
+ end
225
+ end
226
+ end