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.
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
data/lib/dentaku/token.rb CHANGED
@@ -16,6 +16,10 @@ module Dentaku
16
16
  raw_value.to_s.length
17
17
  end
18
18
 
19
+ def grouping?
20
+ is?(:grouping)
21
+ end
22
+
19
23
  def is?(c)
20
24
  category == c
21
25
  end
@@ -0,0 +1,29 @@
1
+ module Dentaku
2
+ module TokenMatchers
3
+ def self.token_matchers(*symbols)
4
+ symbols.map { |s| matcher(s) }
5
+ end
6
+
7
+ def self.function_token_matchers(function_name, *symbols)
8
+ token_matchers(:fopen, *symbols, :close).unshift(
9
+ TokenMatcher.send(function_name)
10
+ )
11
+ end
12
+
13
+ def self.matcher(symbol)
14
+ @matchers ||= [
15
+ :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
16
+ :comparator, :comp_gt, :comp_lt, :fopen, :open, :close, :comma,
17
+ :non_close_plus, :non_group, :non_group_star, :arguments,
18
+ :logical, :combinator, :if, :round, :roundup, :rounddown, :not,
19
+ :anchored_minus, :math_neg_pow, :math_neg_mul
20
+ ].each_with_object({}) do |name, matchers|
21
+ matchers[name] = TokenMatcher.send(name)
22
+ end
23
+
24
+ @matchers.fetch(symbol) do
25
+ raise "Unknown token symbol #{ symbol }"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,15 +1,17 @@
1
+ require 'bigdecimal'
1
2
  require 'dentaku/token'
2
3
 
3
4
  module Dentaku
4
5
  class TokenScanner
5
- def initialize(category, regexp, converter=nil)
6
+ def initialize(category, regexp, converter=nil, condition=nil)
6
7
  @category = category
7
8
  @regexp = %r{\A(#{ regexp })}i
8
9
  @converter = converter
10
+ @condition = condition || ->(*) { true }
9
11
  end
10
12
 
11
- def scan(string)
12
- if m = @regexp.match(string)
13
+ def scan(string, last_token=nil)
14
+ if (m = @regexp.match(string)) && @condition.call(last_token)
13
15
  value = raw = m.to_s
14
16
  value = @converter.call(raw) if @converter
15
17
 
@@ -28,6 +30,7 @@ module Dentaku
28
30
  numeric,
29
31
  double_quoted_string,
30
32
  single_quoted_string,
33
+ negate,
31
34
  operator,
32
35
  grouping,
33
36
  comparator,
@@ -53,6 +56,18 @@ module Dentaku
53
56
  new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
54
57
  end
55
58
 
59
+ def negate
60
+ new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
61
+ last_token.nil? ||
62
+ last_token.is?(:operator) ||
63
+ last_token.is?(:comparator) ||
64
+ last_token.is?(:combinator) ||
65
+ last_token.value == :open ||
66
+ last_token.value == :fopen ||
67
+ last_token.value == :comma
68
+ })
69
+ end
70
+
56
71
  def operator
57
72
  names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
58
73
  new(:operator, '\^|\+|-|\*|\/|%', lambda { |raw| names[raw] })
@@ -10,7 +10,7 @@ module Dentaku
10
10
  def tokenize(string)
11
11
  @nesting = 0
12
12
  @tokens = []
13
- input = string.to_s.dup
13
+ input = strip_comments(string.to_s.dup)
14
14
 
15
15
  until input.empty?
16
16
  raise "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
@@ -24,8 +24,12 @@ module Dentaku
24
24
  @tokens
25
25
  end
26
26
 
27
+ def last_token
28
+ @tokens.last
29
+ end
30
+
27
31
  def scan(string, scanner)
28
- if tokens = scanner.scan(string)
32
+ if tokens = scanner.scan(string, last_token)
29
33
  tokens.each do |token|
30
34
  raise "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
31
35
 
@@ -42,5 +46,9 @@ module Dentaku
42
46
  [false, string]
43
47
  end
44
48
  end
49
+
50
+ def strip_comments(input)
51
+ input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
52
+ end
45
53
  end
