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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e51005fddbd5c542da8892bb3db03358e7d4cdf
4
- data.tar.gz: 7c13a0a048a14e5bb40151224a95f7a2635ae9fa
3
+ metadata.gz: 064d1f2a40ca1fa6caf9b07f5fb535a673fcbe32
4
+ data.tar.gz: 0fe2cb4349c5423da447fa81173d3b70636bacff
5
5
  SHA512:
6
- metadata.gz: 46026444a4b39bdc8df5f8913422619ac54b52d671e030b5599aa2db9642beabfdfd0c4e79ac9f40dc211de34e3ec791420aed2e162f86dd373c460229e6350a
7
- data.tar.gz: 14409916459c8b1481e6f9e1005bd7c2bf49f9c84e7b29a3a97cc2161d92bf0615bde9b3b08005e177e9fcd188d76298324041fd54b36f966961e52ee4ecdf4f
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
- => 20
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
- => 7
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
- => 10
41
+ #=> 10
42
42
  calculator.evaluate('peaches >= 15')
43
- => true
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
- => 11
53
+ #=> 11
54
54
  calculator.evaluate('(5 + 3) * 2')
55
- => 16
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 counterparts in Excel:
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
- => 10
76
+ #=> 10
65
77
  calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
66
- => 20
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
- => 8
85
+ #=> 8
74
86
  calculator.evaluate('round(8.2759, 2)')
75
- => 8.28
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
- => 3.0
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
- => 9
221
+ #=> 9
152
222
  > c.evaluate('EXP(2,3)')
153
- => 8
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
- => 9
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -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.1.0
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-07-31 00:00:00.000000000 Z
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.0
99
+ rubygems_version: 2.2.2
99
100
  signing_key:
100
101
  specification_version: 4
101
102
  summary: A formula language parser and evaluator