colinfang
colinfang

Reputation: 21727

How to pattern match this efficiently?

I would like to match an integer array against the following pseudo patterns: where a means these numbers are the equal or 0 (i.e. assume any number "equals" 0)

[| a; a; a; a; a; |]          // matches [| 1; 1; 0; 0; 1 |]
[| a; a; a; not a; a; |]      // matches [| 3; 3; 0; 2; 3 |]
[| a; a; not a; a; a; |]      // matches [| 4; 4; 2; 0; 4 |]
[| a; not a; a; a; not a; |]  // matches [| 1; 2; 0; 0; 3 |]
[| a; a; a; not a; a; |]      // matches [| 1; 1; 0; 4; 1 |]
[| not a; a; a; a; a; |]      // matches [| 5; 1; 0; 1; 1 |]

How do I do this?

Upvotes: 4

Views: 317

Answers (3)

Tomas Petricek
Tomas Petricek

Reputation: 243051

You can define an active pattern that is parameterized by a pattern that you want to detect. In your example, you write these patterns using a pseudo-syntax with array containing either a or not a, but you could quite easily write them as actual arrays that contain the same value when you want to have matching values in the input. For example, something like:

[| 'a'; 'a'; 'b'; 'a'; 'a'; |]

It could even be a string as in @bytebuster's solution (you can convert string to char[] using the ToCharArray method). Then you can define active pattern:

let inline (|ArrayPattern|_|) pattern input =
  let matches =
    Array.zip pattern input
    |> Seq.groupBy fst 
    |> Seq.forall (fun (_, values) -> 
        let cases = values |> Seq.map snd |> set
        Set.count (cases - Set.singleton 0) <= 1)
  if matches then Some() else None  

I guess this is essentially the same as what @bytebuster implements. Here, I group the elements of the input array by their corresponding key in the pattern and then use set operations to check that there is at most one non-zero value for each group.

To use it, you can now match an int[] against ArrayPattern [| ... |] where the array parameter specifies the specific pattern you're looking for:

match [| 1; 1; 2; 0; 1 |] with 
| ArrayPattern [| 'a'; 'a'; 'b'; 'a'; 'a'; |] -> printfn "match"
| _ -> printfn "not"

Upvotes: 2

Be Brave Be Like Ukraine
Be Brave Be Like Ukraine

Reputation: 7735

I think, it is just a computational task, so it can't look very beautiful. Nevertheless, here's my try. Note I used a string for "a" versus "not a" for better readability.

// Define possible return values
type TriState<'T> = | Yes of 'T | No of 'T | Unknown with 
    override this.ToString() =
        match this with
        | Unknown   -> "Unknown"
        | Yes value -> sprintf "Array matches, common value %A" value
        | No value  -> sprintf "Array mismatches, common value %A" value

// a boilerplate function
let matchA (pat: string) xs =
    let mustBeA, mustNotBeA =
        List.zip
            (xs |> Array.toList)
            (pat.ToCharArray() |> Array.toList)
        |> List.partition (snd >> ( (=) 'A'))
    // these values must be all equal
    let mustBeA' =
        mustBeA |> List.map fst |> List.filter ( (<>) 0)
    // these values must NOT be equal to the value taken from above
    let mustNotBeA' =
        mustNotBeA |> List.map fst
    match mustBeA' with
    | []            -> Unknown   // can't find the "must" value
                                 // due all "must" values are zero
    | mustValue::_  ->           // we have bootstrap value to compare against
        if (List.forall ( (=) mustValue) mustBeA')
            && (List.forall ( (<>) mustValue) mustNotBeA')
        then Yes mustValue
        else No mustValue

Now, testing:

[| 1; 2; 0; 0; 3 |] |> matchA "ABAAB" |> printf "%A\n" // Yes 1
[| 4; 4; 2; 0; 4 |] |> matchA "AABAA" |> printf "%A\n" // Yes 4
[| 5; 1; 0; 1; 1 |] |> matchA "BAAAA" |> printf "%A\n" // Yes 1

Of course, you can wrap it into a parameterized Active Pattern:

let (|MatchesA|_|) pattern arr =
    matchA pattern arr |> function | Yes x -> Some x | _ -> None

match [| 1; 2; 0; 0; 3 |] with
| MatchesA ("AAAAB") x -> sprintf "Pattern1 %d" x
| MatchesA ("ABAAB") x -> sprintf "Pattern2 %d" x
| _                    -> "Unknown"
|> printf "%A\n"

Upvotes: 1

pad
pad

Reputation: 41290

In your examples, the first element of the array is always a you can match on a boolean array:

match Array.map (fun x -> x = Array.get arr 0 || x = 0) arr with
| [| true; true; true; true; true; |] -> ... // matches [| 1; 1; 0; 0; 1 |]
| [| true; true; true; false; true; |] -> ... // matches [| 3; 3; 0; 2; 3 |]
| [| true; true; false; true; true; |] -> ... // matches [| 4; 4; 2; 0; 4 |]
| [| true; false; true; true; false; |] -> ... // matches [| 1; 2; 0; 0; 3 |]
| [| true; true; true; false; true; |] -> ... // matches [| 1; 1; 0; 4; 1 |]
| _ -> ...

This will create a temporary array, but it isn't a big deal with arrays of small sizes.

UPDATE:

If the first a is in another column, you simply project on that column and do another pattern matching (there are at most 5 matches for 5 columns).

match Array.map (fun x -> x = Array.get arr 1 || x = 0) arr with
| [| false; true; true; true; true; |] -> ... // matches [| 5; 1; 0; 1; 1 |]
| ...

That said, when the number and the complexity of pseudo-patterns grow, it's better to use functions instead of pattern matching.

Upvotes: 5

Related Questions