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