Search code examples
f#comparison

How to determine a single property difference between two types?


I have been looking for a more concise way to code this (below). Basically, I need a way to determine if only one of a set of properties does not equal its counterpart on a different type (address and alternateAddress are different types). If only ONE of the 5 values is doesn't match then I want a specific error, otherwise I want a more general error.

Note the == and != are custom infix operators we have for case-insensitive comparisons.

   open System

   type Errors =
   | InvalidStreet
   | InvalidCity
   | InvalidState
   | InvalidPostalCode
   | InvalidCountry
   | InvalidAddress

   type Address =
       {
           Street: string
           City: string
           Region: string
           PostalCode: string
           Country: string
       }

   type AlternateAddress =
       {
           Street: string
           City: string
           Region: string
           PostalCode: string
           Country: string
       }

   let inline (==) (s1: string) (s2: string) = s1.Equals(s2, StringComparison.CurrentCultureIgnoreCase)
   let inline (!=) s1 s2 = s1 == s2 |> not

   let address = {Address.Street = "123 Main St."; City = "Happytown"; Region = "CA"; PostalCode = "90210"; Country = "USA"}
   let alternateAddress = Some {AlternateAddress.Street = "123 Main"; City = "Happytown"; Region = "CA"; PostalCode = "90210"; Country = "USA"}

   match alternateAddress with
   | Some alternateAddress ->
       if
              address.Street      != alternateAddress.Street
           && address.City        == alternateAddress.City
           && address.Region      == alternateAddress.Region
           && address.PostalCode  == alternateAddress.PostalCode
           && address.Country     == alternateAddress.Country
       then InvalidStreet
       elif
              address.Street      == alternateAddress.Street
           && address.City        != alternateAddress.City
           && address.Region      == alternateAddress.Region
           && address.PostalCode  == alternateAddress.PostalCode
           && address.Country     == alternateAddress.Country
       then InvalidCity
       elif
              address.Street       == alternateAddress.Street
           && address.City         == alternateAddress.City
           && address.Region       != alternateAddress.Region
           && address.PostalCode   == alternateAddress.PostalCode
           && address.Country      == alternateAddress.Country
       then InvalidState
       elif
              address.Street       == alternateAddress.Street
           && address.City         == alternateAddress.City
           && address.Region       == alternateAddress.Region
           && address.PostalCode   != alternateAddress.PostalCode
           && address.Country      == alternateAddress.Country
       then InvalidPostalCode
       elif
              address.Street       == alternateAddress.Street
           && address.City         == alternateAddress.City
           && address.Region       == alternateAddress.Region
           && address.PostalCode   == alternateAddress.PostalCode
           && address.Country      != alternateAddress.Country
       then InvalidCountry
       else InvalidAddress
   | _ -> InvalidAddress

Solution

  • One way to do this would be to define a list of corresponding "getters" (functions that take one of your records and return a string) for each of the fields together with associated error. For simplicity, I made alternateAddress non-optional:

    let comparers : ((Address -> _) * (AlternateAddress -> _) * _) list = 
      [ (fun x -> x.Street), (fun x -> x.Street), InvalidStreet;
        (fun x -> x.City), (fun x -> x.City), InvalidStreet 
        (fun x -> x.Region), (fun x -> x.Region), InvalidStreet 
        (fun x -> x.PostalCode), (fun x -> x.PostalCode), InvalidStreet 
        (fun x -> x.Country), (fun x -> x.Country), InvalidStreet ]
    

    Now, you can iterate over comparers and use the first function to get a field from Address, second function to get a field from AlternateAddress and if they do not match, return the third element of the tuple, which is the error to report.

    You can use List.choose to get a list that is empty when all fields match and, otherwise, contains a list of errors when there were mismatching fields:

    let errors = comparers |> List.choose (fun (getAddr, getAlt, err) ->
      if getAddr address != getAlt alternateAddress then Some err else None)
    

    The errors list will be empty if there are no issues, contain one error if there was just one error or contain multiple errors:

    match errors with
    | [] -> printfn "All good!"
    | [err] -> printfn "One error: %A" err
    | _ -> printfn "Multiple errors!"
    

    It is worth noting that depending on your particular situation, it might perhaps be a good idea to restructure the code to make this operation easier - but it's hard to say without knowing more about your situation.