Search code examples
regexrubyincludepattern-matching

What might be a clean Rubyist way of checking an array's contents against partially known values?


I have a 3d array, which I need to check the 2nd 'dimension' of against a partially known value. Illustration of array composition below:

a = [[[1,1], nil]], [[2,3], [4,8]], [[6,1],[9,9]], [[5,7], nil]]]

I need to check this array for presence of an array containing two values, the first of which I'll know the exact contents of, but the second of which all I know is that it'll either be nil or a 2 element array with unknown single digit values. In pseudo code what I'm trying to do is; a.include?([[1,1],*unknown*]).

Maybe regular expression matcher is the way to go here (?). Perhaps something like;

a.include?([[1,1],/\[\d,\d\]||(nil)]) (I'm trying to say either; [any single 0-9 digit, any single 0-9 digit] or 'nil')

Any help appreciated.


Solution

  • While "Find Pattern" is still experimental Your use case is perfect for Pattern matching

    For Example:

    arr = [[[1,1], nil], [[2,3], [4,8]], [[6,1], [9,9]],
           [[2,3], [3,7]], [[5,7], nil],[[7,9],[42,1]]]
    
    def matches_known_pattern?(arr,known)  
      arr in [*,[^known,nil|[0..9,0..9]],*]
    end 
    
    matches_known_pattern?(arr,[1,1]) #=> true
    matches_known_pattern?(arr,[2,3]) #=> true 
    matches_known_pattern?(arr,[7,8]) #=> false
    matches_known_pattern?(arr,[7,9]) #=> false because 42 does not match 0..9
    matches_known_pattern?(arr,[1,'a']) #=> false
    

    The pattern is fairly self explanatory but it breaks down as a[0] == known and a[1] == nil or a[1] is an array of any 2 Integers Numerics (Including Integer, Float, etc.) between 0 and 9. If you want to allow any Integer you can replace [0..9,0..9] with [Integer,Integer]. If you don't care at all what a[1] is then you can use * (which comes reasonably close to your proposed pattern of [[1,1],*unknown*])

    The [*,...,*] on either side is the "find pattern" part that is experimental. Essentially it just means find this pattern anywhere in arr (first match wins).

    One other note is ^known because pattern matching allows for variable binding without the caret (^) the variable known would be assigned the value of the first element (a[0][0]) in the pattern. The ^ "pins" the local variable known or as described in the docs "For this case, the pin operator ^ can be used, to tell Ruby 'just use this value as part of the pattern'"

    Thank you @steenslag for pointing out that Find Pattern is no longer experimental

    UPDATE To cover the case @CarySwoveland pointed out ([[8,8],[1,4.6]]) this would need to be changed to:

    case arr 
    in [*,[^known,[Integer=>a,Integer=>b]],*]
        (0..9).cover?(a) && (0..9).cover?(b)
    in [*,[^known,nil],*]
        true
    else 
        false 
    end 
    

    Where the Integer and nil checks are separated because variable binding is not available for alternate expressions (separated with |)