dentaku 2.0.11 → 3.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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +0 -1
- data/CHANGELOG.md +19 -0
- data/README.md +3 -2
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/ast.rb +4 -0
- data/lib/dentaku/ast/access.rb +27 -0
- data/lib/dentaku/ast/arithmetic.rb +49 -7
- data/lib/dentaku/ast/case.rb +17 -3
- data/lib/dentaku/ast/combinators.rb +8 -2
- data/lib/dentaku/ast/function.rb +16 -0
- data/lib/dentaku/ast/function_registry.rb +15 -2
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/max.rb +1 -1
- data/lib/dentaku/ast/functions/min.rb +1 -1
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/round.rb +2 -2
- data/lib/dentaku/ast/functions/rounddown.rb +3 -2
- data/lib/dentaku/ast/functions/roundup.rb +3 -2
- data/lib/dentaku/ast/functions/ruby_math.rb +3 -3
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/identifier.rb +3 -2
- data/lib/dentaku/ast/negation.rb +5 -1
- data/lib/dentaku/ast/node.rb +3 -0
- data/lib/dentaku/bulk_expression_solver.rb +1 -2
- data/lib/dentaku/calculator.rb +7 -6
- data/lib/dentaku/exceptions.rb +75 -1
- data/lib/dentaku/parser.rb +73 -12
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_scanner.rb +20 -3
- data/lib/dentaku/tokenizer.rb +31 -4
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +6 -6
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +56 -0
- data/spec/ast/division_spec.rb +1 -1
- data/spec/ast/function_spec.rb +43 -6
- data/spec/ast/max_spec.rb +15 -0
- data/spec/ast/min_spec.rb +15 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/round_spec.rb +25 -0
- data/spec/ast/rounddown_spec.rb +25 -0
- data/spec/ast/roundup_spec.rb +25 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/calculator_spec.rb +26 -4
- data/spec/exceptions_spec.rb +1 -1
- data/spec/parser_spec.rb +22 -3
- data/spec/spec_helper.rb +12 -2
- data/spec/token_scanner_spec.rb +0 -4
- data/spec/tokenizer_spec.rb +40 -2
- metadata +39 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99f7f7efeefb4c1096618e028100c52bb15ca134
|
4
|
+
data.tar.gz: 1afd93ef547f630fb0ea121a6e8c813ea8abeeac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31227f42fb7f0a262e82b2ccdc334adc4458ba7339c87f0fbf81eb49ffb51bac72654e05de9495d8dad73c9669b8d82bf9bdf8c4f7ee1a5b5830aba123dc6f17
|
7
|
+
data.tar.gz: 2f78631eb082dc6c2d52ccac04d057c11c05b212ab05ffaa197e4f5ebf80aafc23113691d1dc0653c6af5df2827d6a4b45af58d4bfac73aa0156e3e91fbdb063
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v3.0.0] 2017-10-11
|
4
|
+
- add && and || as aliases for AND and OR
|
5
|
+
- add hexadecimal literal support
|
6
|
+
- add the SWITCH function
|
7
|
+
- add AND and OR functions
|
8
|
+
- add array access
|
9
|
+
- make UnboundVariableError show all missing values
|
10
|
+
- cast inputs to numeric function to numeric
|
11
|
+
- fix issue with zero-arity functions used as function args
|
12
|
+
- fix division when context values contain one or more strings
|
13
|
+
- drop Ruby 1.9 support
|
14
|
+
|
3
15
|
## [v2.0.11] 2017-05-08
|
4
16
|
- fix dependency checking for logical AST nodes
|
5
17
|
- make `CONCAT` variadic
|
@@ -8,6 +20,12 @@
|
|
8
20
|
- add `&` (bitwise and) and `|` (bitwise or) operators
|
9
21
|
- fix incompatibility with 'mathn' module
|
10
22
|
- add `CONTAINS` string function
|
23
|
+
- allow storage of nested hashes in calculator memory
|
24
|
+
- allow duck type arithmetic
|
25
|
+
- fix error handling code to work with Ruby 2.4.0
|
26
|
+
- allow calculators to store own registry of functions
|
27
|
+
- add timezone support to time literals
|
28
|
+
- optimizations
|
11
29
|
|
12
30
|
## [v2.0.10] 2016-12-30
|
13
31
|
- fix string function initialization bug
|
@@ -125,6 +143,7 @@
|
|
125
143
|
## [v0.1.0] 2012-01-20
|
126
144
|
- initial release
|
127
145
|
|
146
|
+
[v3.0.0]: https://github.com/rubysolo/dentaku/compare/v2.0.11...v3.0.0
|
128
147
|
[v2.0.11]: https://github.com/rubysolo/dentaku/compare/v2.0.10...v2.0.11
|
129
148
|
[v2.0.10]: https://github.com/rubysolo/dentaku/compare/v2.0.9...v2.0.10
|
130
149
|
[v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
|
data/README.md
CHANGED
@@ -6,6 +6,7 @@ Dentaku
|
|
6
6
|
[](https://travis-ci.org/rubysolo/dentaku)
|
7
7
|
[](https://codeclimate.com/github/rubysolo/dentaku)
|
8
8
|
[](https://hakiri.io/github/rubysolo/dentaku)
|
9
|
+
[](https://coveralls.io/github/rubysolo/dentaku)
|
9
10
|
|
10
11
|
DESCRIPTION
|
11
12
|
-----------
|
@@ -129,9 +130,9 @@ Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
|
129
130
|
|
130
131
|
Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
|
131
132
|
|
132
|
-
Logic: `IF`, `AND`, `OR`, `NOT`
|
133
|
+
Logic: `IF`, `AND`, `OR`, `NOT`, `SWITCH`
|
133
134
|
|
134
|
-
|
135
|
+
Numeric: `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
|
135
136
|
|
136
137
|
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
|
137
138
|
|
data/dentaku.gemspec
CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.add_development_dependency('rake')
|
20
20
|
s.add_development_dependency('rspec')
|
21
21
|
s.add_development_dependency('pry')
|
22
|
+
s.add_development_dependency('coveralls')
|
22
23
|
|
23
24
|
s.files = `git ls-files`.split("\n")
|
24
25
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
data/lib/dentaku/ast.rb
CHANGED
@@ -10,15 +10,19 @@ require_relative './ast/bitwise'
|
|
10
10
|
require_relative './ast/negation'
|
11
11
|
require_relative './ast/comparators'
|
12
12
|
require_relative './ast/combinators'
|
13
|
+
require_relative './ast/access'
|
13
14
|
require_relative './ast/grouping'
|
14
15
|
require_relative './ast/case'
|
15
16
|
require_relative './ast/function_registry'
|
17
|
+
require_relative './ast/functions/and'
|
16
18
|
require_relative './ast/functions/if'
|
17
19
|
require_relative './ast/functions/max'
|
18
20
|
require_relative './ast/functions/min'
|
19
21
|
require_relative './ast/functions/not'
|
22
|
+
require_relative './ast/functions/or'
|
20
23
|
require_relative './ast/functions/round'
|
21
24
|
require_relative './ast/functions/roundup'
|
22
25
|
require_relative './ast/functions/rounddown'
|
23
26
|
require_relative './ast/functions/ruby_math'
|
24
27
|
require_relative './ast/functions/string_functions'
|
28
|
+
require_relative './ast/functions/switch'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class Access
|
4
|
+
def self.arity
|
5
|
+
2
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.peek(*)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(data_structure, index)
|
12
|
+
@structure = data_structure
|
13
|
+
@index = index
|
14
|
+
end
|
15
|
+
|
16
|
+
def value(context={})
|
17
|
+
structure = @structure.value(context)
|
18
|
+
index = @index.value(context)
|
19
|
+
structure[index]
|
20
|
+
end
|
21
|
+
|
22
|
+
def dependencies(context={})
|
23
|
+
@structure.dependencies(context) + @index.dependencies(context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -7,8 +7,14 @@ module Dentaku
|
|
7
7
|
class Arithmetic < Operation
|
8
8
|
def initialize(*)
|
9
9
|
super
|
10
|
-
|
11
|
-
|
10
|
+
|
11
|
+
unless valid_left?
|
12
|
+
raise NodeError.new(:numeric, left.type, :left),
|
13
|
+
"#{self.class} requires numeric operands"
|
14
|
+
end
|
15
|
+
unless valid_right?
|
16
|
+
raise NodeError.new(:numeric, right.type, :right),
|
17
|
+
"#{self.class} requires numeric operands"
|
12
18
|
end
|
13
19
|
end
|
14
20
|
|
@@ -17,7 +23,7 @@ module Dentaku
|
|
17
23
|
end
|
18
24
|
|
19
25
|
def operator
|
20
|
-
raise
|
26
|
+
raise NotImplementedError
|
21
27
|
end
|
22
28
|
|
23
29
|
def value(context={})
|
@@ -29,8 +35,7 @@ module Dentaku
|
|
29
35
|
private
|
30
36
|
|
31
37
|
def cast(val, prefer_integer=true)
|
32
|
-
|
33
|
-
validate_format(val) if val.is_a?(::String)
|
38
|
+
validate_value(val)
|
34
39
|
numeric(val, prefer_integer)
|
35
40
|
end
|
36
41
|
|
@@ -56,15 +61,25 @@ module Dentaku
|
|
56
61
|
valid_node?(right)
|
57
62
|
end
|
58
63
|
|
64
|
+
def validate_value(val)
|
65
|
+
if val.is_a?(::String)
|
66
|
+
validate_format(val)
|
67
|
+
else
|
68
|
+
validate_operation(val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
59
72
|
def validate_operation(val)
|
60
73
|
unless val.respond_to?(operator)
|
61
|
-
|
74
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
|
75
|
+
"#{ self.class } requires operands that respond to #{operator}"
|
62
76
|
end
|
63
77
|
end
|
64
78
|
|
65
79
|
def validate_format(string)
|
66
80
|
unless string =~ /\A-?\d+(\.\d+)?\z/
|
67
|
-
|
81
|
+
raise Dentaku::ArgumentError.for(:invalid_value, value: string, for: BigDecimal),
|
82
|
+
"String input '#{string}' is not coercible to numeric"
|
68
83
|
end
|
69
84
|
end
|
70
85
|
end
|
@@ -117,6 +132,33 @@ module Dentaku
|
|
117
132
|
end
|
118
133
|
|
119
134
|
class Modulo < Arithmetic
|
135
|
+
def self.arity
|
136
|
+
@arity
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.peek(input)
|
140
|
+
@arity = 1
|
141
|
+
@arity = 2 if input.length > 1
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize(left, right=nil)
|
145
|
+
if right
|
146
|
+
@left = left
|
147
|
+
@right = right
|
148
|
+
else
|
149
|
+
@right = left
|
150
|
+
end
|
151
|
+
|
152
|
+
unless valid_left?
|
153
|
+
raise NodeError.new(%i[numeric nil], left.type, :left),
|
154
|
+
"#{self.class} requires numeric operands or nil"
|
155
|
+
end
|
156
|
+
unless valid_right?
|
157
|
+
raise NodeError.new(:numeric, right.type, :right),
|
158
|
+
"#{self.class} requires numeric operands"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
120
162
|
def percent?
|
121
163
|
left.nil?
|
122
164
|
end
|
data/lib/dentaku/ast/case.rb
CHANGED
@@ -42,9 +42,23 @@ module Dentaku
|
|
42
42
|
|
43
43
|
def dependencies(context={})
|
44
44
|
# TODO: should short-circuit
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
switch_dependencies(context) +
|
46
|
+
condition_dependencies(context) +
|
47
|
+
else_dependencies(context)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def switch_dependencies(context={})
|
53
|
+
@switch.dependencies(context)
|
54
|
+
end
|
55
|
+
|
56
|
+
def condition_dependencies(context={})
|
57
|
+
@conditions.flat_map { |condition| condition.dependencies(context) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def else_dependencies(context={})
|
61
|
+
@else ? @else.dependencies(context) : []
|
48
62
|
end
|
49
63
|
end
|
50
64
|
end
|
@@ -5,8 +5,14 @@ module Dentaku
|
|
5
5
|
class Combinator < Operation
|
6
6
|
def initialize(*)
|
7
7
|
super
|
8
|
-
|
9
|
-
|
8
|
+
|
9
|
+
unless valid_node?(left)
|
10
|
+
raise NodeError.new(:logical, left.type, :left),
|
11
|
+
"#{self.class} requires logical operands"
|
12
|
+
end
|
13
|
+
unless valid_node?(right)
|
14
|
+
raise NodeError.new(:logical, right.type, :right),
|
15
|
+
"#{self.class} requires logical operands"
|
10
16
|
end
|
11
17
|
end
|
12
18
|
|
data/lib/dentaku/ast/function.rb
CHANGED
@@ -4,6 +4,9 @@ require_relative 'function_registry'
|
|
4
4
|
module Dentaku
|
5
5
|
module AST
|
6
6
|
class Function < Node
|
7
|
+
# @return [Integer] with the number of significant decimal digits to use.
|
8
|
+
DIG = Float::DIG + 1
|
9
|
+
|
7
10
|
def initialize(*args)
|
8
11
|
@args = args
|
9
12
|
end
|
@@ -27,6 +30,19 @@ module Dentaku
|
|
27
30
|
def self.registry
|
28
31
|
@registry ||= FunctionRegistry.new
|
29
32
|
end
|
33
|
+
|
34
|
+
# @return [Numeric] where possible it returns an Integer otherwise a BigDecimal.
|
35
|
+
# An Exception will be raised if a value is passed that cannot be cast to a Number.
|
36
|
+
def self.numeric(value)
|
37
|
+
return value if value.is_a?(::Numeric)
|
38
|
+
|
39
|
+
if value.is_a?(::String)
|
40
|
+
number = value[/\A-?\d*\.?\d+\z/]
|
41
|
+
return number.include?('.') ? ::BigDecimal.new(number, DIG) : number.to_i if number
|
42
|
+
end
|
43
|
+
|
44
|
+
raise TypeError, "#{value || value.class} could not be cast to a number."
|
45
|
+
end
|
30
46
|
end
|
31
47
|
end
|
32
48
|
end
|
@@ -5,11 +5,19 @@ module Dentaku
|
|
5
5
|
name = function_name(name)
|
6
6
|
return self[name] if has_key?(name)
|
7
7
|
return default[name] if default.has_key?(name)
|
8
|
-
|
8
|
+
nil
|
9
9
|
end
|
10
10
|
|
11
11
|
def register(name, type, implementation)
|
12
12
|
function = Class.new(Function) do
|
13
|
+
def self.name=(name)
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.name
|
18
|
+
@name
|
19
|
+
end
|
20
|
+
|
13
21
|
def self.implementation=(impl)
|
14
22
|
@implementation = impl
|
15
23
|
end
|
@@ -26,6 +34,10 @@ module Dentaku
|
|
26
34
|
@type
|
27
35
|
end
|
28
36
|
|
37
|
+
def self.arity
|
38
|
+
@implementation.arity < 0 ? nil : @implementation.arity
|
39
|
+
end
|
40
|
+
|
29
41
|
def value(context={})
|
30
42
|
args = @args.map { |a| a.value(context) }
|
31
43
|
self.class.implementation.call(*args)
|
@@ -36,8 +48,9 @@ module Dentaku
|
|
36
48
|
end
|
37
49
|
end
|
38
50
|
|
39
|
-
function.
|
51
|
+
function.name = name
|
40
52
|
function.type = type
|
53
|
+
function.implementation = implementation
|
41
54
|
|
42
55
|
self[function_name(name)] = function
|
43
56
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
require_relative '../../exceptions'
|
3
|
+
|
4
|
+
Dentaku::AST::Function.register(:and, :logical, lambda { |*args|
|
5
|
+
if args.empty?
|
6
|
+
raise Dentaku::ArgumentError.for(
|
7
|
+
:too_few_arguments,
|
8
|
+
function_name: 'AND()', at_least: 1, given: 0
|
9
|
+
), 'AND() requires at least one argument'
|
10
|
+
end
|
11
|
+
|
12
|
+
args.all? do |arg|
|
13
|
+
case arg
|
14
|
+
when TrueClass, nil
|
15
|
+
true
|
16
|
+
when FalseClass
|
17
|
+
false
|
18
|
+
else
|
19
|
+
raise Dentaku::ArgumentError.for(
|
20
|
+
:incompatible_type,
|
21
|
+
function_name: 'AND()', expect: :logical, actual: arg.class
|
22
|
+
), 'AND() requires arguments to be logical expressions'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
})
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
require_relative '../../exceptions'
|
3
|
+
|
4
|
+
Dentaku::AST::Function.register(:or, :logical, lambda { |*args|
|
5
|
+
if args.empty?
|
6
|
+
raise Dentaku::ArgumentError.for(
|
7
|
+
:too_few_arguments,
|
8
|
+
function_name: 'OR()', at_least: 1, given: 0
|
9
|
+
), 'OR() requires at least one argument'
|
10
|
+
end
|
11
|
+
|
12
|
+
args.any? do |arg|
|
13
|
+
case arg
|
14
|
+
when TrueClass, nil
|
15
|
+
true
|
16
|
+
when FalseClass
|
17
|
+
false
|
18
|
+
else
|
19
|
+
raise Dentaku::ArgumentError.for(
|
20
|
+
:incompatible_type,
|
21
|
+
function_name: 'AND()', expect: :logical, actual: arg.class
|
22
|
+
), 'AND() requires arguments to be logical expressions'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
})
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require_relative '../function'
|
2
2
|
|
3
|
-
Dentaku::AST::Function.register(:round, :numeric,
|
4
|
-
numeric.round(places
|
3
|
+
Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
|
4
|
+
Dentaku::AST::Function.numeric(numeric).round(places.to_i)
|
5
5
|
})
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require_relative '../function'
|
2
2
|
|
3
|
-
Dentaku::AST::Function.register(:rounddown, :numeric,
|
3
|
+
Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
|
4
|
+
precision = precision.to_i
|
4
5
|
tens = 10.0**precision
|
5
|
-
result = (numeric * tens).floor / tens
|
6
|
+
result = (Dentaku::AST::Function.numeric(numeric) * tens).floor / tens
|
6
7
|
precision <= 0 ? result.to_i : result
|
7
8
|
})
|