dentaku 2.0.11 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
  })