summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2016-11-07 14:28:54 +0100
committerGitHub <noreply@github.com>2016-11-07 14:28:54 +0100
commitbe296c3274a48af8049b940a0f016bbc80091f62 (patch)
tree34f35b0af300da7d86e795433dc50648aaeac8fb
parentd7bfafaa42eb13926d8c4109e665ce11fd5b3bf7 (diff)
parent6e604e2f9f59c75b198e84462a709ba847c519ea (diff)
downloadNim-be296c3274a48af8049b940a0f016bbc80091f62.tar.gz
Merge pull request #4984 from flyx/timezonefix
Fixed timezone handling
-rw-r--r--lib/pure/times.nim229
-rw-r--r--tests/stdlib/ttime.nim117
-rw-r--r--web/news/e029_version_0_16_0.rst4
3 files changed, 153 insertions, 197 deletions
diff --git a/lib/pure/times.nim b/lib/pure/times.nim
index db09f94c1..41f513b73 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: "<time.h>".}: int
-  var
-    tzname {.importc, header: "<time.h>" .}: array[0..1, cstring]
-  # we also need tzset() to make sure that tzname is initialized
-  proc tzset() {.importc, header: "<time.h>".}
-  # 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: "<time.h>".}: int
-      tzname {.importc: "_tzname", header: "<time.h>"}: array[0..1, cstring]
   else:
     type TimeImpl {.importc: "time_t", header: "<time.h>".} = int
     var
       timezone {.importc, header: "<time.h>".}: int
-      tzname {.importc, header: "<time.h>" .}: array[0..1, cstring]
 
   type
     Time* = distinct TimeImpl
@@ -154,9 +146,11 @@ 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.
+                              ## 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
@@ -184,7 +178,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 +188,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,11 +206,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: [], 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.
@@ -235,12 +225,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.
 
@@ -369,7 +353,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))
@@ -389,7 +373,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))
@@ -424,7 +408,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".},
@@ -461,12 +446,6 @@ when not defined(JS):
     importc: "time", header: "<time.h>", tags: [].}
   proc mktime(t: StructTM): Time {.
     importc: "mktime", header: "<time.h>", tags: [].}
-  proc asctime(tblock: StructTM): cstring {.
-    importc: "asctime", header: "<time.h>", tags: [].}
-  proc ctime(time: ptr Time): cstring {.
-    importc: "ctime", header: "<time.h>", tags: [].}
-  #  strftime(s: CString, maxsize: int, fmt: CString, t: tm): int {.
-  #    importc: "strftime", header: "<time.h>".}
   proc getClock(): Clock {.importc: "clock", header: "<time.h>", tags: [TimeEffect].}
   proc difftime(a, b: Time): float {.importc: "difftime", header: "<time.h>",
     tags: [].}
@@ -479,46 +458,17 @@ 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
-          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
-      )
+    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,
+      timezone: if local: getTimezone() else: 0
+    )
 
 
   proc timeInfoToTM(t: TimeInfo): StructTM =
@@ -569,29 +519,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))
-
-  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)
-
-  proc `$`(timeInfo: TimeInfo): string =
-    # BUGFIX: asctime returns a newline at the end!
-    var p = asctime(timeInfoToTM(timeInfo))
-    result = toStringTillNL(p)
-
-  proc `$`(time: Time): string =
-    # BUGFIX: ctime returns a newline at the end!
-    var a = time
-    return toStringTillNL(ctime(addr(a)))
+    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)
 
   const
     epochDiff = 116444736000000000'i64
@@ -605,9 +544,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)
@@ -675,26 +611,16 @@ 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()
-    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 `$`(timeInfo: TimeInfo): string = return $(toTime(timeInfo))
-  proc `$`(time: Time): string = return $time.toLocaleString()
+    result.setSeconds(timeInfo.second + timeInfo.timezone)
 
   proc `-` (a, b: Time): int64 =
     return a.getTime() - b.getTime()
@@ -802,6 +728,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.
   ##
@@ -891,24 +823,28 @@ 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 hours = abs(info.timezone) div secondsInHour
+    if info.timezone < 0: 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 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 hrs = (info.timezone div 60) div 60
-
-    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 "ZZZ":
-    buf.add(info.tzname)
+    let
+      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)
+    buf.add(':')
+    if minutes < 10: buf.add('0')
+    buf.add($minutes)
+
   of "":
     discard
   else:
@@ -945,8 +881,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
@@ -984,6 +919,18 @@ 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 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.} =
+  ## 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) =
@@ -1142,34 +1089,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]) * secondsInHour
     elif value[j] == '-':
-      info.timezone = 0-parseInt($value[j+1])
+      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 = value[j+1..j+2].parseInt()
+      info.timezone = 0 - value[j+1..j+2].parseInt() * secondsInHour
     elif value[j] == '-':
-      info.timezone = 0-value[j+1..j+2].parseInt()
+      info.timezone = value[j+1..j+2].parseInt() * secondsInHour
     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
-  of "ZZZ":
-    info.tzname = value[j..j+2].toUpperAscii()
-    j += 3
+    info.timezone = factor * value[j+1..j+2].parseInt() * secondsInHour
+    j += 4
+    info.timezone += factor * value[j..j+1].parseInt() * 60
+    j += 2
   else:
     # Ignore the token and move forward in the value string by the same length
     j += token.len
@@ -1203,8 +1149,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      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
@@ -1257,7 +1202,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))
 
@@ -1290,12 +1235,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.
diff --git a/tests/stdlib/ttime.nim b/tests/stdlib/ttime.nim
index 065009535..3a097cda5 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 ZZZ yyyy") == "Tue 19 Jan 03:14:07 UTC 2038"
-doAssert t.format("ddd ddMMMhh:mm:ssZZZyyyy") == "Tue 19Jan03:14:07UTC2038"
-
-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"
-
-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"
+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
+
+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")
+
+t.checkFormat("yyyyMMddhhmmss", "20380119031407")
+
+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 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"
-    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 MST 2006", "ddd MMM dd HH:mm:ss ZZZ 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 MST", "dd MMM yy HH:mm ZZZ",
-    "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 MST", "dddd, dd-MMM-yy HH:mm:ss ZZZ",
-    "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 MST", "ddd, dd MMM yyyy HH:mm:ss ZZZ",
-    "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
diff --git a/web/news/e029_version_0_16_0.rst b/web/news/e029_version_0_16_0.rst
index 2f6c72c82..94c9757a7 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 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
 -----------------