dentaku 1.2.6 → 2.0.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 +52 -57
- data/Rakefile +1 -1
- data/lib/dentaku.rb +8 -0
- data/lib/dentaku/ast.rb +22 -0
- data/lib/dentaku/ast/addition.rb +15 -0
- data/lib/dentaku/ast/combinators.rb +15 -0
- data/lib/dentaku/ast/comparators.rb +47 -0
- data/lib/dentaku/ast/division.rb +15 -0
- data/lib/dentaku/ast/exponentiation.rb +15 -0
- data/lib/dentaku/ast/function.rb +54 -0
- data/lib/dentaku/ast/functions/if.rb +26 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +5 -0
- data/lib/dentaku/ast/functions/roundup.rb +5 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +8 -0
- data/lib/dentaku/ast/grouping.rb +13 -0
- data/lib/dentaku/ast/identifier.rb +29 -0
- data/lib/dentaku/ast/multiplication.rb +15 -0
- data/lib/dentaku/ast/negation.rb +25 -0
- data/lib/dentaku/ast/nil.rb +9 -0
- data/lib/dentaku/ast/node.rb +13 -0
- data/lib/dentaku/ast/numeric.rb +17 -0
- data/lib/dentaku/ast/operation.rb +20 -0
- data/lib/dentaku/ast/string.rb +17 -0
- data/lib/dentaku/ast/subtraction.rb +15 -0
- data/lib/dentaku/bulk_expression_solver.rb +6 -11
- data/lib/dentaku/calculator.rb +26 -20
- data/lib/dentaku/parser.rb +131 -0
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +18 -3
- data/lib/dentaku/tokenizer.rb +10 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/function_spec.rb +19 -0
- data/spec/ast/node_spec.rb +37 -0
- data/spec/bulk_expression_solver_spec.rb +12 -5
- data/spec/calculator_spec.rb +14 -1
- data/spec/external_function_spec.rb +12 -28
- data/spec/parser_spec.rb +88 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/token_scanner_spec.rb +4 -3
- data/spec/tokenizer_spec.rb +32 -6
- metadata +36 -16
- data/lib/dentaku/binary_operation.rb +0 -35
- data/lib/dentaku/evaluator.rb +0 -166
- data/lib/dentaku/expression.rb +0 -56
- data/lib/dentaku/external_function.rb +0 -10
- data/lib/dentaku/rule_set.rb +0 -153
- data/spec/binary_operation_spec.rb +0 -45
- data/spec/evaluator_spec.rb +0 -145
- data/spec/expression_spec.rb +0 -25
- data/spec/rule_set_spec.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85e18c02f88a1fc906c69667776e00ccb1f969f5
|
4
|
+
data.tar.gz: 6fba624e3b93a4e64e2ce55fb6fe13f42d9138be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: befe922779a83e64b95ed17907846a29901c59badb2810d2c28f2555aabc5359f7a21a1aeac6cca12b63c39bb59dad225ce233bfccc59b91af9eea027f3ab3cf
|
7
|
+
data.tar.gz: 2bac8daaa567d69860f8c5d00ed52375f989bde4893bb7b405ca513c257edf987a742ea7dc97d5817d8fa6023a82a77040bd69e17ca487c4f2fc45c8c6fba4e1
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
Dentaku
|
2
2
|
=======
|
3
3
|
|
4
|
+
[](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
5
|
[](http://badge.fury.io/rb/dentaku)
|
5
6
|
[](https://travis-ci.org/rubysolo/dentaku)
|
6
7
|
[](https://codeclimate.com/github/rubysolo/dentaku)
|
@@ -66,10 +67,9 @@ calculator.evaluate!('10 * x')
|
|
66
67
|
Dentaku::UnboundVariableError: Dentaku::UnboundVariableError
|
67
68
|
```
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
counterparts in Excel:
|
70
|
+
Dentaku has built-in functions (including `if`, `not`, `min`, `max`, and
|
71
|
+
`round`) and the ability to define custom functions (see below). Functions
|
72
|
+
generally work like their counterparts in Excel:
|
73
73
|
|
74
74
|
```ruby
|
75
75
|
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
|
@@ -78,7 +78,7 @@ calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
|
|
78
78
|
#=> 20
|
79
79
|
```
|
80
80
|
|
81
|
-
`round
|
81
|
+
`round` can be called with or without the number of decimal places:
|
82
82
|
|
83
83
|
```ruby
|
84
84
|
calculator.evaluate('round(8.2)')
|
@@ -87,7 +87,8 @@ calculator.evaluate('round(8.2759, 2)')
|
|
87
87
|
#=> 8.28
|
88
88
|
```
|
89
89
|
|
90
|
-
`round`
|
90
|
+
`round` follows rounding rules, while `roundup` and `rounddown` are `ceil` and
|
91
|
+
`floor`, respectively.
|
91
92
|
|
92
93
|
If you're too lazy to be building calculator objects, there's a shortcut just
|
93
94
|
for you:
|
@@ -105,7 +106,9 @@ Math: `+ - * / %`
|
|
105
106
|
|
106
107
|
Logic: `< > <= >= <> != = AND OR`
|
107
108
|
|
108
|
-
Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`
|
109
|
+
Functions: `IF NOT MIN MAX ROUND ROUNDDOWN ROUNDUP`
|
110
|
+
|
111
|
+
Math: all functions from Ruby's Math module, including `SIN, COS, TAN, ...`
|
109
112
|
|
110
113
|
RESOLVING DEPENDENCIES
|
111
114
|
----------------------
|
@@ -125,7 +128,7 @@ need_to_compute = {
|
|
125
128
|
In the example, `annual_income` needs to be computed (and stored) before
|
126
129
|
`income_taxes`.
|
127
130
|
|
128
|
-
Dentaku provides two methods to help resolve formulas in order
|
131
|
+
Dentaku provides two methods to help resolve formulas in order:
|
129
132
|
|
130
133
|
#### Calculator.dependencies
|
131
134
|
Pass a (string) expression to Dependencies and get back a list of variables (as
|
@@ -144,7 +147,7 @@ calc.dependencies("annual_income / 5")
|
|
144
147
|
#### Calculator.solve!
|
145
148
|
Have Dentaku figure out the order in which your formulas need to be evaluated.
|
146
149
|
|
147
|
-
Pass in a hash of {eventual_variable_name: "expression"} to `solve!` and
|
150
|
+
Pass in a hash of `{eventual_variable_name: "expression"}` to `solve!` and
|
148
151
|
have Dentaku figure out dependencies (using `TSort`) for you.
|
149
152
|
|
150
153
|
Raises `TSort::Cyclic` when a valid expression order cannot be found.
|
@@ -153,7 +156,7 @@ Raises `TSort::Cyclic` when a valid expression order cannot be found.
|
|
153
156
|
calc = Dentaku::Calculator.new
|
154
157
|
calc.store(monthly_income: 50)
|
155
158
|
need_to_compute = {
|
156
|
-
income_taxes:
|
159
|
+
income_taxes: "annual_income / 5",
|
157
160
|
annual_income: "monthly_income * 12"
|
158
161
|
}
|
159
162
|
calc.solve!(need_to_compute)
|
@@ -166,6 +169,29 @@ calc.solve!(
|
|
166
169
|
#=> raises TSort::Cyclic
|
167
170
|
```
|
168
171
|
|
172
|
+
INLINE COMMENTS
|
173
|
+
---------------------------------
|
174
|
+
|
175
|
+
If your expressions grow long or complex, you may add inline comments for future
|
176
|
+
reference. This is particularly useful if you save your expressions in a model.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
calculator.evaluate('kiwi + 5 /* This is a comment */', kiwi: 2)
|
180
|
+
#=> 7
|
181
|
+
```
|
182
|
+
|
183
|
+
Comments can be single or multi-line. The following are also valid.
|
184
|
+
|
185
|
+
```
|
186
|
+
/*
|
187
|
+
* This is a multi-line comment
|
188
|
+
*/
|
189
|
+
|
190
|
+
/*
|
191
|
+
This is another type of multi-line comment
|
192
|
+
*/
|
193
|
+
```
|
194
|
+
|
169
195
|
EXTERNAL FUNCTIONS
|
170
196
|
------------------
|
171
197
|
|
@@ -174,69 +200,39 @@ need. Please implement your favorites and send a pull request! Okay, so maybe
|
|
174
200
|
that's not feasible because:
|
175
201
|
|
176
202
|
1. You can't be bothered to share
|
177
|
-
|
178
|
-
|
203
|
+
1. You can't wait for me to respond to a pull request, you need it `NOW()`
|
204
|
+
1. The formula is the secret sauce for your startup
|
179
205
|
|
180
206
|
Whatever your reasons, Dentaku supports adding functions at runtime. To add a
|
181
|
-
function, you'll need to specify
|
182
|
-
|
183
|
-
* Name
|
184
|
-
* Return type
|
185
|
-
* Signature
|
186
|
-
* Body
|
187
|
-
|
188
|
-
Naming can be the hardest part, so you're on your own for that.
|
189
|
-
|
190
|
-
`:type` specifies the type of value that will be returned, most likely
|
191
|
-
`:numeric`, `:string`, or `:logical`.
|
192
|
-
|
193
|
-
`:signature` specifies the types and order of the parameters for your function.
|
207
|
+
function, you'll need to specify a name and a lambda that accepts all function
|
208
|
+
arguments and returns the result value.
|
194
209
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
As an example, the exponentiation function takes two parameters, the mantissa
|
199
|
-
and the exponent, so the token list could be defined as: `[:numeric,
|
200
|
-
:numeric]`. Other functions might be variadic -- consider `max`, a function
|
201
|
-
that takes any number of numeric inputs and returns the largest one. Its token
|
202
|
-
list could be defined as: `[:arguments]` (one or more numeric, string, or logical
|
203
|
-
values, separated by commas). See the
|
204
|
-
[rules definitions](https://github.com/rubysolo/dentaku/blob/master/lib/dentaku/token_matcher.rb#L87)
|
205
|
-
for the names of token patterns you can use.
|
206
|
-
|
207
|
-
Functions can be added individually using Calculator#add_function, or en masse using
|
208
|
-
Calculator#add_functions.
|
209
|
-
|
210
|
-
Here's an example of adding the `exp` function:
|
210
|
+
Here's an example of adding a function named `POW` that implements
|
211
|
+
exponentiation.
|
211
212
|
|
212
213
|
```ruby
|
213
214
|
> c = Dentaku::Calculator.new
|
214
|
-
> c.add_function(
|
215
|
-
|
216
|
-
type: :numeric,
|
217
|
-
signature: [:numeric, :numeric],
|
218
|
-
body: ->(mantissa, exponent) { mantissa ** exponent }
|
219
|
-
)
|
220
|
-
> c.evaluate('EXP(3,2)')
|
215
|
+
> c.add_function(:pow, ->(mantissa, exponent) { mantissa ** exponent })
|
216
|
+
> c.evaluate('POW(3,2)')
|
221
217
|
#=> 9
|
222
|
-
> c.evaluate('
|
218
|
+
> c.evaluate('POW(2,3)')
|
223
219
|
#=> 8
|
224
220
|
```
|
225
221
|
|
226
|
-
Here's an example of adding
|
222
|
+
Here's an example of adding a variadic function:
|
227
223
|
|
228
224
|
```ruby
|
229
225
|
> c = Dentaku::Calculator.new
|
230
|
-
> c.add_function(
|
231
|
-
name: :max,
|
232
|
-
type: :numeric,
|
233
|
-
signature: [:arguments],
|
234
|
-
body: ->(*args) { args.max }
|
235
|
-
)
|
226
|
+
> c.add_function(:max, ->(*args) { args.max })
|
236
227
|
> c.evaluate 'MAX(8,6,7,5,3,0,9)'
|
237
228
|
#=> 9
|
238
229
|
```
|
239
230
|
|
231
|
+
(However both of these are already built-in -- the `^` operator and the `MAX`
|
232
|
+
function)
|
233
|
+
|
234
|
+
Functions can be added individually using Calculator#add_function, or en masse
|
235
|
+
using Calculator#add_functions.
|
240
236
|
|
241
237
|
THANKS
|
242
238
|
------
|
@@ -283,4 +279,3 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
283
279
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
284
280
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
285
281
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
286
|
-
|
data/Rakefile
CHANGED
data/lib/dentaku.rb
CHANGED
data/lib/dentaku/ast.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative './ast/node'
|
2
|
+
require_relative './ast/nil'
|
3
|
+
require_relative './ast/numeric'
|
4
|
+
require_relative './ast/string'
|
5
|
+
require_relative './ast/identifier'
|
6
|
+
require_relative './ast/addition'
|
7
|
+
require_relative './ast/subtraction'
|
8
|
+
require_relative './ast/multiplication'
|
9
|
+
require_relative './ast/division'
|
10
|
+
require_relative './ast/exponentiation'
|
11
|
+
require_relative './ast/negation'
|
12
|
+
require_relative './ast/comparators'
|
13
|
+
require_relative './ast/combinators'
|
14
|
+
require_relative './ast/grouping'
|
15
|
+
require_relative './ast/functions/if'
|
16
|
+
require_relative './ast/functions/max'
|
17
|
+
require_relative './ast/functions/min'
|
18
|
+
require_relative './ast/functions/not'
|
19
|
+
require_relative './ast/functions/round'
|
20
|
+
require_relative './ast/functions/roundup'
|
21
|
+
require_relative './ast/functions/rounddown'
|
22
|
+
require_relative './ast/functions/ruby_math'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class And < Operation
|
4
|
+
def value(context={})
|
5
|
+
left.value(context) && right.value(context)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Or < Operation
|
10
|
+
def value(context={})
|
11
|
+
left.value(context) || right.value(context)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative './operation'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
module AST
|
5
|
+
class Comparator < Operation
|
6
|
+
def self.precedence
|
7
|
+
5
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class LessThan < Comparator
|
12
|
+
def value(context={})
|
13
|
+
left.value(context) < right.value(context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class LessThanOrEqual < Comparator
|
18
|
+
def value(context={})
|
19
|
+
left.value(context) <= right.value(context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class GreaterThan < Comparator
|
24
|
+
def value(context={})
|
25
|
+
left.value(context) > right.value(context)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class GreaterThanOrEqual < Comparator
|
30
|
+
def value(context={})
|
31
|
+
left.value(context) >= right.value(context)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class NotEqual < Comparator
|
36
|
+
def value(context={})
|
37
|
+
left.value(context) != right.value(context)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Equal < Comparator
|
42
|
+
def value(context={})
|
43
|
+
left.value(context) == right.value(context)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
module AST
|
5
|
+
class Function < Node
|
6
|
+
def initialize(*args)
|
7
|
+
@args = args
|
8
|
+
end
|
9
|
+
|
10
|
+
def dependencies(context={})
|
11
|
+
@args.flat_map { |a| a.dependencies(context) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get(name)
|
15
|
+
registry.fetch(function_name(name)) { fail "Undefined function #{ name } "}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.register(name, implementation)
|
19
|
+
function = Class.new(self) do
|
20
|
+
def self.implementation=(impl)
|
21
|
+
@implementation = impl
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.implementation
|
25
|
+
@implementation
|
26
|
+
end
|
27
|
+
|
28
|
+
def value(context={})
|
29
|
+
args = @args.flat_map { |a| a.value(context) }
|
30
|
+
self.class.implementation.call(*args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
function.implementation = implementation
|
35
|
+
|
36
|
+
registry[function_name(name)] = function
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.register_class(name, function_class)
|
40
|
+
registry[function_name(name)] = function_class
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def self.function_name(name)
|
46
|
+
name.to_s.downcase
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.registry
|
50
|
+
@registry ||= {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
module AST
|
5
|
+
class If < Function
|
6
|
+
attr_reader :predicate, :left, :right
|
7
|
+
|
8
|
+
def initialize(predicate, left, right)
|
9
|
+
@predicate = predicate
|
10
|
+
@left = left
|
11
|
+
@right = right
|
12
|
+
end
|
13
|
+
|
14
|
+
def value(context={})
|
15
|
+
predicate.value(context) ? left.value(context) : right.value(context)
|
16
|
+
end
|
17
|
+
|
18
|
+
def dependencies(context={})
|
19
|
+
# TODO : short-circuit?
|
20
|
+
(predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Dentaku::AST::Function.register_class(:if, Dentaku::AST::If)
|