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
2 Answers 2
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 Maybe
s
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)
-
\$\begingroup\$ The final pattern match can be replaced by
fromMaybe
. \$\endgroup\$melpomene– melpomene2019年03月16日 13:18:14 +00:00Commented 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\$Zeta– Zeta2019年03月16日 13:22:41 +00:00Commented 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\$Zeta– Zeta2019年03月16日 13:32:20 +00:00Commented Mar 16, 2019 at 13:32
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 $
...
-
2\$\begingroup\$ Why not just
shortMonth <|> longMonth <|> monthPoint <|> ...
(using<|>
from Control.Applicative)? \$\endgroup\$melpomene– melpomene2019年03月16日 12:37:29 +00:00Commented Mar 16, 2019 at 12:37 -
\$\begingroup\$ @melpomene Yes, that's even better! I completely forgot about
(<|>)
! I'll edit that in now. \$\endgroup\$bradrn– bradrn2019年03月16日 12:39:12 +00:00Commented 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\$bradrn– bradrn2019年03月16日 12:42:14 +00:00Commented Mar 16, 2019 at 12:42 -
1\$\begingroup\$
<|>
can still be used, you would just unwrap the end result withfromMaybe
(for the error case) \$\endgroup\$castletheperson– castletheperson2019年03月16日 13:03:47 +00:00Commented 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
tofromJust $ a <|> b <|> Just c
. \$\endgroup\$bradrn– bradrn2019年03月16日 13:08:51 +00:00Commented Mar 16, 2019 at 13:08
>>=
combinator instead of this hugecase
expression. \$\endgroup\$>>=
constructors. Can you show me the start of the chain? \$\endgroup\$>>=
continues the computation onJust
, whereas here we need to continue it onNothing
. \$\endgroup\$>>=
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\$