polly 0.0.4

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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.irb_tempfile ADDED
File without changes
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@polly --create
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in polly.gemspec
4
+ gemspec
5
+ gem 'pry'
6
+ gem 'pry-nav'
7
+ gem 'pry-stack_explorer'
8
+ gem 'rake'
9
+ gem 'rspec'
10
+ gem 'rcov', '0.9.11'
11
+ gem 'simplecov', :require => false, :group => :test
12
+ gem 'metrical'
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Alex Skryl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ ## About
2
+
3
+ Polly is a DSL for manipulating and evaluating symbolic expressions.
4
+
5
+ ## Features
6
+
7
+ * Create calculations consisting of one or more s-expressions
8
+ * Evaluate full s-expressions or any constituent parts
9
+ * Add custom function primitives for complex calculations
10
+ * Print s-expression parse trees in prefix or infix notation
11
+ * Perform data dependence analysis
12
+ * Analyze runtime performance
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'polly'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install polly
27
+
28
+
29
+ ## Usage
30
+
31
+ TODO: for now just check out spec/examples.rb
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "metric_fu"
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ - test universal types
2
+ - prevent setting variables to non ruby types
3
+ - injecting custom math functions easily
4
+ - test serialization for big calcs
5
+ - test direct var references and the self NOOP
@@ -0,0 +1,46 @@
1
+ class Polly::Calculation
2
+ extend Forwardable
3
+ include Polly::Common
4
+
5
+ attr_reader :env, :context
6
+ def_delegators :@env, :print, :to_s, :inspect, :pretty_inspect, :atomic_variables,
7
+ :defined_variables, :undefined_variables
8
+
9
+ def_delegator :@env, :values, :result
10
+ def_delegator :@env, :values!, :result!
11
+
12
+ meta_eval { attr_accessor :verbose }
13
+
14
+ def initialize(env = {}, &block)
15
+ @env = Env.new(env)
16
+ @context = Context.new(@env)
17
+ @context.evaluate(block)
18
+ end
19
+
20
+ def evaluate(inputs = {}, &block)
21
+ inputs.each { |k,v| @context.var(k,v) }
22
+ @context.evaluate(block) if block
23
+ self
24
+ end
25
+
26
+ def method_missing(method, *args, &block)
27
+ method.match(/^(\w+)=?$/)
28
+ method_name = $1.to_sym
29
+
30
+ if @env.keys.include?(method_name)
31
+ method == method_name ? @env[method_name] : @context.var(method_name, args[0])
32
+ else super
33
+ end
34
+ end
35
+
36
+ def verbose_toggle
37
+ Calculation.verbose = !Calculation.verbose
38
+ end
39
+
40
+ # Rails compatible serialization
41
+
42
+ def dump; env.to_yaml end
43
+ def self.load(yml); new(YAML::load(yml)) if yml end
44
+ def self.dump(obj); obj.dump if obj end
45
+
46
+ end
@@ -0,0 +1,8 @@
1
+ module Polly::Common
2
+ include Polly
3
+
4
+ BINARY_OPS = [:*, :/, :%, :+, :-, :**, :<<, :>>, :&, :|, :^, :>, :>=, :<, :<=, :<=>, :==, :===, :=~ ]
5
+ UNARY_OPS = [:-, :+, :!, :~]
6
+ UNDEFINED = [nil]
7
+
8
+ end
@@ -0,0 +1,49 @@
1
+ class Polly::Context < BasicObject
2
+ include ::Polly::Common
3
+ include ::Kernel
4
+
5
+ def initialize(env)
6
+ @env = env
7
+ @env.clean.each { |name, expr| var_reader name.to_sym }
8
+ end
9
+
10
+ def evaluate(proc)
11
+ instance_eval(&proc) if proc
12
+ end
13
+
14
+ def var(name, val = nil, opts = {})
15
+ if @env[name]
16
+ @env[name].replace(val)
17
+ else
18
+ @env[name] = Sexpr.build(val, @env, name)
19
+ end
20
+
21
+ var_reader name
22
+ end
23
+
24
+ def Sexpr(val)
25
+ Polly::Sexpr.build(val, @env)
26
+ end
27
+
28
+ alias_method :const, :var
29
+ alias_method :eq, :var
30
+
31
+ # magix
32
+
33
+ # convert method calls on self to s-expressions
34
+ #
35
+ def method_missing(method, *args, &block)
36
+ Sexpr.build([method, *args], @env)
37
+ end
38
+
39
+ def self.const_missing(name)
40
+ ::Object.const_get(name)
41
+ end
42
+
43
+ private
44
+
45
+ def var_reader(name)
46
+ define_singleton_method(name) { @env[name] }
47
+ end
48
+
49
+ end
data/lib/polly/env.rb ADDED
@@ -0,0 +1,52 @@
1
+ class Polly::Env < Hash
2
+ include Polly::Common
3
+
4
+ def initialize(env = {})
5
+ Math.singleton_methods.each do |m|
6
+ self[m.to_sym] = lambda { |*args| Math.send(m, *args) }
7
+ end
8
+
9
+ env.each { |name, expr| self[name] = Sexpr.build(expr, self) }
10
+ end
11
+
12
+ def clean
13
+ Env[self.select { |name, expr| expr.is_a?(Sexpr) }]
14
+ end
15
+
16
+ def values
17
+ clean.inject({}) { |h, (name, expr)| h[name] = expr.value; h }
18
+ end
19
+
20
+ def values!
21
+ clean.inject({}) { |h, (name, expr)| h[name] = expr.value!; h }
22
+ end
23
+
24
+ def atomic_variables
25
+ clean.select { |name, expr| expr.atomic? }.keys
26
+ end
27
+
28
+ def defined_variables
29
+ clean.select { |name, expr| expr.atomic? && expr.defined? }.keys
30
+ end
31
+
32
+ def undefined_variables
33
+ clean.select { |name, expr| expr.atomic? && !expr.defined? }.keys
34
+ end
35
+
36
+ # printing and conversion
37
+
38
+ def print(opts = {}); puts to_s(opts) end
39
+ def to_s(opts = {}); clean.map { |(k,v)| "#{k.inspect} => #{v.to_s(opts)}" }.join("\n") end
40
+ def to_yaml(*args); dump.to_yaml(*args) end
41
+
42
+ def ==(env)
43
+ env.is_a?(Hash) ? Hash[self.clean] == Hash[env.clean] : false
44
+ end
45
+
46
+ private
47
+
48
+ def dump
49
+ clean.inject({}) { |h, (name, expr)| h[name] = expr.to_ary; h }
50
+ end
51
+
52
+ end
@@ -0,0 +1,4 @@
1
+ class Array
2
+ def car; self.first end
3
+ def cdr; self.drop(1) end
4
+ end
@@ -0,0 +1,5 @@
1
+ class Numeric
2
+ def percent
3
+ self/100.00
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ class BasicObject
2
+
3
+ # this shit is meta
4
+ def metaclass; class << self; self; end; end
5
+ def meta_eval &blk; metaclass.instance_eval &blk; end
6
+
7
+ end
8
+
9
+ class Object
10
+
11
+ def to_sexpr
12
+ Polly::Sexpr.build(self)
13
+ end
14
+
15
+ end
data/lib/polly/math.rb ADDED
@@ -0,0 +1,53 @@
1
+ module Polly::Math
2
+
3
+ class << self
4
+
5
+ def min(*args)
6
+ args.min
7
+ end
8
+
9
+ def max(*args)
10
+ args.max
11
+ end
12
+
13
+ def ceil(val, ceil)
14
+ (val.to_f / ceil.to_i).to_i * ceil.to_i
15
+ end
16
+
17
+ def pv(i, length, pmt)
18
+ pmt.to_f / i * (1 - (1 + i) ** -length.to_f)
19
+ end
20
+
21
+ # Some binary operators are not methods but part of Ruby's syntax. Since
22
+ # there is no way to latch on to them, they'll have to be redefined.
23
+
24
+ def br(test, exp1, exp2)
25
+ test ? exp1 : exp2
26
+ end
27
+
28
+ def and(val1, val2)
29
+ val1 && val2
30
+ end
31
+
32
+ def or(val1, val2)
33
+ val1 || val2
34
+ end
35
+
36
+ def not(val)
37
+ !val
38
+ end
39
+ alias_method :!, :not
40
+
41
+ def not_eq(val1, val2)
42
+ !(val1 == val2)
43
+ end
44
+ alias_method :!=, :not_eq
45
+
46
+ # NOOP
47
+ def self(obj)
48
+ obj
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,176 @@
1
+ class Polly::Sexpr
2
+ include Polly::Common
3
+ include Enumerable
4
+
5
+ # instance accessors
6
+ attr_reader :op, :args, :sexpr, :env
7
+ attr_accessor :name, :env, :dirty
8
+ protected :sexpr, :env=, :name=, :dirty=
9
+
10
+ # class accessors
11
+ meta_eval { protected :new }
12
+
13
+ def self.build(val, env = nil, name = nil)
14
+ val = \
15
+ case val
16
+ when Sexpr
17
+ val
18
+ when Symbol
19
+ env_val = env && env[val]
20
+ env_val.is_a?(Sexpr) ? env_val : Array(val)
21
+ when Array
22
+ if val.size > 1
23
+ val.cdr.map do |arg|
24
+ arg.is_a?(Sexpr) ? arg : Sexpr.build(arg, env, name)
25
+ end.unshift(val.car)
26
+ else
27
+ Sexpr.build(val.first, env, name)
28
+ end
29
+ when NilClass
30
+ UNDEFINED
31
+ else Array(val)
32
+ end
33
+
34
+ val.is_a?(Sexpr) ? Sexpr.update(val, env, name) : Sexpr.new(val, env, name)
35
+ end
36
+
37
+ # If an sexpr directly references another named sexpr then the named sexpr
38
+ # should be wrapped in a self call, which is a noop, in order to avoid
39
+ # having two references to the same sexpr.
40
+ #
41
+ def self.update(sexpr, env, name)
42
+ if name && sexpr.name && name != sexpr.name
43
+ Sexpr.build([:self, sexpr])
44
+ else
45
+ sexpr.send(:name=, name) unless sexpr.name
46
+ sexpr.send(:env=, env) unless sexpr.env
47
+ sexpr
48
+ end
49
+ end
50
+
51
+ def initialize(sexpr, env, name)
52
+ @sexpr = sexpr
53
+ @op = @sexpr[0]
54
+ @args = @sexpr.cdr
55
+ @name = name
56
+ @env = env || Env.new
57
+ end
58
+
59
+ # use cached values unless some part of expression tree is dirty
60
+ #
61
+ def value(env = @env)
62
+ @value = \
63
+ (clean? && @value) ||
64
+ (self.defined? ? (atomic? ? @sexpr.first : self.send(:eval)) : nil)
65
+ @dirty = false
66
+ @value
67
+ end
68
+
69
+ # force a recalc of all sub-expressions
70
+ #
71
+ def value!(env = @env)
72
+ each { |a| a.dirty = true }
73
+ value(env)
74
+ end
75
+
76
+ def replace(val)
77
+ @dirty = true
78
+ @sexpr = self.class.build(val, @env).sexpr
79
+ end
80
+
81
+ def atomic?
82
+ @sexpr.size == 1
83
+ end
84
+
85
+ def defined?; !undefined? end
86
+ def undefined?; any? { |s| s.sexpr == UNDEFINED } end
87
+ def undefined_variables; select { |s| s.atomic? && s.undefined? && s.name }.map(&:name) end
88
+
89
+ def clean?; !dirty? end
90
+ def dirty?; any? { |s| s.dirty } end
91
+ def dirty_variables; select { |s| s.atomic? && s.dirty? && s.name }.map(&:name) end
92
+
93
+ def ==(val)
94
+ case val
95
+ when Sexpr, Array
96
+ val == @sexpr
97
+ else
98
+ val == self.value
99
+ end
100
+ end
101
+
102
+ def each(&block)
103
+ return to_enum unless block_given?
104
+
105
+ yield self
106
+ args.each { |s| s.each(&block) }
107
+ end
108
+
109
+ # magix
110
+
111
+ # convert any method call to an s-expression
112
+ #
113
+ def method_missing(method, *args, &block)
114
+ Sexpr.build([method, self, *args], @env)
115
+ end
116
+
117
+ # printing and conversion
118
+
119
+ def print(opts = {}); puts to_s(opts) end
120
+ def inspect(opts = {}); @sexpr.inspect end
121
+ def to_s(opts = {}); print(_to_ary(opts)) end
122
+ def to_ary(opts = {}); _to_ary(opts) end
123
+
124
+ protected
125
+
126
+ def _to_ary(opts = {})
127
+ if atomic?
128
+ Array(value)
129
+ else
130
+ args.map do |a|
131
+ if a.atomic? || (!opts[:expand] && a.name)
132
+ Array(opts[:numeric] ? a.value : (a.name || a.value))
133
+ else
134
+ a._to_ary(opts)
135
+ end
136
+ end.unshift(op)
137
+ end
138
+ end
139
+
140
+ def print(sexpr)
141
+ op, args = sexpr.car, sexpr.cdr
142
+
143
+ if args.empty?
144
+ op.inspect
145
+ elsif BINARY_OPS.include?(op)
146
+ "(#{print(args[0])} #{op} #{print(args[1])})"
147
+ else
148
+ "#{op}(#{args.map {|a| print(a)}.join(', ')})"
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ # Wizard hats on ;)
155
+
156
+ def eval
157
+ atomic? ? value : apply
158
+ end
159
+
160
+ def apply
161
+ result = \
162
+ if @env[op].respond_to?(:call)
163
+ @env[op].call(*arg_values)
164
+ else
165
+ arg_values[0].send(op, *arg_values.cdr)
166
+ end
167
+
168
+ puts " -> #{op}(#{arg_values.join(', ')}) = #{result}" if Calculation.verbose
169
+ result
170
+ end
171
+
172
+ def arg_values
173
+ args.map { |a| a.value }
174
+ end
175
+
176
+ end
@@ -0,0 +1,3 @@
1
+ module Polly
2
+ VERSION = "0.0.4"
3
+ end
data/lib/polly.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'polly/version'
2
+ require 'polly/extensions/object'
3
+ require 'polly/extensions/array'
4
+ require 'polly/extensions/numeric'
5
+
6
+ module Polly; end
7
+
8
+ require 'polly/common'
9
+ require 'polly/env'
10
+ require 'polly/context'
11
+ require 'polly/sexpr'
12
+ require 'polly/math'
13
+ require 'polly/calculation'
data/polly.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'polly/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.authors = ["Alex Skryl"]
9
+ gem.email = ["rut216@gmail.com"]
10
+ gem.description = %q{ A Polynomial Solver DSL }
11
+ gem.summary = %q{ A Rails compatible polynomial solver with persistence }
12
+ gem.homepage = ""
13
+
14
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.name = "polly"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Polly::VERSION
20
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polly::Calculation do
4
+
5
+ let(:calc_class) { Polly::Calculation }
6
+
7
+ before :each do
8
+ @calc = calc_class.new do
9
+ var :a, 1
10
+ var :b, 2
11
+ var :c, 3
12
+ var :d, a + b + c
13
+ end
14
+ end
15
+
16
+ it 'should be able to toggle calculation options' do
17
+ @calc.verbose_toggle
18
+ calc_class.verbose.should be_true
19
+ @calc.verbose_toggle
20
+ calc_class.verbose.should be_false
21
+ end
22
+
23
+ it 'should eval the initialization block inside the context' do
24
+ @calc.context.should_not respond_to(:e, :f, :g)
25
+ @calc.context.should respond_to(:a, :b, :c, :d)
26
+ end
27
+
28
+ it 'should get and set variables in the context' do
29
+ @calc.a.should == 1
30
+ @calc.a = 2
31
+ @calc.a.should == 2
32
+ end
33
+
34
+ it 'should respond to delegated methods' do
35
+ @calc.should respond_to(:atomic_variables)
36
+ @calc.should respond_to(:defined_variables)
37
+ @calc.should respond_to(:undefined_variables)
38
+ @calc.should respond_to(:evaluate)
39
+ @calc.should respond_to(:to_yaml)
40
+ end
41
+
42
+ it 'should initialize from a yaml dump' do
43
+ dump = calc_class.dump(@calc)
44
+ c = calc_class.load(dump)
45
+ c.context.should respond_to(:a, :b, :c, :d)
46
+ c.a.should == 1
47
+ c.b.should == 2
48
+ c.c.should == 3
49
+ c.d.should == 6
50
+ end
51
+
52
+ end
data/spec/context.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polly::Context do
4
+
5
+ let(:env_class) { Polly::Env }
6
+ let(:sexpr_class) { Polly::Sexpr }
7
+ let(:context_class) { Polly::Context }
8
+
9
+ before :each do
10
+ @c = context_class.new(env_class.new)
11
+ @c.instance_eval do
12
+ var :a
13
+ const :b, 2
14
+ const :c, 3
15
+
16
+ eq :calc, c * (a + b)
17
+ eq :complex, min(10, 15, max(10,20,30, a, b, c), calc)
18
+ end
19
+ end
20
+
21
+ it 'should define aliases for specifying variables' do
22
+ [:const, :var, :eq].all? { |m| @c.should respond_to(m) }
23
+ end
24
+
25
+ it 'should define a variable as an s-expression' do
26
+ [:d, :e, :f].all? { |m| @c.should_not respond_to(m) }
27
+ [:a, :b, :c, :calc, :complex].all? { |m| @c.should respond_to(m) }
28
+ [:a, :b, :c, :calc, :complex].all? { |m| @c.send(m).should be_a(sexpr_class) }
29
+ @c.a.should == nil
30
+ @c.b.should == 2
31
+ @c.c.should == 3
32
+ @c.calc.should == nil
33
+ end
34
+
35
+ it 'should re-define a variable' do
36
+ @c.var(:a, 1)
37
+ @c.var(:calc, 2)
38
+ @c.a.should == 1
39
+ @c.calc.should == 2
40
+ end
41
+
42
+ it 'should respond to any unknown method by converting it to an s-expression' do
43
+ @c.foo.should be_a(sexpr_class)
44
+ @c.bar.should be_a(sexpr_class)
45
+ end
46
+
47
+ it 'should cast a literal to an s-expression' do
48
+ @c.instance_eval { Sexpr(5) }.should be_a(sexpr_class)
49
+ @c.instance_eval { 5.to_sexpr }.should be_a(sexpr_class)
50
+ end
51
+
52
+ it 'should evaluate a calculation block before or after initialization' do
53
+ p = Proc.new do
54
+ eq :another, min(complex, calc) ** 2
55
+ end
56
+ @c.evaluate(p)
57
+
58
+ [:a, :b, :c, :calc, :complex, :another].all? { |m| @c.should respond_to(m) }
59
+ [:a, :b, :c, :calc, :complex, :another].all? { |m| @c.send(m).should be_a(sexpr_class) }
60
+ @c.another.should == nil
61
+ @c.var(:a, 1)
62
+ @c.another.should == 81
63
+ end
64
+
65
+ end
data/spec/env.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polly::Env do
4
+
5
+ let(:math_class) { Polly::Math }
6
+ let(:env_class) { Polly::Env }
7
+ let(:sexpr_class) { Polly::Sexpr }
8
+
9
+ before :each do
10
+ @env = env_class.new(
11
+ cat: sexpr_class.build("meow"),
12
+ dog: sexpr_class.build("gruff")
13
+ )
14
+
15
+ @env[:foo] = sexpr_class.build(0)
16
+ @env[:bar] = sexpr_class.build(:a)
17
+ @env[:buz] = sexpr_class.build('a')
18
+ @env[:him] = sexpr_class.build(true)
19
+ @env[:her] = sexpr_class.build(false)
20
+ @env[:it] = sexpr_class.build([:*,1,2])
21
+ @env[:nil] = sexpr_class.build(nil)
22
+ end
23
+
24
+ it 'should include all functions included in the Math module' do
25
+ math_class.singleton_methods.all? { |m| @env[m] }.should == true
26
+ end
27
+
28
+ it 'should return only the s-expressions' do
29
+ @env.clean.should be_a(env_class)
30
+ @env.clean.size.should == 9
31
+ @env.clean.all? { |k,v| [:cat, :dog, :foo, :bar, :buz, :him, :her, :it, :nil].include?(k) }.should == true
32
+ end
33
+
34
+ it 'should return only the atomic s-expressions' do
35
+ @env.atomic_variables.size.should == 8
36
+ @env.atomic_variables.all? { |k,v| [:cat, :dog, :foo, :bar, :buz, :him, :her, :nil].include?(k) }.should == true
37
+ end
38
+
39
+ it 'should return only atomic, defined s-expressions' do
40
+ @env.defined_variables.size.should == 7
41
+ @env.defined_variables.all? { |k,v| [:cat, :dog, :foo, :bar, :buz, :him, :her].include?(k) }.should == true
42
+ end
43
+
44
+ it 'should return the names of the atomic, undefined s-expressions' do
45
+ @env.undefined_variables.size.should == 1
46
+ @env.undefined_variables.should include(:nil)
47
+ end
48
+
49
+ it 'should marshal to native yaml' do
50
+ e = env_class.new(YAML::load(@env.to_yaml))
51
+ @env.should == e
52
+ end
53
+
54
+ it 'should return a value set' do
55
+ r = { cat: 'meow', dog: 'gruff', foo: 0, bar: :a, buz: 'a', him: true, her: false, it: 2, nil: nil }
56
+ @env.values.should == r
57
+ @env.values!.should == r
58
+ end
59
+
60
+ end
data/spec/examples.rb ADDED
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polly::Calculation do
4
+
5
+ before :each do
6
+ @calc = \
7
+ Polly::Calculation.new do
8
+ name :name
9
+ version 1
10
+
11
+ # constants
12
+ const :a, 150.percent
13
+ const :b, 5000
14
+ const :c, 12
15
+ const :d, 80.percent
16
+
17
+ # inputs
18
+ var :e
19
+ var :f
20
+ var :g
21
+ var :h
22
+ var :i, b
23
+ end
24
+
25
+ @calc.evaluate do
26
+ # eq can nest other equations or vars
27
+ eq :eq1, (e * f + g) / 3.0 - f + -e
28
+ eq :eq2, br(h.length > 4, eq1.to_i, e)
29
+ eq :eq3, max(ceil(pv(a.round(1), c, eq2), 50), e)
30
+ eq :eq4, eq3
31
+
32
+ eq :final, 100.to_sexpr + Sexpr(1000) + eq4 + b
33
+ end
34
+
35
+ @calc.h = "str"
36
+ @calc.e = 3000
37
+ @calc.f = 2
38
+ @calc.g = 10000
39
+ end
40
+
41
+ it 'should perform a complex calculation' do
42
+ @calc.final.should == 9100
43
+ end
44
+
45
+ it 'should perform a complex calculation' do
46
+ @calc.h = "string"
47
+ @calc.final.should == 9100
48
+ end
49
+
50
+ it 'should perform a complex calculation' do
51
+ @calc.e = 20000
52
+ @calc.final.should == 26100
53
+ end
54
+
55
+ it 'should convert the calculation to an array' do
56
+ @calc.final.to_ary.should == \
57
+ [:+, [:+, [:+, [100], [1000]], [:self, [:eq3]]], [:b]]
58
+ @calc.final.to_ary(numeric: true).should == \
59
+ [:+, [:+, [:+, [100], [1000]], [:self, [3000]]], [5000]]
60
+ @calc.final.to_ary(expand: true).should == \
61
+ [:+, [:+, [:+, [100], [1000]], [:self, [:max, [:ceil, [:pv, [:round, [:a], [1]], [:c], [:br, [:>, [:length, [:h]], [4]], [:to_i, [:+, [:-, [:/, [:+, [:*, [:e], [:f]], [:g]], [3.0]], [:f]], [:-@, [:e]]]], [:e]]], [50]], [:e]]]], [:b]]
62
+ @calc.final.to_ary(numeric: true, expand: true).should == \
63
+ [:+, [:+, [:+, [100], [1000]], [:self, [:max, [:ceil, [:pv, [:round, [1.5], [1]], [12], [:br, [:>, [:length, ["str"]], [4]], [:to_i, [:+, [:-, [:/, [:+, [:*, [3000], [2]], [10000]], [3.0]], [2]], [:-@, [3000]]]], [3000]]], [50]], [3000]]]], [5000]]
64
+ end
65
+
66
+ it 'should convert the calculation to a human readable string' do
67
+ @calc.final.to_s.should == \
68
+ "(((100 + 1000) + self(:eq3)) + :b)"
69
+ @calc.final.to_s(numeric: true).should == \
70
+ "(((100 + 1000) + self(3000)) + 5000)"
71
+ @calc.final.to_s(expand: true).should == \
72
+ "(((100 + 1000) + self(max(ceil(pv(round(:a, 1), :c, br((length(:h) > 4), to_i((((((:e * :f) + :g) / 3.0) - :f) + -@(:e))), :e)), 50), :e))) + :b)"
73
+ @calc.final.to_s(numeric: true, expand: true).should == \
74
+ "(((100 + 1000) + self(max(ceil(pv(round(1.5, 1), 12, br((length(\"str\") > 4), to_i((((((3000 * 2) + 10000) / 3.0) - 2) + -@(3000))), 3000)), 50), 3000))) + 5000)"
75
+ end
76
+
77
+ end
data/spec/math.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe Polly::Math do
5
+ let(:math_class) { Polly::Math }
6
+
7
+ describe 'mathematical functions' do
8
+
9
+ it 'should perform a min function' do
10
+ nums = [34, 893, 12, 1234, 17]
11
+ math_class.min(0).should == 0
12
+ math_class.min(15,10).should == 10
13
+ math_class.min(*nums).should == 12
14
+ end
15
+
16
+ it 'should perform a max function' do
17
+ nums = [34, 893, 12, 1234, 17]
18
+ math_class.max(0).should == 0
19
+ math_class.max(15,10).should == 15
20
+ math_class.max(*nums).should == 1234
21
+ end
22
+
23
+ it 'should perform a ceiling function' do
24
+ math_class.ceil(153,10).should == 150
25
+ math_class.ceil(153,20).should == 140
26
+ math_class.ceil(153,30).should == 150
27
+ math_class.ceil(153,40).should == 120
28
+ math_class.ceil(153,50).should == 150
29
+ math_class.ceil(153,100).should == 100
30
+ end
31
+
32
+ it 'should perform a net present value function' do
33
+ math_class.pv(0.100, 12, 50).round(2).should == 340.68
34
+ math_class.pv(0.100, 12, 75).round(2).should == 511.03
35
+ math_class.pv(0.100, 12, 100).round(2).should == 681.37
36
+
37
+ math_class.pv(0.150, 1, 100).round(2).should == 86.96
38
+ math_class.pv(0.150, 6, 100).round(2).should == 378.45
39
+ math_class.pv(0.150, 12, 100).round(2).should == 542.06
40
+
41
+ math_class.pv(0.200, 12, 100).round(2).should == 443.92
42
+ math_class.pv(0.300, 12, 100).round(2).should == 319.03
43
+ math_class.pv(0.400, 12, 100).round(2).should == 245.59
44
+ end
45
+
46
+ end
47
+
48
+ describe 'ruby replacement functions' do
49
+
50
+ it 'should perform a branch function' do
51
+ math_class.br(true, 0, 1).should == 0
52
+ math_class.br(false, 0, 1).should == 1
53
+ end
54
+
55
+ it 'should perform an and function' do
56
+ math_class.and(true, true).should == true
57
+ math_class.and(true, false).should == false
58
+ math_class.and(false, true).should == false
59
+ math_class.and(false, false).should == false
60
+ end
61
+
62
+ it 'should perform an or function' do
63
+ math_class.or(true, true).should == true
64
+ math_class.or(true, false).should == true
65
+ math_class.or(false, true).should == true
66
+ math_class.or(false, false).should == false
67
+ end
68
+
69
+ it 'should perform a not function' do
70
+ math_class.not(true).should == false
71
+ math_class.not(false).should == true
72
+
73
+ math_class.!(true).should == false
74
+ math_class.!(false).should == true
75
+ end
76
+
77
+ it 'should perform a not equal function' do
78
+ math_class.not_eq(true, true).should == false
79
+ math_class.not_eq(true, false).should == true
80
+
81
+ math_class.!=(true, true).should == false
82
+ math_class.!=(true, false).should == true
83
+ end
84
+
85
+ end
86
+
87
+ end
data/spec/sexpr.rb ADDED
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+
3
+ describe Polly::Sexpr do
4
+
5
+ let(:env_class) { Polly::Env }
6
+ let(:sexpr_class) { Polly::Sexpr }
7
+
8
+ before :each do
9
+ @env = env_class.new
10
+
11
+ @s = []
12
+ @s[0] = sexpr_class.build(nil, nil, 'a')
13
+ @s[1] = sexpr_class.build(0)
14
+ @s[2] = sexpr_class.build(:a)
15
+ @s[3] = sexpr_class.build('a')
16
+ @s[4] = sexpr_class.build(true)
17
+ @s[5] = sexpr_class.build(false)
18
+ @s[6] = sexpr_class.build([:+, 1, 2])
19
+ @s[7] = sexpr_class.build([:*, [:+, 1, 2], 3])
20
+ @s[8] = sexpr_class.build([:*, [:+, 1, 2], @s[0]])
21
+ end
22
+
23
+ it 'should recursively build an s-expression from a supported native type' do
24
+ @s.all? { |s| s.should be_a(sexpr_class) }
25
+ end
26
+
27
+ it 'should save the s-expressions name if provided' do
28
+ @s[0].name.should == 'a'
29
+ end
30
+
31
+ it 'should determine the operation and arguments of any s-expression' do
32
+ @s[0].op.should == nil
33
+ @s[0].args.should be_empty
34
+ @s[6].op.should == :+
35
+ @s[6].args.should == [1,2]
36
+ @s[7].op.should == :*
37
+ @s[7].args.should == [@s[6], 3]
38
+ @s[8].op.should == :*
39
+ @s[8].args.should == [@s[6], @s[0]]
40
+ end
41
+
42
+ it 'should compare an s-expression to another s-expression, an array, or a value' do
43
+ sexpr = [:*, [:+, 1, 2], 3]
44
+ @s[7].should == sexpr_class.build(sexpr, @env)
45
+ @s[7].should == sexpr
46
+ @s[7].should == 9
47
+ end
48
+
49
+ it 'should evaluate a valid s-expression' do
50
+ @s[0].should == nil
51
+ @s[1].should == 0
52
+ @s[2].should == :a
53
+ @s[3].should == 'a'
54
+ @s[4].should == true
55
+ @s[5].should == false
56
+ @s[6].should == 3
57
+ @s[7].should == 9
58
+ @s[8].should == nil
59
+ end
60
+
61
+ it 'should replace an s-expression value in place' do
62
+ old_oid = @s[0].object_id
63
+ @s[0].replace(1)
64
+ @s[0].should == 1
65
+ @s[0].object_id.should == old_oid
66
+ end
67
+
68
+ it 'should be able to tell if an s-expression is atomic' do
69
+ @s.values_at(0,1,2,3,4,5).all? { |s| s.atomic?.should be_true }
70
+ @s.values_at(6,7,8).all? { |s| s.atomic?.should be_false }
71
+ end
72
+
73
+ it 'should be able to tell if an s-expression is defined' do
74
+ @s.values_at(1,2,3,4,5,6,7).all? { |s| s.defined?.should be_true }
75
+ @s.values_at(0,8).all? { |s| s.defined?.should be_false }
76
+ @s.values_at(0,8).all? { |s| s.undefined?.should be_true}
77
+ @s[0].undefined_variables.should == ['a']
78
+ @s[8].undefined_variables.should == ['a']
79
+ end
80
+
81
+ it 'should mark a modified s-expression as dirty' do
82
+ @s[0].replace(1)
83
+ @s[0].should be_dirty
84
+ @s[8].should be_dirty
85
+ @s[7].should be_clean
86
+ @s[7].should_not be_dirty
87
+ @s[0].dirty_variables.should == ['a']
88
+ @s[8].dirty_variables.should == ['a']
89
+ end
90
+
91
+ it 'should convert all method calls to an s-expression' do
92
+ (@s[1] + 5).should be_a(sexpr_class)
93
+ @s[1].foo.should be_a(sexpr_class)
94
+ @s[1].bar(1,2).should be_a(sexpr_class)
95
+
96
+ (@s[1] + 5).should == [:+, 0, 5]
97
+ @s[1].foo.should == [:foo, 0]
98
+ @s[1].bar(1,2).should == [:bar, 0, 1, 2]
99
+ @s[1].foo.bar(1,2).should == [:bar, [:foo, 0], 1, 2]
100
+ end
101
+
102
+ it 'should convert to an array' do
103
+ @s[6].to_ary.should == [:+, [1], [2]]
104
+ @s[7].to_ary.should == [:*, [:+, [1], [2]], [3]]
105
+ end
106
+
107
+ describe 'make sure caching internals are working' do
108
+
109
+ before :each do
110
+ @s[7].value
111
+ end
112
+
113
+ it 'should re-evaluate same s-expression using cache' do
114
+ @s[7].should_not_receive(:eval)
115
+ @s[7].value
116
+ @s[7].value
117
+ end
118
+
119
+ it 'should re-evaluate same s-expression and sub-expressions' do
120
+ @s[7].should_receive(:eval).twice
121
+ @s[7].value!
122
+ @s[7].value!
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
@@ -0,0 +1,15 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+ Bundler.require(:default)
7
+
8
+ require 'pry'
9
+ require 'polly'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.filter_run :focus => true
14
+ config.run_all_when_everything_filtered = true
15
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Skryl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-29 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! ' A Polynomial Solver DSL '
15
+ email:
16
+ - rut216@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - .irb_tempfile
23
+ - .rspec
24
+ - .rvmrc
25
+ - Gemfile
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - TODO
30
+ - lib/polly.rb
31
+ - lib/polly/calculation.rb
32
+ - lib/polly/common.rb
33
+ - lib/polly/context.rb
34
+ - lib/polly/env.rb
35
+ - lib/polly/extensions/array.rb
36
+ - lib/polly/extensions/numeric.rb
37
+ - lib/polly/extensions/object.rb
38
+ - lib/polly/math.rb
39
+ - lib/polly/sexpr.rb
40
+ - lib/polly/version.rb
41
+ - polly.gemspec
42
+ - spec/calculation.rb
43
+ - spec/context.rb
44
+ - spec/env.rb
45
+ - spec/examples.rb
46
+ - spec/math.rb
47
+ - spec/sexpr.rb
48
+ - spec/spec_helper.rb
49
+ homepage: ''
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.23
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: A Rails compatible polynomial solver with persistence
73
+ test_files:
74
+ - spec/calculation.rb
75
+ - spec/context.rb
76
+ - spec/env.rb
77
+ - spec/examples.rb
78
+ - spec/math.rb
79
+ - spec/sexpr.rb
80
+ - spec/spec_helper.rb
81
+ has_rdoc: