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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +0 -1
  4. data/CHANGELOG.md +19 -0
  5. data/README.md +3 -2
  6. data/dentaku.gemspec +1 -0
  7. data/lib/dentaku/ast.rb +4 -0
  8. data/lib/dentaku/ast/access.rb +27 -0
  9. data/lib/dentaku/ast/arithmetic.rb +49 -7
  10. data/lib/dentaku/ast/case.rb +17 -3
  11. data/lib/dentaku/ast/combinators.rb +8 -2
  12. data/lib/dentaku/ast/function.rb +16 -0
  13. data/lib/dentaku/ast/function_registry.rb +15 -2
  14. data/lib/dentaku/ast/functions/and.rb +25 -0
  15. data/lib/dentaku/ast/functions/max.rb +1 -1
  16. data/lib/dentaku/ast/functions/min.rb +1 -1
  17. data/lib/dentaku/ast/functions/or.rb +25 -0
  18. data/lib/dentaku/ast/functions/round.rb +2 -2
  19. data/lib/dentaku/ast/functions/rounddown.rb +3 -2
  20. data/lib/dentaku/ast/functions/roundup.rb +3 -2
  21. data/lib/dentaku/ast/functions/ruby_math.rb +3 -3
  22. data/lib/dentaku/ast/functions/switch.rb +8 -0
  23. data/lib/dentaku/ast/identifier.rb +3 -2
  24. data/lib/dentaku/ast/negation.rb +5 -1
  25. data/lib/dentaku/ast/node.rb +3 -0
  26. data/lib/dentaku/bulk_expression_solver.rb +1 -2
  27. data/lib/dentaku/calculator.rb +7 -6
  28. data/lib/dentaku/exceptions.rb +75 -1
  29. data/lib/dentaku/parser.rb +73 -12
  30. data/lib/dentaku/token.rb +4 -0
  31. data/lib/dentaku/token_scanner.rb +20 -3
  32. data/lib/dentaku/tokenizer.rb +31 -4
  33. data/lib/dentaku/version.rb +1 -1
  34. data/spec/ast/addition_spec.rb +6 -6
  35. data/spec/ast/and_function_spec.rb +35 -0
  36. data/spec/ast/and_spec.rb +1 -1
  37. data/spec/ast/arithmetic_spec.rb +56 -0
  38. data/spec/ast/division_spec.rb +1 -1
  39. data/spec/ast/function_spec.rb +43 -6
  40. data/spec/ast/max_spec.rb +15 -0
  41. data/spec/ast/min_spec.rb +15 -0
  42. data/spec/ast/or_spec.rb +35 -0
  43. data/spec/ast/round_spec.rb +25 -0
  44. data/spec/ast/rounddown_spec.rb +25 -0
  45. data/spec/ast/roundup_spec.rb +25 -0
  46. data/spec/ast/switch_spec.rb +30 -0
  47. data/spec/calculator_spec.rb +26 -4
  48. data/spec/exceptions_spec.rb +1 -1
  49. data/spec/parser_spec.rb +22 -3
  50. data/spec/spec_helper.rb +12 -2
  51. data/spec/token_scanner_spec.rb +0 -4
  52. data/spec/tokenizer_spec.rb +40 -2
  53. metadata +39 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0febf923ab551e0cfce23bc38794927a3acedbe2
4
- data.tar.gz: 3b4db1f045f21acd1e6a7c864ed9a4c272a2f765
3
+ metadata.gz: 99f7f7efeefb4c1096618e028100c52bb15ca134
4
+ data.tar.gz: 1afd93ef547f630fb0ea121a6e8c813ea8abeeac
5
5
  SHA512:
6
- metadata.gz: dad6f87cf37ba49f355b0e30b92a80dfc063eeb9444653ea09832066831dc09c19e022883b2c31c23189a1c46c842ce652fba133725fa4f887db1fb5a9becf2a
7
- data.tar.gz: 5b01fa83b5ddd2f3123946196ff6f0be42f1bd196b38195249d24ee2c6e08c1ad077ffd0da2f2cf33dd9c664e447ffe6864ab0f021a79e30e51769189225ef53
6
+ metadata.gz: 31227f42fb7f0a262e82b2ccdc334adc4458ba7339c87f0fbf81eb49ffb51bac72654e05de9495d8dad73c9669b8d82bf9bdf8c4f7ee1a5b5830aba123dc6f17
7
+ data.tar.gz: 2f78631eb082dc6c2d52ccac04d057c11c05b212ab05ffaa197e4f5ebf80aafc23113691d1dc0653c6af5df2827d6a4b45af58d4bfac73aa0156e3e91fbdb063
data/.gitignore CHANGED
@@ -9,3 +9,6 @@ vendor/*
9
9
  /.ruby-gemset
10
10
  /.ruby-version
11
11
  /.rspec
12
+
13
+ .byebug_history
14
+ coverage
@@ -1,7 +1,6 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 1.9.3
5
4
  - 2.0.0
6
5
  - 2.1.0
7
6
  - 2.1.1
@@ -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
- Functions: `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
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
 
@@ -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")
@@ -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
- unless valid_left? && valid_right?
11
- fail ParseError, "#{ self.class } requires numeric operands"
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 "Not implemented"
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
- validate_operation(val)
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
- fail Dentaku::ArgumentError, "#{ self.class } requires operands that respond to #{ operator }"
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
- fail Dentaku::ArgumentError, "String input '#{ string }' is not coercible to numeric"
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
@@ -42,9 +42,23 @@ module Dentaku
42
42
 
43
43
  def dependencies(context={})
44
44
  # TODO: should short-circuit
45
- @switch.dependencies(context) +
46
- @conditions.flat_map { |condition| condition.dependencies(context) } +
47
- @else.dependencies(context)
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
- unless valid_node?(left) && valid_node?(right)
9
- fail ParseError, "#{ self.class } requires logical operands"
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
 
@@ -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
- fail ParseError, "Undefined function #{ name }"
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.implementation = implementation
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
+ })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:max, :numeric, ->(*args) {
4
- args.max
4
+ args.map { |arg| Dentaku::AST::Function.numeric(arg) }.max
5
5
  })
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:min, :numeric, ->(*args) {
4
- args.min
4
+ args.map { |arg| Dentaku::AST::Function.numeric(arg) }.min
5
5
  })
@@ -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, ->(numeric, places=nil) {
4
- numeric.round(places || 0)
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, ->(numeric, precision=0) {
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
  })