46
54
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "1.2.6"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/function'
3
+
4
+ describe Dentaku::AST::Function do
5
+ it 'maintains a function registry' do
6
+ expect(described_class).to respond_to(:get)
7
+ end
8
+
9
+ it 'raises an exception when trying to access an undefined function' do
10
+ expect { described_class.get("flarble") }.to raise_error
11
+ end
12
+
13
+ it 'registers a custom function' do
14
+ described_class.register("flarble", -> { "flarble" })
15
+ expect { described_class.get("flarble") }.not_to raise_error
16
+ function = described_class.get("flarble").new
17
+ expect(function.value).to eq "flarble"
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/node'
3
+ require 'dentaku/tokenizer'
4
+ require 'dentaku/parser'
5
+
6
+ describe Dentaku::AST::Node do
7
+ it 'returns list of dependencies' do
8
+ node = make_node('x + 5')
9
+ expect(node.dependencies).to eq ['x']
10
+
11
+ node = make_node('5 < x')
12
+ expect(node.dependencies).to eq ['x']
13
+
14
+ node = make_node('5 < 7')
15
+ expect(node.dependencies).to eq []
16
+
17
+ node = make_node('(y * 7)')
18
+ expect(node.dependencies).to eq ['y']
19
+
20
+ node = make_node('if(x > 5, y, z)')
21
+ expect(node.dependencies).to eq ['x', 'y', 'z']
22
+
23
+ node = make_node('if(x > 5, y, z)')
24
+ expect(node.dependencies('x' => 7)).to eq ['y', 'z']
25
+ end
26
+
27
+ it 'returns unique list of dependencies' do
28
+ node = make_node('x + x')
29
+ expect(node.dependencies).to eq ['x']
30
+ end
31
+
32
+ private
33
+
34
+ def make_node(expression)
35
+ Dentaku::Parser.new(Dentaku::Tokenizer.new.tokenize(expression)).parse
36
+ end
37
+ end
@@ -1,3 +1,4 @@
1
+ require 'spec_helper'
1
2
  require 'dentaku/bulk_expression_solver'
2
3
 
3
4
  RSpec.describe Dentaku::BulkExpressionSolver do
@@ -17,34 +18,40 @@ RSpec.describe Dentaku::BulkExpressionSolver do
17
18
  it "lets you know if a variable is unbound" do
18
19
  expressions = {more_apples: "apples + 1"}
19
20
  expect {
20
- described_class.new(expressions, {}).solve!()
21
+ described_class.new(expressions, {}).solve!
21
22
  }.to raise_error(Dentaku::UnboundVariableError)
22
23
  end
23
24
 
24
25
  it "lets you know if the result is a div/0 error" do
25
26
  expressions = {more_apples: "1/0"}
26
27
  expect {
27
- described_class.new(expressions, {}).solve!()
28
+ described_class.new(expressions, {}).solve!
28
29
  }.to raise_error(ZeroDivisionError)
29
30
  end
31
+
32
+ it "does not require keys to be parseable" do
33
+ expressions = { "the value of x, incremented" => "x + 1" }
34
+ solver = described_class.new(expressions, "x" => 3)
35
+ expect(solver.solve!).to eq({ "the value of x, incremented" => 4 })
36
+ end
30
37
  end
31
38
 
32
39
  describe "#solve" do
33
40
  it "returns :undefined when variables are unbound" do
34
41
  expressions = {more_apples: "apples + 1"}
35
- expect(described_class.new(expressions, {}).solve())
42
+ expect(described_class.new(expressions, {}).solve)
36
43
  .to eq(more_apples: :undefined)
37
44
  end
38
45
 
39
46
  it "allows passing in a custom value to an error handler when a variable is unbound" do
40
47
  expressions = {more_apples: "apples + 1"}
41
- expect(described_class.new(expressions, {}).solve() { :foo })
48
+ expect(described_class.new(expressions, {}).solve { :foo })
42
49
  .to eq(more_apples: :foo)
43
50
  end
44
51
 
45
52
  it "allows passing in a custom value to an error handler when there is a div/0 error" do
46
53
  expressions = {more_apples: "1/0"}
47
- expect(described_class.new(expressions, {}).solve() { :foo })
54
+ expect(described_class.new(expressions, {}).solve { :foo })
48
55
  .to eq(more_apples: :foo)
49
56
  end
50
57
  end
