3
\$\begingroup\$

I have a problem similar to the question, namely reading in text which describes a date - in various formats. I want to use the parseTimeM function from Date.Time module in time package.

My current solution can probably be improved style wise but it should remain easy to read and easy to extend. Suggestion?

readDate3 :: Text -> UTCTime
readDate3 datestring =
 case shortMonth of
 Just t -> t
 Nothing -> case longMonth of
 Just t2 -> t2
 Nothing -> case monthPoint of
 Just t3 -> t3
 Nothing -> case germanNumeralShort of
 Just t3 -> t3
 Nothing -> case germanNumeral of
 Just t3 -> t3
 Nothing -> case isoformat of
 Just t4 -> t4
 Nothing -> errorT ["readDate3", datestring, "is not parsed"]
 where
 shortMonth = parseTimeM True defaultTimeLocale
 "%b %-d, %Y" (t2s datestring) :: Maybe UTCTime
 longMonth = parseTimeM True defaultTimeLocale
 "%B %-d, %Y" (t2s datestring) :: Maybe UTCTime
 monthPoint = parseTimeM True defaultTimeLocale
 "%b. %-d, %Y" (t2s datestring) :: Maybe UTCTime
 germanNumeral = parseTimeM True defaultTimeLocale
 "%-d.%-m.%Y" (t2s datestring) :: Maybe UTCTime
 germanNumeralShort = parseTimeM True defaultTimeLocale
 "%-d.%-m.%y" (t2s datestring) :: Maybe UTCTime
 isoformat = parseTimeM True defaultTimeLocale
 "%Y-%m-%d" (t2s datestring) :: Maybe UTCTime
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Mar 16, 2019 at 11:30
\$\endgroup\$
4
  • \$\begingroup\$ I think you can use the >>= combinator instead of this huge case expression. \$\endgroup\$ Commented Mar 16, 2019 at 11:48
  • \$\begingroup\$ I tried but did not succeed (I nearly never use the >>= constructors. Can you show me the start of the chain? \$\endgroup\$ Commented Mar 16, 2019 at 11:53
  • 1
    \$\begingroup\$ @ForceBru That's not applicable in this case - >>= continues the computation on Just, whereas here we need to continue it on Nothing. \$\endgroup\$ Commented Mar 16, 2019 at 11:59
  • \$\begingroup\$ @user855443 Small detail: >>= is a function, not a constructor. I know that's nitpicking a bit, but these details can start to matter in more advanced stuff (e.g. you can pattern-match on constructors but not on functions). \$\endgroup\$ Commented Mar 16, 2019 at 13:10

2 Answers 2

4
\$\begingroup\$

Don't repeat yourself

Your code violates the DRY principle. If we replace

germanNumeralShort = parseTimeM True defaultTimeLocale
 "%-d.%-m.%y" (t2s datestring) :: Maybe UTCTime
isoformat = parseTimeM True defaultTimeLocale
 "%Y-%m-%d" (t2s datestring) :: Maybe UTCTime
...

with

parse format = parseTimeM True defaultTimeLocale format (t2s datestring)
germanNumeralShort = parse "%-d.%-m.%y"
isoformat = parse "%Y-%m-%d"
...

then we immediately notice that we use parse on all formats after another till we find a suitable one.

This can be modelled with map parse, e.g.

map parse
 [ "%b %-d, %Y"
 , "%B %-d, %Y"
 , "%b. %-d, %Y"
 , "%-d.%-m.%Y"
 , "%-d.%-m.%y"
 , "%Y-%m-%d"
 ]

We could use <|> alternatively, e.g.

parse "%b %-d, %Y" <|> parse "%B %-d, %Y" <|> ...

but that's less flexible than the list approach.

Use asum to get a single Maybe from a list of Maybes

To get a single Maybe a from [Maybe a], we can use asum. To get the errorT, we just need to pattern match on a single result and end up with

readDate3 :: Text -> UTCTime
readDate3 datestring = case result of
 Nohing -> errorT ["readDate3", datestring, "is not parsed"]
 Just t -> t
 where 
 parse format = parseTimeM True defaultTimeLocale format (t2s datestring) 
 result = asum . map parse $
 [ "%b %-d, %Y"
 , "%B %-d, %Y"
 , "%b. %-d, %Y"
 , "%-d.%-m.%Y"
 , "%-d.%-m.%y"
 , "%Y-%m-%d"
 ]

As the current strings are missing some documentation, we could introduce additional types to remedy that:

data DateFormat = ShortMonth
 | LongMonth
 | MonthPoint
 | GermanNumeral
 | GermanNumeralShort
 | ISOFormat
toFormatString :: DateFormat -> String
toFormatString f = case f of
 ShortMonth -> "%b %-d, %Y"
 LongMonth -> "%B %-d, %Y"
 MonthPoint -> "%b. %-d, %Y"
 -- other left as an exercise

We can also use fromMaybe to get rid of the last pattern match and end up with

import Data.Foldable (asum)
import Data.Maybe (fromMaybe)
readDate :: Text -> UTCTime
readDate datestring = 
 fromMaybe (errorT ["readDate", datestring, "is not parsed"]) $
 asum . map parse $
 [ ShortMonth
 , LongMonth
 , MonthPoint
 , GermanNumeral
 , GermanNumeralShort
 , ISOFormat
 ]
 where 
 parse format = parseTimeM True defaultTimeLocale (toFormatString format) (t2s datestring) 
answered Mar 16, 2019 at 13:13
\$\endgroup\$
3
  • \$\begingroup\$ The final pattern match can be replaced by fromMaybe. \$\endgroup\$ Commented Mar 16, 2019 at 13:18
  • \$\begingroup\$ @melpomene added that; it has been a while since I answered Haskell questions and used the language, thanks. \$\endgroup\$ Commented Mar 16, 2019 at 13:22
  • \$\begingroup\$ @4castle thanks for the edit. I don't have a programming environment or editor at the moment at hand (not even a spell checker). \$\endgroup\$ Commented Mar 16, 2019 at 13:32
2
\$\begingroup\$

Define an auxilliary function:

replaceIfNothing :: Maybe a -> a -> a
replaceIfNothing (Just x) _ = x
replaceIfNothing Nothing x = x

And then you can do:

replaceIfNothing shortMonth $
replaceIfNothing longMonth $
replaceIfNothing monthPoint $
-- I think you get the idea now

You can also do it as an operator, which I personally think is nicer:

(&>) :: Maybe a -> a -> a
(&>) (Just x) _ = x
(&>) Nothing x = x
infixr 1 &>
shortMonth &> longMonth &> monthPoint &> ...

Of course, since this is Haskell, a quick search shows that this is just Data.Maybe.fromMaybe with the arguments reversed; you can thus just define replaceIfNothing = flip fromMaybe (although you have to do import Data.Maybe first). It's possible to use fromMaybe directly as well, although it feels a little clumsy:

flip fromMaybe shortMonth $
flip fromMaybe longMonth $
flip fromMaybe monthPoint $
...
answered Mar 16, 2019 at 12:08
\$\endgroup\$
7
  • 2
    \$\begingroup\$ Why not just shortMonth <|> longMonth <|> monthPoint <|> ... (using <|> from Control.Applicative)? \$\endgroup\$ Commented Mar 16, 2019 at 12:37
  • \$\begingroup\$ @melpomene Yes, that's even better! I completely forgot about (<|>)! I'll edit that in now. \$\endgroup\$ Commented Mar 16, 2019 at 12:39
  • \$\begingroup\$ @melpomene After a bit more thought, it actually turns out that (<|>) is not exactly the same as what I've defined: I defined (&>) :: Maybe a -> a -> a, whereas (<|>) :: Maybe a -> Maybe a -> Maybe a. \$\endgroup\$ Commented Mar 16, 2019 at 12:42
  • 1
    \$\begingroup\$ <|> can still be used, you would just unwrap the end result with fromMaybe (for the error case) \$\endgroup\$ Commented Mar 16, 2019 at 13:03
  • \$\begingroup\$ @4castle That is definitely one way to go about it. I do think that approach is slightly more boilerplate-ey than necessary though; compare a &> b &> c to fromJust $ a <|> b <|> Just c. \$\endgroup\$ Commented Mar 16, 2019 at 13:08

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.