dentaku 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +86 -15
- data/lib/dentaku/calculator.rb +23 -1
- data/lib/dentaku/dependency_resolver.rb +24 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/calculator_spec.rb +36 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 064d1f2a40ca1fa6caf9b07f5fb535a673fcbe32
|
4
|
+
data.tar.gz: 0fe2cb4349c5423da447fa81173d3b70636bacff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55fdf11b6a81a851c4d5db09bd2d1ee3385d7ca0f08d2ba806bb9cf9dc914acda0b90d38b817353d28ab99c1b89b198a7d10e998360b2870ca04d485616a97cf
|
7
|
+
data.tar.gz: f68bc08dd8d1ff2f3d4e79f5af7f3131b59ce539d2001360e78c8fe340421f993c6e67d4ea1490b69928861c19b74615caea09cfd0e74cd251f0affd142eb0bd
|
data/README.md
CHANGED
@@ -21,7 +21,7 @@ This is probably simplest to illustrate in code:
|
|
21
21
|
```ruby
|
22
22
|
calculator = Dentaku::Calculator.new
|
23
23
|
calculator.evaluate('10 * 2')
|
24
|
-
|
24
|
+
#=> 20
|
25
25
|
```
|
26
26
|
|
27
27
|
Okay, not terribly exciting. But what if you want to have a reference to a
|
@@ -29,7 +29,7 @@ variable, and evaluate it at run-time? Here's how that would look:
|
|
29
29
|
|
30
30
|
```ruby
|
31
31
|
calculator.evaluate('kiwi + 5', kiwi: 2)
|
32
|
-
|
32
|
+
#=> 7
|
33
33
|
```
|
34
34
|
|
35
35
|
You can also store the variable values in the calculator's memory and then
|
@@ -38,9 +38,9 @@ evaluate expressions against those stored values:
|
|
38
38
|
```ruby
|
39
39
|
calculator.store(peaches: 15)
|
40
40
|
calculator.evaluate('peaches - 5')
|
41
|
-
|
41
|
+
#=> 10
|
42
42
|
calculator.evaluate('peaches >= 15')
|
43
|
-
|
43
|
+
#=> true
|
44
44
|
```
|
45
45
|
|
46
46
|
For maximum CS geekery, `bind` is an alias of `store`.
|
@@ -50,29 +50,41 @@ to ensure proper evaluation:
|
|
50
50
|
|
51
51
|
```ruby
|
52
52
|
calculator.evaluate('5 + 3 * 2')
|
53
|
-
|
53
|
+
#=> 11
|
54
54
|
calculator.evaluate('(5 + 3) * 2')
|
55
|
-
|
55
|
+
#=> 16
|
56
|
+
```
|
57
|
+
|
58
|
+
The `evalutate` method will return `nil` if there is an error in the formula.
|
59
|
+
If this is not the desired behavior, use `evaluate!`, which will raise an
|
60
|
+
exception.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
calculator.evaluate('10 * x')
|
64
|
+
#=> nil
|
65
|
+
calculator.evaluate!('10 * x')
|
66
|
+
Dentaku::UnboundVariableError: Dentaku::UnboundVariableError
|
56
67
|
```
|
57
68
|
|
58
69
|
A number of functions are also supported. Okay, the number is currently five,
|
59
70
|
but more will be added soon. The current functions are
|
60
|
-
`if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their
|
71
|
+
`if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their
|
72
|
+
counterparts in Excel:
|
61
73
|
|
62
74
|
```ruby
|
63
75
|
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
|
64
|
-
|
76
|
+
#=> 10
|
65
77
|
calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
|
66
|
-
|
78
|
+
#=> 20
|
67
79
|
```
|
68
80
|
|
69
81
|
`round`, `rounddown`, and `roundup` can be called with or without the number of decimal places:
|
70
82
|
|
71
83
|
```ruby
|
72
84
|
calculator.evaluate('round(8.2)')
|
73
|
-
|
85
|
+
#=> 8
|
74
86
|
calculator.evaluate('round(8.2759, 2)')
|
75
|
-
|
87
|
+
#=> 8.28
|
76
88
|
```
|
77
89
|
|
78
90
|
`round` and `rounddown` round down, while `roundup` rounds up.
|
@@ -82,7 +94,7 @@ for you:
|
|
82
94
|
|
83
95
|
```ruby
|
84
96
|
Dentaku('plums * 1.5', plums: 2)
|
85
|
-
|
97
|
+
#=> 3.0
|
86
98
|
```
|
87
99
|
|
88
100
|
|
@@ -95,6 +107,64 @@ Logic: `< > <= >= <> != = AND OR`
|
|
95
107
|
|
96
108
|
Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`
|
97
109
|
|
110
|
+
RESOLVING DEPENDENCIES
|
111
|
+
----------------------
|
112
|
+
|
113
|
+
If your formulas rely on one another, they may need to be resolved in a
|
114
|
+
particular order. For example:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
calc = Dentaku::Calculator.new
|
118
|
+
calc.store(monthly_income: 50)
|
119
|
+
need_to_compute = {
|
120
|
+
income_taxes: "annual_income / 5",
|
121
|
+
annual_income: "monthly_income * 12"
|
122
|
+
}
|
123
|
+
```
|
124
|
+
|
125
|
+
In the example, `annual_income` needs to be computed (and stored) before
|
126
|
+
`income_taxes`.
|
127
|
+
|
128
|
+
Dentaku provides two methods to help resolve formulas in order`:
|
129
|
+
|
130
|
+
#### Calculator.dependencies
|
131
|
+
Pass a (string) expression to Dependencies and get back a list of variables (as
|
132
|
+
`:symbols`) that are required for the expression. `Dependencies` also takes
|
133
|
+
into account variables already (explicitly) stored into the calculator.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
calc.dependencies("monthly_income * 12")
|
137
|
+
#=> []
|
138
|
+
# (since monthly_income is in memory)
|
139
|
+
|
140
|
+
calc.dependencies("annual_income / 5")
|
141
|
+
#=> [:annual_income]
|
142
|
+
```
|
143
|
+
|
144
|
+
#### Calculator.solve!
|
145
|
+
Have Dentaku figure out the order in which your formulas need to be evaluated.
|
146
|
+
|
147
|
+
Pass in a hash of {eventual_variable_name: "expression"} to `solve!` and
|
148
|
+
have Dentaku figure out dependencies (using `TSort`) for you.
|
149
|
+
|
150
|
+
Raises `TSort::Cyclic` when a valid expression order cannot be found.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
calc = Dentaku::Calculator.new
|
154
|
+
calc.store(monthly_income: 50)
|
155
|
+
need_to_compute = {
|
156
|
+
income_taxes: "annual_income / 5",
|
157
|
+
annual_income: "monthly_income * 12"
|
158
|
+
}
|
159
|
+
calc.solve!(need_to_compute)
|
160
|
+
#=> {annual_income: 600, income_taxes: 120}
|
161
|
+
|
162
|
+
calc.solve!(
|
163
|
+
make_money: "have_money",
|
164
|
+
have_money: "make_money"
|
165
|
+
}
|
166
|
+
#=> raises TSort::Cyclic
|
167
|
+
```
|
98
168
|
|
99
169
|
EXTERNAL FUNCTIONS
|
100
170
|
------------------
|
@@ -148,9 +218,9 @@ Here's an example of adding the `exp` function:
|
|
148
218
|
body: ->(mantissa, exponent) { mantissa ** exponent }
|
149
219
|
)
|
150
220
|
> c.evaluate('EXP(3,2)')
|
151
|
-
|
221
|
+
#=> 9
|
152
222
|
> c.evaluate('EXP(2,3)')
|
153
|
-
|
223
|
+
#=> 8
|
154
224
|
```
|
155
225
|
|
156
226
|
Here's an example of adding the `max` function:
|
@@ -164,7 +234,7 @@ Here's an example of adding the `max` function:
|
|
164
234
|
body: ->(*args) { args.max }
|
165
235
|
)
|
166
236
|
> c.evaluate 'MAX(5,3,9,6,2)'
|
167
|
-
|
237
|
+
#=> 9
|
168
238
|
```
|
169
239
|
|
170
240
|
|
@@ -182,6 +252,7 @@ contributors:
|
|
182
252
|
* [mvbrocato](https://github.com/mvbrocato)
|
183
253
|
* [brixen](https://github.com/brixen)
|
184
254
|
* [0xCCD](https://github.com/0xCCD)
|
255
|
+
* [AlexeyMK](https://github.com/AlexeyMK)
|
185
256
|
|
186
257
|
|
187
258
|
LICENSE
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -2,6 +2,7 @@ require 'dentaku/evaluator'
|
|
2
2
|
require 'dentaku/expression'
|
3
3
|
require 'dentaku/rules'
|
4
4
|
require 'dentaku/token'
|
5
|
+
require 'dentaku/dependency_resolver'
|
5
6
|
|
6
7
|
module Dentaku
|
7
8
|
class Calculator
|
@@ -36,10 +37,31 @@ module Dentaku
|
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
40
|
+
def solve!(expression_hash)
|
41
|
+
# expression_hash: { variable_name: "string expression" }
|
42
|
+
# TSort thru the expressions' dependencies, then evaluate all
|
43
|
+
expression_dependencies = Hash[expression_hash.map do |var, expr|
|
44
|
+
[var, dependencies(expr)]
|
45
|
+
end]
|
46
|
+
variables_in_resolve_order = DependencyResolver::find_resolve_order(
|
47
|
+
expression_dependencies)
|
48
|
+
|
49
|
+
results = {}
|
50
|
+
variables_in_resolve_order.each do |var_name|
|
51
|
+
results[var_name] = evaluate!(expression_hash[var_name], results)
|
52
|
+
end
|
53
|
+
|
54
|
+
results
|
55
|
+
end
|
56
|
+
|
57
|
+
def dependencies(expression)
|
58
|
+
Expression.new(expression, @memory).identifiers
|
59
|
+
end
|
60
|
+
|
39
61
|
def store(key_or_hash, value=nil)
|
40
62
|
restore = @memory.dup
|
41
63
|
|
42
|
-
if value
|
64
|
+
if !value.nil?
|
43
65
|
@memory[key_or_hash.to_sym] = value
|
44
66
|
else
|
45
67
|
key_or_hash.each do |key, value|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
class DependencyResolver
|
5
|
+
include TSort
|
6
|
+
|
7
|
+
def self.find_resolve_order(vars_to_dependencies_hash)
|
8
|
+
self.new(vars_to_dependencies_hash).tsort
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(vars_to_dependencies_hash)
|
12
|
+
# ensure variables are symbols
|
13
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_sym, v]}]
|
14
|
+
end
|
15
|
+
|
16
|
+
def tsort_each_node(&block)
|
17
|
+
@vars_to_deps.each_key(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def tsort_each_child(node, &block)
|
21
|
+
@vars_to_deps[node.to_sym].each(&block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/dentaku/version.rb
CHANGED
data/spec/calculator_spec.rb
CHANGED
@@ -17,6 +17,42 @@ describe Dentaku::Calculator do
|
|
17
17
|
expect(calculator.evaluate('pears * 2', :pears => 5)).to eq(10)
|
18
18
|
expect(calculator).to be_empty
|
19
19
|
end
|
20
|
+
|
21
|
+
it 'can store the value `false`' do
|
22
|
+
calculator.store('i_am_false', false)
|
23
|
+
expect(calculator.evaluate!('i_am_false')).to eq false
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'can store multiple values' do
|
27
|
+
calculator.store(first: 1, second: 2)
|
28
|
+
expect(calculator.evaluate!('first')).to eq 1
|
29
|
+
expect(calculator.evaluate!('second')).to eq 2
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'dependencies' do
|
34
|
+
it "finds dependencies in a generic statement" do
|
35
|
+
expect(calculator.dependencies("bob + dole / 3")).to eq([:bob, :dole])
|
36
|
+
end
|
37
|
+
it "doesn't consider variables in memory as dependencies" do
|
38
|
+
expect(with_memory.dependencies("apples + oranges")).to eq([:oranges])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'solve!' do
|
43
|
+
it "evaluates properly with variables, even if some in memory" do
|
44
|
+
expect(with_memory.solve!(
|
45
|
+
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
46
|
+
weekly_apple_budget: "apples * 7",
|
47
|
+
pear: "1"
|
48
|
+
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "lets you know about a cycle if one occurs" do
|
52
|
+
expect do
|
53
|
+
calculator.solve!(health: "happiness", happiness: "health")
|
54
|
+
end.to raise_error (TSort::Cyclic)
|
55
|
+
end
|
20
56
|
end
|
21
57
|
|
22
58
|
it 'evaluates a statement with no variables' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -55,6 +55,7 @@ files:
|
|
55
55
|
- lib/dentaku.rb
|
56
56
|
- lib/dentaku/binary_operation.rb
|
57
57
|
- lib/dentaku/calculator.rb
|
58
|
+
- lib/dentaku/dependency_resolver.rb
|
58
59
|
- lib/dentaku/evaluator.rb
|
59
60
|
- lib/dentaku/expression.rb
|
60
61
|
- lib/dentaku/external_function.rb
|
@@ -95,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
96
|
version: '0'
|
96
97
|
requirements: []
|
97
98
|
rubyforge_project: dentaku
|
98
|
-
rubygems_version: 2.2.
|
99
|
+
rubygems_version: 2.2.2
|
99
100
|
signing_key:
|
100
101
|
specification_version: 4
|
101
102
|
summary: A formula language parser and evaluator
|