@@ -1,3 +1,4 @@
1
+ require 'spec_helper'
1
2
  require 'dentaku/calculator'
2
3
 
3
4
  describe Dentaku::Calculator do
@@ -19,7 +20,7 @@ describe Dentaku::Calculator do
19
20
  expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
20
21
  expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
21
22
  expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
22
- expect(calculator.evaluate('1 + -2 ^ 2')).to eq(-3)
23
+ expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
23
24
  expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
24
25
  expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
25
26
  expect(calculator.evaluate('10 ^ 2')).to eq(100)
@@ -198,4 +199,16 @@ describe Dentaku::Calculator do
198
199
  expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
199
200
  end
200
201
  end
202
+
203
+ describe 'math functions' do
204
+ Math.methods(false).each do |method|
205
+ it method do
206
+ if Math.method(method).arity == 2
207
+ expect(calculator.evaluate("#{method}(1,2)")).to eq Math.send(method, 1, 2)
208
+ else
209
+ expect(calculator.evaluate("#{method}(1)")).to eq Math.send(method, 1)
210
+ end
211
+ end
212
+ end
213
+ end
201
214
  end
@@ -8,28 +8,12 @@ describe Dentaku::Calculator do
8
8
  let(:with_external_funcs) do
9
9
  c = described_class.new
10
10
 
11
- now = { name: :now, type: :string, signature: [], body: -> { Time.now.to_s } }
12
- c.add_function(now)
11
+ c.add_function(:now, -> { Time.now.to_s })
13
12
 
14
13
  fns = [
15
- {
16
- name: :exp,
17
- type: :numeric,
18
- signature: [ :numeric, :numeric ],
19
- body: ->(mantissa, exponent) { mantissa ** exponent }
20
- },
21
- {
22
- name: :max,
23
- type: :numeric,
24
- signature: [ :arguments ],
25
- body: ->(*args) { args.max }
26
- },
27
- {
28
- name: :min,
29
- type: :numeric,
30
- signature: [ :arguments ],
31
- body: ->(*args) { args.min }
32
- }
14
+ [:pow, ->(mantissa, exponent) { mantissa ** exponent }],
15
+ [:biggest, ->(*args) { args.max }],
16
+ [:smallest, ->(*args) { args.min }],
33
17
  ]
34
18
 
35
19
  c.add_functions(fns)
@@ -41,18 +25,18 @@ describe Dentaku::Calculator do
41
25
  expect(now).not_to be_empty
42
26
  end
43
27
 
