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 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