Haskell date parsing and formatting

16,155

Solution 1

Here is some old code that contains two types of homemade dates, dates with just YMD, no time or timezones, etc.

It shows how to parse strings into dates using readDec. See the parseDate function. With readDec, read the number, it doesn't matter about leading spaces(because of filter) or leading zeros,and the parse stops at the first non-digit. Then used tail (to skip the non digit) to get to the next numerical field of the date.

It shows several ways of formatting for output, but the most flexible way is to use Text.printf. See instance Show LtDate. With printf, anything is possible!

import Char
import Numeric
import Data.Time.Calendar
import Data.Time.Clock
import Text.Printf
-- ================================================================
--                        LtDate
-- ================================================================
type Date=(Int,Int,Int)
data LtDate = LtDate 
  { ltYear :: Int,
    ltMonth:: Int,
    ltDay  :: Int
  } 
instance Show LtDate 
  where show d = printf "%4d-%02d-%02d" (ltYear d) (ltMonth d) (ltDay d)

toLtDate :: Date -> LtDate
toLtDate (y,m,d)= LtDate y m d

-- =============================================================
--                         Date
-- =============================================================
-- | Parse a String mm/dd/yy into tuple (y,m,d)
-- accepted formats
--
-- @
-- 12\/01\/2004
-- 12\/ 1\' 4
-- 12-01-99
-- @
parseDate :: String -> Date
parseDate s = (y,m,d)
    where [(m,rest) ] = readDec (filter (not . isSpace) s)
          [(d,rest1)] = readDec (tail rest)
          [(y, _)   ] = parseDate' rest1

-- | parse the various year formats used by Quicken dates
parseDate':: String -> [(Int,String)]
parseDate' (y:ys) =
  let [(iy,rest)] = readDec ys
      year=case y of '\''      -> iy + 2000
                     _  ->
                       if iy < 1900 then  iy + 1900 else iy
   in [(year,rest)]

