From 2ec6fe56ade00e0bbe0b2c8c49b8554c3723bcd9 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 17:22:50 +0100 Subject: [PATCH 1/9] Fixed timezone handling * mktime always interprets its input as local time even on systems where gmtoff is present, so using it is utterly useless for anything but getting the local timezone. Removed all other usage of gmtoff to avoid confusion. * Properly handle timezone offset in toTime() * Properly handle timezone offset in `$` because asctime also interprets its input as local time * Also tried to fix the JavaScript implementation --- lib/pure/times.nim | 96 ++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index db09f94c13..1d712e3281 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -184,7 +184,8 @@ proc getGMTime*(t: Time): TimeInfo {.tags: [TimeEffect], raises: [], benign.} ## converts the calendar time `t` to broken-down time representation, ## expressed in Coordinated Universal Time (UTC). -proc timeInfoToTime*(timeInfo: TimeInfo): Time {.tags: [], benign, deprecated.} +proc timeInfoToTime*(timeInfo: TimeInfo): Time + {.tags: [TimeEffect], benign, deprecated.} ## converts a broken-down time structure to ## calendar time representation. The function ignores the specified ## contents of the structure members `weekday` and `yearday` and recomputes @@ -193,7 +194,7 @@ proc timeInfoToTime*(timeInfo: TimeInfo): Time {.tags: [], benign, deprecated.} ## **Warning:** This procedure is deprecated since version 0.14.0. ## Use ``toTime`` instead. -proc toTime*(timeInfo: TimeInfo): Time {.tags: [], benign.} +proc toTime*(timeInfo: TimeInfo): Time {.tags: [TimeEffect], benign.} ## converts a broken-down time structure to ## calendar time representation. The function ignores the specified ## contents of the structure members `weekday` and `yearday` and recomputes @@ -211,7 +212,8 @@ proc fromSeconds*(since1970: int64): Time {.tags: [], raises: [], benign.} = proc toSeconds*(time: Time): float {.tags: [], raises: [], benign.} ## Returns the time in seconds since the unix epoch. -proc `$` *(timeInfo: TimeInfo): string {.tags: [], raises: [], benign.} +proc `$` *(timeInfo: TimeInfo): string + {.tags: [TimeEffect], raises: [], benign.} ## converts a `TimeInfo` object to a string representation. proc `$` *(time: Time): string {.tags: [], raises: [], benign.} ## converts a calendar time to a string representation. @@ -424,7 +426,8 @@ when not defined(JS): when not defined(JS): # C wrapper: - when defined(freebsd) or defined(netbsd) or defined(openbsd): + when defined(freebsd) or defined(netbsd) or defined(openbsd) or + defined(macosx): type StructTM {.importc: "struct tm", final.} = object second {.importc: "tm_sec".}, @@ -479,46 +482,24 @@ when not defined(JS): const weekDays: array[0..6, WeekDay] = [ dSun, dMon, dTue, dWed, dThu, dFri, dSat] - when defined(freebsd) or defined(netbsd) or defined(openbsd): - TimeInfo(second: int(tm.second), - minute: int(tm.minute), - hour: int(tm.hour), - monthday: int(tm.monthday), - month: Month(tm.month), - year: tm.year + 1900'i32, - weekday: weekDays[int(tm.weekday)], - yearday: int(tm.yearday), - isDST: tm.isdst > 0, - tzname: if local: - if tm.isdst > 0: - getTzname().DST - else: - getTzname().nonDST + TimeInfo(second: int(tm.second), + minute: int(tm.minute), + hour: int(tm.hour), + monthday: int(tm.monthday), + month: Month(tm.month), + year: tm.year + 1900'i32, + weekday: weekDays[int(tm.weekday)], + yearday: int(tm.yearday), + isDST: tm.isdst > 0, + tzname: if local: + if tm.isdst > 0: + getTzname().DST else: - "UTC", - # BSD stores in `gmtoff` offset east of UTC in seconds, - # but posix systems using west of UTC in seconds - timezone: if local: -(tm.gmtoff) else: 0 - ) - else: - TimeInfo(second: int(tm.second), - minute: int(tm.minute), - hour: int(tm.hour), - monthday: int(tm.monthday), - month: Month(tm.month), - year: tm.year + 1900'i32, - weekday: weekDays[int(tm.weekday)], - yearday: int(tm.yearday), - isDST: tm.isdst > 0, - tzname: if local: - if tm.isdst > 0: - getTzname().DST - else: - getTzname().nonDST - else: - "UTC", - timezone: if local: getTimezone() else: 0 - ) + getTzname().nonDST + else: + "UTC", + timezone: if local: getTimezone() else: 0 + ) proc timeInfoToTM(t: TimeInfo): StructTM = @@ -569,12 +550,18 @@ when not defined(JS): proc timeInfoToTime(timeInfo: TimeInfo): Time = var cTimeInfo = timeInfo # for C++ we have to make a copy, # because the header of mktime is broken in my version of libc - return mktime(timeInfoToTM(cTimeInfo)) + result = mktime(timeInfoToTM(cTimeInfo)) + # mktime is defined to interpret the input as local time. As timeInfoToTM + # does ignore the timezone, we need to adjust this here. + result = Time(TimeImpl(result) - getTimezone() + timeInfo.timezone) proc toTime(timeInfo: TimeInfo): Time = var cTimeInfo = timeInfo # for C++ we have to make a copy, # because the header of mktime is broken in my version of libc - return mktime(timeInfoToTM(cTimeInfo)) + result = mktime(timeInfoToTM(cTimeInfo)) + # mktime is defined to interpret the input as local time. As timeInfoToTM + # does ignore the timezone, we need to adjust this here. + result = Time(TimeImpl(result) - getTimezone() + timeInfo.timezone) proc toStringTillNL(p: cstring): string = result = "" @@ -584,8 +571,13 @@ when not defined(JS): inc(i) proc `$`(timeInfo: TimeInfo): string = + # asctime interprets its input as local time, so we first convert the value + # to local time if necessary + let + localTimeInfo = if timeInfo.timezone == getTimezone(): timeInfo else: + getLocalTime(toTime(timeInfo)) + p = asctime(timeInfoToTM(localTimeInfo)) # BUGFIX: asctime returns a newline at the end! - var p = asctime(timeInfoToTM(timeInfo)) result = toStringTillNL(p) proc `$`(time: Time): string = @@ -675,14 +667,7 @@ elif defined(JS): result.weekday = weekDays[t.getUTCDay()] result.yearday = 0 - proc timeInfoToTime*(timeInfo: TimeInfo): Time = - result = internGetTime() - result.setSeconds(timeInfo.second) - result.setMinutes(timeInfo.minute) - result.setHours(timeInfo.hour) - result.setMonth(ord(timeInfo.month)) - result.setFullYear(timeInfo.year) - result.setDate(timeInfo.monthday) + proc timeInfoToTime*(timeInfo: TimeInfo): Time = toTime(timeInfo) proc toTime*(timeInfo: TimeInfo): Time = result = internGetTime() @@ -692,6 +677,7 @@ elif defined(JS): result.setMonth(ord(timeInfo.month)) result.setFullYear(timeInfo.year) result.setDate(timeInfo.monthday) + result = result + initInterval(seconds=timeInfo.timezone) proc `$`(timeInfo: TimeInfo): string = return $(toTime(timeInfo)) proc `$`(time: Time): string = return $time.toLocaleString() @@ -1257,7 +1243,7 @@ proc parse*(value, layout: string): TimeInfo = let correctDST = getLocalTime(toTime(info)) info.isDST = correctDST.isDST - # Now we preocess it again with the correct isDST to correct things like + # Now we process it again with the correct isDST to correct things like # weekday and yearday. return getLocalTime(toTime(info)) From 96234f36caf3bfb1399c8127df54e093e3001165 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 18:42:22 +0100 Subject: [PATCH 2/9] Made times.nim compile again to JS --- lib/pure/times.nim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 1d712e3281..56775e42b4 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -671,13 +671,12 @@ elif defined(JS): proc toTime*(timeInfo: TimeInfo): Time = result = internGetTime() - result.setSeconds(timeInfo.second) result.setMinutes(timeInfo.minute) result.setHours(timeInfo.hour) result.setMonth(ord(timeInfo.month)) result.setFullYear(timeInfo.year) result.setDate(timeInfo.monthday) - result = result + initInterval(seconds=timeInfo.timezone) + result.setSeconds(timeInfo.second + timeInfo.timezone) proc `$`(timeInfo: TimeInfo): string = return $(toTime(timeInfo)) proc `$`(time: Time): string = return $time.toLocaleString() From c1f0b7643cc738ab216fd7b58658310fdf37725e Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 19:33:43 +0100 Subject: [PATCH 3/9] Fixed timezone offset parsing --- lib/pure/times.nim | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 56775e42b4..ab0741d9c1 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -1127,31 +1127,33 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = j += 4 of "z": if value[j] == '+': - info.timezone = parseInt($value[j+1]) + info.timezone = 0-parseInt($value[j+1]) * 3600 elif value[j] == '-': - info.timezone = 0-parseInt($value[j+1]) + info.timezone = parseInt($value[j+1]) * 3600 else: raise newException(ValueError, "Couldn't parse timezone offset (z), got: " & value[j]) j += 2 of "zz": if value[j] == '+': - info.timezone = value[j+1..j+2].parseInt() + info.timezone = 0-value[j+1..j+2].parseInt() * 3600 elif value[j] == '-': - info.timezone = 0-value[j+1..j+2].parseInt() + info.timezone = value[j+1..j+2].parseInt() * 3600 else: raise newException(ValueError, "Couldn't parse timezone offset (zz), got: " & value[j]) j += 3 of "zzz": - if value[j] == '+': - info.timezone = value[j+1..j+2].parseInt() - elif value[j] == '-': - info.timezone = 0-value[j+1..j+2].parseInt() + var factor = 0 + if value[j] == '+': factor = -1 + elif value[j] == '-': factor = 1 else: raise newException(ValueError, "Couldn't parse timezone offset (zzz), got: " & value[j]) - j += 6 + info.timezone = factor * value[j+1..j+2].parseInt() * 3600 + j += 4 + info.timezone += factor * value[j..j+1].parseInt() * 60 + j += 2 of "ZZZ": info.tzname = value[j..j+2].toUpperAscii() j += 3 @@ -1188,7 +1190,7 @@ proc parse*(value, layout: string): TimeInfo = ## yyyy Displays the year to four digits. ``2012 -> 2012`` ## z Displays the timezone offset from UTC. ``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 ``:00``. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00`` + ## zzz Same as above but with ``:mm`` where *mm* represents minutes. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00`` ## ZZZ Displays the name of the timezone. ``GMT -> GMT``, ``EST -> EST`` ## ========== ================================================================================= ================================================ ## From 170745eb39d8607004a6be8a9d8968137d99824d Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 20:26:50 +0100 Subject: [PATCH 4/9] Removed tzname because it's broken * No mapping between TimeInfo.tzname and TimeInfo.timezone * tzname of time.h is not well-defined, may have almost arbitrary length, and localization may differ * Code used hardcoded "UTC" string --- lib/pure/times.nim | 34 ++-------------------------------- tests/stdlib/ttime.nim | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index ab0741d9c1..242118f676 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -66,12 +66,6 @@ when defined(posix) and not defined(JS): when not defined(freebsd) and not defined(netbsd) and not defined(openbsd): var timezone {.importc, header: "".}: int - var - tzname {.importc, header: "" .}: array[0..1, cstring] - # we also need tzset() to make sure that tzname is initialized - proc tzset() {.importc, header: "".} - # calling tzset() implicitly to initialize tzname data. - tzset() elif defined(windows): import winlean @@ -82,12 +76,10 @@ elif defined(windows): # visual c's c runtime exposes these under a different name var timezone {.importc: "_timezone", header: "".}: int - tzname {.importc: "_tzname", header: ""}: array[0..1, cstring] else: type TimeImpl {.importc: "time_t", header: "".} = int var timezone {.importc, header: "".}: int - tzname {.importc, header: "" .}: array[0..1, cstring] type Time* = distinct TimeImpl @@ -154,7 +146,6 @@ type ## Always 0 if the target is JS. isDST*: bool ## Determines whether DST is in effect. Always ## ``False`` if time is UTC. - tzname*: string ## The timezone this time is in. E.g. GMT timezone*: int ## The offset of the (non-DST) timezone in seconds ## west of UTC. @@ -237,12 +228,6 @@ proc `==`*(a, b: Time): bool {. ## returns true if ``a == b``, that is if both times represent the same value result = a - b == 0 -when not defined(JS): - proc getTzname*(): tuple[nonDST, DST: string] {.tags: [TimeEffect], raises: [], - benign.} - ## returns the local timezone; ``nonDST`` is the name of the local non-DST - ## timezone, ``DST`` is the name of the local DST timezone. - proc getTimezone*(): int {.tags: [TimeEffect], raises: [], benign.} ## returns the offset of the local (non-DST) timezone in seconds west of UTC. @@ -371,7 +356,7 @@ proc `+`*(a: TimeInfo, interval: TimeInterval): TimeInfo = ## very accurate. let t = toSeconds(toTime(a)) let secs = toSeconds(a, interval) - if a.tzname == "UTC": + if a.timezone == 0: result = getGMTime(fromSeconds(t + secs)) else: result = getLocalTime(fromSeconds(t + secs)) @@ -391,7 +376,7 @@ proc `-`*(a: TimeInfo, interval: TimeInterval): TimeInfo = intval.months = - interval.months intval.years = - interval.years let secs = toSeconds(a, intval) - if a.tzname == "UTC": + if a.timezone == 0: result = getGMTime(fromSeconds(t + secs)) else: result = getLocalTime(fromSeconds(t + secs)) @@ -491,13 +476,6 @@ when not defined(JS): weekday: weekDays[int(tm.weekday)], yearday: int(tm.yearday), isDST: tm.isdst > 0, - tzname: if local: - if tm.isdst > 0: - getTzname().DST - else: - getTzname().nonDST - else: - "UTC", timezone: if local: getTimezone() else: 0 ) @@ -597,9 +575,6 @@ when not defined(JS): ## converts a Windows time to a UNIX `Time` (``time_t``) result = Time((t - epochDiff) div rateDiff) - proc getTzname(): tuple[nonDST, DST: string] = - return ($tzname[0], $tzname[1]) - proc getTimezone(): int = when defined(freebsd) or defined(netbsd) or defined(openbsd): var a = timec(nil) @@ -892,8 +867,6 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = if hrs.abs < 10: var atIndex = buf.len-(($hrs & ":00").len-(if hrs < 0: 1 else: 0)) buf.insert("0", atIndex) - of "ZZZ": - buf.add(info.tzname) of "": discard else: @@ -1154,9 +1127,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 - of "ZZZ": - info.tzname = value[j..j+2].toUpperAscii() - j += 3 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 0650095350..569d465cc8 100644 --- a/tests/stdlib/ttime.nim +++ b/tests/stdlib/ttime.nim @@ -10,26 +10,26 @@ import # Tue 19 Jan 03:14:07 GMT 2038 var t = getGMTime(fromSeconds(2147483647)) -doAssert t.format("ddd dd MMM hh:mm:ss ZZZ yyyy") == "Tue 19 Jan 03:14:07 UTC 2038" -doAssert t.format("ddd ddMMMhh:mm:ssZZZyyyy") == "Tue 19Jan03:14:07UTC2038" +doAssert t.format("ddd dd MMM hh:mm:ss yyyy") == "Tue 19 Jan 03:14:07 2038" +doAssert t.format("ddd ddMMMhh:mm:ssyyyy") == "Tue 19Jan03:14:072038" doAssert t.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz ZZZ") == - "19 19 Tue Tuesday 3 03 3 03 14 14 1 01 Jan January 7 07 A AM 8 38 038 2038 02038 0 00 00:00 UTC" + " ss t tt y yy yyy yyyy yyyyy z zz zzz") == + "19 19 Tue Tuesday 3 03 3 03 14 14 1 01 Jan January 7 07 A AM 8 38 038 2038 02038 0 00 00:00" doAssert t.format("yyyyMMddhhmmss") == "20380119031407" var t2 = getGMTime(fromSeconds(160070789)) # Mon 27 Jan 16:06:29 GMT 1975 doAssert t2.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz ZZZ") == - "27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 0 00 00:00 UTC" + " ss t tt y yy yyy yyyy yyyyy z zz zzz") == + "27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 0 00 00:00" when not defined(JS): when sizeof(Time) == 8: var t3 = getGMTime(fromSeconds(889067643645)) # Fri 7 Jun 19:20:45 BST 30143 doAssert t3.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz ZZZ") == - "7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 0 00 00:00 UTC" + " ss t tt y yy yyy yyyy yyyyy z zz zzz") == + "7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 0 00 00:00" doAssert t3.format(":,[]()-/") == ":,[]()-/" var t4 = getGMTime(fromSeconds(876124714)) # Mon 6 Oct 08:58:34 BST 1997 @@ -52,22 +52,22 @@ parseTest("Tuesday at 09:04am on Dec 15, 2015", parseTest("Thu Jan 12 15:04:05 2006", "ddd MMM dd HH:mm:ss yyyy", "Thu Jan 12 15:04:05 2006", 11) # UnixDate = "Mon Jan _2 15:04:05 MST 2006" -parseTest("Thu Jan 12 15:04:05 MST 2006", "ddd MMM dd HH:mm:ss ZZZ yyyy", +parseTest("Thu Jan 12 15:04:05 2006", "ddd MMM dd HH:mm:ss yyyy", "Thu Jan 12 15:04:05 2006", 11) # RubyDate = "Mon Jan 02 15:04:05 -0700 2006" parseTest("Mon Feb 29 15:04:05 -07:00 2016", "ddd MMM dd HH:mm:ss zzz yyyy", "Mon Feb 29 15:04:05 2016", 59) # leap day # RFC822 = "02 Jan 06 15:04 MST" -parseTest("12 Jan 16 15:04 MST", "dd MMM yy HH:mm ZZZ", +parseTest("12 Jan 16 15:04", "dd MMM yy HH:mm", "Tue Jan 12 15:04:00 2016", 11) # RFC822Z = "02 Jan 06 15:04 -0700" # RFC822 with numeric zone parseTest("01 Mar 16 15:04 -07:00", "dd MMM yy HH:mm zzz", "Tue Mar 1 15:04:00 2016", 60) # day after february in leap year # RFC850 = "Monday, 02-Jan-06 15:04:05 MST" -parseTest("Monday, 12-Jan-06 15:04:05 MST", "dddd, dd-MMM-yy HH:mm:ss ZZZ", +parseTest("Monday, 12-Jan-06 15:04:05", "dddd, dd-MMM-yy HH:mm:ss", "Thu Jan 12 15:04:05 2006", 11) # RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" -parseTest("Sun, 01 Mar 2015 15:04:05 MST", "ddd, dd MMM yyyy HH:mm:ss ZZZ", +parseTest("Sun, 01 Mar 2015 15:04:05", "ddd, dd MMM yyyy HH:mm:ss", "Sun Mar 1 15:04:05 2015", 59) # day after february in non-leap year # RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" # RFC1123 with numeric zone parseTest("Thu, 12 Jan 2006 15:04:05 -07:00", "ddd, dd MMM yyyy HH:mm:ss zzz", From 9d5de8021b3543ea4a01a294efb6a69329ffba75 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 21:14:52 +0100 Subject: [PATCH 5/9] Use ISO 8601 format for times.`$`. Fixed tests. * `$` now uses format() with explicit time zone. * Fixed errors in rendering "z", "zz" and "zzz" * Updated tests --- lib/pure/times.nim | 82 +++++++++++++----------------- tests/stdlib/ttime.nim | 111 +++++++++++++++++++++++------------------ 2 files changed, 99 insertions(+), 94 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 242118f676..b24f12ff05 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -203,12 +203,6 @@ proc fromSeconds*(since1970: int64): Time {.tags: [], raises: [], benign.} = proc toSeconds*(time: Time): float {.tags: [], raises: [], benign.} ## Returns the time in seconds since the unix epoch. -proc `$` *(timeInfo: TimeInfo): string - {.tags: [TimeEffect], raises: [], benign.} - ## converts a `TimeInfo` object to a string representation. -proc `$` *(time: Time): string {.tags: [], raises: [], benign.} - ## converts a calendar time to a string representation. - proc `-`*(a, b: Time): int64 {. rtl, extern: "ntDiffTime", tags: [], raises: [], benign.} ## computes the difference of two calendar times. Result is in seconds. @@ -449,12 +443,6 @@ when not defined(JS): importc: "time", header: "", tags: [].} proc mktime(t: StructTM): Time {. importc: "mktime", header: "", tags: [].} - proc asctime(tblock: StructTM): cstring {. - importc: "asctime", header: "", tags: [].} - proc ctime(time: ptr Time): cstring {. - importc: "ctime", header: "", tags: [].} - # strftime(s: CString, maxsize: int, fmt: CString, t: tm): int {. - # importc: "strftime", header: "".} proc getClock(): Clock {.importc: "clock", header: "", tags: [TimeEffect].} proc difftime(a, b: Time): float {.importc: "difftime", header: "", tags: [].} @@ -548,21 +536,6 @@ when not defined(JS): add(result, p[i]) inc(i) - proc `$`(timeInfo: TimeInfo): string = - # asctime interprets its input as local time, so we first convert the value - # to local time if necessary - let - localTimeInfo = if timeInfo.timezone == getTimezone(): timeInfo else: - getLocalTime(toTime(timeInfo)) - p = asctime(timeInfoToTM(localTimeInfo)) - # BUGFIX: asctime returns a newline at the end! - result = toStringTillNL(p) - - proc `$`(time: Time): string = - # BUGFIX: ctime returns a newline at the end! - var a = time - return toStringTillNL(ctime(addr(a))) - const epochDiff = 116444736000000000'i64 rateDiff = 10000000'i64 # 100 nsecs @@ -653,9 +626,6 @@ elif defined(JS): result.setDate(timeInfo.monthday) result.setSeconds(timeInfo.second + timeInfo.timezone) - proc `$`(timeInfo: TimeInfo): string = return $(toTime(timeInfo)) - proc `$`(time: Time): string = return $time.toLocaleString() - proc `-` (a, b: Time): int64 = return a.getTime() - b.getTime() @@ -851,22 +821,33 @@ 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 hrs = (info.timezone div 60) div 60 - buf.add($hrs) + let + factor = if info.timezone <= 0: -1 else: 1 + hours = (factor * info.timezone) div 3600 + if factor == 1: buf.add('-') + else: buf.add('+') + buf.add($hours) of "zz": - let hrs = (info.timezone div 60) div 60 - - buf.add($hrs) - if hrs.abs < 10: - var atIndex = buf.len-(($hrs).len-(if hrs < 0: 1 else: 0)) - buf.insert("0", atIndex) + let + factor = if info.timezone <= 0: -1 else: 1 + hours = (factor * info.timezone) div 3600 + if factor == 1: buf.add('-') + else: buf.add('+') + if hours < 10: buf.add('0') + buf.add($hours) of "zzz": - let hrs = (info.timezone div 60) div 60 + let + factor = if info.timezone <= 0: -1 else: 1 + hours = (factor * info.timezone) div 3600 + minutes = (factor * info.timezone) mod 60 + if factor == 1: 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) - buf.add($hrs & ":00") - if hrs.abs < 10: - var atIndex = buf.len-(($hrs & ":00").len-(if hrs < 0: 1 else: 0)) - buf.insert("0", atIndex) of "": discard else: @@ -903,8 +884,7 @@ proc format*(info: TimeInfo, f: string): string = ## yyyy Displays the year to four digits. ``2012 -> 2012`` ## z Displays the timezone offset from UTC. ``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 ``:00``. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00`` - ## ZZZ Displays the name of the timezone. ``GMT -> GMT``, ``EST -> EST`` + ## zzz Same as above but with ``:mm`` where *mm* represents minutes. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00`` ## ========== ================================================================================= ================================================ ## ## Other strings can be inserted by putting them in ``''``. For example @@ -942,6 +922,17 @@ proc format*(info: TimeInfo, f: string): string = inc(i) +proc `$`*(timeInfo: TimeInfo): string {.tags: [], raises: [], benign.} = + ## converts a `TimeInfo` object to a string representation. + ## it will use the format ``yyyy-MM-dd'T'HH-mm-sszzz``. + try: result = format(timeInfo, "yyyy-MM-dd'T'HH:mm:sszzz") + except ValueError: assert false # cannot happen because format string is valid + +proc `$`*(time: Time): string {.tags: [TimeEffect], raises: [], benign.} = + ## converts a `Time` value to a string representation. It will use the local + ## time zone and use the format ``yyyy-MM-dd'T'HH-mm-sszzz``. + $getLocalTime(time) + {.pop.} proc parseToken(info: var TimeInfo; token, value: string; j: var int) = @@ -1161,7 +1152,6 @@ proc parse*(value, layout: string): TimeInfo = ## z Displays the timezone offset from UTC. ``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`` - ## ZZZ Displays the name of the timezone. ``GMT -> GMT``, ``EST -> EST`` ## ========== ================================================================================= ================================================ ## ## Other strings can be inserted by putting them in ``''``. For example diff --git a/tests/stdlib/ttime.nim b/tests/stdlib/ttime.nim index 569d465cc8..3a097cda5d 100644 --- a/tests/stdlib/ttime.nim +++ b/tests/stdlib/ttime.nim @@ -9,77 +9,93 @@ import # $ date --date='@2147483647' # Tue 19 Jan 03:14:07 GMT 2038 -var t = getGMTime(fromSeconds(2147483647)) -doAssert t.format("ddd dd MMM hh:mm:ss yyyy") == "Tue 19 Jan 03:14:07 2038" -doAssert t.format("ddd ddMMMhh:mm:ssyyyy") == "Tue 19Jan03:14:072038" +proc checkFormat(t: TimeInfo, format, expected: string) = + let actual = t.format(format) + if actual != expected: + echo "Formatting failure!" + echo "expected: ", expected + echo "actual : ", actual + doAssert false -doAssert t.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz") == - "19 19 Tue Tuesday 3 03 3 03 14 14 1 01 Jan January 7 07 A AM 8 38 038 2038 02038 0 00 00:00" +let t = getGMTime(fromSeconds(2147483647)) +t.checkFormat("ddd dd MMM hh:mm:ss yyyy", "Tue 19 Jan 03:14:07 2038") +t.checkFormat("ddd ddMMMhh:mm:ssyyyy", "Tue 19Jan03:14:072038") +t.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & + " ss t tt y yy yyy yyyy yyyyy z zz zzz", + "19 19 Tue Tuesday 3 03 3 03 14 14 1 01 Jan January 7 07 A AM 8 38 038 2038 02038 +0 +00 +00:00") -doAssert t.format("yyyyMMddhhmmss") == "20380119031407" +t.checkFormat("yyyyMMddhhmmss", "20380119031407") -var t2 = getGMTime(fromSeconds(160070789)) # Mon 27 Jan 16:06:29 GMT 1975 -doAssert t2.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz") == - "27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 0 00 00:00" +let t2 = getGMTime(fromSeconds(160070789)) # Mon 27 Jan 16:06:29 GMT 1975 +t2.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & + " ss t tt y yy yyy yyyy yyyyy z zz zzz", + "27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 +0 +00 +00:00") when not defined(JS): when sizeof(Time) == 8: var t3 = getGMTime(fromSeconds(889067643645)) # Fri 7 Jun 19:20:45 BST 30143 - doAssert t3.format("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & - " ss t tt y yy yyy yyyy yyyyy z zz zzz") == - "7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 0 00 00:00" - doAssert t3.format(":,[]()-/") == ":,[]()-/" + t3.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & + " ss t tt y yy yyy yyyy yyyyy z zz zzz", + "7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 +0 +00 +00:00") + t3.checkFormat(":,[]()-/", ":,[]()-/") var t4 = getGMTime(fromSeconds(876124714)) # Mon 6 Oct 08:58:34 BST 1997 -doAssert t4.format("M MM MMM MMMM") == "10 10 Oct October" +t4.checkFormat("M MM MMM MMMM", "10 10 Oct October") # Interval tests -doAssert((t4 - initInterval(years = 2)).format("yyyy") == "1995") -doAssert((t4 - initInterval(years = 7, minutes = 34, seconds = 24)).format("yyyy mm ss") == "1990 24 10") +(t4 - initInterval(years = 2)).checkFormat("yyyy", "1995") +(t4 - initInterval(years = 7, minutes = 34, seconds = 24)).checkFormat("yyyy mm ss", "1990 24 10") proc parseTest(s, f, sExpected: string, ydExpected: int) = - let parsed = s.parse(f) - doAssert($parsed == sExpected) + let + parsed = s.parse(f) + parsedStr = $getGMTime(toTime(parsed)) + if parsedStr != sExpected: + echo "Parsing failure!" + echo "expected: ", sExpected + echo "actual : ", parsedStr + doAssert false doAssert(parsed.yearday == ydExpected) proc parseTestTimeOnly(s, f, sExpected: string) = doAssert(sExpected in $s.parse(f)) -parseTest("Tuesday at 09:04am on Dec 15, 2015", - "dddd at hh:mmtt on MMM d, yyyy", "Tue Dec 15 09:04:00 2015", 348) +# because setting a specific timezone for testing is platform-specific, we use +# explicit timezone offsets in all tests. + +parseTest("Tuesday at 09:04am on Dec 15, 2015 +0", + "dddd at hh:mmtt on MMM d, yyyy z", "2015-12-15T09:04:00+00:00", 348) # ANSIC = "Mon Jan _2 15:04:05 2006" -parseTest("Thu Jan 12 15:04:05 2006", "ddd MMM dd HH:mm:ss yyyy", - "Thu Jan 12 15:04:05 2006", 11) +parseTest("Thu Jan 12 15:04:05 2006 +0", "ddd MMM dd HH:mm:ss yyyy z", + "2006-01-12T15:04:05+00:00", 11) # UnixDate = "Mon Jan _2 15:04:05 MST 2006" -parseTest("Thu Jan 12 15:04:05 2006", "ddd MMM dd HH:mm:ss yyyy", - "Thu Jan 12 15:04:05 2006", 11) +parseTest("Thu Jan 12 15:04:05 2006 +0", "ddd MMM dd HH:mm:ss yyyy z", + "2006-01-12T15:04:05+00:00", 11) # RubyDate = "Mon Jan 02 15:04:05 -0700 2006" -parseTest("Mon Feb 29 15:04:05 -07:00 2016", "ddd MMM dd HH:mm:ss zzz yyyy", - "Mon Feb 29 15:04:05 2016", 59) # leap day +parseTest("Mon Feb 29 15:04:05 -07:00 2016 +0", "ddd MMM dd HH:mm:ss zzz yyyy z", + "2016-02-29T15:04:05+00:00", 59) # leap day # RFC822 = "02 Jan 06 15:04 MST" -parseTest("12 Jan 16 15:04", "dd MMM yy HH:mm", - "Tue Jan 12 15:04:00 2016", 11) +parseTest("12 Jan 16 15:04 +0", "dd MMM yy HH:mm z", + "2016-01-12T15:04:00+00:00", 11) # RFC822Z = "02 Jan 06 15:04 -0700" # RFC822 with numeric zone parseTest("01 Mar 16 15:04 -07:00", "dd MMM yy HH:mm zzz", - "Tue Mar 1 15:04:00 2016", 60) # day after february in leap year + "2016-03-01T22:04:00+00:00", 60) # day after february in leap year # RFC850 = "Monday, 02-Jan-06 15:04:05 MST" -parseTest("Monday, 12-Jan-06 15:04:05", "dddd, dd-MMM-yy HH:mm:ss", - "Thu Jan 12 15:04:05 2006", 11) +parseTest("Monday, 12-Jan-06 15:04:05 +0", "dddd, dd-MMM-yy HH:mm:ss z", + "2006-01-12T15:04:05+00:00", 11) # RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" -parseTest("Sun, 01 Mar 2015 15:04:05", "ddd, dd MMM yyyy HH:mm:ss", - "Sun Mar 1 15:04:05 2015", 59) # day after february in non-leap year +parseTest("Sun, 01 Mar 2015 15:04:05 +0", "ddd, dd MMM yyyy HH:mm:ss z", + "2015-03-01T15:04:05+00:00", 59) # day after february in non-leap year # RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" # RFC1123 with numeric zone parseTest("Thu, 12 Jan 2006 15:04:05 -07:00", "ddd, dd MMM yyyy HH:mm:ss zzz", - "Thu Jan 12 15:04:05 2006", 11) + "2006-01-12T22:04:05+00:00", 11) # RFC3339 = "2006-01-02T15:04:05Z07:00" parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-ddTHH:mm:ssZzzz", - "Thu Jan 12 15:04:05 2006", 11) + "2006-01-12T22:04:05+00:00", 11) parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-dd'T'HH:mm:ss'Z'zzz", - "Thu Jan 12 15:04:05 2006", 11) + "2006-01-12T22:04:05+00:00", 11) # RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" parseTest("2006-01-12T15:04:05.999999999Z-07:00", - "yyyy-MM-ddTHH:mm:ss.999999999Zzzz", "Thu Jan 12 15:04:05 2006", 11) + "yyyy-MM-ddTHH:mm:ss.999999999Zzzz", "2006-01-12T22:04:05+00:00", 11) # Kitchen = "3:04PM" parseTestTimeOnly("3:04PM", "h:mmtt", "15:04:00") #when not defined(testing): @@ -101,21 +117,20 @@ doAssert getDayOfWeekJulian(21, 9, 1970) == dMon doAssert getDayOfWeekJulian(1, 1, 2000) == dSat doAssert getDayOfWeekJulian(1, 1, 2021) == dFri -# toSeconds tests with GM and Local timezones -#var t4 = getGMTime(fromSeconds(876124714)) # Mon 6 Oct 08:58:34 BST 1997 -var t4L = getLocalTime(fromSeconds(876124714)) -doAssert toSeconds(timeInfoToTime(t4L)) == 876124714 # fromSeconds is effectively "localTime" -doAssert toSeconds(timeInfoToTime(t4L)) + t4L.timezone.float == toSeconds(timeInfoToTime(t4)) +# toSeconds tests with GM timezone +let t4L = getGMTime(fromSeconds(876124714)) +doAssert toSeconds(toTime(t4L)) == 876124714 +doAssert toSeconds(toTime(t4L)) + t4L.timezone.float == toSeconds(toTime(t4)) # adding intervals var - a1L = toSeconds(timeInfoToTime(t4L + initInterval(hours = 1))) + t4L.timezone.float - a1G = toSeconds(timeInfoToTime(t4)) + 60.0 * 60.0 + a1L = toSeconds(toTime(t4L + initInterval(hours = 1))) + t4L.timezone.float + a1G = toSeconds(toTime(t4)) + 60.0 * 60.0 doAssert a1L == a1G # subtracting intervals -a1L = toSeconds(timeInfoToTime(t4L - initInterval(hours = 1))) + t4L.timezone.float -a1G = toSeconds(timeInfoToTime(t4)) - (60.0 * 60.0) +a1L = toSeconds(toTime(t4L - initInterval(hours = 1))) + t4L.timezone.float +a1G = toSeconds(toTime(t4)) - (60.0 * 60.0) doAssert a1L == a1G # add/subtract TimeIntervals and Time/TimeInfo From 5d438ab05b3e51ed729f8f5a698c9e1af19594a5 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 21:29:19 +0100 Subject: [PATCH 6/9] info about TimeInfo.tzname in news --- web/news/e029_version_0_16_0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/news/e029_version_0_16_0.rst b/web/news/e029_version_0_16_0.rst index 2f6c72c829..3222542963 100644 --- a/web/news/e029_version_0_16_0.rst +++ b/web/news/e029_version_0_16_0.rst @@ -26,7 +26,9 @@ Changes affecting backwards compatibility - ``staticExec`` now uses the directory of the nim file that contains the ``staticExec`` call as the current working directory. - +- ``TimeInfo.tzname`` has been removed because it was broken. Because of this, + the option `"ZZZ"` will no longer work in format strings for formatting and + parsing. Library Additions ----------------- From 88f152e7dde6d2f389e918888ed2ec5d5a585972 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 1 Nov 2016 21:31:14 +0100 Subject: [PATCH 7/9] Improved news formatting --- web/news/e029_version_0_16_0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/news/e029_version_0_16_0.rst b/web/news/e029_version_0_16_0.rst index 3222542963..94c9757a73 100644 --- a/web/news/e029_version_0_16_0.rst +++ b/web/news/e029_version_0_16_0.rst @@ -26,9 +26,9 @@ Changes affecting backwards compatibility - ``staticExec`` now uses the directory of the nim file that contains the ``staticExec`` call as the current working directory. -- ``TimeInfo.tzname`` has been removed because it was broken. Because of this, - the option `"ZZZ"` will no longer work in format strings for formatting and - parsing. +- ``TimeInfo.tzname`` has been removed from ``times`` module because it was + broken. Because of this, the option ``"ZZZ"`` will no longer work in format + strings for formatting and parsing. Library Additions ----------------- From f500b9f47beb1026d00f34ec3c87c1b6ec526a7c Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Thu, 3 Nov 2016 18:45:52 +0100 Subject: [PATCH 8/9] Cosmetic fixes * Improved comments * Improved spacing * Use consts instead of magic numbers --- lib/pure/times.nim | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index b24f12ff05..6aeec24a30 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -732,6 +732,12 @@ proc `-`*(t: Time, ti: TimeInterval): Time = ## ``echo getTime() - 1.day`` result = toTime(getLocalTime(t) - ti) +const + secondsInMin = 60 + secondsInHour = 60*60 + secondsInDay = 60*60*24 + epochStartYear = 1970 + proc formatToken(info: TimeInfo, token: string, buf: var string) = ## Helper of the format proc to parse individual tokens. ## @@ -823,14 +829,14 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = of "z": let factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div 3600 + hours = (factor * info.timezone) div secondsInHour if factor == 1: buf.add('-') else: buf.add('+') buf.add($hours) of "zz": let factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div 3600 + hours = (factor * info.timezone) div secondsInHour if factor == 1: buf.add('-') else: buf.add('+') if hours < 10: buf.add('0') @@ -838,7 +844,7 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = of "zzz": let factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div 3600 + hours = (factor * info.timezone) div secondsInHour minutes = (factor * info.timezone) mod 60 if factor == 1: buf.add('-') else: buf.add('+') @@ -924,8 +930,9 @@ proc format*(info: TimeInfo, f: string): string = proc `$`*(timeInfo: TimeInfo): string {.tags: [], raises: [], benign.} = ## converts a `TimeInfo` object to a string representation. - ## it will use the format ``yyyy-MM-dd'T'HH-mm-sszzz``. - try: result = format(timeInfo, "yyyy-MM-dd'T'HH:mm:sszzz") + ## It uses the format ``yyyy-MM-dd'T'HH-mm-sszzz``. + try: + result = format(timeInfo, "yyyy-MM-dd'T'HH:mm:sszzz") # todo: optimize this except ValueError: assert false # cannot happen because format string is valid proc `$`*(time: Time): string {.tags: [TimeEffect], raises: [], benign.} = @@ -1091,18 +1098,18 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = j += 4 of "z": if value[j] == '+': - info.timezone = 0-parseInt($value[j+1]) * 3600 + info.timezone = 0 - parseInt($value[j+1]) * secondsInHour elif value[j] == '-': - info.timezone = parseInt($value[j+1]) * 3600 + info.timezone = parseInt($value[j+1]) * secondsInHour else: raise newException(ValueError, "Couldn't parse timezone offset (z), got: " & value[j]) j += 2 of "zz": if value[j] == '+': - info.timezone = 0-value[j+1..j+2].parseInt() * 3600 + info.timezone = 0 - value[j+1..j+2].parseInt() * secondsInHour elif value[j] == '-': - info.timezone = value[j+1..j+2].parseInt() * 3600 + info.timezone = value[j+1..j+2].parseInt() * secondsInHour else: raise newException(ValueError, "Couldn't parse timezone offset (zz), got: " & value[j]) @@ -1114,7 +1121,7 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = else: raise newException(ValueError, "Couldn't parse timezone offset (zzz), got: " & value[j]) - info.timezone = factor * value[j+1..j+2].parseInt() * 3600 + info.timezone = factor * value[j+1..j+2].parseInt() * secondsInHour j += 4 info.timezone += factor * value[j..j+1].parseInt() * 60 j += 2 @@ -1237,12 +1244,6 @@ proc countYearsAndDays*(daySpan: int): tuple[years: int, days: int] = result.years = days div 365 result.days = days mod 365 -const - secondsInMin = 60 - secondsInHour = 60*60 - secondsInDay = 60*60*24 - epochStartYear = 1970 - proc getDayOfWeek*(day, month, year: int): WeekDay = ## Returns the day of the week enum from day, month and year. # Day & month start from one. From 6e604e2f9f59c75b198e84462a709ba847c519ea Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Mon, 7 Nov 2016 11:08:31 +0100 Subject: [PATCH 9/9] More cosmetic changes * Don't use factor var, it's overly complicated * Removed proc that's now unused * Better documented timezone field --- lib/pure/times.nim | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 6aeec24a30..41f513b738 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -147,7 +147,10 @@ type isDST*: bool ## Determines whether DST is in effect. Always ## ``False`` if time is UTC. timezone*: int ## The offset of the (non-DST) timezone in seconds - ## west of UTC. + ## west of UTC. Note that the sign of this number + ## is the opposite of the one in a formatted + ## timezone string like ``+01:00`` (which would be + ## parsed into the timezone ``-3600``). ## I make some assumptions about the data in here. Either ## everything should be positive or everything negative. Zero is @@ -529,13 +532,6 @@ when not defined(JS): # does ignore the timezone, we need to adjust this here. result = Time(TimeImpl(result) - getTimezone() + timeInfo.timezone) - proc toStringTillNL(p: cstring): string = - result = "" - var i = 0 - while p[i] != '\0' and p[i] != '\10' and p[i] != '\13': - add(result, p[i]) - inc(i) - const epochDiff = 116444736000000000'i64 rateDiff = 10000000'i64 # 100 nsecs @@ -827,26 +823,21 @@ 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 - factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div secondsInHour - if factor == 1: buf.add('-') + let hours = abs(info.timezone) div secondsInHour + if info.timezone < 0: buf.add('-') else: buf.add('+') buf.add($hours) of "zz": - let - factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div secondsInHour - if factor == 1: buf.add('-') + let hours = abs(info.timezone) div secondsInHour + if info.timezone < 0: buf.add('-') else: buf.add('+') if hours < 10: buf.add('0') buf.add($hours) of "zzz": let - factor = if info.timezone <= 0: -1 else: 1 - hours = (factor * info.timezone) div secondsInHour - minutes = (factor * info.timezone) mod 60 - if factor == 1: buf.add('-') + hours = abs(info.timezone) div secondsInHour + minutes = abs(info.timezone) mod 60 + if info.timezone < 0: buf.add('-') else: buf.add('+') if hours < 10: buf.add('0') buf.add($hours)