dentaku 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +5 -4
  4. data/lib/dentaku/ast.rb +2 -5
  5. data/lib/dentaku/ast/arithmetic.rb +72 -0
  6. data/lib/dentaku/ast/combinators.rb +17 -2
  7. data/lib/dentaku/ast/comparators.rb +4 -0
  8. data/lib/dentaku/ast/function.rb +18 -1
  9. data/lib/dentaku/ast/functions/if.rb +4 -0
  10. data/lib/dentaku/ast/functions/max.rb +1 -1
  11. data/lib/dentaku/ast/functions/min.rb +1 -1
  12. data/lib/dentaku/ast/functions/not.rb +1 -1
  13. data/lib/dentaku/ast/functions/round.rb +1 -1
  14. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  15. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  16. data/lib/dentaku/ast/functions/ruby_math.rb +1 -1
  17. data/lib/dentaku/ast/grouping.rb +4 -0
  18. data/lib/dentaku/ast/literal.rb +20 -0
  19. data/lib/dentaku/ast/logical.rb +8 -0
  20. data/lib/dentaku/ast/negation.rb +11 -0
  21. data/lib/dentaku/ast/numeric.rb +3 -12
  22. data/lib/dentaku/ast/operation.rb +2 -0
  23. data/lib/dentaku/ast/string.rb +3 -12
  24. data/lib/dentaku/calculator.rb +3 -3
  25. data/lib/dentaku/parser.rb +3 -0
  26. data/lib/dentaku/token_scanner.rb +5 -0
  27. data/lib/dentaku/version.rb +1 -1
  28. data/spec/ast/addition_spec.rb +29 -0
  29. data/spec/ast/and_spec.rb +27 -0
  30. data/spec/ast/function_spec.rb +1 -1
  31. data/spec/ast/numeric_spec.rb +16 -0
  32. data/spec/external_function_spec.rb +4 -4
  33. data/spec/parser_spec.rb +9 -0
  34. data/spec/token_scanner_spec.rb +2 -2
  35. data/spec/tokenizer_spec.rb +12 -0
  36. metadata +12 -7
  37. data/lib/dentaku/ast/addition.rb +0 -15
  38. data/lib/dentaku/ast/division.rb +0 -15
  39. data/lib/dentaku/ast/exponentiation.rb +0 -15
  40. data/lib/dentaku/ast/multiplication.rb +0 -15
  41. data/lib/dentaku/ast/subtraction.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85e18c02f88a1fc906c69667776e00ccb1f969f5
4
- data.tar.gz: 6fba624e3b93a4e64e2ce55fb6fe13f42d9138be
3
+ metadata.gz: fa37d9abba8c7bf0ea6d99f1f0ac472058772436
4
+ data.tar.gz: c07c67b22659bb6c26d7a70edd119bee53ada064
5
5
  SHA512:
