dentaku 3.5.4 → 3.5.6
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 +4 -4
 - data/.github/workflows/rspec.yml +26 -0
 - data/.github/workflows/rubocop.yml +14 -0
 - data/CHANGELOG.md +14 -6
 - data/README.md +1 -4
 - data/dentaku.gemspec +0 -1
 - data/lib/dentaku/ast/arithmetic.rb +35 -11
 - data/lib/dentaku/ast/case.rb +12 -0
 - data/lib/dentaku/ast/comparators.rb +1 -1
 - data/lib/dentaku/ast/operation.rb +5 -0
 - data/lib/dentaku/ast.rb +1 -1
 - data/lib/dentaku/parser.rb +185 -226
 - data/lib/dentaku/print_visitor.rb +2 -2
 - data/lib/dentaku/version.rb +1 -1
 - data/lib/dentaku/visitor/infix.rb +1 -1
 - data/spec/ast/addition_spec.rb +10 -5
 - data/spec/ast/arithmetic_spec.rb +7 -0
 - data/spec/ast/division_spec.rb +8 -4
 - data/spec/bulk_expression_solver_spec.rb +8 -1
 - data/spec/calculator_spec.rb +8 -1
 - data/spec/external_function_spec.rb +1 -1
 - data/spec/parser_spec.rb +11 -2
 - data/spec/print_visitor_spec.rb +5 -0
 - data/spec/spec_helper.rb +1 -3
 - metadata +5 -20
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: fc1bbfde891d6c98eda08b9176f9ed791744b2043176f07d3aac997fae34a3e9
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: b8658f3d74364672f1a4538928e40d7b4d7aff7489273b573dbd1970303a60b3
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: bc53fba03a05ddf47875ffd08aa54266b3f6894a91d292f18236aba4db4c4c270bfeb4f039f84f10d4ac7ce2ca0f6a86580d65c1f5a0169b74bd7c2dfacf9363
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 88a65a6e103b40fbf3ef0dfc89173ced19e3df7f568514f4a12b451f7d4a91c69b1e25af89b5f9f1c9cadf34b71c200fc4a14ea11ef49aeeb2f2d4be6e36da2e
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            name: rspec
         
     | 
| 
      
 2 
     | 
    
         
            +
            on:
         
     | 
| 
      
 3 
     | 
    
         
            +
              push:
         
     | 
| 
      
 4 
     | 
    
         
            +
              pull_request:
         
     | 
| 
      
 5 
     | 
    
         
            +
            jobs:
         
     | 
| 
      
 6 
     | 
    
         
            +
              rspec:
         
     | 
| 
      
 7 
     | 
    
         
            +
                runs-on: ubuntu-latest
         
     | 
| 
      
 8 
     | 
    
         
            +
                strategy:
         
     | 
| 
      
 9 
     | 
    
         
            +
                  matrix:
         
     | 
| 
      
 10 
     | 
    
         
            +
                    ruby:
         
     | 
| 
      
 11 
     | 
    
         
            +
                    - '2.5'
         
     | 
| 
      
 12 
     | 
    
         
            +
                    - '2.6'
         
     | 
| 
      
 13 
     | 
    
         
            +
                    - '2.7'
         
     | 
| 
      
 14 
     | 
    
         
            +
                    - '3.0'
         
     | 
| 
      
 15 
     | 
    
         
            +
                    - '3.1'
         
     | 
| 
      
 16 
     | 
    
         
            +
                    - '3.2'
         
     | 
| 
      
 17 
     | 
    
         
            +
                    - '3.3'
         
     | 
| 
      
 18 
     | 
    
         
            +
                    - '3.4'
         
     | 
| 
      
 19 
     | 
    
         
            +
                  fail-fast: false
         
     | 
| 
      
 20 
     | 
    
         
            +
                steps:
         
     | 
| 
      
 21 
     | 
    
         
            +
                - uses: actions/checkout@v4
         
     | 
| 
      
 22 
     | 
    
         
            +
                - uses: ruby/setup-ruby@v1
         
     | 
| 
      
 23 
     | 
    
         
            +
                  with:
         
     | 
| 
      
 24 
     | 
    
         
            +
                    ruby-version: "${{ matrix.ruby }}"
         
     | 
| 
      
 25 
     | 
    
         
            +
                    bundler-cache: true
         
     | 
| 
      
 26 
     | 
    
         
            +
                - run: bundle exec rspec --format documentation
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -1,13 +1,20 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Change Log
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            ## [v3.5. 
     | 
| 
      
 3 
     | 
    
         
            +
            ## [v3.5.5] 2025-08-20
         
     | 
| 
      
 4 
     | 
    
         
            +
            - fix percentages in print visitor
         
     | 
| 
      
 5 
     | 
    
         
            +
            - repo cleanup
         
     | 
| 
      
 6 
     | 
    
         
            +
            - fix modulo zero
         
     | 
| 
      
 7 
     | 
    
         
            +
            - fix array arithmetic
         
     | 
| 
      
 8 
     | 
    
         
            +
            - refactor parser
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            ## [v3.5.4] 2024-08-13
         
     | 
| 
       4 
11 
     | 
    
         
             
            - add support for default value for PLUCK function
         
     | 
| 
       5 
12 
     | 
    
         
             
            - improve error handling for MAP/ANY/ALL functions
         
     | 
| 
       6 
13 
     | 
    
         
             
            - fix modulo / percentage operator determination
         
     | 
| 
       7 
14 
     | 
    
         
             
            - fix string casing bug with bulk expressions
         
     | 
| 
       8 
15 
     | 
    
         
             
            - add explicit gem dependency for BigDecimal
         
     | 
| 
       9 
16 
     | 
    
         | 
| 
       10 
     | 
    
         
            -
            ## [v3.5.3]
         
     | 
| 
      
 17 
     | 
    
         
            +
            ## [v3.5.3] 2024-07-04
         
     | 
| 
       11 
18 
     | 
    
         
             
            - add support for empty array literals
         
     | 
| 
       12 
19 
     | 
    
         
             
            - add support for quoted identifiers
         
     | 
| 
       13 
20 
     | 
    
         
             
            - add REDUCE function
         
     | 
| 
         @@ -16,7 +23,7 @@ 
     | 
|
| 
       16 
23 
     | 
    
         
             
            - improve custom class arithmetic
         
     | 
| 
       17 
24 
     | 
    
         
             
            - fix IF dependency
         
     | 
| 
       18 
25 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
            ## [v3.5.2]
         
     | 
| 
      
 26 
     | 
    
         
            +
            ## [v3.5.2] 2023-12-06
         
     | 
| 
       20 
27 
     | 
    
         
             
            - add ABS function
         
     | 
| 
       21 
28 
     | 
    
         
             
            - add array support for AST visitors
         
     | 
| 
       22 
29 
     | 
    
         
             
            - add support for function callbacks
         
     | 
| 
         @@ -29,14 +36,14 @@ 
     | 
|
| 
       29 
36 
     | 
    
         
             
            - fix handling of Math::DomainError
         
     | 
| 
       30 
37 
     | 
    
         
             
            - fix invalid cast
         
     | 
| 
       31 
38 
     | 
    
         | 
| 
       32 
     | 
    
         
            -
            ## [v3.5.1]
         
     | 
| 
      
 39 
     | 
    
         
            +
            ## [v3.5.1] 2022-10-24
         
     | 
| 
       33 
40 
     | 
    
         
             
            - add bitwise shift left and shift right operators
         
     | 
| 
       34 
41 
     | 
    
         
             
            - improve numeric conversions
         
     | 
| 
       35 
42 
     | 
    
         
             
            - improve parse exceptions
         
     | 
| 
       36 
43 
     | 
    
         
             
            - improve bitwise exceptions
         
     | 
| 
       37 
44 
     | 
    
         
             
            - include variable name in bulk expression exceptions
         
     | 
| 
       38 
45 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
            ## [v3.5.0]
         
     | 
| 
      
 46 
     | 
    
         
            +
            ## [v3.5.0] 2022-03-17
         
     | 
| 
       40 
47 
     | 
    
         
             
            - fix bug with function argument count
         
     | 
| 
       41 
48 
     | 
    
         
             
            - add XOR operator
         
     | 
| 
       42 
49 
     | 
    
         
             
            - make function args publicly accessible
         
     | 
| 
         @@ -48,7 +55,7 @@ 
     | 
|
| 
       48 
55 
     | 
    
         
             
            - respect case sensitivity in nested case statments
         
     | 
| 
       49 
56 
     | 
    
         
             
            - add visitor pattern
         
     | 
| 
       50 
57 
     | 
    
         | 
| 
       51 
     | 
    
         
            -
            ## [v3.4.2]
         
     | 
| 
      
 58 
     | 
    
         
            +
            ## [v3.4.2] 2021-07-14
         
     | 
| 
       52 
59 
     | 
    
         
             
            - add FILTER function
         
     | 
| 
       53 
60 
     | 
    
         
             
            - add concurrent-ruby dependency to make global calculator object thread safe
         
     | 
| 
       54 
61 
     | 
    
         
             
            - add Ruby 3 support
         
     | 
| 
         @@ -260,6 +267,7 @@ 
     | 
|
| 
       260 
267 
     | 
    
         
             
            ## [v0.1.0] 2012-01-20
         
     | 
| 
       261 
268 
     | 
    
         
             
            - initial release
         
     | 
| 
       262 
269 
     | 
    
         | 
