From 544a2cfe1a9c4805568f6dd781cd85fa4537cb73 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Thu, 10 Nov 2016 22:22:48 +0100 Subject: Fixed daylight saving time * When formatting timezone, substract 1 hour from timezone when isDST * Do not depend DST in current timezone when parsing arbitrary date because formatted timestamps are never in DST. * On the way, removed an unnecessary line in parsing code which could cause bugs. * Added DST tests --- lib/pure/times.nim | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/pure/times.nim b/lib/pure/times.nim index f74003820..cfc39bc55 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -144,8 +144,9 @@ type yearday*: range[0..365] ## The number of days since January 1, ## in the range 0 to 365. ## Always 0 if the target is JS. - isDST*: bool ## Determines whether DST is in effect. Always - ## ``False`` if time is UTC. + isDST*: bool ## Determines whether DST is in effect. + ## Semantically, this adds another negative hour + ## offset to the time in addition to the timezone. timezone*: int ## The offset of the (non-DST) timezone in seconds ## west of UTC. Note that the sign of this number ## is the opposite of the one in a formatted @@ -824,28 +825,32 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = if fyear.len != 5: fyear = repeat('0', 5-fyear.len()) & fyear buf.add(fyear) of "z": - let hours = abs(info.timezone) div secondsInHour - if info.timezone <= 0: buf.add('+') + let + nonDstTz = info.timezone - int(info.isDst) * secondsInHour + hours = abs(nonDstTz) div secondsInHour + if nonDstTz <= 0: buf.add('+') else: buf.add('-') buf.add($hours) of "zz": - let hours = abs(info.timezone) div secondsInHour - if info.timezone <= 0: buf.add('+') + let + nonDstTz = info.timezone - int(info.isDst) * secondsInHour + hours = abs(nonDstTz) div secondsInHour + if nonDstTz <= 0: buf.add('+') else: buf.add('-') if hours < 10: buf.add('0') buf.add($hours) of "zzz": let - hours = abs(info.timezone) div secondsInHour - minutes = (abs(info.timezone) div secondsInMin) mod minutesInHour - if info.timezone <= 0: buf.add('+') + nonDstTz = info.timezone - int(info.isDst) * secondsInHour + hours = abs(nonDstTz) div secondsInHour + minutes = (abs(nonDstTz) div secondsInMin) mod minutesInHour + if nonDstTz <= 0: buf.add('+') else: buf.add('-') if hours < 10: buf.add('0') buf.add($hours) buf.add(':') if minutes < 10: buf.add('0') buf.add($minutes) - of "": discard else: @@ -1000,7 +1005,6 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = of "M": var pd = parseInt(value[j..j+1], sv) info.month = Month(sv-1) - info.monthday = sv j += pd of "MM": var month = value[j..j+1].parseInt() @@ -1166,6 +1170,7 @@ proc parse*(value, layout: string): TimeInfo = info.hour = 0 info.minute = 0 info.second = 0 + info.isDST = false # DST is never encoded in timestamps. while true: case layout[i] of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',': @@ -1195,14 +1200,6 @@ proc parse*(value, layout: string): TimeInfo = parseToken(info, token, value, j) token = "" - # We are going to process the date to find out if we are in DST, because the - # default based on the current time may be wrong. Calling getLocalTime will - # set this correctly, but the actual time may be offset from when we called - # toTime with a possibly incorrect DST setting, so we are only going to take - # the isDST from this result. - let correctDST = getLocalTime(toTime(info)) - info.isDST = correctDST.isDST - # Now we process it again with the correct isDST to correct things like # weekday and yearday. return getLocalTime(toTime(info)) -- cgit 1.4.1-2-gfad0 From aa08c32c2b6a32da1aa1f98234512f37330a6691 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Fri, 11 Nov 2016 17:52:53 +0100 Subject: Improved `-`; fixed tests * added prefix `-` operator for TimeInterval * improved `-` for both TimeInterval and TimeInfo * Fixed a DST test --- lib/pure/times.nim | 33 +++++++++++++++------------------ tests/stdlib/ttime.nim | 4 ++-- 2 files changed, 17 insertions(+), 20 deletions(-) (limited to 'lib') diff --git a/lib/pure/times.nim b/lib/pure/times.nim index cfc39bc55..b6b9fad5b 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -279,16 +279,20 @@ proc `+`*(ti1, ti2: TimeInterval): TimeInterval = carryO = `div`(ti1.months + ti2.months, 12) result.years = carryO + ti1.years + ti2.years +proc `-`*(ti: TimeInterval): TimeInterval = + result = TimeInterval( + milliseconds: -ti.milliseconds, + seconds: -ti.seconds, + minutes: -ti.minutes, + hours: -ti.hours, + days: -ti.days, + months: -ti.months, + years: -ti.years + ) + proc `-`*(ti1, ti2: TimeInterval): TimeInterval = ## Subtracts TimeInterval ``ti1`` from ``ti2``. - result = ti1 - result.milliseconds -= ti2.milliseconds - result.seconds -= ti2.seconds - result.minutes -= ti2.minutes - result.hours -= ti2.hours - result.days -= ti2.days - result.months -= ti2.months - result.years -= ti2.years + result = ti1 + (-ti2) proc isLeapYear*(year: int): bool = ## returns true if ``year`` is a leap year @@ -364,16 +368,9 @@ proc `-`*(a: TimeInfo, interval: TimeInterval): TimeInfo = ## ## **Note:** This has been only briefly tested, it is inaccurate especially ## when you subtract so much that you reach the Julian calendar. - let t = toSeconds(toTime(a)) - var intval: TimeInterval - intval.milliseconds = - interval.milliseconds - intval.seconds = - interval.seconds - intval.minutes = - interval.minutes - intval.hours = - interval.hours - intval.days = - interval.days - intval.months = - interval.months - intval.years = - interval.years - let secs = toSeconds(a, intval) + let + t = toSeconds(toTime(a)) + secs = toSeconds(a, -interval) if a.timezone == 0: result = getGMTime(fromSeconds(t + secs)) else: diff --git a/tests/stdlib/ttime.nim b/tests/stdlib/ttime.nim index 6d3d7c93a..39106f1e0 100644 --- a/tests/stdlib/ttime.nim +++ b/tests/stdlib/ttime.nim @@ -219,7 +219,7 @@ block dstTest: # January and one in July to maximize the probability to hit one date with DST # and one without on the local machine. However, this is not guaranteed. let + parsedJan = parse("2016-01-05 04:00:00+01:00", "yyyy-MM-dd HH:mm:sszzz") parsedJul = parse("2016-07-01 04:00:00+01:00", "yyyy-MM-dd HH:mm:sszzz") - parsedJan = parse("2016-01-05 04:00:00+01:00", "yyyy-MM-ss HH:mm:sszzz") - doAssert toTime(parsedJan) == fromSeconds(1452394800) + doAssert toTime(parsedJan) == fromSeconds(1451962800) doAssert toTime(parsedJul) == fromSeconds(1467342000) -- cgit 1.4.1-2-gfad0 From 0587a578075498dffaadbe7cf0ba1885eb597536 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Mon, 14 Nov 2016 18:36:03 +0100 Subject: Assume local DST iff no timezone is given --- lib/pure/times.nim | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pure/times.nim b/lib/pure/times.nim index b6b9fad5b..ef23c4cf9 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -1097,6 +1097,7 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = else: raise newException(ValueError, "Couldn't parse timezone offset (z), got: " & value[j]) + info.isDST = false j += 2 of "zz": if value[j] == '+': @@ -1106,6 +1107,7 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = else: raise newException(ValueError, "Couldn't parse timezone offset (zz), got: " & value[j]) + info.isDST = false j += 3 of "zzz": var factor = 0 @@ -1118,6 +1120,7 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = j += 4 info.timezone += factor * value[j..j+1].parseInt() * 60 j += 2 + info.isDST = false else: # Ignore the token and move forward in the value string by the same length j += token.len @@ -1167,7 +1170,8 @@ proc parse*(value, layout: string): TimeInfo = info.hour = 0 info.minute = 0 info.second = 0 - info.isDST = false # DST is never encoded in timestamps. + info.isDST = true # using this is flag for checking whether a timezone has \ + # been read (because DST is always false when a tz is parsed) while true: case layout[i] of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',': @@ -1197,8 +1201,17 @@ proc parse*(value, layout: string): TimeInfo = parseToken(info, token, value, j) token = "" - # Now we process it again with the correct isDST to correct things like - # weekday and yearday. + if info.isDST: + # means that no timezone has been parsed. In this case, we need to check + # whether the date is within DST of the local time. + let tmp = getLocalTime(toTime(info)) + # correctly set isDST so that the following step works on the correct time + info.isDST = tmp.isDST + + # Correct weekday and yearday; transform timestamp to local time. + # There currently is no way of returning this with the original (parsed) + # timezone while also setting weekday and yearday (we are depending on stdlib + # to provide this calculation). return getLocalTime(toTime(info)) # Leap year calculations are adapted from: -- cgit 1.4.1-2-gfad0 From 434c27343e72c4e90530d9b90c0851bc20c8ea32 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Mon, 14 Nov 2016 18:46:35 +0100 Subject: Parse 'Z' as valid timezone if offset is expected --- lib/pure/times.nim | 18 +++++++++++++++--- tests/stdlib/ttime.nim | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pure/times.nim b/lib/pure/times.nim index ef23c4cf9..0ab14e183 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -1090,29 +1090,42 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = info.year = value[j..j+3].parseInt() j += 4 of "z": + info.isDST = false if value[j] == '+': info.timezone = 0 - parseInt($value[j+1]) * secondsInHour elif value[j] == '-': info.timezone = parseInt($value[j+1]) * secondsInHour + elif value[j] == 'Z': + info.timezone = 0 + j += 1 + return else: raise newException(ValueError, "Couldn't parse timezone offset (z), got: " & value[j]) - info.isDST = false j += 2 of "zz": + info.isDST = false if value[j] == '+': info.timezone = 0 - value[j+1..j+2].parseInt() * secondsInHour elif value[j] == '-': info.timezone = value[j+1..j+2].parseInt() * secondsInHour + elif value[j] == 'Z': + info.timezone = 0 + j += 1 + return else: raise newException(ValueError, "Couldn't parse timezone offset (zz), got: " & value[j]) - info.isDST = false j += 3 of "zzz": + info.isDST = false var factor = 0 if value[j] == '+': factor = -1 elif value[j] == '-': factor = 1 + elif value[j] == 'Z': + info.timezone = 0 + j += 1 + return else: raise newException(ValueError, "Couldn't parse timezone offset (zzz), got: " & value[j]) @@ -1120,7 +1133,6 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = j += 4 info.timezone += factor * value[j..j+1].parseInt() * 60 j += 2 - info.isDST = false else: # Ignore the token and move forward in the value string by the same length j += token.len diff --git a/tests/stdlib/ttime.nim b/tests/stdlib/ttime.nim index 39106f1e0..b28d8aecd 100644 --- a/tests/stdlib/ttime.nim +++ b/tests/stdlib/ttime.nim @@ -96,6 +96,10 @@ parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-dd'T'HH:mm:ss'Z'zzz", # RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" parseTest("2006-01-12T15:04:05.999999999Z-07:00", "yyyy-MM-ddTHH:mm:ss.999999999Zzzz", "2006-01-12T22:04:05+00:00", 11) +for tzFormat in ["z", "zz", "zzz"]: + # formatting timezone as 'Z' for UTC + parseTest("2001-01-12T22:04:05Z", "yyyy-MM-dd'T'HH:mm:ss" & tzFormat, + "2001-01-12T22:04:05+00:00", 11) # Kitchen = "3:04PM" parseTestTimeOnly("3:04PM", "h:mmtt", "15:04:00") #when not defined(testing): -- cgit 1.4.1-2-gfad0 From 0ffd14e169cdaa94aa3f8a9bb29fbf02596042e8 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Mon, 14 Nov 2016 19:18:23 +0100 Subject: Updated times.parse() documentation --- lib/pure/times.nim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 0ab14e183..1767a37be 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -1138,8 +1138,11 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = j += token.len proc parse*(value, layout: string): TimeInfo = - ## This function parses a date/time string using the standard format identifiers (below) - ## The function defaults information not provided in the format string from the running program (timezone, month, year, etc) + ## This function parses a date/time string using the standard format + ## identifiers as listed below. The function defaults information not provided + ## in the format string from the running program (timezone, month, year, etc). + ## Daylight saving time is only set if no timezone is given and the given date + ## lies within the DST period of the current locale. ## ## ========== ================================================================================= ================================================ ## Specifier Description Example @@ -1164,7 +1167,7 @@ proc parse*(value, layout: string): TimeInfo = ## tt Same as above, but ``AM`` and ``PM`` instead of ``A`` and ``P`` respectively. ## yy Displays the year to two digits. ``2012 -> 12`` ## yyyy Displays the year to four digits. ``2012 -> 2012`` - ## z Displays the timezone offset from UTC. ``GMT+7 -> +7``, ``GMT-5 -> -5`` + ## z Displays the timezone offset from UTC. ``Z`` is parsed as ``+0`` ``GMT+7 -> +7``, ``GMT-5 -> -5`` ## zz Same as above but with leading 0. ``GMT+7 -> +07``, ``GMT-5 -> -05`` ## zzz Same as above but with ``:mm`` where *mm* represents minutes. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00`` ## ========== ================================================================================= ================================================ -- cgit 1.4.1-2-gfad0