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
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
-