6
- metadata.gz: befe922779a83e64b95ed17907846a29901c59badb2810d2c28f2555aabc5359f7a21a1aeac6cca12b63c39bb59dad225ce233bfccc59b91af9eea027f3ab3cf
7
- data.tar.gz: 2bac8daaa567d69860f8c5d00ed52375f989bde4893bb7b405ca513c257edf987a742ea7dc97d5817d8fa6023a82a77040bd69e17ca487c4f2fc45c8c6fba4e1
6
+ metadata.gz: 4b489326fecca7a6a114c64d01efd17149992338167e42cec9fd71f1e12b96bafb72a91ccf2b8b4f6ab895b85d72a74ceb12ad3c3a2b574bff7f3a43471bbb2f
7
+ data.tar.gz: 58bf32696c4c604269f117fa062dc5ea155dd6c9d2eab7007611dd8bc75b129b332d63dfa15aff3deb4f5c6bd1153aed9546fa3ffbca0b59a8c9fa398dfc0868
data/CHANGELOG.md ADDED
@@ -0,0 +1,97 @@
1
+ # Change Log
2
+
3
+ ## [v2.0.1] 2015-08-15
4
+ - add support for boolean literals
5
+ - implement basic parse-time type checking
6
+
7
+ ## [v2.0.0] 2015-08-07
8
+ - shunting-yard parser for performance enhancement and AST generation
9
+ - AST caching for performance enhancement
10
+ - support comments in formulas
11
+ - support all functions from the Ruby Math module
12
+
13
+ ## [v1.2.6] 2015-05-30
14
+ - support custom error handlers for systems of formulas
15
+
16
+ ## [v1.2.5] 2015-05-23
17
+ - fix memory leak
18
+
19
+ ## [v1.2.2] 2014-12-19
20
+ - performance enhancements
21
+ - unary minus bug fixes
22
+ - preserve provided hash keys for systems of formulas
23
+
24
+ ## [v1.2.0] 2014-10-21
25
+ - add dependency resolution to automatically solve systems of formulas
26
+
27
+ ## [v1.1.0] 2014-07-30
28
+ - add strict evaluation mode to raise `UnboundVariableError` if not all variable values are provided
29
+ - return division results as `BigDecimal` values
30
+
31
+ ## [v1.0.0] 2014-03-06
32
+ - cleanup and 1.0 release
33
+
34
+ ## [v0.2.14] 2014-01-24
35
+ - add modulo operator
36
+ - add unary percentage operator
37
+ - support registration of custom functions at runtime
38
+
39
+ ## [v0.2.10] 2012-12-10
40
+ - return integer result for exact division, decimal otherwise
41
+
42
+ ## [v0.2.9] 2012-10-17
43
+ - add `ROUNDUP` / `ROUNDDOWN` functions
44
+
45
+ ## [v0.2.8] 2012-09-30
46
+ - make function name matching case-insensitive
47
+
48
+ ## [v0.2.7] 2012-09-26
49
+ - support passing arbitrary expressions as function arguments
50
+
51
+ ## [v0.2.6] 2012-09-19
52
+ - add `NOT` function
53
+
54
+ ## [v0.2.5] 2012-06-20
55
+ - add exponent operator
56
+ - add support for digits in variable identifiers
57
+
58
+ ## [v0.2.4] 2012-02-29
59
+ - add support for `min < x < max` syntax for inequality ranges
60
+
61
+ ## [v0.2.2] 2012-02-22
62
+ - support `ROUND` to arbitrary decimal place on older Rubies
63
+ - ensure case is preserved for string values
64
+
65
+ ## [v0.2.1] 2012-02-12
66
+ - add `ROUND` function
67
+
68
+ ## [v0.1.3] 2012-01-31
69
+ - add support for string datatype
70
+
71
+ ## [v0.1.1] 2012-01-24
72
+ - change from square bracket to parentheses for top-level evaluation
73
+ - add `IF` function
74
+
75
+ ## [v0.1.0] 2012-01-20
76
+ - initial release
77
+
78
+ [v2.0.0]: https://github.com/rubysolo/dentaku/compare/v1.2.6...v2.0.0
79
+ [v1.2.6]: https://github.com/rubysolo/dentaku/compare/v1.2.5...v1.2.6
80
+ [v1.2.5]: https://github.com/rubysolo/dentaku/compare/v1.2.2...v1.2.5
81
+ [v1.2.2]: https://github.com/rubysolo/dentaku/compare/v1.2.0...v1.2.2
82
+ [v1.2.0]: https://github.com/rubysolo/dentaku/compare/v1.1.0...v1.2.0
83
+ [v1.1.0]: https://github.com/rubysolo/dentaku/compare/v1.0.0...v1.1.0
84
+ [v1.0.0]: https://github.com/rubysolo/dentaku/compare/v0.2.14...v1.0.0
85
+ [v0.2.14]: https://github.com/rubysolo/dentaku/compare/v0.2.10...v0.2.14
86
+ [v0.2.10]: https://github.com/rubysolo/dentaku/compare/v0.2.9...v0.2.10
87
+ [v0.2.9]: https://github.com/rubysolo/dentaku/compare/v0.2.8...v0.2.9
88
+ [v0.2.8]: https://github.com/rubysolo/dentaku/compare/v0.2.7...v0.2.8
89
+ [v0.2.7]: https://github.com/rubysolo/dentaku/compare/v0.2.6...v0.2.7
90
+ [v0.2.6]: https://github.com/rubysolo/dentaku/compare/v0.2.5...v0.2.6
91
+ [v0.2.5]: https://github.com/rubysolo/dentaku/compare/v0.2.4...v0.2.5
92
+ [v0.2.4]: https://github.com/rubysolo/dentaku/compare/v0.2.2...v0.2.4
93
+ [v0.2.2]: https://github.com/rubysolo/dentaku/compare/v0.2.1...v0.2.2
94
+ [v0.2.1]: https://github.com/rubysolo/dentaku/compare/v0.1.3...v0.2.1
95
+ [v0.1.3]: https://github.com/rubysolo/dentaku/compare/v0.1.1...v0.1.3
96
+ [v0.1.1]: https://github.com/rubysolo/dentaku/compare/v0.1.0...v0.1.1
97
+ [v0.1.0]: https://github.com/rubysolo/dentaku/commit/68724fd9c8fa637baf7b9d5515df0caa31e226bd
data/README.md CHANGED
@@ -5,6 +5,7 @@ Dentaku
5
5
  [![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
6
6
  [![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
7
7
  [![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
8
+ [![Hakiri](https://hakiri.io/github/rubysolo/dentaku/master.svg)](https://hakiri.io/github/rubysolo/dentaku)
8
9
 
9
10
  DESCRIPTION
10
11
  -----------
@@ -204,15 +205,15 @@ that's not feasible because:
204
205
  1. The formula is the secret sauce for your startup
205
206
 
206
207
  Whatever your reasons, Dentaku supports adding functions at runtime. To add a
207
- function, you'll need to specify a name and a lambda that accepts all function
208
- arguments and returns the result value.
208
+ function, you'll need to specify a name, a return type, and a lambda that
209
+ accepts all function arguments and returns the result value.
209
210
 
210
211
  Here's an example of adding a function named `POW` that implements
211
212
  exponentiation.
212
213
 
213
214
  ```ruby
214
215
  > c = Dentaku::Calculator.new
215
- > c.add_function(:pow, ->(mantissa, exponent) { mantissa ** exponent })
216
+ > c.add_function(:pow, :numeric, ->(mantissa, exponent) { mantissa ** exponent })
216
217
  > c.evaluate('POW(3,2)')
217
218
  #=> 9
218
219
  > c.evaluate('POW(2,3)')
@@ -223,7 +224,7 @@ Here's an example of adding a variadic function:
223
224
 
224
225
  ```ruby
225
226
  > c = Dentaku::Calculator.new
226
- > c.add_function(:max, ->(*args) { args.max })
227
+ > c.add_function(:max, :numeric, ->(*args) { args.max })
227
228
  > c.evaluate 'MAX(8,6,7,5,3,0,9)'
228
229
  #=> 9
229
230
  ```
data/lib/dentaku/ast.rb CHANGED
@@ -1,13 +1,10 @@
1
1
  require_relative './ast/node'
2
2
  require_relative './ast/nil'
3
3
  require_relative './ast/numeric'
4
+ require_relative './ast/logical'
4
5
  require_relative './ast/string'
5
6
  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'
7
+ require_relative './ast/arithmetic'
11
8
  require_relative './ast/negation'
12
9
  require_relative './ast/comparators'
13
10
  require_relative './ast/combinators'
@@ -0,0 +1,72 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Arithmetic < Operation
6
+ def initialize(*)
7
+ super
8
+ fail "#{ self.class } requires numeric operands" unless valid_node?(left) && valid_node?(right)
9
+ end
10
+
11
+ def type
12
+ :numeric
13
+ end
14
+
15
+ private
16
+
17
+ def valid_node?(node)
18
+ node.is_a?(Identifier) || node.type == :numeric
19
+ end
20
+ end
21
+
22
+ class Addition < Arithmetic
23
+ def value(context={})
24
+ left.value(context) + right.value(context)
25
+ end
26
+
27
+ def self.precedence
28
+ 10
29
+ end
30
+ end
31
+
32
+ class Subtraction < Arithmetic
33
+ def value(context={})
34
+ left.value(context) - right.value(context)
35
+ end
36
+
37
+ def self.precedence
38
+ 10
39
+ end
40
+ end
41
+
42
+ class Multiplication < Arithmetic
43
+ def value(context={})
44
+ left.value(context) * right.value(context)
45
+ end
46
+
47
+ def self.precedence
48
+ 20
49
+ end
50
+ end
51
+
52
+ class Division < Arithmetic
53
+ def value(context={})
54
+ left.value(context) / right.value(context)
55
+ end
56
+
57
+ def self.precedence
58
+ 20
59
+ end
60
+ end
61
+
62
+ class Exponentiation < Arithmetic
63
+ def value(context={})
64
+ left.value(context) ** right.value(context)
65
+ end
66
+
67
+ def self.precedence
68
+ 30
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,12 +1,27 @@
1
+ require_relative './operation'
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class And < Operation
5
+ class Combinator < Operation
6
+ def initialize(*)
7
+ super
8
+ fail "#{ self.class } requires logical operands" unless valid_node?(left) && valid_node?(right)
9
+ end
10
+
11
+ private
12
+
13
+ def valid_node?(node)
14
+ node.is_a?(Identifier) || node.type == :logical
15
+ end
16
+ end
17
+
18
+ class And < Combinator
4
19
  def value(context={})
5
20
  left.value(context) && right.value(context)
6
21
  end
7
22
  end
8
23
 
9
- class Or < Operation
24
+ class Or < Combinator
10
25
  def value(context={})
11
26
  left.value(context) || right.value(context)
12
27
  end
@@ -6,6 +6,10 @@ module Dentaku
6
6
  def self.precedence
7
7
  5
8
8
  end
9
+
10
+ def type
11
+ :logical
12
+ end
9
13
  end
10
14
 
11
15
  class LessThan < Comparator
@@ -15,7 +15,7 @@ module Dentaku
15
15
  registry.fetch(function_name(name)) { fail "Undefined function #{ name } "}
16
16
  end
17
17
 
18
- def self.register(name, implementation)
18
+ def self.register(name, type, implementation)
19
19
  function = Class.new(self) do
20
20
  def self.implementation=(impl)
21
21
  @implementation = impl
@@ -25,13 +25,30 @@ module Dentaku
25
25
  @implementation
26
26
  end
27
27
 
28
+ def self.type=(type)
29
+ @type = type
30
+ end
31
+
32
+ def self.type
33
+ @type
34
+ end
35
+
28
36
  def value(context={})
29
37
  args = @args.flat_map { |a| a.value(context) }
30
38
  self.class.implementation.call(*args)
31
39
  end
40
+
41
+ def type
42
+ self.class.type
43
+ end
32
44
  end
33
45
 
46
+ function_class = name.to_s.capitalize
47
+ Dentaku::AST.send(:remove_const, function_class) if Dentaku::AST.const_defined?(function_class)
48
+ Dentaku::AST.const_set(function_class, function)
49
+
34
50
  function.implementation = implementation
51
+ function.type = type
35
52
 
36
53
  registry[function_name(name)] = function
37
54
  end
@@ -15,6 +15,10 @@ module Dentaku
15
15
  predicate.value(context) ? left.value(context) : right.value(context)
16
16
  end
17
17
 
18
+ def type
19
+ left.type
20
+ end
21
+
18
22
  def dependencies(context={})
19
23
  # TODO : short-circuit?
20
24
  (predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:max, ->(*args) {
3
+ Dentaku::AST::Function.register(:max, :numeric, ->(*args) {
4
4
  args.max
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:min, ->(*args) {
3
+ Dentaku::AST::Function.register(:min, :numeric, ->(*args) {
4
4
  args.min
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:not, ->(logical) {
3
+ Dentaku::AST::Function.register(:not, :logical, ->(logical) {
4
4
  ! logical
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:round, ->(numeric, places=nil) {
3
+ Dentaku::AST::Function.register(:round, :numeric, ->(numeric, places=nil) {
4
4
  numeric.round(places || 0)
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:rounddown, ->(numeric) {
3
+ Dentaku::AST::Function.register(:rounddown, :numeric, ->(numeric) {
4
4
  numeric.floor
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
- Dentaku::AST::Function.register(:roundup, ->(numeric) {
3
+ Dentaku::AST::Function.register(:roundup, :numeric, ->(numeric) {
4
4
  numeric.ceil
5
5
  })
@@ -2,7 +2,7 @@
2
2
  require_relative "../function"
3
3
 
4
4
  Math.methods(false).each do |method|
5
- Dentaku::AST::Function.register(method, ->(*args) {
5
+ Dentaku::AST::Function.register(method, :numeric, ->(*args) {
6
6
  Math.send(method, *args)
7
7
  })
8
8
  end
@@ -8,6 +8,10 @@ module Dentaku
8
8
  def value(context={})
9
9
  @node.value(context)
10
10
  end
11
+
12
+ def type
13
+ @node.type
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,20 @@
1
+ module Dentaku
2
+ module AST
3
+ class Literal < Node
4
+ attr_reader :type
5
+
6
+ def initialize(token)
7
+ @value = token.value
8
+ @type = token.category
9
+ end
10
+
11
+ def value(*)
12
+ @value
13
+ end
14
+
15
+ def dependencies(*)
16
+ []
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "./literal"
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Logical < Literal
6
+ end
7
+ end
8
+ end
@@ -3,12 +3,17 @@ module Dentaku
3
3
  class Negation < Operation
4
4
  def initialize(node)
5
5
  @node = node
6
+ fail "Negation requires numeric operand" unless valid_node?(node)
6
7
  end
7
8
 
8
9
  def value(context={})
9
10
  @node.value(context) * -1
10
11
  end
11
12
 
13
+ def type
14
+ :numeric
15
+ end
16
+
12
17
  def self.arity
13
18
  1
14
19
  end
@@ -20,6 +25,12 @@ module Dentaku
20
25
  def self.precedence
21
26
  40
22
27
  end
28
+
29
+ private
30
+
31
+ def valid_node?(node)
32
+ node.is_a?(Identifier) || node.type == :numeric
33
+ end
23
34
  end
24
35
  end
25
36
  end
@@ -1,17 +1,8 @@
1
+ require_relative "./literal"
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class Numeric < Node
4
- def initialize(token)
5
- @value = token.value
6
- end
7
-
8
- def value(*)
9
- @value
10
- end
11
-
12
- def dependencies(*)
13
- []
14
- end
5
+ class Numeric < Literal
15
6
  end
16
7
  end
17
8
  end
@@ -1,3 +1,5 @@
1
+ require_relative './node'
2
+
1
3
  module Dentaku
2
4
  module AST
3
5
  class Operation < Node
@@ -1,17 +1,8 @@
1
+ require_relative "./literal"
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class String < Node
4
- def initialize(token)
5
- @value = token.value
6
- end
7
-
8
- def value(*)
9
- @value
10
- end
11
-
12
- def dependencies(*)
13
- []
14
- end
5
+ class String < Literal
15
6
  end
16
7
  end
17
8
  end
@@ -14,13 +14,13 @@ module Dentaku
14
14
  @ast_cache = {}
15
15
  end
16
16
 
17
- def add_function(name, body)
18
- Dentaku::AST::Function.register(name, body)
17
+ def add_function(name, type, body)
18
+ Dentaku::AST::Function.register(name, type, body)
19
19
  self
20
20
  end
21
21
 
22
22
  def add_functions(fns)
23
- fns.each { |(name, body)| add_function(name, body) }
23
+ fns.each { |(name, type, body)| add_function(name, type, body) }
24
24
  self
25
25
  end
26
26
 
@@ -28,6 +28,9 @@ module Dentaku
28
28
  when :numeric
29
29
  output.push AST::Numeric.new(token)
30
30
 
31
+ when :logical
32
+ output.push AST::Logical.new(token)
33
+
31
34
  when :string
32
35
  output.push AST::String.new(token)
33
36
 
@@ -35,6 +35,7 @@ module Dentaku
35
35
  grouping,
36
36
  comparator,
37
37
  combinator,
38
+ boolean,
38
39
  function,
39
40
  identifier
40
41
  ]
@@ -88,6 +89,10 @@ module Dentaku
88
89
  new(:combinator, '(and|or)\b', lambda { |raw| raw.strip.downcase.to_sym })
89
90
  end
90
91
 
92
+ def boolean
93
+ new(:logical, '(true|false)\b', lambda { |raw| raw.strip.downcase == 'true' })
94
+ end
95
+
91
96
  def function
92
97
  new(:function, '\w+\s*\(', lambda do |raw|
93
98
  function_name = raw.gsub('(', '')
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.1"
3
3
  end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/arithmetic'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::Addition do
7
+ let(:five) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 5) }
8
+ let(:six) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 6) }
9
+
10
+ let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
+
12
+ it 'performs addition' do
13
+ node = described_class.new(five, six)
14
+ expect(node.value).to eq 11
15
+ end
16
+
17
+ it 'requires numeric operands' do
18
+ expect {
19
+ described_class.new(five, t)
20
+ }.to raise_error
21
+
22
+ expression = Dentaku::AST::Multiplication.new(five, five)
23
+ group = Dentaku::AST::Grouping.new(expression)
24
+
25
+ expect {
26
+ described_class.new(group, five)
27
+ }.not_to raise_error
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/combinators'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::And do
7
+ let(:t) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, true) }
8
+ let(:f) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, false) }
9
+
10
+ let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
11
+
12
+ it 'performs logical AND' do
13
+ node = described_class.new(t, f)
14
+ expect(node.value).to eq false
15
+ end
16
+
17
+ it 'requires logical operands' do
18
+ expect {
19
+ described_class.new(t, five)
20
+ }.to raise_error
21
+
22
+ expression = Dentaku::AST::LessThanOrEqual.new(five, five)
23
+ expect {
24
+ described_class.new(t, expression)
25
+ }.not_to raise_error
26
+ end
27
+ end
@@ -11,7 +11,7 @@ describe Dentaku::AST::Function do
11
11
  end
12
12
 
13
13
  it 'registers a custom function' do
14
- described_class.register("flarble", -> { "flarble" })
14
+ described_class.register("flarble", :string, -> { "flarble" })
15
15
  expect { described_class.get("flarble") }.not_to raise_error
16
16
  function = described_class.get("flarble").new
17
17
  expect(function.value).to eq "flarble"
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/numeric'
3
+
4
+ require 'dentaku/token'
5
+
6
+ describe Dentaku::AST::Numeric do
7
+ subject { described_class.new(Dentaku::Token.new(:numeric, 5)) }
8
+
9
+ it 'has numeric type' do
10
+ expect(subject.type).to eq :numeric
11
+ end
12
+
13
+ it 'has no dependencies' do
14
+ expect(subject.dependencies).to be_empty
15
+ end
16
+ end
@@ -8,12 +8,12 @@ describe Dentaku::Calculator do
8
8
  let(:with_external_funcs) do
9
9
  c = described_class.new
10
10
 
11
- c.add_function(:now, -> { Time.now.to_s })
11
+ c.add_function(:now, :string, -> { Time.now.to_s })
12
12
 
13
13
  fns = [
14
- [:pow, ->(mantissa, exponent) { mantissa ** exponent }],
15
- [:biggest, ->(*args) { args.max }],
16
- [:smallest, ->(*args) { args.min }],
14
+ [:pow, :numeric, ->(mantissa, exponent) { mantissa ** exponent }],
15
+ [:biggest, :numeric, ->(*args) { args.max }],
16
+ [:smallest, :numeric, ->(*args) { args.min }],
17
17
  ]
18
18
 
19
19
  c.add_functions(fns)
data/spec/parser_spec.rb CHANGED
@@ -85,4 +85,13 @@ describe Dentaku::Parser do
85
85
  expect { node.value }.to raise_error
86
86
  expect(node.value(x: 3)).to eq 15
87
87
  end
88
+
89
+ it 'evaluates boolean expressions' do
90
+ d_true = Dentaku::Token.new(:logical, true)
91
+ d_and = Dentaku::Token.new(:combinator, :and)
92
+ d_false = Dentaku::Token.new(:logical, false)
93
+
94
+ node = described_class.new([d_true, d_and, d_false]).parse
95
+ expect(node.value).to eq false
96
+ end
88
97
  end
@@ -3,7 +3,7 @@ require 'dentaku/token_scanner'
3
3
  describe Dentaku::TokenScanner do
4
4
  let(:whitespace) { described_class.new(:whitespace, '\s') }
5
5
  let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
6
- lambda{|raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
6
+ lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
7
7
  }
8
8
 
9
9
  it 'returns a token for a matching string' do
@@ -23,6 +23,6 @@ describe Dentaku::TokenScanner do
23
23
  end
24
24
 
25
25
  it 'returns a list of all configured scanners' do
26
- expect(described_class.scanners.length).to eq 11
26
+ expect(described_class.scanners.length).to eq 12
27
27
  end
28
28
  end
@@ -153,6 +153,18 @@ describe Dentaku::Tokenizer do
153
153
  expect(tokens.map(&:value)).to eq(['andover', :lt, 10])
154
154
  end
155
155
 
156
+ it 'tokenizes TRUE and FALSE literals' do
157
+ tokens = tokenizer.tokenize('true and false')
158
+ expect(tokens.length).to eq(3)
159
+ expect(tokens.map(&:category)).to eq([:logical, :combinator, :logical])
160
+ expect(tokens.map(&:value)).to eq([true, :and, false])
161
+
162
+ tokens = tokenizer.tokenize('true_lies and falsehoods')
163
+ expect(tokens.length).to eq(3)
164
+ expect(tokens.map(&:category)).to eq([:identifier, :combinator, :identifier])
165
+ expect(tokens.map(&:value)).to eq(['true_lies', :and, 'falsehoods'])
166
+ end
167
+
156
168
  describe 'functions' do
157
169
  it 'include IF' do
158
170
  tokens = tokenizer.tokenize('if(x < 10, y, z)')
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: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-07 00:00:00.000000000 Z
11
+ date: 2015-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -63,17 +63,16 @@ files:
63
63
  - ".gitignore"
64
64
  - ".pryrc"
65
65
  - ".travis.yml"
66
+ - CHANGELOG.md
66
67
  - Gemfile
67
68
  - README.md
68
69
  - Rakefile
69
70
  - dentaku.gemspec
70
71
  - lib/dentaku.rb
71
72
  - lib/dentaku/ast.rb
72
- - lib/dentaku/ast/addition.rb
73
+ - lib/dentaku/ast/arithmetic.rb
73
74
  - lib/dentaku/ast/combinators.rb
74
75
  - lib/dentaku/ast/comparators.rb
75
- - lib/dentaku/ast/division.rb
76
- - lib/dentaku/ast/exponentiation.rb
77
76
  - lib/dentaku/ast/function.rb
78
77
  - lib/dentaku/ast/functions/if.rb
79
78
  - lib/dentaku/ast/functions/max.rb
@@ -85,14 +84,14 @@ files:
85
84
  - lib/dentaku/ast/functions/ruby_math.rb
86
85
  - lib/dentaku/ast/grouping.rb
87
86
  - lib/dentaku/ast/identifier.rb
88
- - lib/dentaku/ast/multiplication.rb
87
+ - lib/dentaku/ast/literal.rb
88
+ - lib/dentaku/ast/logical.rb
89
89
  - lib/dentaku/ast/negation.rb
90
90
  - lib/dentaku/ast/nil.rb
91
91
  - lib/dentaku/ast/node.rb
92
92
  - lib/dentaku/ast/numeric.rb
93
93
  - lib/dentaku/ast/operation.rb
94
94
  - lib/dentaku/ast/string.rb
95
- - lib/dentaku/ast/subtraction.rb
96
95
  - lib/dentaku/bulk_expression_solver.rb
97
96
  - lib/dentaku/calculator.rb
98
97
  - lib/dentaku/dependency_resolver.rb
@@ -104,8 +103,11 @@ files:
104
103
  - lib/dentaku/token_scanner.rb
105
104
  - lib/dentaku/tokenizer.rb
106
105
  - lib/dentaku/version.rb
106
+ - spec/ast/addition_spec.rb
107
+ - spec/ast/and_spec.rb
107
108
  - spec/ast/function_spec.rb
108
109
  - spec/ast/node_spec.rb
110
+ - spec/ast/numeric_spec.rb
109
111
  - spec/benchmark.rb
110
112
  - spec/bulk_expression_solver_spec.rb
111
113
  - spec/calculator_spec.rb
@@ -142,8 +144,11 @@ signing_key:
142
144
  specification_version: 4
143
145
  summary: A formula language parser and evaluator
144
146
  test_files:
147
+ - spec/ast/addition_spec.rb
148
+ - spec/ast/and_spec.rb
145
149
  - spec/ast/function_spec.rb
146
150
  - spec/ast/node_spec.rb
151
+ - spec/ast/numeric_spec.rb
147
152
  - spec/benchmark.rb
148
153
  - spec/bulk_expression_solver_spec.rb
149
154
  - spec/calculator_spec.rb
@@ -1,15 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- require_relative './operation'
2
-
3
- module Dentaku
4
- module AST
5
- class Multiplication < 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
@@ -1,15 +0,0 @@
1
- require_relative './operation'
2
-
3
- module Dentaku
4
- module AST
5
- class Subtraction < 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