-- | Note some functions sort by this format
-- | So be careful when changing it.
showDate::(Int, Int, Int) -> String
showDate (y,m,d)= yy ++ '-':mm ++ '-':dd
    where dd=zpad (show d)
          mm = zpad (show m)
          yy = show y
          zpad ds@(_:ds')
           | ds'==[] = '0':ds
           | otherwise = ds


-- | from LtDate to Date
fromLtDate :: LtDate -> Date
fromLtDate  lt = (ltYear lt, ltMonth lt, ltDay lt)

Once you have (Y,M,D), it's easy to convert to a Haskell library type for data manipulations. Once you are done with the HS libraries, Text.printf can be used to format a date for display.

Solution 2

You can use the functions in Data.Time.Format to read in the dates. I've included a trivial program below that reads in a date in one format and writes that date back out in two different formats. To read in single-digit months or days, place a single hyphen (-) between the % and format specifier. In other words to parse dates formatted like 9-9-2012 then include a single hyphen between the % and the format characters. So to parse "9-9-2012" you would need the format string "%-d-%-m-%Y".

Note: This answer is getting a bit long-in-the-tooth given the rate at which Haskell packages evolve. It might be time to look for a better solution. I'm no longer writing Haskell so it would be nice if someone else created another answer since this question ranks highly in Google results.

As of July 2017, you are encouraged to use parseTimeOrError. The code becomes:

import Data.Time

main =
  do
    let dateString = "26 Jan 2012 10:54 AM"
    let timeFromString = parseTimeOrError True defaultTimeLocale "%d %b %Y %l:%M %p" dateString :: UTCTime
    -- Format YYYY/MM/DD HH:MM
    print $ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" timeFromString
    -- Format MM/DD/YYYY hh:MM AM/PM
    print $ formatTime defaultTimeLocale "%m/%d/%Y %I:%M %p" timeFromString

    -- now for a string with single digit months and days:
    let dateString = "9-8-2012 10:54 AM"
    let timeFromString = parseTimeOrError True defaultTimeLocale "%-d-%-m-%Y %l:%M %p" dateString :: UTCTime
    -- Format YYYY/MM/DD HH:MM
    print $ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" timeFromString

The versions from the .cabal file: build-depends: base >=4.9 && <4.10, time >= 1.6.0.1

As of August, 2014, the locale was best obtained from the "System.Locale" package rather than the Haskell 1998 "Locale" package. With that in mind, the sample code from above now reads:

import System.Locale
import Data.Time
import Data.Time.Format

main =
  do
    let dateString = "26 Jan 2012 10:54 AM"
    let timeFromString = readTime defaultTimeLocale "%d %b %Y %l:%M %p" dateString :: UTCTime
    -- Format YYYY/MM/DD HH:MM
    print $ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" timeFromString
    -- Format MM/DD/YYYY hh:MM AM/PM
    print $ formatTime defaultTimeLocale "%m/%d/%Y %I:%M %p" timeFromString

    -- now for a string with single digit months and days:
    let dateString = "9-8-2012 10:54 AM"
    let timeFromString = readTime defaultTimeLocale "%-d-%-m-%Y %l:%M %p" dateString :: UTCTime
    -- Format YYYY/MM/DD HH:MM
    print $ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" timeFromString

output now looks like this:

"2012/01/26 10:54"
"01/26/2012 10:54 AM"
"2012/08/09 10:54"

**Original, January 2012 ** answer:

import Locale
import Data.Time
import Data.Time.Format

main =
  do
    let dateString = "26 Jan 2012 10:54 AM"
    let timeFromString = readTime defaultTimeLocale "%d %b %Y %l:%M %p" dateString :: UTCTime
    -- Format YYYY/MM/DD HH:MM
    print $ formatTime defaultTimeLocale "%Y/%m/%d %H:%M" timeFromString
    -- Format MM/DD/YYYY hh:MM AM/PM
    print $ formatTime defaultTimeLocale "%m/%d/%Y %I:%M %p" timeFromString

The output looks like this:

"2012/01/26 10:54"
"01/26/2012 10:54 AM"

Data.Time.Format is available from the "time" package. If you need to parse single-digit months or days, in other words dates like 9-9-2012 then include a single hyphen between the % and the format characters. So to parse "9-9-2012" you would need the format string "%-d-%-m-%Y".

Solution 3

Using %-d and %-m instead of %d and %m means single digit day/month is OK, i.e.

parseDay :: String -> Day
parseDay s = readTime defaultTimeLocale "%-m%-d%Y" s

This may be what sclv meant, but his comment was a little too cryptic for me.

Solution 4

Since recently, I'll advice to use strptime package for all your date/time parsing needs.

Share:
16,155

Related videos on Youtube

Alex Baranosky
Author by

Alex Baranosky

Check me out on Github too: https://github.com/alexbaranosky

Updated on November 04, 2021

Comments

  • Alex Baranosky
    Alex Baranosky over 2 years

    I've been working with Haskell's Date.Time modules to parse a date like 12-4-1999 or 1-31-1999. I tried:

    parseDay :: String -> Day
    parseDay s = readTime defaultTimeLocale "%m%d%Y" s
    

    And I think it wants my months and days to have exactly two digits instead of 1 or 2...

    What's the proper way to do this?

    Also, I'd like to print out my Day in this format: 12/4/1999 what's the Haskell way to?

    Thanks for the help.

    • sclv
      sclv over 13 years
      don't you need dashes between the %m, %d, and %Y anyway?
    • Ben
      Ben over 7 years
      @AlexBaranosky you may want to consider accepting a different answer to this question :)
  • Alex Baranosky
    Alex Baranosky over 13 years
    that seems ridiculously complicated! There must be a simpler way, god help me
  • frayser
    frayser over 13 years
    Please take a closer look, the code was an example and is not all necessary: To parse the dateparseDate requires only 4 lines; call readDec 3 times. To show a date use Text.printf` once. Load the file and run it on your date to see that it works, then look at those parseDate and printf. This parseDate ignores spaces and doesn't care about the seperator '-', or '/' in the dates. It changes '55 to 1955, etc. It is 10 lines(includes parseDate')ShowDate, and fromLtDate and the data statements are not needed. Neither are Clock and Calender includes.
  • frayser
    frayser over 13 years
    Delete everything except parseDate and parseDate' it will make it look simpler. Keep the includes for Data.Char and Numeric The year parser, parseDate' can be replaced with another call to readDec.
  • user239558
    user239558 almost 11 years
    For the lazy, this is in the 'time' package.
  • recursion.ninja
    recursion.ninja almost 10 years
    +1 for the - flag for indicating the possible single digit month/day. I was pulling my hair out over this and about to turn to regexes!
  • peer
    peer almost 7 years
    I get Ambiguous occurrence ‘defaultTimeLocale’ which is it, Data.Time.defaultTimeLocale or System.Locale.defaultTimeLocale?
  • Tim Perry
    Tim Perry almost 7 years
    See my edits. This answer is starting to show its age, but it worked on OS X with cabal-install 1.24.0.2 and ghc 8.0.2.
  • Chris Stryczynski
    Chris Stryczynski almost 6 years
    I think in cases like these it would be better to post another answer with the updated code, possibly even update the original answer and post a new answer with the 'old' answer. Right now the oldest 'solution' appears on top of the question.
  • PHPirate
    PHPirate over 5 years
  • Tim Perry
    Tim Perry over 5 years
    @ChrisStryczynski: I see your point. I re-ordered the sections so the most recent one is first. Feel free to edit it for clarity.