dentaku 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +52 -57
  3. data/Rakefile +1 -1
  4. data/lib/dentaku.rb +8 -0
  5. data/lib/dentaku/ast.rb +22 -0
  6. data/lib/dentaku/ast/addition.rb +15 -0
  7. data/lib/dentaku/ast/combinators.rb +15 -0
  8. data/lib/dentaku/ast/comparators.rb +47 -0
  9. data/lib/dentaku/ast/division.rb +15 -0
  10. data/lib/dentaku/ast/exponentiation.rb +15 -0
  11. data/lib/dentaku/ast/function.rb +54 -0
  12. data/lib/dentaku/ast/functions/if.rb +26 -0
  13. data/lib/dentaku/ast/functions/max.rb +5 -0
  14. data/lib/dentaku/ast/functions/min.rb +5 -0
  15. data/lib/dentaku/ast/functions/not.rb +5 -0
  16. data/lib/dentaku/ast/functions/round.rb +5 -0
  17. data/lib/dentaku/ast/functions/rounddown.rb +5 -0
  18. data/lib/dentaku/ast/functions/roundup.rb +5 -0
  19. data/lib/dentaku/ast/functions/ruby_math.rb +8 -0
  20. data/lib/dentaku/ast/grouping.rb +13 -0
  21. data/lib/dentaku/ast/identifier.rb +29 -0
  22. data/lib/dentaku/ast/multiplication.rb +15 -0
  23. data/lib/dentaku/ast/negation.rb +25 -0
  24. data/lib/dentaku/ast/nil.rb +9 -0
  25. data/lib/dentaku/ast/node.rb +13 -0
  26. data/lib/dentaku/ast/numeric.rb +17 -0
  27. data/lib/dentaku/ast/operation.rb +20 -0
  28. data/lib/dentaku/ast/string.rb +17 -0
  29. data/lib/dentaku/ast/subtraction.rb +15 -0
  30. data/lib/dentaku/bulk_expression_solver.rb +6 -11
  31. data/lib/dentaku/calculator.rb +26 -20
  32. data/lib/dentaku/parser.rb +131 -0
  33. data/lib/dentaku/token.rb +4 -0
  34. data/lib/dentaku/token_matchers.rb +29 -0
  35. data/lib/dentaku/token_scanner.rb +18 -3
  36. data/lib/dentaku/tokenizer.rb +10 -2
  37. data/lib/dentaku/version.rb +1 -1
  38. data/spec/ast/function_spec.rb +19 -0
  39. data/spec/ast/node_spec.rb +37 -0
  40. data/spec/bulk_expression_solver_spec.rb +12 -5
  41. data/spec/calculator_spec.rb +14 -1
  42. data/spec/external_function_spec.rb +12 -28
  43. data/spec/parser_spec.rb +88 -0
  44. data/spec/spec_helper.rb +2 -1
  45. data/spec/token_scanner_spec.rb +4 -3
  46. data/spec/tokenizer_spec.rb +32 -6
  47. metadata +36 -16
  48. data/lib/dentaku/binary_operation.rb +0 -35
  49. data/lib/dentaku/evaluator.rb +0 -166
  50. data/lib/dentaku/expression.rb +0 -56
  51. data/lib/dentaku/external_function.rb +0 -10
  52. data/lib/dentaku/rule_set.rb +0 -153
  53. data/spec/binary_operation_spec.rb +0 -45
  54. data/spec/evaluator_spec.rb +0 -145
  55. data/spec/expression_spec.rb +0 -25
  56. data/spec/rule_set_spec.rb +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3007daa3af2e8cf5e738f4d98d046f23422004e3
4
- data.tar.gz: 7994b1e07961a589580275cfb14978a9fb6ab091
3
+ metadata.gz: 85e18c02f88a1fc906c69667776e00ccb1f969f5
4
+ data.tar.gz: 6fba624e3b93a4e64e2ce55fb6fe13f42d9138be
5
5
  SHA512:
6
- metadata.gz: b5287d0c47550c976e373a1b291526e79da3fae5aa4e78ebd6e479e3ee467a701f577b45d9acacf9949466186f429f6b9d0ec211917a776e0e29230bba6075d7
7
- data.tar.gz: f7ac853d45ed2a50b3b5d0b4ee8cab110db7adea2dce84f291a328051a19da1c955e058d2d68d23ea1cb367deac0387b94b6039b836f7310a625a7a599861236
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
- A number of functions are also supported. Okay, the number is currently five,
70
- but more will be added soon. The current functions are
71
- `if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their
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`, `rounddown`, and `roundup` can be called with or without the number of decimal places:
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` and `rounddown` round down, while `roundup` rounds up.
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: "annual_income / 5",
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
- 2. You can't wait for me to respond to a pull request, you need it `NOW()`
178
- 3. The formula is the secret sauce for your startup
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
- `:body` is a lambda that implements your function. It is passed the arguments
196
- and should return the calculated value.
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
- name: :exp,
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('EXP(2,3)')
218
+ > c.evaluate('POW(2,3)')
223
219
  #=> 8
224
220
  ```
225
221
 
226
- Here's an example of adding the `max` function:
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
@@ -5,7 +5,7 @@ desc "Run specs"
5
5
  task :spec do
6
6
  RSpec::Core::RakeTask.new(:spec) do |t|
7
7
  t.rspec_opts = %w{--colour --format progress}
8
- t.pattern = 'spec/*_spec.rb'
8
+ t.pattern = 'spec/**/*_spec.rb'
9
9
  end
10
10
  end
11
11
 
data/lib/dentaku.rb CHANGED
@@ -7,6 +7,14 @@ module Dentaku
7
7
  calculator.evaluate(expression, data)
8
8
  end
9
9
 
10
+ def self.enable_ast_cache!
11
+ @enable_ast_caching = true
12
+ end
13
+
14
+ def self.cache_ast?
15
+ @enable_ast_caching
16
+ end
17
+
10
18
  private
11
19
 
12
20
  def self.calculator
@@ -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
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Addition < Operation
6
+ def value(context={})
7
+ left.value(context) + right.value(context)
8
+ end
9
+
10
+ def self.precedence
11
+ 10
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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,15 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Division < Operation
6
+ def value(context={})
7
+ left.value(context) / right.value(context)
8
+ end
9
+
10
+ def self.precedence
11
+ 20
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Exponentiation < Operation
6
+ def value(context={})
7
+ left.value(context) ** right.value(context)
8
+ end
9
+
10
+ def self.precedence
11
+ 30
12
+ end
13
+ end
14
+ end
15
+ 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)