hayadentaku 3.5.7
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 +7 -0
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/Rakefile +31 -0
- data/hayadentaku.gemspec +35 -0
- data/lib/dentaku/ast/access.rb +44 -0
- data/lib/dentaku/ast/arithmetic.rb +292 -0
- data/lib/dentaku/ast/array.rb +38 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +93 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +88 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +107 -0
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/all.rb +19 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +19 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +54 -0
- data/lib/dentaku/ast/functions/filter.rb +21 -0
- data/lib/dentaku/ast/functions/if.rb +47 -0
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +19 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +34 -0
- data/lib/dentaku/ast/functions/reduce.rb +60 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +29 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +44 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +42 -0
- data/lib/dentaku/bulk_expression_solver.rb +158 -0
- data/lib/dentaku/calculator.rb +192 -0
- data/lib/dentaku/date_arithmetic.rb +60 -0
- data/lib/dentaku/dependency_resolver.rb +29 -0
- data/lib/dentaku/exceptions.rb +116 -0
- data/lib/dentaku/flat_hash.rb +161 -0
- data/lib/dentaku/parser.rb +318 -0
- data/lib/dentaku/print_visitor.rb +112 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +48 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +240 -0
- data/lib/dentaku/tokenizer.rb +127 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +86 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +67 -0
- data/spec/ast/all_spec.rb +38 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +36 -0
- data/spec/ast/arithmetic_spec.rb +147 -0
- data/spec/ast/avg_spec.rb +42 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +64 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +40 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +49 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +241 -0
- data/spec/calculator_spec.rb +1003 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +177 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/print_visitor_spec.rb +77 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +375 -0
- data/spec/visitor/infix_spec.rb +52 -0
- data/spec/visitor_spec.rb +139 -0
- metadata +353 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require_relative './operation'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Comparator < Operation
|
|
6
|
+
def self.precedence
|
|
7
|
+
5
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def type
|
|
11
|
+
:logical
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def operator
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def value(context = {})
|
|
19
|
+
l = validate_value(cast(left.value(context)))
|
|
20
|
+
r = validate_value(cast(right.value(context)))
|
|
21
|
+
|
|
22
|
+
l.public_send(operator, r)
|
|
23
|
+
rescue ::ArgumentError, ::TypeError => e
|
|
24
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def cast(val)
|
|
30
|
+
return val unless val.is_a?(::String)
|
|
31
|
+
return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
|
|
32
|
+
|
|
33
|
+
v = BigDecimal(val, Float::DIG + 1)
|
|
34
|
+
v = v.to_i if v.frac.zero?
|
|
35
|
+
v
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_value(value)
|
|
39
|
+
unless value.respond_to?(operator)
|
|
40
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
|
|
41
|
+
"#{ self.class } requires operands that respond to #{operator}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class LessThan < Comparator
|
|
49
|
+
def operator
|
|
50
|
+
:<
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class LessThanOrEqual < Comparator
|
|
55
|
+
def operator
|
|
56
|
+
:<=
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class GreaterThan < Comparator
|
|
61
|
+
def operator
|
|
62
|
+
:>
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class GreaterThanOrEqual < Comparator
|
|
67
|
+
def operator
|
|
68
|
+
:>=
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class NotEqual < Comparator
|
|
73
|
+
def operator
|
|
74
|
+
:!=
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Equal < Comparator
|
|
79
|
+
def operator
|
|
80
|
+
:==
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def display_operator
|
|
84
|
+
"="
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require_relative 'node'
|
|
2
|
+
require_relative 'function_registry'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Function < Node
|
|
7
|
+
attr_reader :args
|
|
8
|
+
|
|
9
|
+
# @return [Integer] with the number of significant decimal digits to use.
|
|
10
|
+
DIG = Float::DIG + 1
|
|
11
|
+
|
|
12
|
+
def initialize(*args)
|
|
13
|
+
@args = args
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def accept(visitor)
|
|
17
|
+
visitor.visit_function(self)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dependencies(context = {})
|
|
21
|
+
@args.each_with_index
|
|
22
|
+
.flat_map { |a, _| a.dependencies(context) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.get(name)
|
|
26
|
+
registry.get(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.register(name, type, implementation)
|
|
30
|
+
registry.register(name, type, implementation)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.register_class(name, function_class)
|
|
34
|
+
registry.register_class(name, function_class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.registry
|
|
38
|
+
@registry ||= FunctionRegistry.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Numeric] where possible it returns an Integer otherwise a BigDecimal.
|
|
42
|
+
# An Exception will be raised if a value is passed that cannot be cast to a Number.
|
|
43
|
+
def self.numeric(value)
|
|
44
|
+
return value if value.is_a?(::Numeric)
|
|
45
|
+
|
|
46
|
+
if value.is_a?(::String)
|
|
47
|
+
number = value[/\A-?\d*\.?\d+\z/]
|
|
48
|
+
return number.include?('.') ? BigDecimal(number, DIG) : number.to_i if number
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
|
|
52
|
+
"'#{value || value.class}' is not coercible to numeric"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
module AST
|
|
3
|
+
class FunctionRegistry < Hash
|
|
4
|
+
def get(name)
|
|
5
|
+
name = function_name(name)
|
|
6
|
+
return self[name] if has_key?(name)
|
|
7
|
+
return default[name] if default.has_key?(name)
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(name, type, implementation, callback = nil)
|
|
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
|
+
|
|
21
|
+
def self.implementation=(impl)
|
|
22
|
+
@implementation = impl
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.implementation
|
|
26
|
+
@implementation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.type=(type)
|
|
30
|
+
@type = type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.type
|
|
34
|
+
@type
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.callback=(callback)
|
|
38
|
+
@callback = callback
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.callback
|
|
42
|
+
@callback
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.arity
|
|
46
|
+
@implementation.arity < 0 ? nil : @implementation.arity
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.min_param_count
|
|
50
|
+
@implementation.parameters.select { |type, _name| type == :req }.count
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.max_param_count
|
|
54
|
+
@implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def value(context = {})
|
|
58
|
+
args = @args.map { |a| a.value(context) }
|
|
59
|
+
self.class.implementation.call(*args)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def type
|
|
63
|
+
self.class.type
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
define_class(name, function)
|
|
68
|
+
|
|
69
|
+
function.name = name
|
|
70
|
+
function.type = type
|
|
71
|
+
function.implementation = implementation
|
|
72
|
+
function.callback = callback
|
|
73
|
+
|
|
74
|
+
self[function_name(name)] = function
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_class(name, function_class)
|
|
78
|
+
self[function_name(name)] = function_class
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default
|
|
82
|
+
self.class.default
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.default
|
|
86
|
+
Dentaku::AST::Function.registry
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def function_name(name)
|
|
92
|
+
name.to_s.downcase
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def normalize_name(function_name)
|
|
96
|
+
function_name.to_s.capitalize.gsub(/\W/, '_')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def define_class(function_name, function)
|
|
100
|
+
class_name = normalize_name(function_name)
|
|
101
|
+
return if Dentaku::AST::Function.const_defined?(class_name)
|
|
102
|
+
|
|
103
|
+
Dentaku::AST::Function.const_set(class_name, function)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative './enum'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class All < Enum
|
|
6
|
+
def value(context = {})
|
|
7
|
+
collection = Array(@args[0].value(context))
|
|
8
|
+
item_identifier = @args[1].identifier
|
|
9
|
+
expression = @args[2]
|
|
10
|
+
|
|
11
|
+
collection.all? do |item_value|
|
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Dentaku::AST::Function.register_class(:all, Dentaku::AST::All)
|
|
@@ -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
|
|
15
|
+
true
|
|
16
|
+
when FalseClass, nil
|
|
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,19 @@
|
|
|
1
|
+
require_relative './enum'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Any < Enum
|
|
6
|
+
def value(context = {})
|
|
7
|
+
collection = Array(@args[0].value(context))
|
|
8
|
+
item_identifier = @args[1].identifier
|
|
9
|
+
expression = @args[2]
|
|
10
|
+
|
|
11
|
+
collection.any? do |item_value|
|
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Dentaku::AST::Function.register_class(:any, Dentaku::AST::Any)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
|
|
4
|
+
flatten_args = args.flatten
|
|
5
|
+
if flatten_args.empty?
|
|
6
|
+
raise Dentaku::ArgumentError.for(
|
|
7
|
+
:too_few_arguments,
|
|
8
|
+
function_name: 'AVG()', at_least: 1, given: 0
|
|
9
|
+
), 'AVG() requires at least one argument'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
|
|
13
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Count < Function
|
|
6
|
+
def self.min_param_count
|
|
7
|
+
0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.max_param_count
|
|
11
|
+
Float::INFINITY
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def value(context = {})
|
|
15
|
+
if @args.length == 1
|
|
16
|
+
first_arg = @args[0].value(context)
|
|
17
|
+
return first_arg.length if first_arg.respond_to?(:length)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@args.length
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Dentaku::AST::Function.register_class(:count, Dentaku::AST::Count)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Duration < Function
|
|
6
|
+
def self.min_param_count
|
|
7
|
+
2
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.max_param_count
|
|
11
|
+
2
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Value
|
|
15
|
+
attr_reader :value, :unit
|
|
16
|
+
|
|
17
|
+
def initialize(value, unit)
|
|
18
|
+
@value = value
|
|
19
|
+
@unit = validate_unit(unit)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_unit(unit)
|
|
23
|
+
case unit.downcase
|
|
24
|
+
when /years?/ then :year
|
|
25
|
+
when /months?/ then :month
|
|
26
|
+
when /days?/ then :day
|
|
27
|
+
else
|
|
28
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, value: unit, for: Duration),
|
|
29
|
+
"'#{unit || unit.class}' is not a valid duration unit"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def type
|
|
35
|
+
:duration
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def value(context = {})
|
|
39
|
+
value_node, unit_node = *@args
|
|
40
|
+
Value.new(value_node.value(context), unit_node.identifier)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dependencies(context = {})
|
|
44
|
+
value_node = @args.first
|
|
45
|
+
value_node.dependencies(context)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Dentaku::AST::Function.register_class(:duration, Dentaku::AST::Duration)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
require_relative '../../exceptions'
|
|
3
|
+
|
|
4
|
+
module Dentaku
|
|
5
|
+
module AST
|
|
6
|
+
class Enum < Function
|
|
7
|
+
def self.min_param_count
|
|
8
|
+
3
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.max_param_count
|
|
12
|
+
3
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(*args)
|
|
16
|
+
super
|
|
17
|
+
validate_identifier(@args[1])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dependencies(context = {})
|
|
21
|
+
collection = @args[0]
|
|
22
|
+
item_identifier = @args[1].identifier
|
|
23
|
+
expression = @args[2]
|
|
24
|
+
|
|
25
|
+
collection_deps = collection.dependencies(context)
|
|
26
|
+
expression_deps = (expression&.dependencies(context) || []).reject do |i|
|
|
27
|
+
i == item_identifier || i.start_with?("#{item_identifier}.")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
collection_deps + expression_deps
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
|
|
34
|
+
raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def mapped_value(expression, context, item_context)
|
|
40
|
+
# Avoid the `context.merge(FlatHash.from_hash_with_intermediates(...))`
|
|
41
|
+
# double allocation per iteration: dup the context once and write the
|
|
42
|
+
# flattened keys directly into it.
|
|
43
|
+
scratch = context.dup
|
|
44
|
+
item_context.each do |k, v|
|
|
45
|
+
FlatHash.write_pair_with_intermediates!(k, v, scratch)
|
|
46
|
+
end
|
|
47
|
+
expression.value(scratch)
|
|
48
|
+
rescue => e
|
|
49
|
+
raise e if context["__evaluation_mode"] == :strict
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative './enum'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Filter < Enum
|
|
6
|
+
def value(context = {})
|
|
7
|
+
collection = Array(@args[0].value(context))
|
|
8
|
+
item_identifier = @args[1].identifier
|
|
9
|
+
expression = @args[2]
|
|
10
|
+
|
|
11
|
+
collection.select do |item_value|
|
|
12
|
+
scratch = context.dup
|
|
13
|
+
FlatHash.write_pair_with_intermediates!(item_identifier, item_value, scratch)
|
|
14
|
+
expression.value(scratch)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Dentaku::AST::Function.register_class(:filter, Dentaku::AST::Filter)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class If < Function
|
|
6
|
+
attr_reader :predicate, :left, :right
|
|
7
|
+
|
|
8
|
+
def self.min_param_count
|
|
9
|
+
3
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.max_param_count
|
|
13
|
+
3
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(predicate, left, right)
|
|
17
|
+
@predicate = predicate
|
|
18
|
+
@left = left
|
|
19
|
+
@right = right
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def args
|
|
23
|
+
[predicate, left, right]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def value(context = {})
|
|
27
|
+
predicate.value(context) ? left.value(context) : right.value(context)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def node_type
|
|
31
|
+
:condition
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def type
|
|
35
|
+
left.type
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dependencies(context = {})
|
|
39
|
+
predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
|
|
40
|
+
rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError
|
|
41
|
+
args.flat_map { |arg| arg.dependencies(context) }.uniq
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Dentaku::AST::Function.register_class(:if, Dentaku::AST::If)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
|
|
4
|
+
if args.length != 2
|
|
5
|
+
raise Dentaku::ArgumentError.for(
|
|
6
|
+
:wrong_number_of_arguments,
|
|
7
|
+
function_name: 'INTERCEPT()', exact: 2, given: args.length
|
|
8
|
+
), 'INTERCEPT() requires exactly two arrays of numbers'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
x_values, y_values = args
|
|
12
|
+
if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
|
|
13
|
+
raise Dentaku::ArgumentError.for(
|
|
14
|
+
:invalid_value,
|
|
15
|
+
function_name: 'INTERCEPT()'
|
|
16
|
+
), 'INTERCEPT() requires arrays of equal length'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
n = x_values.length.to_f
|
|
20
|
+
x_values = x_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
|
|
21
|
+
y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
|
|
22
|
+
|
|
23
|
+
x_avg = x_values.sum / n
|
|
24
|
+
y_avg = y_values.sum / n
|
|
25
|
+
|
|
26
|
+
xy_sum = x_values.zip(y_values).map { |x, y| (x_avg - x) * (y_avg - y) }.sum
|
|
27
|
+
x_square_sum = x_values.map { |x| (x_avg - x)**2 }.sum
|
|
28
|
+
|
|
29
|
+
slope = xy_sum / x_square_sum
|
|
30
|
+
intercept = x_values.zip(y_values).map { |x, y| y - slope * x }.sum / n
|
|
31
|
+
|
|
32
|
+
BigDecimal(intercept, Float::DIG + 1)
|
|
33
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative './enum'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
module AST
|
|
5
|
+
class Map < Enum
|
|
6
|
+
def value(context = {})
|
|
7
|
+
collection = Array(@args[0].value(context))
|
|
8
|
+
item_identifier = @args[1].identifier
|
|
9
|
+
expression = @args[2]
|
|
10
|
+
|
|
11
|
+
collection.map do |item_value|
|
|
12
|
+
mapped_value(expression, context, item_identifier => item_value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Dentaku::AST::Function.register_class(:map, Dentaku::AST::Map)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require_relative '../function'
|
|
2
|
+
|
|
3
|
+
Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
|
|
4
|
+
if args.empty?
|
|
5
|
+
raise Dentaku::ArgumentError.for(
|
|
6
|
+
:too_few_arguments,
|
|
7
|
+
function_name: 'MUL()', at_least: 1, given: 0
|
|
8
|
+
), 'MUL() requires at least one argument'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
|
|
12
|
+
})
|
|
@@ -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
|
|
15
|
+
true
|
|
16
|
+
when FalseClass, nil
|
|
17
|
+
false
|
|
18
|
+
else
|
|
19
|
+
raise Dentaku::ArgumentError.for(
|
|
20
|
+
:incompatible_type,
|
|
21
|
+
function_name: 'OR()', expect: :logical, actual: arg.class
|
|
22
|
+
), 'OR() requires arguments to be logical expressions'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
})
|