Search code examples
datetimetimezoneutcelm

Given an IANA Timezone and a local time, parse it into a Maybe Posix?


Is there a way in Elm to take a local time (such as the string 2019-03-18T09:10:12.4; no offset specifed) and a timezone (such as Australia/Sydney) to a possible Posix value (i.e, that time converted to UTC), without using ports?


There's waratuman/time-extra, but it seems to only work on the Date portion. And sadly rtfeldman/elm-iso8601-date-strings doesn't take timezones.

In JS, there's options such as moment-tz and date-fns-timezone, but it would be much simpler to avoid JS interop for frequent date parsing.


Solution

  • By combining justinmimbs/time-extra and justinmimbs/timezone-data, you should be able to get valid posix entirely in Elm.

    Demo: https://ellie-app.com/54tyw9yvsQga1


    First, you need to convert your timestampWithoutTimezone to Parts:

    toParts : String -> Maybe Parts
    toParts timestampWithoutTimezone =
        timestampWithoutTimezone
            |> Regex.find regex
            |> List.map .submatches
            |> List.head
            |> Maybe.andThen Maybe.Extra.combine
            |> Maybe.andThen listToParts
    
    regex : Regex
    regex =
        Maybe.withDefault Regex.never <|
            Regex.fromString "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})\\.(\\d)$"
    
    
    monthLookup : Dict String Month
    monthLookup =
        Dict.fromList [ ( "01", Jan ), ( "02", Feb ), ( "03", Mar ), ( "04", Apr ), ( "05", May ), ( "06", Jun ), ( "07", Jul ), ( "08", Aug ), ( "09", Sep ), ( "10", Oct ), ( "11", Nov ), ( "12", Dec ) ]
    
    
    listToParts : List String -> Maybe Parts
    listToParts list =
        let
            toInt : Int -> Maybe Int
            toInt index =
                list |> List.Extra.getAt index |> Maybe.andThen String.toInt
        in
        Maybe.map2 Parts
            (toInt 0)
            (list |> List.Extra.getAt 1 |> Maybe.andThen (\month -> Dict.get month monthLookup))
            |> Maybe.andThen (\parts -> Maybe.map5 parts (toInt 2) (toInt 3) (toInt 4) (toInt 5) (toInt 6))
    

    Then, using partsToPosix with the appropriate Zone you can get a posix value:

    toPosix : Time.Zone -> String -> Maybe Posix
    toPosix zone timestampWithoutTimezone =
        timestampWithoutTimezone
            |> toParts
            |> Maybe.map (Time.Extra.partsToPosix zone)
    

    The library author recommends you store evaluated Zone values in your model:

    model = { zone = TimeZone.australia__sydney () }
    
    toPosix model.zone "2019-03-18T09:10:12.4"