Reputation: 6888
I am new to haskell and wanted to approach learngin by attempting bits of a real world application. One of the components is being able to parse dates in ISO formatted strings into it's components. This Stack Overflow post helped me get started, but it isn't enough and I am quite confused.
I have the following code:
import System.Locale
import Data.Time
import Data.Time.Format
data IsoDate = IsoDate {
year :: Int
, month :: Int
, day :: Int
} deriving (Show)
parseIsoDate :: String -> IsoDate
parseIsoDate dateString =
IsoDate year month day
where
timeFromString = readTime defaultTimeLocale "%Y %m %d" dateString :: UTCTime
year = 2013
month = 10
day = 31
Which is fine and dandy for Halloween 2013. I have attmepted to rewrite year as:
year = formatTime defaultTimeLocale "%y" timeFromString
which I knew would fail (can't construct my IsoDate
type with a String
). And then attempted to read the string into an Int.
year = read (formatTime defaultTimeLocale "%y" timeFromString)
with the following response:
parseIsoDate "2012-12-23"
IsoDate {year = *** Exception: readsTime: bad input "2012-12-23"
There were a couple other attemps at getting this converted - but what I posted was the most rational attempt, o I am not going to post the other attempts.
I wanted to figure out how to work with my current code (as I am trying to learn the constructs), in addition (since date parsing is essential) I would like to know the better way (perhaps the most idiomatic) to handle this in Haskell.
Upvotes: 1
Views: 319
Reputation: 15967
I think the stuff you need to do is
parseIsoDate :: String -> Maybe IsoDate
because not every String
you supply will be a valid date. Implementing it you already got most of the ingredients right, but I don't think yo uwant to parse a UTCTime
but a Day
which can be converted into your data-structure.
import Data.Time
data IsoDate = ...
parseIsoDate :: String -> Maybe IsoDate
parseIsoDate str = do julianDay <- parse str
let (y, m, d) = toGregorian julianDay
return $ IsoDate (fromIntegral y) m d
where parse:: String -> Maybe Day
parse = parseTimeM True defaultTimeLocale "%F"
now a bit explaining and advice:
I would change the datatype IsoDate
to using Integer
for years - as they are possibly big (at least bigger than Int
- just look at the age of our universe). this is also the choice of the result of toGregorian
which converts a Day -> (Integer, Int, Int)
, if not you have to convert the Integer
produced by it to an Int
with the help of fromIntegral
as you see in my example.
the syntax I use is called do-syntax for Maybe
, which is a handy thing in the first line I extract a value inside the monad and bind it to a name - julianDay
.
Then I transform the value to a Gregorian Day.
And then return
it into the Maybe
again. If the first step fails and produces a Nothing
, i.e. the String
is just gobbledygook, then none of the other operations are done and your program finishes without doing any work (that's the power of lazy evaluation).
If you are using the RecordWildCards
extension, and the fact that maybe is a Functor
you can do the following
{-# LANGUAGE Record
module MyLib
import Data.Time
data IsoDate = IsoDate { year :: Integer
, month :: Int
, day :: Int}
deriving (Show)
parseIsoDate :: String -> Maybe IsoDate
parseIsoDate str = do (year, month, day) <- toGregorian <$> parse str
return IsoDate{..}
where parse:: String -> Maybe Day
parse = parseTimeM True defaultTimeLocale "%F"
Upvotes: 1
Reputation: 6888
Here is one answer:
data IsoDate = IsoDate {
year :: Int
, month :: Int
, day :: Int
} deriving (Show)
parseIsoDate :: String -> IsoDate
parseIsoDate dateString =
IsoDate year month day
where
timeFromString = readTime defaultTimeLocale "%Y %m %d" dateString :: UTCTime
year = read (formatTime defaultTimeLocale "%0Y" timeFromString) :: Int
month = read (formatTime defaultTimeLocale "%m" timeFromString) :: Int
day = read (formatTime defaultTimeLocale "%d" timeFromString) :: Int
Which refactors to:
data DatePart = Year | Month | Day deriving(Enum, Show)
datePart :: DatePart -> UTCTime -> Int
datePart Year utcTime = read (formatTime defaultTimeLocale "%0Y" utcTime)
datePart Month utcTime = read (formatTime defaultTimeLocale "%m" utcTime)
datePart Day utcTime = read (formatTime defaultTimeLocale "%d" utcTime)
parseIsoDate :: String -> IsoDate
parseIsoDate dateString =
IsoDate year month day
where
timeFromString = readTime defaultTimeLocale "%Y %m %d" dateString :: UTCTime
year = datePart Year timeFromString
month = datePart Month timeFromString
day = datePart Day timeFromString
in usage
parseIsoDate "2012 12 02"
THat data isn't in ISO format, still working to get it to read "2012-12-01". Also still looking for the preferred way of getting this to work within the language.
update the dashes are trivial change "%Y %m %d" to "%Y-%m-%d" I thought I had tried that eariler, but it must have been with other code in error.
Upvotes: 1