dentaku 1.1.0 → 1.2.0
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.
- 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
|