44
- it 'includes EXP' do
45
- expect(with_external_funcs.evaluate('EXP(2,3)')).to eq(8)
46
- expect(with_external_funcs.evaluate('EXP(3,2)')).to eq(9)
47
- expect(with_external_funcs.evaluate('EXP(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
28
+ it 'includes POW' do
29
+ expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
30
+ expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
31
+ expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
48
32
  end
49
33
 
50
- it 'includes MAX' do
51
- expect(with_external_funcs.evaluate('MAX(8,6,7,5,3,0,9)')).to eq(9)
34
+ it 'includes BIGGEST' do
35
+ expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
52
36
  end
53
37
 
54
- it 'includes MIN' do
55
- expect(with_external_funcs.evaluate('MIN(8,6,7,5,3,0,9)')).to eq(0)
38
+ it 'includes SMALLEST' do
39
+ expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
56
40
  end
57
41
  end
58
42
  end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/parser'
3
+
4
+ describe Dentaku::Parser do
5
+ it 'is constructed from a token' do
6
+ token = Dentaku::Token.new(:numeric, 5)
7
+ node = described_class.new([token]).parse
8
+ expect(node.value).to eq 5
9
+ end
10
+
11
+ it 'performs simple addition' do
12
+ five = Dentaku::Token.new(:numeric, 5)
13
+ plus = Dentaku::Token.new(:operator, :add)
14
+ four = Dentaku::Token.new(:numeric, 4)
15
+
16
+ node = described_class.new([five, plus, four]).parse
17
+ expect(node.value).to eq 9
18
+ end
19
+
20
+ it 'compares two numbers' do
21
+ five = Dentaku::Token.new(:numeric, 5)
22
+ lt = Dentaku::Token.new(:comparator, :lt)
23
+ four = Dentaku::Token.new(:numeric, 4)
24
+
25
+ node = described_class.new([five, lt, four]).parse
26
+ expect(node.value).to eq false
27
+ end
28
+
29
+ it 'performs multiple operations in one stream' do
30
+ five = Dentaku::Token.new(:numeric, 5)
31
+ plus = Dentaku::Token.new(:operator, :add)
32
+ four = Dentaku::Token.new(:numeric, 4)
33
+ times = Dentaku::Token.new(:operator, :multiply)
34
+ three = Dentaku::Token.new(:numeric, 3)
35
+
36
+ node = described_class.new([five, plus, four, times, three]).parse
37
+ expect(node.value).to eq 17
38
+ end
39
+
40
+ it 'respects order of operations' do
41
+ five = Dentaku::Token.new(:numeric, 5)
42
+ times = Dentaku::Token.new(:operator, :multiply)
43
+ four = Dentaku::Token.new(:numeric, 4)
44
+ plus = Dentaku::Token.new(:operator, :add)
45
+ three = Dentaku::Token.new(:numeric, 3)
46
+
47
+ node = described_class.new([five, times, four, plus, three]).parse
48
+ expect(node.value).to eq 23
49
+ end
50
+
51
+ it 'respects grouping by parenthesis' do
52
+ lpar = Dentaku::Token.new(:grouping, :open)
53
+ five = Dentaku::Token.new(:numeric, 5)
54
+ plus = Dentaku::Token.new(:operator, :add)
55
+ four = Dentaku::Token.new(:numeric, 4)
56
+ rpar = Dentaku::Token.new(:grouping, :close)
57
+ times = Dentaku::Token.new(:operator, :multiply)
58
+ three = Dentaku::Token.new(:numeric, 3)
59
+
60
+ node = described_class.new([lpar, five, plus, four, rpar, times, three]).parse
61
+ expect(node.value).to eq 27
62
+ end
63
+
64
+ it 'evaluates functions' do
65
+ fn = Dentaku::Token.new(:function, :if)
66
+ fopen = Dentaku::Token.new(:grouping, :fopen)
67
+ five = Dentaku::Token.new(:numeric, 5)
68
+ lt = Dentaku::Token.new(:comparator, :lt)
69
+ four = Dentaku::Token.new(:numeric, 4)
70
+ comma = Dentaku::Token.new(:grouping, :comma)
71
+ three = Dentaku::Token.new(:numeric, 3)
72
+ two = Dentaku::Token.new(:numeric, 2)
73
+ rpar = Dentaku::Token.new(:grouping, :close)
74
+
75
+ node = described_class.new([fn, fopen, five, lt, four, comma, three, comma, two, rpar]).parse
76
+ expect(node.value).to eq 2
77
+ end
78
+
79
+ it 'represents formulas with variables' do
80
+ five = Dentaku::Token.new(:numeric, 5)
81
+ times = Dentaku::Token.new(:operator, :multiply)
82
+ x = Dentaku::Token.new(:identifier, :x)
83
+
84
+ node = described_class.new([five, times, x]).parse
85
+ expect { node.value }.to raise_error
86
+ expect(node.value(x: 3)).to eq 15
87
+ end
88
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'pry'
2
+
1
3
  # automatically create a token stream from bare values
2
4
  def token_stream(*args)
3
5
  args.map do |value|
@@ -29,4 +31,3 @@ def type_for(value)
29
31
  :identifier
30
32
  end
31
33
  end
32
-
@@ -2,7 +2,9 @@ require 'dentaku/token_scanner'
2
2
 
3
3
  describe Dentaku::TokenScanner do
4
4
  let(:whitespace) { described_class.new(:whitespace, '\s') }
5
- let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)', lambda{|raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i }) }
5
+ let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
6
+ lambda{|raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
7
+ }
6
8
 
7
9
  it 'returns a token for a matching string' do
8
10
  token = whitespace.scan(' ').first
@@ -21,7 +23,6 @@ describe Dentaku::TokenScanner do
21
23
  end
22
24
 
23
25
  it 'returns a list of all configured scanners' do
24
- expect(described_class.scanners.length).to eq 10
26
+ expect(described_class.scanners.length).to eq 11
25
27
  end
26
28
  end
27
-