akaphenom
akaphenom

Reputation: 6888

Parse an ISO formatted date string into its components

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

Answers (2)

epsilonhalbe
epsilonhalbe

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:

  1. 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.

  2. 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).

update

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

akaphenom
akaphenom

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

Related Questions