dentaku 2.0.11 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
|
7
7
|
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
|
8
8
|
[![Hakiri](https://hakiri.io/github/rubysolo/dentaku/master.svg)](https://hakiri.io/github/rubysolo/dentaku)
|
9
|
+
[![Coverage Status](https://coveralls.io/repos/github/rubysolo/dentaku/badge.svg)](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
|
})
|