dentaku 1.2.6 → 2.0.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 +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
|
+
[![Join the chat at https://gitter.im/rubysolo/dentaku](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
5
|
[![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
|
5
6
|
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
|
6
7
|
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](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)
|