polly 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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: