dentaku 3.2.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +4 -6
  4. data/CHANGELOG.md +86 -2
  5. data/README.md +7 -6
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +21 -1
  8. data/lib/dentaku/ast/arithmetic.rb +51 -15
  9. data/lib/dentaku/ast/array.rb +41 -0
  10. data/lib/dentaku/ast/bitwise.rb +30 -5
  11. data/lib/dentaku/ast/case/case_conditional.rb +17 -2
  12. data/lib/dentaku/ast/case/case_else.rb +17 -3
  13. data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
  14. data/lib/dentaku/ast/case/case_then.rb +17 -3
  15. data/lib/dentaku/ast/case/case_when.rb +21 -3
  16. data/lib/dentaku/ast/case.rb +19 -3
  17. data/lib/dentaku/ast/comparators.rb +38 -28
  18. data/lib/dentaku/ast/function.rb +11 -3
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +23 -0
  21. data/lib/dentaku/ast/functions/and.rb +2 -2
  22. data/lib/dentaku/ast/functions/any.rb +23 -0
  23. data/lib/dentaku/ast/functions/avg.rb +2 -2
  24. data/lib/dentaku/ast/functions/count.rb +8 -0
  25. data/lib/dentaku/ast/functions/duration.rb +51 -0
  26. data/lib/dentaku/ast/functions/enum.rb +37 -0
  27. data/lib/dentaku/ast/functions/filter.rb +23 -0
  28. data/lib/dentaku/ast/functions/if.rb +19 -2
  29. data/lib/dentaku/ast/functions/map.rb +23 -0
  30. data/lib/dentaku/ast/functions/or.rb +4 -4
  31. data/lib/dentaku/ast/functions/pluck.rb +30 -0
  32. data/lib/dentaku/ast/functions/round.rb +1 -1
  33. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  34. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  35. data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
  36. data/lib/dentaku/ast/functions/string_functions.rb +105 -12
  37. data/lib/dentaku/ast/functions/xor.rb +44 -0
  38. data/lib/dentaku/ast/grouping.rb +3 -1
  39. data/lib/dentaku/ast/identifier.rb +16 -4
  40. data/lib/dentaku/ast/literal.rb +10 -0
  41. data/lib/dentaku/ast/negation.rb +7 -1
  42. data/lib/dentaku/ast/nil.rb +4 -0
  43. data/lib/dentaku/ast/node.rb +8 -0
  44. data/lib/dentaku/ast/operation.rb +17 -0
  45. data/lib/dentaku/ast/string.rb +7 -0
  46. data/lib/dentaku/ast.rb +8 -0
  47. data/lib/dentaku/bulk_expression_solver.rb +38 -27
  48. data/lib/dentaku/calculator.rb +21 -8
  49. data/lib/dentaku/date_arithmetic.rb +45 -0
  50. data/lib/dentaku/exceptions.rb +11 -8
  51. data/lib/dentaku/flat_hash.rb +9 -2
  52. data/lib/dentaku/parser.rb +57 -16
  53. data/lib/dentaku/print_visitor.rb +101 -0
  54. data/lib/dentaku/token_matcher.rb +1 -1
  55. data/lib/dentaku/token_scanner.rb +9 -3
  56. data/lib/dentaku/tokenizer.rb +7 -2
  57. data/lib/dentaku/version.rb +1 -1
  58. data/lib/dentaku/visitor/infix.rb +82 -0
  59. data/lib/dentaku.rb +20 -7
  60. data/spec/ast/addition_spec.rb +7 -1
  61. data/spec/ast/all_spec.rb +25 -0
  62. data/spec/ast/and_function_spec.rb +6 -6
  63. data/spec/ast/and_spec.rb +1 -1
  64. data/spec/ast/any_spec.rb +23 -0
  65. data/spec/ast/arithmetic_spec.rb +64 -29
  66. data/spec/ast/avg_spec.rb +9 -5
  67. data/spec/ast/comparator_spec.rb +31 -1
  68. data/spec/ast/count_spec.rb +7 -7
  69. data/spec/ast/division_spec.rb +7 -1
  70. data/spec/ast/filter_spec.rb +25 -0
  71. data/spec/ast/function_spec.rb +20 -15
  72. data/spec/ast/map_spec.rb +27 -0
  73. data/spec/ast/max_spec.rb +16 -3
  74. data/spec/ast/min_spec.rb +16 -3
  75. data/spec/ast/mul_spec.rb +11 -6
  76. data/spec/ast/negation_spec.rb +48 -0
  77. data/spec/ast/node_spec.rb +11 -8
  78. data/spec/ast/numeric_spec.rb +1 -1
  79. data/spec/ast/or_spec.rb +7 -7
  80. data/spec/ast/pluck_spec.rb +32 -0
  81. data/spec/ast/round_spec.rb +14 -4
  82. data/spec/ast/rounddown_spec.rb +14 -4
  83. data/spec/ast/roundup_spec.rb +14 -4
  84. data/spec/ast/string_functions_spec.rb +73 -0
  85. data/spec/ast/sum_spec.rb +11 -6
  86. data/spec/ast/switch_spec.rb +5 -5
  87. data/spec/ast/xor_spec.rb +35 -0
  88. data/spec/bulk_expression_solver_spec.rb +37 -1
  89. data/spec/calculator_spec.rb +341 -32
  90. data/spec/dentaku_spec.rb +19 -6
  91. data/spec/external_function_spec.rb +32 -6
  92. data/spec/parser_spec.rb +100 -123
  93. data/spec/print_visitor_spec.rb +66 -0
  94. data/spec/spec_helper.rb +6 -4
  95. data/spec/token_matcher_spec.rb +8 -8
  96. data/spec/token_scanner_spec.rb +4 -4
  97. data/spec/tokenizer_spec.rb +56 -13
  98. data/spec/visitor/infix_spec.rb +31 -0
  99. data/spec/visitor_spec.rb +138 -0
  100. metadata +52 -7
@@ -3,24 +3,36 @@ require_relative './case/case_when'
3
3
  require_relative './case/case_then'
4
4
  require_relative './case/case_switch_variable'
5
5
  require_relative './case/case_else'
6
+ require 'dentaku/exceptions'
6
7
 
7
8
  module Dentaku
8
9
  module AST
9
10
  class Case < Node
11
+ attr_reader :switch, :conditions, :else
12
+
13
+ def self.min_param_count
14
+ 2
15
+ end
16
+
17
+ def self.max_param_count
18
+ Float::INFINITY
19
+ end
20
+
10
21
  def initialize(*nodes)
11
22
  @switch = nodes.shift
12
23
 
13
24
  unless @switch.is_a?(AST::CaseSwitchVariable)
14
- raise 'Case missing switch variable'
25
+ raise ParseError.for(:node_invalid), 'Case missing switch variable'
15
26
  end
16
27
 
17
28
  @conditions = nodes
18
29
 
30
+ @else = nil
19
31
  @else = @conditions.pop if @conditions.last.is_a?(AST::CaseElse)
20
32
 
21
33
  @conditions.each do |condition|
22
34
  unless condition.is_a?(AST::CaseConditional)
23
- raise "#{condition} is not a CaseConditional"
35
+ raise ParseError.for(:node_invalid), "#{condition} is not a CaseConditional"
24
36
  end
25
37
  end
26
38
  end
@@ -36,7 +48,7 @@ module Dentaku
36
48
  if @else
37
49
  return @else.value(context)
38
50
  else
39
- raise "No block matched the switch value '#{switch_value}'"
51
+ raise ArgumentError.for(:invalid_value), "No block matched the switch value '#{switch_value}'"
40
52
  end
41
53
  end
42
54
 
@@ -47,6 +59,10 @@ module Dentaku
47
59
  else_dependencies(context)
48
60
  end
49
61
 
62
+ def accept(visitor)
63
+ visitor.visit_case(self)
64
+ end
65
+
50
66
  private
51
67
 
52
68
  def switch_dependencies(context = {})
@@ -14,65 +14,75 @@ module Dentaku
14
14
  def operator
15
15
  raise NotImplementedError
16
16
  end
17
- end
18
17
 
19
- class LessThan < Comparator
20
18
  def value(context = {})
21
- left.value(context) < right.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 => e
24
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
22
25
  end
23
26
 
24
- def operator
25
- return :<
27
+ private
28
+
29
+ def cast(val)
30
+ return val unless val.is_a?(::String)
31
+ return val if val.empty?
32
+ return val unless val.match?(/\A-?\d*(\.\d+)?\z/)
33
+
34
+ v = BigDecimal(val, Float::DIG + 1)
35
+ v = v.to_i if v.frac.zero?
36
+ v
26
37
  end
27
- end
28
38
 
29
- class LessThanOrEqual < Comparator
30
- def value(context = {})
31
- left.value(context) <= right.value(context)
39
+ def validate_value(value)
40
+ unless value.respond_to?(operator)
41
+ raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
42
+ "#{ self.class } requires operands that respond to #{operator}"
43
+ end
44
+
45
+ value
32
46
  end
47
+ end
33
48
 
49
+ class LessThan < Comparator
34
50
  def operator
35
- return :<=
51
+ :<
36
52
  end
37
53
  end
38
54
 
39
- class GreaterThan < Comparator
40
- def value(context = {})
41
- left.value(context) > right.value(context)
55
+ class LessThanOrEqual < Comparator
56
+ def operator
57
+ :<=
42
58
  end
59
+ end
43
60
 
61
+ class GreaterThan < Comparator
44
62
  def operator
45
- return :>
63
+ :>
46
64
  end
47
65
  end
48
66
 
49
67
  class GreaterThanOrEqual < Comparator
50
- def value(context = {})
51
- left.value(context) >= right.value(context)
52
- end
53
-
54
68
  def operator
55
- return :>=
69
+ :>=
56
70
  end
57
71
  end
58
72
 
59
73
  class NotEqual < Comparator
60
- def value(context = {})
61
- left.value(context) != right.value(context)
62
- end
63
-
64
74
  def operator
65
- return :!=
75
+ :!=
66
76
  end
67
77
  end
68
78
 
69
79
  class Equal < Comparator
70
- def value(context = {})
71
- left.value(context) == right.value(context)
80
+ def operator
81
+ :==
72
82
  end
73
83
 
74
- def operator
75
- return :==
84
+ def display_operator
85
+ "="
76
86
  end
77
87
  end
78
88
  end
@@ -4,6 +4,8 @@ require_relative 'function_registry'
4
4
  module Dentaku
5
5
  module AST
6
6
  class Function < Node
7
+ attr_reader :args
8
+
7
9
  # @return [Integer] with the number of significant decimal digits to use.
8
10
  DIG = Float::DIG + 1
9
11
 
@@ -11,8 +13,13 @@ module Dentaku
11
13
  @args = args
12
14
  end
13
15
 
16
+ def accept(visitor)
17
+ visitor.visit_function(self)
18
+ end
19
+
14
20
  def dependencies(context = {})
15
- @args.flat_map { |a| a.dependencies(context) }
21
+ @args.each_with_index
22
+ .flat_map { |a, _| a.dependencies(context) }
16
23
  end
17
24
 
18
25
  def self.get(name)
@@ -38,10 +45,11 @@ module Dentaku
38
45
 
39
46
  if value.is_a?(::String)
40
47
  number = value[/\A-?\d*\.?\d+\z/]
41
- return number.include?('.') ? ::BigDecimal.new(number, DIG) : number.to_i if number
48
+ return number.include?('.') ? BigDecimal(number, DIG) : number.to_i if number
42
49
  end
43
50
 
44
- raise TypeError, "#{value || value.class} could not be cast to a number."
51
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
52
+ "'#{value || value.class}' is not coercible to numeric"
45
53
  end
46
54
  end
47
55
  end
@@ -38,6 +38,14 @@ module Dentaku
38
38
  @implementation.arity < 0 ? nil : @implementation.arity
39
39
  end
40
40
 
41
+ def self.min_param_count
42
+ @implementation.parameters.select { |type, _name| type == :req }.count
43
+ end
44
+
45
+ def self.max_param_count
46
+ @implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
47
+ end
48
+
41
49
  def value(context = {})
42
50
  args = @args.map { |a| a.value(context) }
43
51
  self.class.implementation.call(*args)
@@ -48,6 +56,8 @@ module Dentaku
48
56
  end
49
57
  end
50
58
 
59
+ define_class(name, function)
60
+
51
61
  function.name = name
52
62
  function.type = type
53
63
  function.implementation = implementation
@@ -72,6 +82,17 @@ module Dentaku
72
82
  def function_name(name)
73
83
  name.to_s.downcase
74
84
  end
85
+
86
+ def normalize_name(function_name)
87
+ function_name.to_s.capitalize.gsub(/\W/, '_')
88
+ end
89
+
90
+ def define_class(function_name, function)
91
+ class_name = normalize_name(function_name)
92
+ return if Dentaku::AST::Function.const_defined?(class_name)
93
+
94
+ Dentaku::AST::Function.const_set(class_name, function)
95
+ end
75
96
  end
76
97
  end
77
98
  end
@@ -0,0 +1,23 @@
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
+ expression.value(
13
+ context.merge(
14
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
+ )
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Dentaku::AST::Function.register_class(:all, Dentaku::AST::All)
@@ -11,9 +11,9 @@ Dentaku::AST::Function.register(:and, :logical, lambda { |*args|
11
11
 
12
12
  args.all? do |arg|
13
13
  case arg
14
- when TrueClass, nil
14
+ when TrueClass
15
15
  true
16
- when FalseClass
16
+ when FalseClass, nil
17
17
  false
18
18
  else
19
19
  raise Dentaku::ArgumentError.for(
@@ -0,0 +1,23 @@
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
+ expression.value(
13
+ context.merge(
14
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
+ )
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Dentaku::AST::Function.register_class(:any, Dentaku::AST::Any)
@@ -1,13 +1,13 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
4
- if args.empty?
4
+ flatten_args = args.flatten
5
+ if flatten_args.empty?
5
6
  raise Dentaku::ArgumentError.for(
6
7
  :too_few_arguments,
7
8
  function_name: 'AVG()', at_least: 1, given: 0
8
9
  ), 'AVG() requires at least one argument'
9
10
  end
10
11
 
11
- flatten_args = args.flatten
12
12
  flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
13
13
  })
@@ -3,6 +3,14 @@ require_relative '../function'
3
3
  module Dentaku
4
4
  module AST
5
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
+
6
14
  def value(context = {})
7
15
  if @args.length == 1
8
16
  first_arg = @args[0].value(context)
@@ -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,37 @@
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 dependencies(context = {})
16
+ validate_identifier(@args[1])
17
+
18
+ collection = @args[0]
19
+ item_identifier = @args[1].identifier
20
+ expression = @args[2]
21
+
22
+ collection_deps = collection.dependencies(context)
23
+ expression_deps = (expression&.dependencies(context) || []).reject do |i|
24
+ i == item_identifier || i.start_with?("#{item_identifier}.")
25
+ end
26
+
27
+ collection_deps + expression_deps
28
+ end
29
+
30
+ def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
31
+ unless arg.is_a?(Identifier)
32
+ raise ArgumentError.for(:incompatible_type, value: arg, for: Identifier), message
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
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
+ expression.value(
13
+ context.merge(
14
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
+ )
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Dentaku::AST::Function.register_class(:filter, Dentaku::AST::Filter)
@@ -5,12 +5,24 @@ module Dentaku
5
5
  class If < Function
6
6
  attr_reader :predicate, :left, :right
7
7
 
8
+ def self.min_param_count
9
+ 3
10
+ end
11
+
12
+ def self.max_param_count
13
+ 3
14
+ end
15
+
8
16
  def initialize(predicate, left, right)
9
17
  @predicate = predicate
10
18
  @left = left
11
19
  @right = right
12
20
  end
13
21
 
22
+ def args
23
+ [predicate, left, right]
24
+ end
25
+
14
26
  def value(context = {})
15
27
  predicate.value(context) ? left.value(context) : right.value(context)
16
28
  end
@@ -24,8 +36,13 @@ module Dentaku
24
36
  end
25
37
 
26
38
  def dependencies(context = {})
27
- # TODO : short-circuit?
28
- (predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
39
+ deps = predicate.dependencies(context)
40
+
41
+ if deps.empty?
42
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
43
+ else
44
+ (deps + left.dependencies(context) + right.dependencies(context)).uniq
45
+ end
29
46
  end
30
47
  end
31
48
  end
@@ -0,0 +1,23 @@
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
+ expression.value(
13
+ context.merge(
14
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
+ )
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Dentaku::AST::Function.register_class(:map, Dentaku::AST::Map)
@@ -11,15 +11,15 @@ Dentaku::AST::Function.register(:or, :logical, lambda { |*args|
11
11
 
12
12
  args.any? do |arg|
13
13
  case arg
14
- when TrueClass, nil
14
+ when TrueClass
15
15
  true
16
- when FalseClass
16
+ when FalseClass, nil
17
17
  false
18
18
  else
19
19
  raise Dentaku::ArgumentError.for(
20
20
  :incompatible_type,
21
- function_name: 'AND()', expect: :logical, actual: arg.class
22
- ), 'AND() requires arguments to be logical expressions'
21
+ function_name: 'OR()', expect: :logical, actual: arg.class
22
+ ), 'OR() requires arguments to be logical expressions'
23
23
  end
24
24
  end
25
25
  })
@@ -0,0 +1,30 @@
1
+ require_relative './enum'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Pluck < Enum
7
+ def self.min_param_count
8
+ 2
9
+ end
10
+
11
+ def self.max_param_count
12
+ 2
13
+ end
14
+
15
+ def value(context = {})
16
+ collection = Array(@args[0].value(context))
17
+ unless collection.all? { |elem| elem.is_a?(Hash) }
18
+ raise ArgumentError.for(:incompatible_type, value: collection),
19
+ 'PLUCK() requires first argument to be an array of hashes'
20
+ end
21
+
22
+ pluck_path = @args[1].identifier
23
+
24
+ collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Dentaku::AST::Function.register_class(:pluck, Dentaku::AST::Pluck)
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
4
- Dentaku::AST::Function.numeric(numeric).round(places.to_i)
4
+ Dentaku::AST::Function.numeric(numeric).round(Dentaku::AST::Function.numeric(places || 0).to_i)
5
5
  })
@@ -1,7 +1,7 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
4
- precision = precision.to_i
4
+ precision = Dentaku::AST::Function.numeric(precision || 0).to_i
5
5
  tens = 10.0**precision
6
6
  result = (Dentaku::AST::Function.numeric(numeric) * tens).floor / tens
7
7
  precision <= 0 ? result.to_i : result
@@ -1,7 +1,7 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
4
- precision = precision.to_i
4
+ precision = Dentaku::AST::Function.numeric(precision || 0).to_i
5
5
  tens = 10.0**precision
6
6
  result = (Dentaku::AST::Function.numeric(numeric) * tens).ceil / tens
7
7
  precision <= 0 ? result.to_i : result
@@ -1,8 +1,55 @@
1
1
  # import all functions from Ruby's Math module
2
2
  require_relative '../function'
3
3
 
4
+ module Dentaku
5
+ module AST
6
+ class RubyMath < Function
7
+ def self.[](method)
8
+ klass_name = method.to_s.capitalize
9
+ klass = const_set(klass_name , Class.new(self))
10
+ klass.implement(method)
11
+ const_get(klass_name)
12
+ end
13
+
14
+ def self.implement(method)
15
+ @name = method
16
+ @implementation = Math.method(method)
17
+ end
18
+
19
+ def self.name
20
+ @name
21
+ end
22
+
23
+ def self.arity
24
+ @implementation.arity < 0 ? nil : @implementation.arity
25
+ end
26
+
27
+ def self.min_param_count
28
+ @implementation.parameters.select { |type, _name| type == :req }.count
29
+ end
30
+
31
+ def self.max_param_count
32
+ @implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
33
+ end
34
+
35
+ def self.call(*args)
36
+ @implementation.call(*args)
37
+ end
38
+
39
+ def value(context = {})
40
+ args = @args.flatten.map { |a| Dentaku::AST::Function.numeric(a.value(context)) }
41
+ self.class.call(*args)
42
+ end
43
+
44
+ ARRAY_RETURN_TYPES = [:frexp, :lgamma].freeze
45
+
46
+ def type
47
+ ARRAY_RETURN_TYPES.include?(@name) ? :array : :numeric
48
+ end
49
+ end
50
+ end
51
+ end
52
+
4
53
  Math.methods(false).each do |method|
5
- Dentaku::AST::Function.register(method, :numeric, lambda { |*args|
6
- Math.send(method, *args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) })
7
- })
54
+ Dentaku::AST::Function.register_class(method, Dentaku::AST::RubyMath[method])
8
55
  end