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.
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).
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"
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.
Related
so in my app, I need to deal with date like \/Date(1440156888750-0700)\/, i think the first part stands for the seconds from Jan 1st, 1970 and the second part is the timezone.
I don't know how to handle data like this and display it the way we all understand in Xcode 7 using Swift 2?
(The previous version of this answer was actually wrong, it did not handle the time zone correctly.)
According to Stand-Alone JSON Serialization:
DateTime values appear as JSON strings in the form of "/Date(700000+0500)/", where the first number (700000 in the example provided) is the number of milliseconds in the GMT time zone, regular (non-daylight savings) time since midnight, January 1, 1970. The number may be negative to represent earlier times. The part that consists of "+0500" in the example is optional and indicates that the time is of the Local kind - that is, should be converted to the local time zone on deserialization. If it is absent, the time is deserialized as Utc. The actual number ("0500" in this example) and its sign (+ or -) are ignored.
and Use JSON.NET to parse json date of format Date(epochTime-offset)
... In this [screwy format][1], the timestamp portion is still based solely on UTC. The offset is extra information. It doesn't change the timestamp. You can give a different offset, or omit it entirely and it's still the same moment in time.
the first number in \/Date(1440156888750-0700)\/ is the number of
milliseconds since the "epoch" January 1, 1970 GMT, and the time
zone part -0700 must simply be ignored.
Here is a Swift 5 extension method for Date which checks
the validity of the string with a regular expression
(accepting both \/Date(...)\/ and /Date(...)/, with or without
a time zone specification) and converts the given number of
milliseconds to a Date:
extension Date {
init?(jsonDate: String) {
let pattern = #"\\?/Date\((\d+)([+-]\d{4})?\)\\?/"#
let regex = try! NSRegularExpression(pattern: pattern)
guard let match = regex.firstMatch(in: jsonDate, range: NSRange(jsonDate.startIndex..., in: jsonDate)) else {
return nil
}
// Extract milliseconds:
let dateString = jsonDate[Range(match.range(at: 1), in: jsonDate)!]
// Convert to UNIX timestamp in seconds:
let timeStamp = Double(dateString)! / 1000.0
// Create Date from timestamp:
self.init(timeIntervalSince1970: timeStamp)
}
}
Example:
let jsonDate = "\\/Date(1440156888750-0700)\\/"
print("JSON Date:", jsonDate)
if let theDate = Date(jsonDate: jsonDate) {
print("Date:", theDate)
} else {
print("wrong format")
}
Output:
JSON Date: \/Date(1440156888750-0700)\/
Date: 2015-08-21 11:34:48 +0000
(Versions for Swift 3 and Swift 4 can be found in the edit history.)
After a bit of experimenting around I came up with the following solution:
let dx = "/Date(1440156888750-0700)/"
let timestamp = (dx as NSString).substringWithRange(NSRange(location: 6,length: 13))
let timezone = (dx as NSString).substringWithRange(NSRange(location: 19,length: 5))
let dateIntermediate = NSDate(timeIntervalSince1970: Double(timestamp)! / 1000)
let outp = NSDateFormatter()
outp.dateFormat = "dd.MM.yyyy hh:mm::ssSSS"
outp.timeZone = NSTimeZone(forSecondsFromGMT: 0)
let input = outp.stringFromDate(dateIntermediate) + " " + timezone
let inp = NSDateFormatter()
inp.dateFormat = "dd.MM.yyyy hh:mm::ssSSS Z"
let finalDate = inp.dateFromString(input)
print(finalDate)
Let me explain:
we extract the millisecond timestamp and the timezone from the original string
we create a date from the timestamp to be able to split it into its different components
we output that date in a more standard way (not as timestamp) and append the previously extracted timezone to that string
we then read that string and parse a date from it again
Note
As #Phoen1xUK mentioned the timestamp might have a different length than 13 digits. You can handle that situation by stripping the /Date( and )/ and then splitting the string before the - (or +).
What I am encountering is quite peculiar.
My Code:
val aa = "2017-01-17 01:33:00"
val bb = "04:33"
val hour = bb.substring(0, bb.indexOf(":"))
val mins = bb.substring(bb.indexOf(":") + 1, bb.length())
val negatedmins = "-" + mins
val ecoffsethour = hour.toLong
val ecoffsetmins = negatedmins.toLong
println(aa)
val datetimeformatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val txn_post_date_hkt_date_parsed = LocalDateTime.parse(aa, datetimeformatter)
println(txn_post_date_hkt_date_parsed)
val minushours = txn_post_date_hkt_date_parsed.minusHours(ecoffsethour)
println(minushours)
val minusmins = minushours.minusMinutes(ecoffsetmins)
println(minusmins)
val offsetPostdateDiff = minusmins.toString().replace("T", " ")
println(offsetPostdateDiff)
Output:
2017-01-17 01:33:00
2017-01-17T01:33
2017-01-16T21:33
2017-01-16T22:06
2017-01-16 22:06
In the same code I am changing only the "aa" value to ==> 2017-01-17 01:33:44
Now the output is :
2017-01-17 01:33:44
2017-01-17T01:33:44
2017-01-16T21:33:44
2017-01-16T22:06:44
2017-01-16 22:06:44
Why is the first method not taking seconds field into consideration?
My Requirement is : However the output should come in "yyyy-MM-dd
HH:mm:ss" format.
I'm quite new to Scala. Please enlighten me.
Default format is ISO 8601
The java.time classes use the standard ISO 8601 formats by default when parsing/generating strings to represent date-time value.
The standard format for a local date-time is what you are seeing with the T in the middle: YYYY-MM-DDTHH:MM:SS.SSSSSSSSS.
LocalDateTime ldt = LocalDateTime.now( ZoneId.of( "America/Montreal" ) ) ;
String output = ldt.toString() ;
2017-01-23T12:34:56.789
Your call println( txn_post_date_hkt_date_parsed ) is implicitly calling the built-in toString method on the LocalDateTime object, and thereby asking for the standard ISO 8601 format with the T.
println( txn_post_date_hkt_date_parsed.toString() )
Offsets
On an unrelated note, you are working too hard. The java.time classes handle offsets. I do not understand why you want an offset of such an odd number (four hours and thirty-three minutes), but so be it.
Here is your code revised, but in Java syntax.
String input = "2017-01-17 01:33:00" ;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ;
LocalDateTime ldt = LocalDateTime.parse( input , f ) ;
OffsetDateTime utc = ldt.atOffset( ZoneOffset.UTC ) ;
ZoneOffset offset = ZoneOffset.of( "-04:33" ) ; // Behind UTC by four hours and thirty-three minutes.
OffsetDateTime odt = utc.withOffsetSameInstant( offset ) ;
You can see this code run live at IdeOne.com. Notice how the wall-clock time of your offset-from-UTC is on the previous date. Same moment in history, same point on the timeline, but viewed through two different wall-clock times (UTC, and four hours and thirty three minutes behind).
The Z on the end is standard ISO 8601 notation, short for Zulu and meaning UTC.
input: 2017-01-17 01:33:00
ldt.toString(): 2017-01-17T01:33
utc.toString(): 2017-01-17T01:33Z
odt.toString(): 2017-01-16T21:00-04:33
It's usually better to explicitly the format in which you want the output.
So, instead of
println datetime
You can do something like this:
println datetimeformat.print(datetime)
Good luck!
Edit: Change made to make the 2 expressions exactly equivalent
I have the following code to validate a date given a date format:
val df = new SimpleDateFormat("MM/dd/yyyy");
df.setLenient(false);
try {
val date = df.parse("11/13/2014");
}
catch {
case pe: ParseException => println("date error")
}
Now, what I need is to obtain the year, month and day in three variables. What is the best way to achieve this? Note that I need a solution based on performance as I need to validate/convert thousands of dates.
java.time
Use Java 8 and the new date/time API. Better, cleaner, future-proof.
val dateFormat = "MM/dd/yyyy"
val dtf = java.time.format.DateTimeFormatter.ofPattern(dateFormat)
val dateString = "11/13/2014"
val d = java.time.LocalDate.parse(dateString, dtf)
val year = d.getYear
2014
val monthNumber = d.getMonthValue
11
You can access a Month enum object.
val month = d.getMonth
Month.NOVEMBER
val dayOfMonth = d.getDayOfMonth
13
Once you have the input parsed into a java.time.LocalDate, you can get the year with getYear, etc.
To validate, catch the DateTimeParseException generated for invalid inputs.
If you want to skip the proper date validation (e.g. for performance) and just extract the Y, M, D - you can split the string and get integers as shown below
val ymd = dateString.split("/").map(_.toInt)
ymd: Array[Int] = Array(11, 13, 2014)
im burning my brains trying to make a function that gives me the ammount of days between todays date and a given date.
possible today function:
today = fmap (formatTime defaultTimeLocale "%Y-%m-%d") getCurrentTime
and thought using diffDays, but wont be able to make it work with a ::Day date
any ideas?
Your formatTime version returns a string, but you want a Day (which looks like your string when you inspect it, but is a different type entirely). Here's one way to write a today function, using utctDay to get a Day out of a UTCTime:
import Data.Time.Calendar
import Data.Time.Clock
today :: IO Day
today = fmap utctDay getCurrentTime
And here's a days-from-today function (which I gave the shorter name daysAway) that uses it:
daysAway :: Day -> IO Integer
daysAway day = fmap (diffDays day) today
If you're always specifying the target as a calendar date, you can do that easily enough:
daysToDate :: Integer -> Int -> Int -> IO Integer
daysToDate year month day = daysAway $ fromGregorian year month day
Given a shorthand function for a commonly-needed relative day:
tomorrow :: IO Day
tomorrow = fmap (addDays 1) today
We can demonstrate the correctness of Annie's Thesis:
ghci> tomorrow >>= daysAway
1
Have a date of birth in format 'MM/dd/yy' for people born in the 1900's. I'm using Zend_Date to
parse and convert the string value
$date = new Zend_Date();
$logger->info(sprintf('Convert DOB %s -> %s',$dateOfBirth,$date->toString('yyyy-M-dd')));
I get
2010-06-24T16:55:50+00:00 INFO (6): DOB 9/13/57
2010-06-24T16:55:50+00:00 INFO (6): Convert DOB : 9/13/57 -> 2057-9-13
I expected
2010-06-24T16:55:50+00:00 INFO (6): Convert 9/13/57 -> 1957-9-13
What am i missing? I don't think this is related to the real year 'yyyy' / ISO year 'YYYY' handling in Zend_Date.
My current horrible hack
$formattedDate = $date->toString('dd/M').'/19'.$date->toString('YY');
short (1 or 2 digit) version of YEAR is always in current century.
so use:
$dob = '9/13/57';
$date = new Zend_Date($dob, 'M/d/yy');
echo $date->subYear(100)->toString('YYYY-MM-d');
Apparently, it's a bit more complicated. According to this site, 2-digit years greater than or equal to 70 become 1970-1999 whereas those less than 70 become 2000-2069.