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
@@ -1,45 +0,0 @@
1
- require 'dentaku/binary_operation'
2
-
3
- describe Dentaku::BinaryOperation do
4
- let(:operation) { described_class.new(2, 3) }
5
- let(:logical) { described_class.new(true, false) }
6
-
7
- it 'raises a number to a power' do
8
- expect(operation.pow).to eq [:numeric, 8]
9
- end
10
-
11
- it 'adds two numbers' do
12
- expect(operation.add).to eq [:numeric, 5]
13
- end
14
-
15
- it 'subtracts two numbers' do
16
- expect(operation.subtract).to eq [:numeric, -1]
17
- end
18
-
19
- it 'multiplies two numbers' do
20
- expect(operation.multiply).to eq [:numeric, 6]
21
- end
22
-
23
- it 'divides two numbers' do
24
- expect(operation.divide).to eq [:numeric, (BigDecimal.new('2.0')/BigDecimal.new('3.0'))]
25
- end
26
-
27
- it 'compares two numbers' do
28
- expect(operation.le).to eq [:logical, true]
29
- expect(operation.lt).to eq [:logical, true]
30
- expect(operation.ne).to eq [:logical, true]
31
-
32
- expect(operation.ge).to eq [:logical, false]
33
- expect(operation.gt).to eq [:logical, false]
34
- expect(operation.eq).to eq [:logical, false]
35
- end
36
-
37
- it 'performs logical AND and OR' do
38
- expect(logical.and).to eq [:logical, false]
39
- expect(logical.or).to eq [:logical, true]
40
- end
41
-
42
- it 'mods two numbers' do
43
- expect(operation.mod).to eq [:numeric, 2%3]
44
- end
45
- end
@@ -1,145 +0,0 @@
1
- require 'spec_helper'
2
- require 'dentaku/evaluator'
3
-
4
- describe Dentaku::Evaluator do
5
- let(:rule_set) { Dentaku::RuleSet.new }
6
- let(:evaluator) { Dentaku::Evaluator.new(rule_set) }
7
-
8
- describe 'rule scanning' do
9
- it 'finds a matching rule' do
10
- rule = [Dentaku::TokenMatcher.new(:numeric, nil)]
11
- stream = [Dentaku::Token.new(:numeric, 1), Dentaku::Token.new(:operator, :add), Dentaku::Token.new(:numeric, 1)]
12
- position, _match = evaluator.find_rule_match(rule, stream)
13
- expect(position).to eq(0)
14
- end
15
- end
16
-
17
- describe 'evaluating' do
18
- it 'empty expression is be truthy' do
19
- expect(evaluator.evaluate([])).to be
20
- end
21
-
22
- it 'empty expression equals 0' do
23
- expect(evaluator.evaluate([])).to eq(0)
24
- end
25
-
26
- it 'single numeric evaluates to its value' do
27
- expect(evaluator.evaluate([Dentaku::Token.new(:numeric, 10)])).to eq(10)
28
- expect(evaluator.evaluate([Dentaku::Token.new(:string, 'a')])).to eq('a')
29
- end
30
-
31
- it 'evaluates one apply step' do
32
- stream = token_stream(1, :add, 1, :add, 1)
33
- expected = token_stream(2, :add, 1)
34
-
35
- expect(evaluator.evaluate_step(stream, 0, 3, :apply)).to eq(expected)
36
- end
37
-
38
- it 'evaluates one grouping step' do
39
- stream = token_stream(:open, 1, :add, 1, :close, :multiply, 5)
40
- expected = token_stream(2, :multiply, 5)
41
-
42
- expect(evaluator.evaluate_step(stream, 0, 5, :evaluate_group)).to eq(expected)
43
- end
44
-
45
- it 'supports unary minus' do
46
- expect(evaluator.evaluate(token_stream(:subtract, 1))).to eq(-1)
47
- expect(evaluator.evaluate(token_stream(1, :subtract, :subtract, 1))).to eq(2)
48
- expect(evaluator.evaluate(token_stream(1, :subtract, :subtract, :subtract, 1))).to eq(0)
49
- expect(evaluator.evaluate(token_stream(:subtract, 1, :add, 1))).to eq(0)
50
- expect(evaluator.evaluate(token_stream(3, :add, 0, :multiply, :subtract, 3))).to eq(3)
51
- end
52
-
53
- it 'evaluates a number multiplied by an exponent' do
54
- expect(evaluator.evaluate(token_stream(10, :pow, 2))).to eq(100)
55
- expect(evaluator.evaluate(token_stream(0, :multiply, 10, :pow, 5))).to eq(0)
56
- expect(evaluator.evaluate(token_stream(0, :multiply, 10, :pow, :subtract, 5))).to eq(0)
57
- end
58
-
59
- it 'supports unary percentage' do
60
- expect(evaluator.evaluate(token_stream(50, :mod))).to eq(0.5)
61
- expect(evaluator.evaluate(token_stream(50, :mod, :multiply, 100))).to eq(50)
62
- end
63
-
64
- describe 'maths' do
65
- it 'performs addition' do
66
- expect(evaluator.evaluate(token_stream(1, :add, 1))).to eq(2)
67
- end
68
-
69
- it 'respects order of precedence' do
70
- expect(evaluator.evaluate(token_stream(1, :add, 1, :multiply, 5))).to eq(6)
71
- expect(evaluator.evaluate(token_stream(2, :add, 10, :mod, 2))).to eq(2)
72
- end
73
-
74
- it 'respects explicit grouping' do
75
- expect(evaluator.evaluate(token_stream(:open, 1, :add, 1, :close, :multiply, 5))).to eq(10)
76
- end
77
-
78
- it 'returns floating point from division when there is a remainder' do
79
- expect(evaluator.evaluate(token_stream(5, :divide, 4))).to eq(1.25)
80
- end
81
- end
82
-
83
- describe 'find_rule_match' do
84
- it 'matches a function call' do
85
- if_pattern, _ = *rule_set.rules.first
86
- position, tokens = evaluator.find_rule_match(if_pattern, token_stream(:if, :fopen, true, :comma, 1, :comma, 2, :close))
87
- expect(position).to eq 0
88
- expect(tokens.length).to eq 8
89
- end
90
-
91
- describe 'with start-anchored token' do
92
- let(:number) { [Dentaku::TokenMatcher.new(:numeric).caret] }
93
- let(:string) { [Dentaku::TokenMatcher.new(:string).caret] }
94
- let(:stream) { token_stream(1, 'foo') }
95
-
96
- it 'matches anchored to the beginning of the token stream' do
97
- position, tokens = evaluator.find_rule_match(number, stream)
98
- expect(position).to eq 0
99
- expect(tokens.length).to eq 1
100
- end
101
-
102
- it 'does not match later in the stream' do
103
- position, _tokens = evaluator.find_rule_match(string, stream)
104
- expect(position).to be_nil
105
- end
106
- end
107
- end
108
-
109
- describe 'functions' do
110
- it 'is evaluated' do
111
- expect(evaluator.evaluate(token_stream(:round, :fopen, 5, :divide, 3.0, :close))).to eq 2
112
- expect(evaluator.evaluate(token_stream(:round, :fopen, 5, :divide, 3.0, :comma, 2, :close))).to eq 1.67
113
- expect(evaluator.evaluate(token_stream(:roundup, :fopen, 5, :divide, 1.2, :close))).to eq 5
114
- expect(evaluator.evaluate(token_stream(:rounddown, :fopen, 5, :divide, 1.2, :close))).to eq 4
115
- end
116
- end
117
-
118
- describe 'logic' do
119
- it 'evaluates conditional' do
120
- expect(evaluator.evaluate(token_stream(5, :gt, 1))).to be_truthy
121
- end
122
-
123
- it 'expands inequality ranges' do
124
- stream = token_stream(5, :lt, 10, :le, 10)
125
- expected = token_stream(5, :lt, 10, :and, 10, :le, 10)
126
- expect(evaluator.evaluate_step(stream, 0, 5, :expand_range)).to eq(expected)
127
-
128
- expect(evaluator.evaluate(token_stream(5, :lt, 10, :le, 10))).to be_truthy
129
- expect(evaluator.evaluate(token_stream(3, :gt, 5, :ge, 1))).to be_falsey
130
-
131
- expect { evaluator.evaluate(token_stream(3, :gt, 2, :lt, 1)) }.to raise_error
132
- end
133
-
134
- it 'evaluates combined conditionals' do
135
- expect(evaluator.evaluate(token_stream(5, :gt, 1, :or, false))).to be_truthy
136
- expect(evaluator.evaluate(token_stream(5, :gt, 1, :and, false))).to be_falsey
137
- end
138
-
139
- it 'negates a logical value' do
140
- expect(evaluator.evaluate(token_stream(:not, :fopen, 5, :gt, 1, :or, false, :close))).to be_falsey
141
- expect(evaluator.evaluate(token_stream(:not, :fopen, 5, :gt, 1, :and, false, :close))).to be_truthy
142
- end
143
- end
144
- end
145
- end
@@ -1,25 +0,0 @@
1
- require 'spec_helper'
2
- require 'dentaku/expression'
3
-
4
- describe Dentaku::Expression do
5
- describe 'an all literal expression' do
6
- it 'is fully bound' do
7
- static = described_class.new('1 + 1')
8
- expect(static).not_to be_unbound
9
- end
10
- end
11
-
12
- describe 'an expression with variable identifiers' do
13
- it 'is unbound' do
14
- dynamic = described_class.new('a > 5')
15
- expect(dynamic).to be_unbound
16
- end
17
-
18
- describe 'with values set for all variables' do
19
- it 'is fully bound' do
20
- dynamic = described_class.new('a > 5', {a: 7})
21
- expect(dynamic).not_to be_unbound
22
- end
23
- end
24
- end
25
- end
@@ -1,43 +0,0 @@
1
- require 'dentaku/rule_set'
2
-
3
- describe Dentaku::RuleSet do
4
- subject { described_class.new }
5
-
6
- it 'yields core rules' do
7
- functions = []
8
- subject.each { |pattern, function| functions << function }
9
- expect(functions).to eq [:if,
10
- :round,
11
- :round_int,
12
- :round_int,
13
- :not,
14
- :evaluate_group,
15
- :negate,
16
- :apply,
17
- :pow_negate,
18
- :apply,
19
- :apply,
20
- :mul_negate,
21
- :apply,
22
- :percentage,
23
- :negate,
24
- :expand_range,
25
- :expand_range,
26
- :apply,
27
- :apply,
28
- :apply,
29
- ]
30
- end
31
-
32
- it 'adds custom function patterns' do
33
- functions = []
34
- subject.add_function(
35
- name: :min,
36
- type: :numeric,
37
- signature: [ :arguments ],
38
- body: ->(*args) { args.min }
39
- )
40
- subject.each { |pattern, function| functions << function }
41
- expect(functions).to include('min')
42
- end
43
- end