| 
      
 270 
     | 
    
         
            +
            [v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
         
     | 
| 
       263 
271 
     | 
    
         
             
            [v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
         
     | 
| 
       264 
272 
     | 
    
         
             
            [v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
         
     | 
| 
       265 
273 
     | 
    
         
             
            [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -3,9 +3,6 @@ Dentaku 
     | 
|
| 
       3 
3 
     | 
    
         | 
| 
       4 
4 
     | 
    
         
             
            [](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
         
     | 
| 
       5 
5 
     | 
    
         
             
            [](http://badge.fury.io/rb/dentaku)
         
     | 
| 
       6 
     | 
    
         
            -
            [](https://travis-ci.org/rubysolo/dentaku)
         
     | 
| 
       7 
     | 
    
         
            -
            [](https://codeclimate.com/github/rubysolo/dentaku)
         
     | 
| 
       8 
     | 
    
         
            -
            [](https://codecov.io/gh/rubysolo/dentaku)
         
     | 
| 
       9 
6 
     | 
    
         | 
| 
       10 
7 
     | 
    
         | 
| 
       11 
8 
     | 
    
         
             
            DESCRIPTION
         
     | 
| 
         @@ -147,7 +144,7 @@ Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH` 
     | 
|
| 
       147 
144 
     | 
    
         | 
| 
       148 
145 
     | 
    
         
             
            Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
         
     | 
| 
       149 
146 
     | 
    
         | 
| 
       150 
     | 
    
         
            -
            Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/ 
     | 
| 
      
 147 
     | 
    
         
            +
            Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/main/lib/dentaku/ast/case.rb))
         
     | 
| 
       151 
148 
     | 
    
         | 
| 
       152 
149 
     | 
    
         
             
            String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
         
     | 
| 
       153 
150 
     | 
    
         | 
    
        data/dentaku.gemspec
    CHANGED
    
    | 
         @@ -17,7 +17,6 @@ Gem::Specification.new do |s| 
     | 
|
| 
       17 
17 
     | 
    
         
             
              s.add_dependency('bigdecimal')
         
     | 
| 
       18 
18 
     | 
    
         
             
              s.add_dependency('concurrent-ruby')
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
              s.add_development_dependency('codecov')
         
     | 
| 
       21 
20 
     | 
    
         
             
              s.add_development_dependency('pry')
         
     | 
| 
       22 
21 
     | 
    
         
             
              s.add_development_dependency('pry-byebug')
         
     | 
| 
       23 
22 
     | 
    
         
             
              s.add_development_dependency('pry-stack_explorer')
         
     | 
| 
         @@ -13,13 +13,13 @@ module Dentaku 
     | 
|
| 
       13 
13 
     | 
    
         
             
                    super
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         
             
                    unless valid_left?
         
     | 
| 
       16 
     | 
    
         
            -
                      raise NodeError.new(: 
     | 
| 
       17 
     | 
    
         
            -
                            "#{self.class} requires numeric  
     | 
| 
      
 16 
     | 
    
         
            +
                      raise NodeError.new(:incompatible, left.type, :left),
         
     | 
| 
      
 17 
     | 
    
         
            +
                            "#{self.class} requires operands that are numeric or compatible types, not #{left.type}"
         
     | 
| 
       18 
18 
     | 
    
         
             
                    end
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
20 
     | 
    
         
             
                    unless valid_right?
         
     | 
| 
       21 
     | 
    
         
            -
                      raise NodeError.new(: 
     | 
| 
       22 
     | 
    
         
            -
                            "#{self.class} requires numeric  
     | 
| 
      
 21 
     | 
    
         
            +
                      raise NodeError.new(:incompatible, right.type, :right),
         
     | 
| 
      
 22 
     | 
    
         
            +
                            "#{self.class} requires operands that are numeric or compatible types, not #{right.type}"
         
     | 
| 
       23 
23 
     | 
    
         
             
                    end
         
     | 
| 
       24 
24 
     | 
    
         
             
                  end
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
         @@ -69,12 +69,25 @@ module Dentaku 
     | 
|
| 
       69 
69 
     | 
    
         
             
                  def datetime?(val)
         
     | 
| 
       70 
70 
     | 
    
         
             
                    # val is a Date, Time, or DateTime
         
     | 
| 
       71 
71 
     | 
    
         
             
                    return true if val.respond_to?(:strftime)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    return false unless val.is_a?(::String)
         
     | 
| 
       72 
73 
     | 
    
         | 
| 
       73 
     | 
    
         
            -
                    val 
     | 
| 
      
 74 
     | 
    
         
            +
                    val =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
         
     | 
| 
       74 
75 
     | 
    
         
             
                  end
         
     | 
| 
       75 
76 
     | 
    
         | 
| 
       76 
77 
     | 
    
         
             
                  def valid_node?(node)
         
     | 
| 
       77 
     | 
    
         
            -
                     
     | 
| 
      
 78 
     | 
    
         
            +
                    return false unless node
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    # Allow nodes with dependencies (identifiers that will be resolved later)
         
     | 
| 
      
 81 
     | 
    
         
            +
                    return true if node.dependencies.any?
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                    # Allow compatible types
         
     | 
| 
      
 84 
     | 
    
         
            +
                    return true if [:numeric, :integer, :array].include?(node.type)
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    # Allow nodes without a type (operations, groupings)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    return true if node.type.nil?
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    # Reject incompatible types
         
     | 
| 
      
 90 
     | 
    
         
            +
                    false
         
     | 
| 
       78 
91 
     | 
    
         
             
                  end
         
     | 
| 
       79 
92 
     | 
    
         | 
| 
       80 
93 
     | 
    
         
             
                  def valid_left?
         
     | 
| 
         @@ -193,6 +206,13 @@ module Dentaku 
     | 
|
| 
       193 
206 
     | 
    
         
             
                  def operator
         
     | 
| 
       194 
207 
     | 
    
         
             
                    :%
         
     | 
| 
       195 
208 
     | 
    
         
             
                  end
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                  def value(context = {})
         
     | 
| 
      
 211 
     | 
    
         
            +
                    r = decimal(cast(right.value(context)))
         
     | 
| 
      
 212 
     | 
    
         
            +
                    raise Dentaku::ZeroDivisionError if r.zero?
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                    cast(cast(left.value(context)) % r)
         
     | 
| 
      
 215 
     | 
    
         
            +
                  end
         
     | 
| 
       196 
216 
     | 
    
         
             
                end
         
     | 
| 
       197 
217 
     | 
    
         | 
| 
       198 
218 
     | 
    
         
             
                class Percentage < Arithmetic
         
     | 
| 
         @@ -201,26 +221,30 @@ module Dentaku 
     | 
|
| 
       201 
221 
     | 
    
         
             
                  end
         
     | 
| 
       202 
222 
     | 
    
         | 
| 
       203 
223 
     | 
    
         
             
                  def initialize(child)
         
     | 
| 
       204 
     | 
    
         
            -
                    @ 
     | 
| 
      
 224 
     | 
    
         
            +
                    @left = child
         
     | 
| 
       205 
225 
     | 
    
         | 
| 
       206 
     | 
    
         
            -
                    unless  
     | 
| 
       207 
     | 
    
         
            -
                      raise NodeError.new(:numeric,  
     | 
| 
      
 226 
     | 
    
         
            +
                    unless valid_left?
         
     | 
| 
      
 227 
     | 
    
         
            +
                      raise NodeError.new(:numeric, left.type, :left),
         
     | 
| 
       208 
228 
     | 
    
         
             
                            "#{self.class} requires a numeric operand"
         
     | 
| 
       209 
229 
     | 
    
         
             
                    end
         
     | 
| 
       210 
230 
     | 
    
         
             
                  end
         
     | 
| 
       211 
231 
     | 
    
         | 
| 
       212 
232 
     | 
    
         
             
                  def dependencies(context = {})
         
     | 
| 
       213 
     | 
    
         
            -
                    @ 
     | 
| 
      
 233 
     | 
    
         
            +
                    @left.dependencies(context)
         
     | 
| 
       214 
234 
     | 
    
         
             
                  end
         
     | 
| 
       215 
235 
     | 
    
         | 
| 
       216 
236 
     | 
    
         
             
                  def value(context = {})
         
     | 
| 
       217 
     | 
    
         
            -
                    cast( 
     | 
| 
      
 237 
     | 
    
         
            +
                    cast(left.value(context)) * 0.01
         
     | 
| 
       218 
238 
     | 
    
         
             
                  end
         
     | 
| 
       219 
239 
     | 
    
         | 
| 
       220 
240 
     | 
    
         
             
                  def operator
         
     | 
| 
       221 
241 
     | 
    
         
             
                    :%
         
     | 
| 
       222 
242 
     | 
    
         
             
                  end
         
     | 
| 
       223 
243 
     | 
    
         | 
| 
      
 244 
     | 
    
         
            +
                  def operator_spacing
         
     | 
| 
      
 245 
     | 
    
         
            +
                    ""
         
     | 
| 
      
 246 
     | 
    
         
            +
                  end
         
     | 
| 
      
 247 
     | 
    
         
            +
             
     | 
| 
       224 
248 
     | 
    
         
             
                  def self.precedence
         
     | 
| 
       225 
249 
     | 
    
         
             
                    30
         
     | 
| 
       226 
250 
     | 
    
         
             
                  end
         
     | 
    
        data/lib/dentaku/ast/case.rb
    CHANGED
    
    | 
         @@ -7,6 +7,18 @@ require 'dentaku/exceptions' 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            module Dentaku
         
     | 
| 
       9 
9 
     | 
    
         
             
              module AST
         
     | 
| 
      
 10 
     | 
    
         
            +
                # Examples of using in a formula:
         
     | 
| 
      
 11 
     | 
    
         
            +
                #
         
     | 
| 
      
 12 
     | 
    
         
            +
                #     CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE END
         
     | 
| 
      
 13 
     | 
    
         
            +
                #
         
     | 
| 
      
 14 
     | 
    
         
            +
                #     CASE fruit
         
     | 
| 
      
 15 
     | 
    
         
            +
                #     WHEN 'apple'
         
     | 
| 
      
 16 
     | 
    
         
            +
                #       THEN 1 * quantity
         
     | 
| 
      
 17 
     | 
    
         
            +
                #     WHEN 'banana'
         
     | 
| 
      
 18 
     | 
    
         
            +
                #       THEN 2 * quantity
         
     | 
| 
      
 19 
     | 
    
         
            +
                #     ELSE
         
     | 
| 
      
 20 
     | 
    
         
            +
                #       3 * quantity
         
     | 
| 
      
 21 
     | 
    
         
            +
                #     END
         
     | 
| 
       10 
22 
     | 
    
         
             
                class Case < Node
         
     | 
| 
       11 
23 
     | 
    
         
             
                  attr_reader :switch, :conditions, :else
         
     | 
| 
       12 
24 
     | 
    
         | 
| 
         @@ -20,7 +20,7 @@ module Dentaku 
     | 
|
| 
       20 
20 
     | 
    
         
             
                    r = validate_value(cast(right.value(context)))
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
       22 
22 
     | 
    
         
             
                    l.public_send(operator, r)
         
     | 
| 
       23 
     | 
    
         
            -
                  rescue ::ArgumentError => e
         
     | 
| 
      
 23 
     | 
    
         
            +
                  rescue ::ArgumentError, ::TypeError => e
         
     | 
| 
       24 
24 
     | 
    
         
             
                    raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
         
     | 
| 
       25 
25 
     | 
    
         
             
                  end
         
     | 
| 
       26 
26 
     | 
    
         | 
    
        data/lib/dentaku/ast.rb
    CHANGED
    
    | 
         @@ -39,4 +39,4 @@ require_relative './ast/functions/ruby_math' 
     | 
|
| 
       39 
39 
     | 
    
         
             
            require_relative './ast/functions/string_functions'
         
     | 
| 
       40 
40 
     | 
    
         
             
            require_relative './ast/functions/sum'
         
     | 
| 
       41 
41 
     | 
    
         
             
            require_relative './ast/functions/switch'
         
     | 
| 
       42 
     | 
    
         
            -
            require_relative './ast/functions/xor'
         
     | 
| 
      
 42 
     | 
    
         
            +
            require_relative './ast/functions/xor'
         
     | 
    
        data/lib/dentaku/parser.rb
    CHANGED
    
    | 
         @@ -37,6 +37,7 @@ module Dentaku 
     | 
|
| 
       37 
37 
     | 
    
         
             
                  @arities           = options.fetch(:arities, [])
         
     | 
| 
       38 
38 
     | 
    
         
             
                  @function_registry = options.fetch(:function_registry, nil)
         
     | 
| 
       39 
39 
     | 
    
         
             
                  @case_sensitive    = options.fetch(:case_sensitive, false)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @skip_indices      = []
         
     | 
| 
       40 
41 
     | 
    
         
             
                end
         
     | 
| 
       41 
42 
     | 
    
         | 
| 
       42 
43 
     | 
    
         
             
                def consume(count = 2)
         
     | 
| 
         @@ -53,17 +54,18 @@ module Dentaku 
     | 
|
| 
       53 
54 
     | 
    
         
             
                    fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
         
     | 
| 
       54 
55 
     | 
    
         
             
                  end
         
     | 
| 
       55 
56 
     | 
    
         | 
| 
       56 
     | 
    
         
            -
                  if output_size > max_size && operations.empty? || args_size > max_size
         
     | 
| 
      
 57 
     | 
    
         
            +
                  if (output_size > max_size && operations.empty?) || args_size > max_size
         
     | 
| 
       57 
58 
     | 
    
         
             
                    expect = min_size == max_size ? min_size : min_size..max_size
         
     | 
| 
       58 
59 
     | 
    
         
             
                    fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
         
     | 
| 
       59 
60 
     | 
    
         
             
                  end
         
     | 
| 
       60 
61 
     | 
    
         | 
| 
      
 62 
     | 
    
         
            +
                  args = []
         
     | 
| 
       61 
63 
     | 
    
         
             
                  if operator == AST::Array && output.empty?
         
     | 
| 
       62 
     | 
    
         
            -
                     
     | 
| 
      
 64 
     | 
    
         
            +
                    # special case: empty array literal '{}'
         
     | 
| 
      
 65 
     | 
    
         
            +
                    output.push(operator.new)
         
     | 
| 
       63 
66 
     | 
    
         
             
                  else
         
     | 
| 
       64 
67 
     | 
    
         
             
                    fail! :invalid_statement if output_size < args_size
         
     | 
| 
       65 
68 
     | 
    
         
             
                    args = Array.new(args_size) { output.pop }.reverse
         
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
69 
     | 
    
         
             
                    output.push operator.new(*args)
         
     | 
| 
       68 
70 
     | 
    
         
             
                  end
         
     | 
| 
       69 
71 
     | 
    
         | 
| 
         @@ -79,233 +81,21 @@ module Dentaku 
     | 
|
| 
       79 
81 
     | 
    
         
             
                def parse
         
     | 
| 
       80 
82 
     | 
    
         
             
                  return AST::Nil.new if input.empty?
         
     | 
| 
       81 
83 
     | 
    
         | 
| 
       82 
     | 
    
         
            -
                   
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
     | 
    
         
            -
                     
     | 
| 
       85 
     | 
    
         
            -
                       
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
                    when :numeric
         
     | 
| 
       88 
     | 
    
         
            -
                      output.push AST::Numeric.new(token)
         
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
                    when :logical
         
     | 
| 
       91 
     | 
    
         
            -
                      output.push AST::Logical.new(token)
         
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
                    when :string
         
     | 
| 
       94 
     | 
    
         
            -
                      output.push AST::String.new(token)
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
                    when :identifier
         
     | 
| 
       97 
     | 
    
         
            -
                      output.push AST::Identifier.new(token, case_sensitive: case_sensitive)
         
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
                    when :operator, :comparator, :combinator
         
     | 
| 
       100 
     | 
    
         
            -
                      op_class = operation(token)
         
     | 
| 
       101 
     | 
    
         
            -
                      op_class = op_class.resolve_class(input.first)
         
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
                      if op_class.right_associative?
         
     | 
| 
       104 
     | 
    
         
            -
                        while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
         
     | 
| 
       105 
     | 
    
         
            -
                          consume
         
     | 
| 
       106 
     | 
    
         
            -
                        end
         
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
       108 
     | 
    
         
            -
                        operations.push op_class
         
     | 
| 
       109 
     | 
    
         
            -
                      else
         
     | 
| 
       110 
     | 
    
         
            -
                        while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
         
     | 
| 
       111 
     | 
    
         
            -
                          consume
         
     | 
| 
       112 
     | 
    
         
            -
                        end
         
     | 
| 
       113 
     | 
    
         
            -
             
     | 
| 
       114 
     | 
    
         
            -
                        operations.push op_class
         
     | 
| 
       115 
     | 
    
         
            -
                      end
         
     | 
| 
       116 
     | 
    
         
            -
             
     | 
| 
       117 
     | 
    
         
            -
                    when :null
         
     | 
| 
       118 
     | 
    
         
            -
                      output.push AST::Nil.new
         
     | 
| 
       119 
     | 
    
         
            -
             
     | 
| 
       120 
     | 
    
         
            -
                    when :function
         
     | 
| 
       121 
     | 
    
         
            -
                      func = function(token)
         
     | 
| 
       122 
     | 
    
         
            -
                      if func.nil?
         
     | 
| 
       123 
     | 
    
         
            -
                        fail! :undefined_function, function_name: token.value
         
     | 
| 
       124 
     | 
    
         
            -
                      end
         
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
                      arities.push 0
         
     | 
| 
       127 
     | 
    
         
            -
                      operations.push func
         
     | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
                    when :case
         
     | 
| 
       130 
     | 
    
         
            -
                      case_index = operations.index { |o| o == AST::Case } || -1
         
     | 
| 
       131 
     | 
    
         
            -
                      token_index = case_index + 1
         
     | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
       133 
     | 
    
         
            -
                      case token.value
         
     | 
| 
       134 
     | 
    
         
            -
                      when :open
         
     | 
| 
       135 
     | 
    
         
            -
                        # special handling for case nesting: strip out inner case
         
     | 
| 
       136 
     | 
    
         
            -
                        # statements and parse their AST segments recursively
         
     | 
| 
       137 
     | 
    
         
            -
                        if operations.include?(AST::Case)
         
     | 
| 
       138 
     | 
    
         
            -
                          open_cases = 0
         
     | 
| 
       139 
     | 
    
         
            -
                          case_end_index = nil
         
     | 
| 
       140 
     | 
    
         
            -
             
     | 
| 
       141 
     | 
    
         
            -
                          input.each_with_index do |input_token, index|
         
     | 
| 
       142 
     | 
    
         
            -
                            if input_token.category == :case
         
     | 
| 
       143 
     | 
    
         
            -
                              if input_token.value == :open
         
     | 
| 
       144 
     | 
    
         
            -
                                open_cases += 1
         
     | 
| 
       145 
     | 
    
         
            -
                              end
         
     | 
| 
       146 
     | 
    
         
            -
             
     | 
| 
       147 
     | 
    
         
            -
                              if input_token.value == :close
         
     | 
| 
       148 
     | 
    
         
            -
                                if open_cases > 0
         
     | 
| 
       149 
     | 
    
         
            -
                                  open_cases -= 1
         
     | 
| 
       150 
     | 
    
         
            -
                                else
         
     | 
| 
       151 
     | 
    
         
            -
                                  case_end_index = index
         
     | 
| 
       152 
     | 
    
         
            -
                                  break
         
     | 
| 
       153 
     | 
    
         
            -
                                end
         
     | 
| 
       154 
     | 
    
         
            -
                              end
         
     | 
| 
       155 
     | 
    
         
            -
                            end
         
     | 
| 
       156 
     | 
    
         
            -
                          end
         
     | 
| 
       157 
     | 
    
         
            -
                          inner_case_inputs = input.slice!(0..case_end_index)
         
     | 
| 
       158 
     | 
    
         
            -
                          subparser = Parser.new(
         
     | 
| 
       159 
     | 
    
         
            -
                            inner_case_inputs,
         
     | 
| 
       160 
     | 
    
         
            -
                            operations: [AST::Case],
         
     | 
| 
       161 
     | 
    
         
            -
                            arities: [0],
         
     | 
| 
       162 
     | 
    
         
            -
                            function_registry: @function_registry,
         
     | 
| 
       163 
     | 
    
         
            -
                            case_sensitive: case_sensitive
         
     | 
| 
       164 
     | 
    
         
            -
                          )
         
     | 
| 
       165 
     | 
    
         
            -
                          subparser.parse
         
     | 
| 
       166 
     | 
    
         
            -
                          output.concat(subparser.output)
         
     | 
| 
       167 
     | 
    
         
            -
                        else
         
     | 
| 
       168 
     | 
    
         
            -
                          operations.push AST::Case
         
     | 
| 
       169 
     | 
    
         
            -
                          arities.push(0)
         
     | 
| 
       170 
     | 
    
         
            -
                        end
         
     | 
| 
       171 
     | 
    
         
            -
                      when :close
         
     | 
| 
       172 
     | 
    
         
            -
                        if operations[token_index] == AST::CaseThen
         
     | 
| 
       173 
     | 
    
         
            -
                          while operations.last != AST::Case
         
     | 
| 
       174 
     | 
    
         
            -
                            consume
         
     | 
| 
       175 
     | 
    
         
            -
                          end
         
     | 
| 
       176 
     | 
    
         
            -
             
     | 
| 
       177 
     | 
    
         
            -
                          operations.push(AST::CaseConditional)
         
     | 
| 
       178 
     | 
    
         
            -
                          consume(2)
         
     | 
| 
       179 
     | 
    
         
            -
                          arities[-1] += 1
         
     | 
| 
       180 
     | 
    
         
            -
                        elsif operations[token_index] == AST::CaseElse
         
     | 
| 
       181 
     | 
    
         
            -
                          while operations.last != AST::Case
         
     | 
| 
       182 
     | 
    
         
            -
                            consume
         
     | 
| 
       183 
     | 
    
         
            -
                          end
         
     | 
| 
       184 
     | 
    
         
            -
             
     | 
| 
       185 
     | 
    
         
            -
                          arities[-1] += 1
         
     | 
| 
       186 
     | 
    
         
            -
                        end
         
     | 
| 
       187 
     | 
    
         
            -
             
     | 
| 
       188 
     | 
    
         
            -
                        unless operations.count >= 1 && operations.last == AST::Case
         
     | 
| 
       189 
     | 
    
         
            -
                          fail! :unprocessed_token, token_name: token.value
         
     | 
| 
       190 
     | 
    
         
            -
                        end
         
     | 
| 
       191 
     | 
    
         
            -
                        consume(arities.pop.succ)
         
     | 
| 
       192 
     | 
    
         
            -
                      when :when
         
     | 
| 
       193 
     | 
    
         
            -
                        if operations[token_index] == AST::CaseThen
         
     | 
| 
       194 
     | 
    
         
            -
                          while ![AST::CaseWhen, AST::Case].include?(operations.last)
         
     | 
| 
       195 
     | 
    
         
            -
                            consume
         
     | 
| 
       196 
     | 
    
         
            -
                          end
         
     | 
| 
       197 
     | 
    
         
            -
                          operations.push(AST::CaseConditional)
         
     | 
| 
       198 
     | 
    
         
            -
                          consume(2)
         
     | 
| 
       199 
     | 
    
         
            -
                          arities[-1] += 1
         
     | 
| 
       200 
     | 
    
         
            -
                        elsif operations.last == AST::Case
         
     | 
| 
       201 
     | 
    
         
            -
                          operations.push(AST::CaseSwitchVariable)
         
     | 
| 
       202 
     | 
    
         
            -
                          consume
         
     | 
| 
       203 
     | 
    
         
            -
                        end
         
     | 
| 
       204 
     | 
    
         
            -
             
     | 
| 
       205 
     | 
    
         
            -
                        operations.push(AST::CaseWhen)
         
     | 
| 
       206 
     | 
    
         
            -
                      when :then
         
     | 
| 
       207 
     | 
    
         
            -
                        if operations[token_index] == AST::CaseWhen
         
     | 
| 
       208 
     | 
    
         
            -
                          while ![AST::CaseThen, AST::Case].include?(operations.last)
         
     | 
| 
       209 
     | 
    
         
            -
                            consume
         
     | 
| 
       210 
     | 
    
         
            -
                          end
         
     | 
| 
       211 
     | 
    
         
            -
                        end
         
     | 
| 
       212 
     | 
    
         
            -
                        operations.push(AST::CaseThen)
         
     | 
| 
       213 
     | 
    
         
            -
                      when :else
         
     | 
| 
       214 
     | 
    
         
            -
                        if operations[token_index] == AST::CaseThen
         
     | 
| 
       215 
     | 
    
         
            -
                          while operations.last != AST::Case
         
     | 
| 
       216 
     | 
    
         
            -
                            consume
         
     | 
| 
       217 
     | 
    
         
            -
                          end
         
     | 
| 
       218 
     | 
    
         
            -
             
     | 
| 
       219 
     | 
    
         
            -
                          operations.push(AST::CaseConditional)
         
     | 
| 
       220 
     | 
    
         
            -
                          consume(2)
         
     | 
| 
       221 
     | 
    
         
            -
                          arities[-1] += 1
         
     | 
| 
       222 
     | 
    
         
            -
                        end
         
     | 
| 
       223 
     | 
    
         
            -
             
     | 
| 
       224 
     | 
    
         
            -
                        operations.push(AST::CaseElse)
         
     | 
| 
       225 
     | 
    
         
            -
                      else
         
     | 
| 
       226 
     | 
    
         
            -
                        fail! :unknown_case_token, token_name: token.value
         
     | 
| 
       227 
     | 
    
         
            -
                      end
         
     | 
| 
       228 
     | 
    
         
            -
             
     | 
| 
       229 
     | 
    
         
            -
                    when :access
         
     | 
| 
       230 
     | 
    
         
            -
                      case token.value
         
     | 
| 
       231 
     | 
    
         
            -
                      when :lbracket
         
     | 
| 
       232 
     | 
    
         
            -
                        operations.push AST::Access
         
     | 
| 
       233 
     | 
    
         
            -
                      when :rbracket
         
     | 
| 
       234 
     | 
    
         
            -
                        while operations.any? && operations.last != AST::Access
         
     | 
| 
       235 
     | 
    
         
            -
                          consume
         
     | 
| 
       236 
     | 
    
         
            -
                        end
         
     | 
| 
       237 
     | 
    
         
            -
             
     | 
| 
       238 
     | 
    
         
            -
                        unless operations.last == AST::Access
         
     | 
| 
       239 
     | 
    
         
            -
                          fail! :unbalanced_bracket, token: token
         
     | 
| 
       240 
     | 
    
         
            -
                        end
         
     | 
| 
       241 
     | 
    
         
            -
                        consume
         
     | 
| 
       242 
     | 
    
         
            -
                      end
         
     | 
| 
       243 
     | 
    
         
            -
             
     | 
| 
       244 
     | 
    
         
            -
                    when :array
         
     | 
| 
       245 
     | 
    
         
            -
                      case token.value
         
     | 
| 
       246 
     | 
    
         
            -
                      when :array_start
         
     | 
| 
       247 
     | 
    
         
            -
                        operations.push AST::Array
         
     | 
| 
       248 
     | 
    
         
            -
                        arities.push 0
         
     | 
| 
       249 
     | 
    
         
            -
                      when :array_end
         
     | 
| 
       250 
     | 
    
         
            -
                        while operations.any? && operations.last != AST::Array
         
     | 
| 
       251 
     | 
    
         
            -
                          consume
         
     | 
| 
       252 
     | 
    
         
            -
                        end
         
     | 
| 
       253 
     | 
    
         
            -
             
     | 
| 
       254 
     | 
    
         
            -
                        unless operations.last == AST::Array
         
     | 
| 
       255 
     | 
    
         
            -
                          fail! :unbalanced_bracket, token: token
         
     | 
| 
       256 
     | 
    
         
            -
                        end
         
     | 
| 
       257 
     | 
    
         
            -
             
     | 
| 
       258 
     | 
    
         
            -
                        consume(arities.pop.succ)
         
     | 
| 
       259 
     | 
    
         
            -
                      end
         
     | 
| 
       260 
     | 
    
         
            -
             
     | 
| 
       261 
     | 
    
         
            -
                    when :grouping
         
     | 
| 
       262 
     | 
    
         
            -
                      case token.value
         
     | 
| 
       263 
     | 
    
         
            -
                      when :open
         
     | 
| 
       264 
     | 
    
         
            -
                        if input.first && input.first.value == :close
         
     | 
| 
       265 
     | 
    
         
            -
                          input.shift
         
     | 
| 
       266 
     | 
    
         
            -
                          arities.pop
         
     | 
| 
       267 
     | 
    
         
            -
                          consume(0)
         
     | 
| 
       268 
     | 
    
         
            -
                        else
         
     | 
| 
       269 
     | 
    
         
            -
                          operations.push AST::Grouping
         
     | 
| 
       270 
     | 
    
         
            -
                        end
         
     | 
| 
       271 
     | 
    
         
            -
             
     | 
| 
       272 
     | 
    
         
            -
                      when :close
         
     | 
| 
       273 
     | 
    
         
            -
                        while operations.any? && operations.last != AST::Grouping
         
     | 
| 
       274 
     | 
    
         
            -
                          consume
         
     | 
| 
       275 
     | 
    
         
            -
                        end
         
     | 
| 
       276 
     | 
    
         
            -
             
     | 
| 
       277 
     | 
    
         
            -
                        lparen = operations.pop
         
     | 
| 
       278 
     | 
    
         
            -
                        unless lparen == AST::Grouping
         
     | 
| 
       279 
     | 
    
         
            -
                          fail! :unbalanced_parenthesis, token
         
     | 
| 
       280 
     | 
    
         
            -
                        end
         
     | 
| 
       281 
     | 
    
         
            -
             
     | 
| 
       282 
     | 
    
         
            -
                        if operations.last && operations.last < AST::Function
         
     | 
| 
       283 
     | 
    
         
            -
                          consume(arities.pop.succ)
         
     | 
| 
       284 
     | 
    
         
            -
                        end
         
     | 
| 
       285 
     | 
    
         
            -
             
     | 
| 
       286 
     | 
    
         
            -
                      when :comma
         
     | 
| 
       287 
     | 
    
         
            -
                        fail! :invalid_statement if arities.empty?
         
     | 
| 
       288 
     | 
    
         
            -
                        arities[-1] += 1
         
     | 
| 
       289 
     | 
    
         
            -
                        while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
         
     | 
| 
       290 
     | 
    
         
            -
                          consume
         
     | 
| 
       291 
     | 
    
         
            -
                        end
         
     | 
| 
       292 
     | 
    
         
            -
             
     | 
| 
       293 
     | 
    
         
            -
                      else
         
     | 
| 
       294 
     | 
    
         
            -
                        fail! :unknown_grouping_token, token_name: token.value
         
     | 
| 
       295 
     | 
    
         
            -
                      end
         
     | 
| 
       296 
     | 
    
         
            -
             
     | 
| 
       297 
     | 
    
         
            -
                    else
         
     | 
| 
       298 
     | 
    
         
            -
                      fail! :not_implemented_token_category, token_category: token.category
         
     | 
| 
      
 84 
     | 
    
         
            +
                  i = 0
         
     | 
| 
      
 85 
     | 
    
         
            +
                  while i < input.length
         
     | 
| 
      
 86 
     | 
    
         
            +
                    if @skip_indices.include?(i)
         
     | 
| 
      
 87 
     | 
    
         
            +
                      i += 1
         
     | 
| 
      
 88 
     | 
    
         
            +
                      next
         
     | 
| 
       299 
89 
     | 
    
         
             
                    end
         
     | 
| 
      
 90 
     | 
    
         
            +
                    token = input[i]
         
     | 
| 
      
 91 
     | 
    
         
            +
                    lookahead = input[i + 1]
         
     | 
| 
      
 92 
     | 
    
         
            +
                    process_token(token, lookahead, i, input)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    i += 1
         
     | 
| 
       300 
94 
     | 
    
         
             
                  end
         
     | 
| 
       301 
95 
     | 
    
         | 
| 
       302 
     | 
    
         
            -
                  while operations.any?
         
     | 
| 
       303 
     | 
    
         
            -
                    consume
         
     | 
| 
       304 
     | 
    
         
            -
                  end
         
     | 
| 
      
 96 
     | 
    
         
            +
                  consume while operations.any?
         
     | 
| 
       305 
97 
     | 
    
         | 
| 
       306 
     | 
    
         
            -
                  unless output.count == 1
         
     | 
| 
       307 
     | 
    
         
            -
                    fail! :invalid_statement
         
     | 
| 
       308 
     | 
    
         
            -
                  end
         
     | 
| 
      
 98 
     | 
    
         
            +
                  fail! :invalid_statement unless output.count == 1
         
     | 
| 
       309 
99 
     | 
    
         | 
| 
       310 
100 
     | 
    
         
             
                  output.first
         
     | 
| 
       311 
101 
     | 
    
         
             
                end
         
     | 
| 
         @@ -324,6 +114,175 @@ module Dentaku 
     | 
|
| 
       324 
114 
     | 
    
         | 
| 
       325 
115 
     | 
    
         
             
                private
         
     | 
| 
       326 
116 
     | 
    
         | 
| 
      
 117 
     | 
    
         
            +
                def process_token(token, lookahead, index, tokens)
         
     | 
| 
      
 118 
     | 
    
         
            +
                  case token.category
         
     | 
| 
      
 119 
     | 
    
         
            +
                  when :datetime      then output << AST::DateTime.new(token)
         
     | 
| 
      
 120 
     | 
    
         
            +
                  when :numeric       then output << AST::Numeric.new(token)
         
     | 
| 
      
 121 
     | 
    
         
            +
                  when :logical       then output << AST::Logical.new(token)
         
     | 
| 
      
 122 
     | 
    
         
            +
                  when :string        then output << AST::String.new(token)
         
     | 
| 
      
 123 
     | 
    
         
            +
                  when :identifier    then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
         
     | 
| 
      
 124 
     | 
    
         
            +
                  when :operator, :comparator, :combinator
         
     | 
| 
      
 125 
     | 
    
         
            +
                    handle_operator(token, lookahead)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  when :null
         
     | 
| 
      
 127 
     | 
    
         
            +
                    output << AST::Nil.new
         
     | 
| 
      
 128 
     | 
    
         
            +
                  when :function
         
     | 
| 
      
 129 
     | 
    
         
            +
                    handle_function(token)
         
     | 
| 
      
 130 
     | 
    
         
            +
                  when :case
         
     | 
| 
      
 131 
     | 
    
         
            +
                    handle_case(token)
         
     | 
| 
      
 132 
     | 
    
         
            +
                  when :access
         
     | 
| 
      
 133 
     | 
    
         
            +
                    handle_access(token)
         
     | 
| 
      
 134 
     | 
    
         
            +
                  when :array
         
     | 
| 
      
 135 
     | 
    
         
            +
                    handle_array(token)
         
     | 
| 
      
 136 
     | 
    
         
            +
                  when :grouping
         
     | 
| 
      
 137 
     | 
    
         
            +
                    handle_grouping(token, lookahead, tokens)
         
     | 
| 
      
 138 
     | 
    
         
            +
                  else
         
     | 
| 
      
 139 
     | 
    
         
            +
                    fail! :not_implemented_token_category, token_category: token.category
         
     | 
| 
      
 140 
     | 
    
         
            +
                  end
         
     | 
| 
      
 141 
     | 
    
         
            +
                end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                def handle_operator(token, lookahead)
         
     | 
| 
      
 144 
     | 
    
         
            +
                  op_class = operation(token).resolve_class(lookahead)
         
     | 
| 
      
 145 
     | 
    
         
            +
                  if op_class.right_associative?
         
     | 
| 
      
 146 
     | 
    
         
            +
                    consume while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
         
     | 
| 
      
 147 
     | 
    
         
            +
                  else
         
     | 
| 
      
 148 
     | 
    
         
            +
                    consume while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
         
     | 
| 
      
 149 
     | 
    
         
            +
                  end
         
     | 
| 
      
 150 
     | 
    
         
            +
                  operations.push op_class
         
     | 
| 
      
 151 
     | 
    
         
            +
                end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                def handle_function(token)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  func = function(token)
         
     | 
| 
      
 155 
     | 
    
         
            +
                  fail! :undefined_function, function_name: token.value if func.nil?
         
     | 
| 
      
 156 
     | 
    
         
            +
                  arities.push 0
         
     | 
| 
      
 157 
     | 
    
         
            +
                  operations.push func
         
     | 
| 
      
 158 
     | 
    
         
            +
                end
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                def handle_case(token)
         
     | 
| 
      
 161 
     | 
    
         
            +
                  # We always operate on the innermost (most recent) CASE on the stack.
         
     | 
| 
      
 162 
     | 
    
         
            +
                  case_index = operations.rindex(AST::Case) || -1
         
     | 
| 
      
 163 
     | 
    
         
            +
                  token_index = case_index + 1
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                  case token.value
         
     | 
| 
      
 166 
     | 
    
         
            +
                  when :open
         
     | 
| 
      
 167 
     | 
    
         
            +
                    # Start a new CASE context.
         
     | 
| 
      
 168 
     | 
    
         
            +
                    operations.push AST::Case
         
     | 
| 
      
 169 
     | 
    
         
            +
                    arities.push(0)
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                  when :close
         
     | 
| 
      
 172 
     | 
    
         
            +
                    # Finalize any trailing THEN/ELSE expression still on the stack.
         
     | 
| 
      
 173 
     | 
    
         
            +
                    if operations[token_index] == AST::CaseThen
         
     | 
| 
      
 174 
     | 
    
         
            +
                      consume_until(AST::Case)
         
     | 
| 
      
 175 
     | 
    
         
            +
                      operations.push(AST::CaseConditional)
         
     | 
| 
      
 176 
     | 
    
         
            +
                      consume(2)
         
     | 
| 
      
 177 
     | 
    
         
            +
                      arities[-1] += 1
         
     | 
| 
      
 178 
     | 
    
         
            +
                    elsif operations[token_index] == AST::CaseElse
         
     | 
| 
      
 179 
     | 
    
         
            +
                      consume_until(AST::Case)
         
     | 
| 
      
 180 
     | 
    
         
            +
                      arities[-1] += 1
         
     | 
| 
      
 181 
     | 
    
         
            +
                    end
         
     | 
| 
      
 182 
     | 
    
         
            +
                    fail! :unprocessed_token, token_name: token.value unless operations.last == AST::Case
         
     | 
| 
      
 183 
     | 
    
         
            +
                    consume(arities.pop.succ)
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                  when :when
         
     | 
| 
      
 186 
     | 
    
         
            +
                    if operations[token_index] == AST::CaseThen
         
     | 
| 
      
 187 
     | 
    
         
            +
                      # Close out previous WHEN/THEN pair.
         
     | 
| 
      
 188 
     | 
    
         
            +
                      consume_until([AST::CaseWhen, AST::Case])
         
     | 
| 
      
 189 
     | 
    
         
            +
                      operations.push(AST::CaseConditional)
         
     | 
| 
      
 190 
     | 
    
         
            +
                      consume(2)
         
     | 
| 
      
 191 
     | 
    
         
            +
                      arities[-1] += 1
         
     | 
| 
      
 192 
     | 
    
         
            +
                    elsif operations.last == AST::Case
         
     | 
| 
      
 193 
     | 
    
         
            +
                      # First WHEN: finalize switch variable expression.
         
     | 
| 
      
 194 
     | 
    
         
            +
                      operations.push(AST::CaseSwitchVariable)
         
     | 
| 
      
 195 
     | 
    
         
            +
                      consume
         
     | 
| 
      
 196 
     | 
    
         
            +
                    end
         
     | 
| 
      
 197 
     | 
    
         
            +
                    operations.push(AST::CaseWhen)
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                  when :then
         
     | 
| 
      
 200 
     | 
    
         
            +
                    if operations[token_index] == AST::CaseWhen
         
     | 
| 
      
 201 
     | 
    
         
            +
                      consume_until([AST::CaseThen, AST::Case])
         
     | 
| 
      
 202 
     | 
    
         
            +
                    end
         
     | 
| 
      
 203 
     | 
    
         
            +
                    operations.push(AST::CaseThen)
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
                  when :else
         
     | 
| 
      
 206 
     | 
    
         
            +
                    if operations[token_index] == AST::CaseThen
         
     | 
| 
      
 207 
     | 
    
         
            +
                      consume_until(AST::Case)
         
     | 
| 
      
 208 
     | 
    
         
            +
                      operations.push(AST::CaseConditional)
         
     | 
| 
      
 209 
     | 
    
         
            +
                      consume(2)
         
     | 
| 
      
 210 
     | 
    
         
            +
                      arities[-1] += 1
         
     | 
| 
      
 211 
     | 
    
         
            +
                    end
         
     | 
| 
      
 212 
     | 
    
         
            +
                    operations.push(AST::CaseElse)
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                  else
         
     | 
| 
      
 215 
     | 
    
         
            +
                    fail! :unknown_case_token, token_name: token.value
         
     | 
| 
      
 216 
     | 
    
         
            +
                  end
         
     | 
| 
      
 217 
     | 
    
         
            +
                end
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                def consume_until(target)
         
     | 
| 
      
 220 
     | 
    
         
            +
                  matcher =
         
     | 
| 
      
 221 
     | 
    
         
            +
                    case target
         
     | 
| 
      
 222 
     | 
    
         
            +
                    when Array then ->(op) { target.include?(op) }
         
     | 
| 
      
 223 
     | 
    
         
            +
                    else ->(op) { op == target }
         
     | 
| 
      
 224 
     | 
    
         
            +
                    end
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                  consume while operations.any? && !matcher.call(operations.last)
         
     | 
| 
      
 227 
     | 
    
         
            +
                end
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                def handle_access(token)
         
     | 
| 
      
 230 
     | 
    
         
            +
                  case token.value
         
     | 
| 
      
 231 
     | 
    
         
            +
                  when :lbracket
         
     | 
| 
      
 232 
     | 
    
         
            +
                    operations.push AST::Access
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                  when :rbracket
         
     | 
| 
      
 235 
     | 
    
         
            +
                    consume while operations.any? && operations.last != AST::Access
         
     | 
| 
      
 236 
     | 
    
         
            +
                    fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
         
     | 
| 
      
 237 
     | 
    
         
            +
                    consume
         
     | 
| 
      
 238 
     | 
    
         
            +
                  end
         
     | 
| 
      
 239 
     | 
    
         
            +
                end
         
     | 
| 
      
 240 
     | 
    
         
            +
             
     | 
| 
      
 241 
     | 
    
         
            +
                def handle_array(token)
         
     | 
| 
      
 242 
     | 
    
         
            +
                  case token.value
         
     | 
| 
      
 243 
     | 
    
         
            +
                  when :array_start
         
     | 
| 
      
 244 
     | 
    
         
            +
                    operations.push AST::Array
         
     | 
| 
      
 245 
     | 
    
         
            +
                    arities.push 0
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
                  when :array_end
         
     | 
| 
      
 248 
     | 
    
         
            +
                    consume while operations.any? && operations.last != AST::Array
         
     | 
| 
      
 249 
     | 
    
         
            +
                    fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
         
     | 
| 
      
 250 
     | 
    
         
            +
                    consume(arities.pop.succ)
         
     | 
| 
      
 251 
     | 
    
         
            +
                  end
         
     | 
| 
      
 252 
     | 
    
         
            +
                end
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
                def handle_grouping(token, lookahead, tokens)
         
     | 
| 
      
 255 
     | 
    
         
            +
                  case token.value
         
     | 
| 
      
 256 
     | 
    
         
            +
                  when :open
         
     | 
| 
      
 257 
     | 
    
         
            +
                    if lookahead && lookahead.value == :close
         
     | 
| 
      
 258 
     | 
    
         
            +
                      # empty grouping (e.g. function with zero arguments) — we trigger consume later
         
     | 
| 
      
 259 
     | 
    
         
            +
                      # skip to the end
         
     | 
| 
      
 260 
     | 
    
         
            +
                      lookahead_index = tokens.index(lookahead)
         
     | 
| 
      
 261 
     | 
    
         
            +
                      @skip_indices << lookahead_index if lookahead_index
         
     | 
| 
      
 262 
     | 
    
         
            +
                      arities.pop
         
     | 
| 
      
 263 
     | 
    
         
            +
                      consume(0)
         
     | 
| 
      
 264 
     | 
    
         
            +
                    else
         
     | 
| 
      
 265 
     | 
    
         
            +
                      operations.push AST::Grouping
         
     | 
| 
      
 266 
     | 
    
         
            +
                    end
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                  when :close
         
     | 
| 
      
 269 
     | 
    
         
            +
                    consume while operations.any? && operations.last != AST::Grouping
         
     | 
| 
      
 270 
     | 
    
         
            +
                    lparen = operations.pop
         
     | 
| 
      
 271 
     | 
    
         
            +
                    fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
         
     | 
| 
      
 272 
     | 
    
         
            +
                    if operations.last && operations.last < AST::Function
         
     | 
| 
      
 273 
     | 
    
         
            +
                      consume(arities.pop.succ)
         
     | 
| 
      
 274 
     | 
    
         
            +
                    end
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
                  when :comma
         
     | 
| 
      
 277 
     | 
    
         
            +
                    fail! :invalid_statement if arities.empty?
         
     | 
| 
      
 278 
     | 
    
         
            +
                    arities[-1] += 1
         
     | 
| 
      
 279 
     | 
    
         
            +
                    consume while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
         
     | 
| 
      
 280 
     | 
    
         
            +
             
     | 
| 
      
 281 
     | 
    
         
            +
                  else
         
     | 
| 
      
 282 
     | 
    
         
            +
                    fail! :unknown_grouping_token, token_name: token.value
         
     | 
| 
      
 283 
     | 
    
         
            +
                  end
         
     | 
| 
      
 284 
     | 
    
         
            +
                end
         
     | 
| 
      
 285 
     | 
    
         
            +
             
     | 
| 
       327 
286 
     | 
    
         
             
                def fail!(reason, **meta)
         
     | 
| 
       328 
287 
     | 
    
         
             
                  message =
         
     | 
| 
       329 
288 
     | 
    
         
             
                    case reason
         
     | 
| 
         @@ -7,13 +7,13 @@ module Dentaku 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
                def visit_operation(node)
         
     | 
| 
       9 
9 
     | 
    
         
             
                  if node.left
         
     | 
| 
       10 
     | 
    
         
            -
                    visit_operand(node.left, node.class.precedence, suffix:  
     | 
| 
      
 10 
     | 
    
         
            +
                    visit_operand(node.left, node.class.precedence, suffix: node.operator_spacing, dir: :left)
         
     | 
| 
       11 
11 
     | 
    
         
             
                  end
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
       13 
13 
     | 
    
         
             
                  @output << node.display_operator
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         
             
                  if node.right
         
     | 
| 
       16 
     | 
    
         
            -
                    visit_operand(node.right, node.class.precedence, prefix:  
     | 
| 
      
 16 
     | 
    
         
            +
                    visit_operand(node.right, node.class.precedence, prefix: node.operator_spacing, dir: :right)
         
     | 
| 
       17 
17 
     | 
    
         
             
                  end
         
     | 
| 
       18 
18 
     | 
    
         
             
                end
         
     | 
| 
       19 
19 
     | 
    
         | 
    
        data/lib/dentaku/version.rb
    CHANGED
    
    
    
        data/spec/ast/addition_spec.rb
    CHANGED
    
    | 
         @@ -20,10 +20,10 @@ describe Dentaku::AST::Addition do 
     | 
|
| 
       20 
20 
     | 
    
         
             
                expect(node.value).to eq(11)
         
     | 
| 
       21 
21 
     | 
    
         
             
              end
         
     | 
| 
       22 
22 
     | 
    
         | 
| 
       23 
     | 
    
         
            -
              it 'requires  
     | 
| 
      
 23 
     | 
    
         
            +
              it 'requires operands that respond to +' do
         
     | 
| 
       24 
24 
     | 
    
         
             
                expect {
         
     | 
| 
       25 
25 
     | 
    
         
             
                  described_class.new(five, t)
         
     | 
| 
       26 
     | 
    
         
            -
                }.to raise_error(Dentaku::NodeError, /requires  
     | 
| 
      
 26 
     | 
    
         
            +
                }.to raise_error(Dentaku::NodeError, /requires operands/)
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
       28 
28 
     | 
    
         
             
                expression = Dentaku::AST::Multiplication.new(five, five)
         
     | 
| 
       29 
29 
     | 
    
         
             
                group = Dentaku::AST::Grouping.new(expression)
         
     | 
| 
         @@ -35,7 +35,6 @@ describe Dentaku::AST::Addition do 
     | 
|
| 
       35 
35 
     | 
    
         | 
| 
       36 
36 
     | 
    
         
             
              it 'allows operands that respond to addition' do
         
     | 
| 
       37 
37 
     | 
    
         
             
                # Sample struct that has a custom definition for addition
         
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
38 
     | 
    
         
             
                Addable = Struct.new(:value) do
         
     | 
| 
       40 
39 
     | 
    
         
             
                  def +(other)
         
     | 
| 
       41 
40 
     | 
    
         
             
                    case other
         
     | 
| 
         @@ -51,12 +50,18 @@ describe Dentaku::AST::Addition do 
     | 
|
| 
       51 
50 
     | 
    
         
             
                operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
         
     | 
| 
       52 
51 
     | 
    
         | 
| 
       53 
52 
     | 
    
         
             
                expect {
         
     | 
| 
       54 
     | 
    
         
            -
                  described_class.new(operand_five, operand_six)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  described_class.new(operand_five, operand_six).value
         
     | 
| 
       55 
54 
     | 
    
         
             
                }.not_to raise_error
         
     | 
| 
       56 
55 
     | 
    
         | 
| 
       57 
56 
     | 
    
         
             
                expect {
         
     | 
| 
       58 
     | 
    
         
            -
                  described_class.new(operand_five, six)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  described_class.new(operand_five, six).value
         
     | 
| 
       59 
58 
     | 
    
         
             
                }.not_to raise_error
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
              it 'does not try to parse nested string as date' do
         
     | 
| 
      
 62 
     | 
    
         
            +
                a = ['2017-01-01', '2017-01-02']
         
     | 
| 
      
 63 
     | 
    
         
            +
                b = ['2017-01-01']
         
     | 
| 
       60 
64 
     | 
    
         | 
| 
      
 65 
     | 
    
         
            +
                expect(Dentaku('a + b', a: a, b: b)).to eq(['2017-01-01', '2017-01-02', '2017-01-01'])
         
     | 
| 
       61 
66 
     | 
    
         
             
              end
         
     | 
| 
       62 
67 
     | 
    
         
             
            end
         
     | 
    
        data/spec/ast/arithmetic_spec.rb
    CHANGED
    
    | 
         @@ -111,6 +111,13 @@ describe Dentaku::AST::Arithmetic do 
     | 
|
| 
       111 
111 
     | 
    
         
             
                end
         
     | 
| 
       112 
112 
     | 
    
         
             
              end
         
     | 
| 
       113 
113 
     | 
    
         | 
| 
      
 114 
     | 
    
         
            +
              it 'does not try to parse nested string as date' do
         
     | 
| 
      
 115 
     | 
    
         
            +
                a = ['2017-01-01', '2017-01-02']
         
     | 
| 
      
 116 
     | 
    
         
            +
                b = ['2017-01-01']
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                expect(Dentaku('a - b', a: a, b: b)).to eq(['2017-01-02'])
         
     | 
| 
      
 119 
     | 
    
         
            +
              end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
       114 
121 
     | 
    
         
             
              it 'raises ArgumentError if given individually valid but incompatible arguments' do
         
     | 
| 
       115 
122 
     | 
    
         
             
                expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
         
     | 
| 
       116 
123 
     | 
    
         
             
                expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
         
     | 
    
        data/spec/ast/division_spec.rb
    CHANGED
    
    | 
         @@ -20,10 +20,10 @@ describe Dentaku::AST::Division do 
     | 
|
| 
       20 
20 
     | 
    
         
             
                expect(node.value.round(4)).to eq(0.8333)
         
     | 
| 
       21 
21 
     | 
    
         
             
              end
         
     | 
| 
       22 
22 
     | 
    
         | 
| 
       23 
     | 
    
         
            -
              it 'requires  
     | 
| 
      
 23 
     | 
    
         
            +
              it 'requires operands that respond to /' do
         
     | 
| 
       24 
24 
     | 
    
         
             
                expect {
         
     | 
| 
       25 
25 
     | 
    
         
             
                  described_class.new(five, t)
         
     | 
| 
       26 
     | 
    
         
            -
                }.to raise_error(Dentaku::NodeError, /requires  
     | 
| 
      
 26 
     | 
    
         
            +
                }.to raise_error(Dentaku::NodeError, /requires operands/)
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
       28 
28 
     | 
    
         
             
                expression = Dentaku::AST::Multiplication.new(five, five)
         
     | 
| 
       29 
29 
     | 
    
         
             
                group = Dentaku::AST::Grouping.new(expression)
         
     | 
| 
         @@ -44,17 +44,21 @@ describe Dentaku::AST::Division do 
     | 
|
| 
       44 
44 
     | 
    
         
             
                      value + other
         
     | 
| 
       45 
45 
     | 
    
         
             
                    end
         
     | 
| 
       46 
46 
     | 
    
         
             
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  def zero?
         
     | 
| 
      
 49 
     | 
    
         
            +
                    value.zero?
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
       47 
51 
     | 
    
         
             
                end
         
     | 
| 
       48 
52 
     | 
    
         | 
| 
       49 
53 
     | 
    
         
             
                operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
         
     | 
| 
       50 
54 
     | 
    
         
             
                operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
         
     | 
| 
       51 
55 
     | 
    
         | 
| 
       52 
56 
     | 
    
         
             
                expect {
         
     | 
| 
       53 
     | 
    
         
            -
                  described_class.new(operand_five, operand_six)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  described_class.new(operand_five, operand_six).value
         
     | 
| 
       54 
58 
     | 
    
         
             
                }.not_to raise_error
         
     | 
| 
       55 
59 
     | 
    
         | 
| 
       56 
60 
     | 
    
         
             
                expect {
         
     | 
| 
       57 
     | 
    
         
            -
                  described_class.new(operand_five, six)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  described_class.new(operand_five, six).value
         
     | 
| 
       58 
62 
     | 
    
         
             
                }.not_to raise_error
         
     | 
| 
       59 
63 
     | 
    
         
             
              end
         
     | 
| 
       60 
64 
     | 
    
         
             
            end
         
     | 
| 
         @@ -33,13 +33,20 @@ RSpec.describe Dentaku::BulkExpressionSolver do 
     | 
|
| 
       33 
33 
     | 
    
         
             
                  }.to raise_error(Dentaku::UnboundVariableError)
         
     | 
| 
       34 
34 
     | 
    
         
             
                end
         
     | 
| 
       35 
35 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
                it "lets you know if the result is a div/0 error" do
         
     | 
| 
      
 36 
     | 
    
         
            +
                it "lets you know if the result is a div/0 error when dividing" do
         
     | 
| 
       37 
37 
     | 
    
         
             
                  expressions = {more_apples: "1/0"}
         
     | 
| 
       38 
38 
     | 
    
         
             
                  expect {
         
     | 
| 
       39 
39 
     | 
    
         
             
                    described_class.new(expressions, calculator).solve!
         
     | 
| 
       40 
40 
     | 
    
         
             
                  }.to raise_error(Dentaku::ZeroDivisionError)
         
     | 
| 
       41 
41 
     | 
    
         
             
                end
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
      
 43 
     | 
    
         
            +
                it "lets you know if the result is a div/0 error when taking modulo" do
         
     | 
| 
      
 44 
     | 
    
         
            +
                  expressions = {more_apples: "1%0"}
         
     | 
| 
      
 45 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 46 
     | 
    
         
            +
                    described_class.new(expressions, calculator).solve!
         
     | 
| 
      
 47 
     | 
    
         
            +
                  }.to raise_error(Dentaku::ZeroDivisionError)
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
       43 
50 
     | 
    
         
             
                it "does not require keys to be parseable" do
         
     | 
| 
       44 
51 
     | 
    
         
             
                  expressions = { "the value of x, incremented" => "x + 1" }
         
     | 
| 
       45 
52 
     | 
    
         
             
                  solver = described_class.new(expressions, calculator.store("x" => 3))
         
     | 
    
        data/spec/calculator_spec.rb
    CHANGED
    
    | 
         @@ -130,6 +130,7 @@ describe Dentaku::Calculator do 
     | 
|
| 
       130 
130 
     | 
    
         
             
                  expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
         
     | 
| 
       131 
131 
     | 
    
         
             
                  expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
         
     | 
| 
       132 
132 
     | 
    
         
             
                  expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
         
     | 
| 
      
 133 
     | 
    
         
            +
                  expect { calculator.evaluate!('data < 1', data: { a: 5 }) }.to raise_error(Dentaku::ArgumentError)
         
     | 
| 
       133 
134 
     | 
    
         
             
                end
         
     | 
| 
       134 
135 
     | 
    
         | 
| 
       135 
136 
     | 
    
         
             
                it 'raises argument error if a function is called with incorrect arity' do
         
     | 
| 
         @@ -235,6 +236,12 @@ describe Dentaku::Calculator do 
     | 
|
| 
       235 
236 
     | 
    
         
             
                  expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
         
     | 
| 
       236 
237 
     | 
    
         
             
                  expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
         
     | 
| 
       237 
238 
     | 
    
         
             
                end
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
                it "raises an error when trying to find dependencies with invalid syntax" do
         
     | 
| 
      
 241 
     | 
    
         
            +
                  expect { calculator.dependencies('bob + / 3') }.to raise_error(Dentaku::ParseError)
         
     | 
| 
      
 242 
     | 
    
         
            +
                  expect { calculator.dependencies('123 * TRUE') }.to raise_error(Dentaku::ParseError)
         
     | 
| 
      
 243 
     | 
    
         
            +
                  expect { calculator.dependencies('4 + "asdf"') }.to raise_error(Dentaku::ParseError)
         
     | 
| 
      
 244 
     | 
    
         
            +
                end
         
     | 
| 
       238 
245 
     | 
    
         
             
              end
         
     | 
| 
       239 
246 
     | 
    
         | 
| 
       240 
247 
     | 
    
         
             
              describe 'solve!' do
         
     | 
| 
         @@ -531,7 +538,7 @@ describe Dentaku::Calculator do 
     | 
|
| 
       531 
538 
     | 
    
         
             
                  expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
         
     | 
| 
       532 
539 
     | 
    
         
             
                  expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
         
     | 
| 
       533 
540 
     | 
    
         
             
                  expect(calculator.evaluate!('value - duration(1, year)', { value: value })).to eq(Time.local(2022, 7, 13, 10, 42, 11))
         
     | 
| 
       534 
     | 
    
         
            -
                  expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq( 
     | 
| 
      
 541 
     | 
    
         
            +
                  expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(value2 - value)
         
     | 
| 
       535 
542 
     | 
    
         
             
                  expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
         
     | 
| 
       536 
543 
     | 
    
         
             
                end
         
     | 
| 
       537 
544 
     | 
    
         
             
              end
         
     | 
| 
         @@ -166,7 +166,7 @@ describe Dentaku::Calculator do 
     | 
|
| 
       166 
166 
     | 
    
         
             
                  it 'adds multiple functions to default/global function registry' do
         
     | 
| 
       167 
167 
     | 
    
         
             
                    described_class.add_functions([
         
     | 
| 
       168 
168 
     | 
    
         
             
                      [:cube, :numeric, ->(x) { x**3 }],
         
     | 
| 
       169 
     | 
    
         
            -
                      [:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c,i| i.even? ? c.upcase : c.downcase }.join() }],
         
     | 
| 
      
 169 
     | 
    
         
            +
                      [:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c, i| i.even? ? c.upcase : c.downcase }.join() }],
         
     | 
| 
       170 
170 
     | 
    
         
             
                    ])
         
     | 
| 
       171 
171 
     | 
    
         | 
| 
       172 
172 
     | 
    
         
             
                    expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
         
     | 
    
        data/spec/parser_spec.rb
    CHANGED
    
    | 
         @@ -27,6 +27,9 @@ describe Dentaku::Parser do 
     | 
|
| 
       27 
27 
     | 
    
         
             
              it 'calculates bitwise OR' do
         
     | 
| 
       28 
28 
     | 
    
         
             
                node = parse('2|3')
         
     | 
| 
       29 
29 
     | 
    
         
             
                expect(node.value).to eq(3)
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                node = parse('(5 | 2) + 1')
         
     | 
| 
      
 32 
     | 
    
         
            +
                expect(node.value).to eq(8)
         
     | 
| 
       30 
33 
     | 
    
         
             
              end
         
     | 
| 
       31 
34 
     | 
    
         | 
| 
       32 
35 
     | 
    
         
             
              it 'performs multiple operations in one stream' do
         
     | 
| 
         @@ -77,11 +80,17 @@ describe Dentaku::Parser do 
     | 
|
| 
       77 
80 
     | 
    
         
             
              end
         
     | 
| 
       78 
81 
     | 
    
         | 
| 
       79 
82 
     | 
    
         
             
              it 'evaluates arrays' do
         
     | 
| 
      
 83 
     | 
    
         
            +
                node = parse('{}')
         
     | 
| 
      
 84 
     | 
    
         
            +
                expect(node.value).to eq([])
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
       80 
86 
     | 
    
         
             
                node = parse('{1, 2, 3}')
         
     | 
| 
       81 
87 
     | 
    
         
             
                expect(node.value).to eq([1, 2, 3])
         
     | 
| 
       82 
88 
     | 
    
         | 
| 
       83 
     | 
    
         
            -
                node = parse('{}')
         
     | 
| 
       84 
     | 
    
         
            -
                expect(node.value).to eq([])
         
     | 
| 
      
 89 
     | 
    
         
            +
                node = parse('{1, 2, 3} + {4,5,6}')
         
     | 
| 
      
 90 
     | 
    
         
            +
                expect(node.value).to eq([1, 2, 3, 4, 5, 6])
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                node = parse('{1, 2, 3} - {2,3}')
         
     | 
| 
      
 93 
     | 
    
         
            +
                expect(node.value).to eq([1])
         
     | 
| 
       85 
94 
     | 
    
         
             
              end
         
     | 
| 
       86 
95 
     | 
    
         | 
| 
       87 
96 
     | 
    
         
             
              context 'invalid expression' do
         
     | 
    
        data/spec/print_visitor_spec.rb
    CHANGED
    
    | 
         @@ -59,6 +59,11 @@ describe Dentaku::PrintVisitor do 
     | 
|
| 
       59 
59 
     | 
    
         
             
                expect(repr).to eq('2017-12-24 23:59:59')
         
     | 
| 
       60 
60 
     | 
    
         
             
              end
         
     | 
| 
       61 
61 
     | 
    
         | 
| 
      
 62 
     | 
    
         
            +
              it 'handles a percentage in a formula' do
         
     | 
| 
      
 63 
     | 
    
         
            +
                repr = roundtrip('((3*4%) * 0.001)')
         
     | 
| 
      
 64 
     | 
    
         
            +
                expect(repr).to eq('3 * 4% * 0.001')
         
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
       62 
67 
     | 
    
         
             
              private
         
     | 
| 
       63 
68 
     | 
    
         | 
| 
       64 
69 
     | 
    
         
             
              def roundtrip(string)
         
     | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    | 
         @@ -1,14 +1,12 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            require 'pry'
         
     | 
| 
       2 
2 
     | 
    
         
             
            require 'simplecov'
         
     | 
| 
       3 
     | 
    
         
            -
            require 'codecov'
         
     | 
| 
       4 
3 
     | 
    
         | 
| 
       5 
4 
     | 
    
         
             
            SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
         
     | 
| 
       6 
5 
     | 
    
         
             
              SimpleCov::Formatter::HTMLFormatter,
         
     | 
| 
       7 
     | 
    
         
            -
              SimpleCov::Formatter::Codecov,
         
     | 
| 
       8 
6 
     | 
    
         
             
            ])
         
     | 
| 
       9 
7 
     | 
    
         | 
| 
       10 
8 
     | 
    
         
             
            SimpleCov.minimum_coverage 90
         
     | 
| 
       11 
     | 
    
         
            -
            SimpleCov.minimum_coverage_by_file 80
         
     | 
| 
      
 9 
     | 
    
         
            +
            # SimpleCov.minimum_coverage_by_file 80
         
     | 
| 
       12 
10 
     | 
    
         | 
| 
       13 
11 
     | 
    
         
             
            SimpleCov.start do
         
     | 
| 
       14 
12 
     | 
    
         
             
              add_filter "spec/"
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: dentaku
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 3.5. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 3.5.6
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Solomon White
         
     | 
| 
       8 
     | 
    
         
            -
            autorequire:
         
     | 
| 
       9 
8 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
9 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date:  
     | 
| 
      
 10 
     | 
    
         
            +
            date: 2025-10-20 00:00:00.000000000 Z
         
     | 
| 
       12 
11 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
12 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
13 
     | 
    
         
             
              name: bigdecimal
         
     | 
| 
         @@ -38,20 +37,6 @@ dependencies: 
     | 
|
| 
       38 
37 
     | 
    
         
             
                - - ">="
         
     | 
| 
       39 
38 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       40 
39 
     | 
    
         
             
                    version: '0'
         
     | 
| 
       41 
     | 
    
         
            -
            - !ruby/object:Gem::Dependency
         
     | 
| 
       42 
     | 
    
         
            -
              name: codecov
         
     | 
| 
       43 
     | 
    
         
            -
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
       44 
     | 
    
         
            -
                requirements:
         
     | 
| 
       45 
     | 
    
         
            -
                - - ">="
         
     | 
| 
       46 
     | 
    
         
            -
                  - !ruby/object:Gem::Version
         
     | 
| 
       47 
     | 
    
         
            -
                    version: '0'
         
     | 
| 
       48 
     | 
    
         
            -
              type: :development
         
     | 
| 
       49 
     | 
    
         
            -
              prerelease: false
         
     | 
| 
       50 
     | 
    
         
            -
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
       51 
     | 
    
         
            -
                requirements:
         
     | 
| 
       52 
     | 
    
         
            -
                - - ">="
         
     | 
| 
       53 
     | 
    
         
            -
                  - !ruby/object:Gem::Version
         
     | 
| 
       54 
     | 
    
         
            -
                    version: '0'
         
     | 
| 
       55 
40 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       56 
41 
     | 
    
         
             
              name: pry
         
     | 
| 
       57 
42 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -157,6 +142,8 @@ executables: [] 
     | 
|
| 
       157 
142 
     | 
    
         
             
            extensions: []
         
     | 
| 
       158 
143 
     | 
    
         
             
            extra_rdoc_files: []
         
     | 
| 
       159 
144 
     | 
    
         
             
            files:
         
     | 
| 
      
 145 
     | 
    
         
            +
            - ".github/workflows/rspec.yml"
         
     | 
| 
      
 146 
     | 
    
         
            +
            - ".github/workflows/rubocop.yml"
         
     | 
| 
       160 
147 
     | 
    
         
             
            - ".gitignore"
         
     | 
| 
       161 
148 
     | 
    
         
             
            - ".pryrc"
         
     | 
| 
       162 
149 
     | 
    
         
             
            - ".rubocop.yml"
         
     | 
| 
         @@ -289,7 +276,6 @@ homepage: http://github.com/rubysolo/dentaku 
     | 
|
| 
       289 
276 
     | 
    
         
             
            licenses:
         
     | 
| 
       290 
277 
     | 
    
         
             
            - MIT
         
     | 
| 
       291 
278 
     | 
    
         
             
            metadata: {}
         
     | 
| 
       292 
     | 
    
         
            -
            post_install_message:
         
     | 
| 
       293 
279 
     | 
    
         
             
            rdoc_options: []
         
     | 
| 
       294 
280 
     | 
    
         
             
            require_paths:
         
     | 
| 
       295 
281 
     | 
    
         
             
            - lib
         
     | 
| 
         @@ -304,8 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       304 
290 
     | 
    
         
             
                - !ruby/object:Gem::Version
         
     | 
| 
       305 
291 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       306 
292 
     | 
    
         
             
            requirements: []
         
     | 
| 
       307 
     | 
    
         
            -
            rubygems_version: 3. 
     | 
| 
       308 
     | 
    
         
            -
            signing_key:
         
     | 
| 
      
 293 
     | 
    
         
            +
            rubygems_version: 3.6.2
         
     | 
| 
       309 
294 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       310 
295 
     | 
    
         
             
            summary: A formula language parser and evaluator
         
     | 
| 
       311 
296 
     | 
    
         
             
            test_files:
         
     |