summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--lib/pure/strformat.nim4
-rw-r--r--lib/pure/times.nim1376
-rw-r--r--tests/js/ttimes.nim6
-rw-r--r--tests/stdlib/ttimes.nim210
4 files changed, 976 insertions, 620 deletions
diff --git a/lib/pure/strformat.nim b/lib/pure/strformat.nim
index 247b9ec5c..f13eb5e8e 100644
--- a/lib/pure/strformat.nim
+++ b/lib/pure/strformat.nim
@@ -683,8 +683,8 @@ when isMainModule:
   # works:
   import times
 
-  var nullTime: DateTime
-  check &"{nullTime:yyyy-mm-dd}", "0000-00-00"
+  var dt = initDateTime(01, mJan, 2000, 00, 00, 00)
+  check &"{dt:yyyy-MM-dd}", "2000-01-01"
 
   var tm = fromUnix(0)
   discard &"{tm}"
diff --git a/lib/pure/times.nim b/lib/pure/times.nim
index a134faef2..05398cfad 100644
--- a/lib/pure/times.nim
+++ b/lib/pure/times.nim
@@ -7,35 +7,127 @@
 #    distribution, for details about the copyright.
 #
 
+##[
+  This module contains routines and types for dealing with time using a proleptic Gregorian calendar.
+  It's also available for the `JavaScript target <backends.html#the-javascript-target>`_.
+
+  Although the types use nanosecond time resolution, the underlying resolution used by ``getTime()``
+  depends on the platform and backend (JS is limited to millisecond precision).
+
+  Examples:
+
+  .. code-block:: nim
+
+    import times, os
+    let time = cpuTime()
+
+    sleep(100)   # replace this with something to be timed
+    echo "Time taken: ",cpuTime() - time
+
+    echo "My formatted time: ", format(now(), "d MMMM yyyy HH:mm")
+    echo "Using predefined formats: ", getClockStr(), " ", getDateStr()
+
+    echo "cpuTime()  float value: ", cpuTime()
+    echo "An hour from now      : ", now() + 1.hours
+    echo "An hour from (UTC) now: ", getTime().utc + initDuration(hours = 1)
+
+  Parsing and Formatting Dates
+  ----------------------------
+
+  The ``DateTime`` type can be parsed and formatted using the different
+  ``parse`` and ``format`` procedures.
+
+  .. code-block:: nim
+
+    let dt = parse("2000-01-01", "yyyy-MM-dd")
+    echo dt.format("yyyy-MM-dd")
+
+  The different format patterns that are supported are documented below.
+
+  =============  =================================================================================  ================================================
+  Pattern        Description                                                                        Example
+  =============  =================================================================================  ================================================
+  ``d``          Numeric value representing the day of the month,                                   | ``1/04/2012 -> 1``
+                 it will be either one or two digits long.                                          | ``21/04/2012 -> 21``
+  ``dd``         Same as above, but is always two digits.                                           | ``1/04/2012 -> 01``
+                                                                                                    | ``21/04/2012 -> 21``
+  ``ddd``        Three letter string which indicates the day of the week.                           | ``Saturday -> Sat``
+                                                                                                    | ``Monday -> Mon``
+  ``dddd``       Full string for the day of the week.                                               | ``Saturday -> Saturday``
+                                                                                                    | ``Monday -> Monday``
+  ``h``          The hours in one digit if possible. Ranging from 1-12.                             | ``5pm -> 5``
+                                                                                                    | ``2am -> 2``
+  ``hh``         The hours in two digits always. If the hour is one digit 0 is prepended.           | ``5pm -> 05``
+                                                                                                    | ``11am -> 11``
+  ``H``          The hours in one digit if possible, ranging from 0-23.                             | ``5pm -> 17``
+                                                                                                    | ``2am -> 2``
+  ``HH``         The hours in two digits always. 0 is prepended if the hour is one digit.           | ``5pm -> 17``
+                                                                                                    | ``2am -> 02``
+  ``m``          The minutes in 1 digit if possible.                                                | ``5:30 -> 30``
+                                                                                                    | ``2:01 -> 1``
+  ``mm``         Same as above but always 2 digits, 0 is prepended if the minute is one digit.      | ``5:30 -> 30``
+                                                                                                    | ``2:01 -> 01``
+  ``M``          The month in one digit if possible.                                                | ``September -> 9``
+                                                                                                    | ``December -> 12``
+  ``MM``         The month in two digits always. 0 is prepended.                                    | ``September -> 09``
+                                                                                                    | ``December -> 12``
+  ``MMM``        Abbreviated three-letter form of the month.                                        | ``September -> Sep``
+                                                                                                    | ``December -> Dec``
+  ``MMMM``       Full month string, properly capitalized.                                           | ``September -> September``
+  ``s``          Seconds as one digit if possible.                                                  | ``00:00:06 -> 6``
+  ``ss``         Same as above but always two digits. 0 is prepended.                               | ``00:00:06 -> 06``
+  ``t``          ``A`` when time is in the AM. ``P`` when time is in the PM.                        | ``5pm -> P``
+                                                                                                    | ``2am -> A``
+  ``tt``         Same as above, but ``AM`` and ``PM`` instead of ``A`` and ``P`` respectively.      | ``5pm -> PM``
+                                                                                                    | ``2am -> AM``
+  ``yy``         The last two digits of the year. When parsing, the current century is assumed.     | ``2012 AD -> 12``
+  ``yyyy``       The year, padded to atleast four digits.                                           | ``2012 AD -> 2012``
+                 Is always positive, even when the year is BC.                                      | ``24 AD -> 0024``
+                 When the year is more than four digits, '+' is prepended.                          | ``24 BC -> 00024``
+                                                                                                    | ``12345 AD -> +12345``
+  ``YYYY``       The year without any padding.                                                      | ``2012 AD -> 2012``
+                 Is always positive, even when the year is BC.                                      | ``24 AD -> 24``
+                                                                                                    | ``24 BC -> 24``
+                                                                                                    | ``12345 AD -> 12345``
+  ``uuuu``       The year, padded to atleast four digits. Will be negative when the year is BC.     | ``2012 AD -> 2012``
+                 When the year is more than four digits, '+' is prepended unless the year is BC.    | ``24 AD -> 0024``
+                                                                                                    | ``24 BC -> -0023``
+                                                                                                    | ``12345 AD -> +12345``
+  ``UUUU``       The year without any padding. Will be negative when the year is BC.                | ``2012 AD -> 2012``
+                                                                                                    | ``24 AD -> 24``
+                                                                                                    | ``24 BC -> -23``
+                                                                                                    | ``12345 AD -> 12345``
+  ``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``
+  ``zzzz``       Same as above but with ``:ss`` where *ss* represents seconds.                      | ``GMT+7 -> +07:00:00``
+                                                                                                    | ``GMT-5 -> -05:00:00``
+  ``g``          Era: AD or BC                                                                      | ``300 AD -> AD``
+                                                                                                    | ``300 BC -> BC``
+  ``fff``        Milliseconds display                                                               | ``1000000 nanoseconds -> 1``
+  ``ffffff``     Microseconds display                                                               | ``1000000 nanoseconds -> 1000``
+  ``fffffffff``  Nanoseconds display                                                                | ``1000000 nanoseconds -> 1000000``
+  =============  =================================================================================  ================================================
+
+  Other strings can be inserted by putting them in ``''``. For example
+  ``hh'->'mm`` will give ``01->56``.  The following characters can be
+  inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]``
+  ``,``. A literal ``'`` can be specified with ``''``.
+
+  However you don't need to necessarily separate format patterns, a
+  unambiguous format string like ``yyyyMMddhhmmss`` is valid too (although
+  only for years in the range 1..9999).
+]##
 
-## This module contains routines and types for dealing with time using a proleptic Gregorian calendar.
-## It's is available for the `JavaScript target <backends.html#the-javascript-target>`_.
-##
-## The types uses nanosecond time resolution, but the underlying resolution used by ``getTime()``
-## depends on the platform and backend (JS is limited to millisecond precision).
-##
-## Examples:
-##
-## .. code-block:: nim
-##
-##  import times, os
-##  let time = cpuTime()
-##
-##  sleep(100)   # replace this with something to be timed
-##  echo "Time taken: ",cpuTime() - time
-##
-##  echo "My formatted time: ", format(now(), "d MMMM yyyy HH:mm")
-##  echo "Using predefined formats: ", getClockStr(), " ", getDateStr()
-##
-##  echo "cpuTime()  float value: ", cpuTime()
-##  echo "An hour from now      : ", now() + 1.hours
-##  echo "An hour from (UTC) now: ", getTime().utc + initDuration(hours = 1)
 
 {.push debugger:off.} # the user does not want to trace a part
                       # of the standard library!
 
 import
-  strutils, parseutils, algorithm, math
+  strutils, parseutils, algorithm, math, options, strformat
 
 include "system/inclrtl"
 
@@ -306,7 +398,7 @@ proc fractional*(dur: Duration): Duration {.inline.} =
 proc fromUnix*(unix: int64): Time {.benign, tags: [], raises: [], noSideEffect.} =
   ## Convert a unix timestamp (seconds since ``1970-01-01T00:00:00Z``) to a ``Time``.
   runnableExamples:
-    doAssert $fromUnix(0).utc == "1970-01-01T00:00:00+00:00"
+    doAssert $fromUnix(0).utc == "1970-01-01T00:00:00Z"
   initTime(unix, 0)
 
 proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} =
@@ -915,7 +1007,7 @@ proc initTimeInterval*(nanoseconds, microseconds, milliseconds,
   runnableExamples:
     let day = initTimeInterval(hours=24)
     let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc())
-    doAssert $(dt + day) == "2000-01-02T12:00:00+00:00"
+    doAssert $(dt + day) == "2000-01-02T12:00:00Z"
   result.nanoseconds = nanoseconds
   result.microseconds = microseconds
   result.milliseconds = milliseconds
@@ -1126,7 +1218,7 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int,
   ## Create a new ``DateTime`` in the specified timezone.
   runnableExamples:
     let dt1 = initDateTime(30, mMar, 2017, 00, 00, 00, 00, utc())
-    doAssert $dt1 == "2017-03-30T00:00:00+00:00"
+    doAssert $dt1 == "2017-03-30T00:00:00Z"
 
   assertValidDate monthday, month, year
   let dt = DateTime(
@@ -1146,7 +1238,7 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int,
   ## Create a new ``DateTime`` in the specified timezone.
   runnableExamples:
     let dt1 = initDateTime(30, mMar, 2017, 00, 00, 00, utc())
-    doAssert $dt1 == "2017-03-30T00:00:00+00:00"
+    doAssert $dt1 == "2017-03-30T00:00:00Z"
   initDateTime(monthday, month, year, hour, minute, second, 0, zone)
 
 
@@ -1162,9 +1254,9 @@ proc `+`*(dt: DateTime, interval: TimeInterval): DateTime =
   ##
   runnableExamples:
     let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc())
-    doAssert $(dt + 1.months) == "2017-04-30T00:00:00+00:00"
+    doAssert $(dt + 1.months) == "2017-04-30T00:00:00Z"
     # This is correct and happens due to monthday overflow.
-    doAssert $(dt - 1.months) == "2017-03-02T00:00:00+00:00"
+    doAssert $(dt - 1.months) == "2017-03-02T00:00:00Z"
   let (adjDur, absDur) = evaluateInterval(dt, interval)
 
   if adjDur != DurationZero:
@@ -1185,7 +1277,7 @@ proc `-`*(dt: DateTime, interval: TimeInterval): DateTime =
   ## component and so on. The returned ``DateTime`` will have the same timezone as the input.
   runnableExamples:
     let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc())
-    doAssert $(dt - 5.days) == "2017-03-25T00:00:00+00:00"
+    doAssert $(dt - 5.days) == "2017-03-25T00:00:00Z"
 
   dt + (-interval)
 
@@ -1193,7 +1285,7 @@ proc `+`*(dt: DateTime, dur: Duration): DateTime =
   runnableExamples:
     let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc())
     let dur = initDuration(hours = 5)
-    doAssert $(dt + dur) == "2017-03-30T05:00:00+00:00"
+    doAssert $(dt + dur) == "2017-03-30T05:00:00Z"
 
   (dt.toTime + dur).inZone(dt.timezone)
 
@@ -1201,7 +1293,7 @@ proc `-`*(dt: DateTime, dur: Duration): DateTime =
   runnableExamples:
     let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc())
     let dur = initDuration(days = 5)
-    doAssert $(dt - dur) == "2017-03-25T00:00:00+00:00"
+    doAssert $(dt - dur) == "2017-03-25T00:00:00Z"
 
   (dt.toTime - dur).inZone(dt.timezone)
 
@@ -1394,228 +1486,726 @@ proc `*=`*[T: TimesMutableTypes, U](a: var T, b: U) =
 
   a = a * b
 
-proc formatToken(dt: DateTime, token: string, buf: var string) =
-  ## Helper of the format proc to parse individual tokens.
-  ##
-  ## Pass the found token in the user input string, and the buffer where the
-  ## final string is being built. This has to be a var value because certain
-  ## formatting tokens require modifying the previous characters.
-  case token
-  of "d":
-    buf.add($dt.monthday)
-  of "dd":
-    if dt.monthday < 10:
-      buf.add("0")
-    buf.add($dt.monthday)
-  of "ddd":
-    buf.add(($dt.weekday)[0 .. 2])
-  of "dddd":
-    buf.add($dt.weekday)
-  of "h":
-    if dt.hour == 0: buf.add("12")
-    else: buf.add($(if dt.hour > 12: dt.hour - 12 else: dt.hour))
-  of "hh":
-    if dt.hour == 0:
-      buf.add("12")
-    else:
-      let amerHour = if dt.hour > 12: dt.hour - 12 else: dt.hour
-      if amerHour < 10:
-        buf.add('0')
-      buf.add($amerHour)
-  of "H":
-    buf.add($dt.hour)
-  of "HH":
-    if dt.hour < 10:
-      buf.add('0')
-    buf.add($dt.hour)
-  of "m":
-    buf.add($dt.minute)
-  of "mm":
-    if dt.minute < 10:
-      buf.add('0')
-    buf.add($dt.minute)
-  of "M":
-    buf.add($ord(dt.month))
-  of "MM":
-    if dt.month < mOct:
-      buf.add('0')
-    buf.add($ord(dt.month))
-  of "MMM":
-    buf.add(($dt.month)[0..2])
-  of "MMMM":
-    buf.add($dt.month)
-  of "s":
-    buf.add($dt.second)
-  of "ss":
-    if dt.second < 10:
-      buf.add('0')
-    buf.add($dt.second)
-  of "t":
-    if dt.hour >= 12:
-      buf.add('P')
-    else: buf.add('A')
-  of "tt":
-    if dt.hour >= 12:
-      buf.add("PM")
-    else: buf.add("AM")
-  of "y":
-    var fr = ($dt.year).len()-1
-    if fr < 0: fr = 0
-    buf.add(($dt.year)[fr .. ($dt.year).len()-1])
-  of "yy":
-    var fr = ($dt.year).len()-2
-    if fr < 0: fr = 0
-    var fyear = ($dt.year)[fr .. ($dt.year).len()-1]
-    if fyear.len != 2: fyear = repeat('0', 2-fyear.len()) & fyear
-    buf.add(fyear)
-  of "yyy":
-    var fr = ($dt.year).len()-3
-    if fr < 0: fr = 0
-    var fyear = ($dt.year)[fr .. ($dt.year).len()-1]
-    if fyear.len != 3: fyear = repeat('0', 3-fyear.len()) & fyear
-    buf.add(fyear)
-  of "yyyy":
-    var fr = ($dt.year).len()-4
-    if fr < 0: fr = 0
-    var fyear = ($dt.year)[fr .. ($dt.year).len()-1]
-    if fyear.len != 4: fyear = repeat('0', 4-fyear.len()) & fyear
-    buf.add(fyear)
-  of "yyyyy":
-    var fr = ($dt.year).len()-5
-    if fr < 0: fr = 0
-    var fyear = ($dt.year)[fr .. ($dt.year).len()-1]
-    if fyear.len != 5: fyear = repeat('0', 5-fyear.len()) & fyear
-    buf.add(fyear)
-  of "z":
-    let
-      nonDstTz = dt.utcOffset
-      hours = abs(nonDstTz) div secondsInHour
-    if nonDstTz <= 0: buf.add('+')
-    else: buf.add('-')
-    buf.add($hours)
-  of "zz":
-    let
-      nonDstTz = dt.utcOffset
-      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
-      nonDstTz = dt.utcOffset
-      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 "fff":
-    buf.add(intToStr(convert(Nanoseconds, Milliseconds, dt.nanosecond), 3))
-  of "ffffff":
-    buf.add(intToStr(convert(Nanoseconds, Microseconds, dt.nanosecond), 6))
-  of "fffffffff":
-    buf.add(intToStr(dt.nanosecond, 9))
-  of "":
-    discard
-  else:
-    raise newException(ValueError, "Invalid format string: " & token)
+#
+# Parse & format implementation
+#
 
-proc format*(dt: DateTime, f: string): string {.tags: [].}=
-  ## This procedure formats `dt` as specified by `f`. The following format
-  ## specifiers are available:
-  ##
-  ## ============  =================================================================================  ================================================
-  ## Specifier     Description                                                                        Example
-  ## ============  =================================================================================  ================================================
-  ##    d          Numeric value of the day of the month, it will be one or two digits long.          ``1/04/2012 -> 1``, ``21/04/2012 -> 21``
-  ##    dd         Same as above, but always two digits.                                              ``1/04/2012 -> 01``, ``21/04/2012 -> 21``
-  ##    ddd        Three letter string which indicates the day of the week.                           ``Saturday -> Sat``, ``Monday -> Mon``
-  ##    dddd       Full string for the day of the week.                                               ``Saturday -> Saturday``, ``Monday -> Monday``
-  ##    h          The hours in one digit if possible. Ranging from 0-12.                             ``5pm -> 5``, ``2am -> 2``
-  ##    hh         The hours in two digits always. If the hour is one digit 0 is prepended.           ``5pm -> 05``, ``11am -> 11``
-  ##    H          The hours in one digit if possible, randing from 0-24.                             ``5pm -> 17``, ``2am -> 2``
-  ##    HH         The hours in two digits always. 0 is prepended if the hour is one digit.           ``5pm -> 17``, ``2am -> 02``
-  ##    m          The minutes in 1 digit if possible.                                                ``5:30 -> 30``, ``2:01 -> 1``
-  ##    mm         Same as above but always 2 digits, 0 is prepended if the minute is one digit.      ``5:30 -> 30``, ``2:01 -> 01``
-  ##    M          The month in one digit if possible.                                                ``September -> 9``, ``December -> 12``
-  ##    MM         The month in two digits always. 0 is prepended.                                    ``September -> 09``, ``December -> 12``
-  ##    MMM        Abbreviated three-letter form of the month.                                        ``September -> Sep``, ``December -> Dec``
-  ##    MMMM       Full month string, properly capitalized.                                           ``September -> September``
-  ##    s          Seconds as one digit if possible.                                                  ``00:00:06 -> 6``
-  ##    ss         Same as above but always two digits. 0 is prepended.                               ``00:00:06 -> 06``
-  ##    t          ``A`` when time is in the AM. ``P`` when time is in the PM.
-  ##    tt         Same as above, but ``AM`` and ``PM`` instead of ``A`` and ``P`` respectively.
-  ##    y(yyyy)    This displays the year to different digits. You most likely only want 2 or 4 'y's
-  ##    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``
-  ##    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``
-  ##    fff        Milliseconds display                                                               ``1000000 nanoseconds -> 1``
-  ##    ffffff     Microseconds display                                                               ``1000000 nanoseconds -> 1000``
-  ##    fffffffff  Nanoseconds display                                                                ``1000000 nanoseconds -> 1000000``
-  ## ============  =================================================================================  ================================================
-  ##
-  ## Other strings can be inserted by putting them in ``''``. For example
-  ## ``hh'->'mm`` will give ``01->56``.  The following characters can be
-  ## inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]``
-  ## ``,``. However you don't need to necessarily separate format specifiers, a
-  ## unambiguous format string like ``yyyyMMddhhmmss`` is valid too.
+type
+  AmPm = enum
+    apUnknown, apAm, apPm
+
+  Era = enum
+    eraUnknown, eraAd, eraBc
+
+  ParsedTime = object
+    amPm: AmPm
+    era: Era
+    year: Option[int]
+    month: Option[int]
+    monthday: Option[int]
+    utcOffset: Option[int]
+
+    # '0' as default for these work fine
+    # so no need for `Option`.
+    hour: int
+    minute: int
+    second: int
+    nanosecond: int
+
+  FormatTokenKind = enum
+    tkPattern, tkLiteral
+
+  FormatPattern {.pure.} = enum
+    d, dd, ddd, dddd
+    h, hh, H, HH
+    m, mm, M, MM, MMM, MMMM
+    s, ss
+    fff, ffffff, fffffffff
+    t, tt
+    y, yy, yyy, yyyy, yyyyy
+    YYYY
+    uuuu
+    UUUU
+    z, zz, zzz, zzzz
+    g
+
+    # This is a special value used to mark literal format values.
+    # See the doc comment for ``TimeFormat.patterns``.
+    Lit
+
+  TimeFormat* = object ## Represents a format for parsing and printing
+                       ## time types.
+    patterns: seq[byte] ## \
+      ## Contains the patterns encoded as bytes.
+      ## Literal values are encoded in a special way.
+      ## They start with ``Lit.byte``, then the length of the literal, then the
+      ## raw char values of the literal. For example, the literal `foo` would
+      ## be encoded as ``@[Lit.byte, 3.byte, 'f'.byte, 'o'.byte, 'o'.byte]``.
+    formatStr: string
+
+const FormatLiterals = { ' ', '-', '/', ':', '(', ')', '[', ']', ',' }
+
+proc `$`*(f: TimeFormat): string =
+  ## Returns the format string that was used to construct ``f``.
   runnableExamples:
-    let dt = initDateTime(01, mJan, 2000, 12, 00, 00, 01, utc())
-    doAssert format(dt, "yyyy-MM-dd'T'HH:mm:ss'.'fffffffffzzz") == "2000-01-01T12:00:00.000000001+00:00"
+    let f = initTimeFormat("yyyy-MM-dd")
+    doAssert $f == "yyyy-MM-dd"
+  f.formatStr
 
-  result = ""
+proc raiseParseException(f: TimeFormat, input: string, msg: string) =
+  raise newException(ValueError,
+                     &"Failed to parse '{input}' with format '{f}'. {msg}")
+
+iterator tokens(f: string): tuple[kind: FormatTokenKind, token: string] =
   var i = 0
-  var currentF = ""
-  while i < f.len:
-    case f[i]
-    of ' ', '-', '/', ':', '\'', '(', ')', '[', ']', ',':
-      formatToken(dt, currentF, result)
+  var currToken = ""
 
-      currentF = ""
+  template yieldCurrToken() =
+    if currToken.len != 0:
+      yield (tkPattern, currToken)
+      currToken = ""
 
-      if f[i] == '\'':
+  while i < f.len:
+    case f[i]
+    of '\'':
+      yieldCurrToken()
+      if i.succ < f.len and f[i.succ] == '\'':
+        yield (tkLiteral, "'")
+        i.inc 2
+      else:
+        var token = ""
         inc(i) # Skip '
-        while i < f.len-1 and f[i] != '\'':
-          result.add(f[i])
-          inc(i)
-      else: result.add(f[i])
-
+        while i < f.len and f[i] != '\'':
+          token.add f[i]
+          i.inc
+
+        if i > f.high:
+          raise newException(ValueError,
+                             &"Unclosed ' in time format string. " &
+                             "For a literal ', use ''.")
+        i.inc
+        yield (tkLiteral, token)
+    of FormatLiterals:
+        yieldCurrToken()
+        yield (tkLiteral, $f[i])
+        i.inc
     else:
       # Check if the letter being added matches previous accumulated buffer.
-      if currentF.len == 0 or currentF[high(currentF)] == f[i]:
-        currentF.add(f[i])
+      if currToken.len == 0 or currToken[0] == f[i]:
+        currToken.add(f[i])
+        i.inc
+      else:
+        yield (tkPattern, currToken)
+        currToken = $f[i]
+        i.inc
+
+  yieldCurrToken()
+
+proc stringToPattern(str: string): FormatPattern =
+  case str
+  of "d": result = d
+  of "dd": result = dd
+  of "ddd": result = ddd
+  of "dddd": result = dddd
+  of "h": result = h
+  of "hh": result = hh
+  of "H": result = H
+  of "HH": result = HH
+  of "m": result = m
+  of "mm": result = mm
+  of "M": result = M
+  of "MM": result = MM
+  of "MMM": result = MMM
+  of "MMMM": result = MMMM
+  of "s": result = s
+  of "ss": result = ss
+  of "fff": result = fff
+  of "ffffff": result = ffffff
+  of "fffffffff": result = fffffffff
+  of "t": result = t
+  of "tt": result = tt
+  of "y": result = y
+  of "yy": result = yy
+  of "yyy": result = yyy
+  of "yyyy": result = yyyy
+  of "yyyyy": result = yyyyy
+  of "YYYY": result = YYYY
+  of "uuuu": result = uuuu
+  of "UUUU": result = UUUU
+  of "z": result = z
+  of "zz": result = zz
+  of "zzz": result = zzz
+  of "zzzz": result = zzzz
+  of "g": result = g
+  else: raise newException(ValueError, &"'{str}' is not a valid pattern")
+
+proc initTimeFormat*(format: string): TimeFormat =
+  ## Construct a new time format for parsing & formatting time types.
+  ##
+  ## See `Parsing and formatting dates`_ for documentation of the
+  ## ``format`` argument.
+  runnableExamples:
+    let f = initTimeFormat("yyyy-MM-dd")
+    doAssert "2000-01-01" == f.format(f.parse("2000-01-01"))
+  result.formatStr = format
+  result.patterns = @[]
+  for kind, token in format.tokens:
+    case kind
+    of tkLiteral:
+      case token
+      else:
+        result.patterns.add(FormatPattern.Lit.byte)
+        if token.len > 255:
+          raise newException(ValueError,
+                             "Format literal is to long:" & token)
+        result.patterns.add(token.len.byte)
+        for c in token:
+          result.patterns.add(c.byte)
+    of tkPattern:
+      result.patterns.add(stringToPattern(token).byte)
+
+proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) =
+  template yearOfEra(dt: DateTime): int =
+    if dt.year <= 0: abs(dt.year) + 1 else: dt.year
+
+  case pattern
+  of d:
+    result.add $dt.monthday
+  of dd:
+    result.add dt.monthday.intToStr(2)
+  of ddd:
+    result.add ($dt.weekday)[0..2]
+  of dddd:
+    result.add $dt.weekday
+  of h:
+    result.add(
+      if dt.hour == 0:   "12"
+      elif dt.hour > 12: $(dt.hour - 12)
+      else:              $dt.hour
+    )
+  of hh:
+    result.add(
+      if dt.hour == 0:   "12"
+      elif dt.hour > 12: (dt.hour - 12).intToStr(2)
+      else:              dt.hour.intToStr(2)
+    )
+  of H:
+    result.add $dt.hour
+  of HH:
+    result.add dt.hour.intToStr(2)
+  of m:
+    result.add $dt.minute
+  of mm:
+    result.add dt.minute.intToStr(2)
+  of M:
+    result.add $ord(dt.month)
+  of MM:
+    result.add ord(dt.month).intToStr(2)
+  of MMM:
+    result.add ($dt.month)[0..2]
+  of MMMM:
+    result.add $dt.month
+  of s:
+    result.add $dt.second
+  of ss:
+    result.add dt.second.intToStr(2)
+  of fff:
+    result.add(intToStr(convert(Nanoseconds, Milliseconds, dt.nanosecond), 3))
+  of ffffff:
+    result.add(intToStr(convert(Nanoseconds, Microseconds, dt.nanosecond), 6))
+  of fffffffff:
+    result.add(intToStr(dt.nanosecond, 9))
+  of t:
+    result.add if dt.hour >= 12: "P" else: "A"
+  of tt:
+    result.add if dt.hour >= 12: "PM" else: "AM"
+  of y: # Deprecated
+    result.add $(dt.yearOfEra mod 10)
+  of yy:
+    result.add (dt.yearOfEra mod 100).intToStr(2)
+  of yyy: # Deprecated
+    result.add (dt.yearOfEra mod 1000).intToStr(3)
+  of yyyy:
+    let year = dt.yearOfEra
+    if year < 10000:
+      result.add year.intToStr(4)
+    else:
+      result.add '+' & $year
+  of yyyyy: # Deprecated
+    result.add (dt.yearOfEra mod 100_000).intToStr(5)
+  of YYYY:
+    if dt.year < 1:
+      result.add $(abs(dt.year) + 1)
+    else:
+      result.add $dt.year
+  of uuuu:
+    let year = dt.year
+    if year < 10000 or year < 0:
+      result.add year.intToStr(4)
+    else:
+      result.add '+' & $year
+  of UUUU:
+      result.add $dt.year
+  of z, zz, zzz, zzzz:
+    if dt.timezone.name == "Etc/UTC":
+      result.add 'Z'
+    else:
+      result.add  if -dt.utcOffset >= 0: '+' else: '-'
+      let absOffset = abs(dt.utcOffset)
+      case pattern:
+      of z:
+        result.add $(absOffset div 3600)
+      of zz:
+        result.add (absOffset div 3600).intToStr(2)
+      of zzz:
+        let h = (absOffset div 3600).intToStr(2)
+        let m = ((absOffset div 60) mod 60).intToStr(2)
+        result.add h & ":" & m
+      of zzzz:
+        let absOffset = abs(dt.utcOffset)
+        let h = (absOffset div 3600).intToStr(2)
+        let m = ((absOffset div 60) mod 60).intToStr(2)
+        let s = (absOffset mod 60).intToStr(2)
+        result.add h & ":" & m & ":" & s
+      else: assert false
+  of g:
+      result.add if dt.year < 1: "BC" else: "AD"
+  of Lit: assert false # Can't happen
+
+proc parsePattern(input: string, pattern: FormatPattern, i: var int,
+                  parsed: var ParsedTime): bool =
+  template takeInt(allowedWidth: Slice[int]): int =
+    var sv: int
+    let max = i + allowedWidth.b - 1
+    var pd =
+      if max > input.high:
+        parseInt(input, sv, i)
+      else:
+        parseInt(input[i..max], sv)
+    if pd notin allowedWidth:
+      return false
+    i.inc pd
+    sv
+
+  template contains[T](t: typedesc[T], i: int): bool =
+    i in low(t)..high(t)
+
+  result = true
+
+  case pattern
+  of d:
+    parsed.monthday = some(takeInt(1..2))
+    result = parsed.monthday.get() in MonthdayRange
+  of dd:
+    parsed.monthday = some(takeInt(2..2))
+    result = parsed.monthday.get() in MonthdayRange
+  of ddd:
+    result = input.substr(i, i+2).toLowerAscii() in [
+      "sun", "mon", "tue", "wed", "thu", "fri", "sat"]
+    if result:
+      i.inc 3
+  of dddd:
+    if input.substr(i, i+5).cmpIgnoreCase("sunday") == 0:
+      i.inc 6
+    elif input.substr(i, i+5).cmpIgnoreCase("monday") == 0:
+      i.inc 6
+    elif input.substr(i, i+6).cmpIgnoreCase("tuesday") == 0:
+      i.inc 7
+    elif input.substr(i, i+8).cmpIgnoreCase("wednesday") == 0:
+      i.inc 9
+    elif input.substr(i, i+7).cmpIgnoreCase("thursday") == 0:
+      i.inc 8
+    elif input.substr(i, i+5).cmpIgnoreCase("friday") == 0:
+      i.inc 6
+    elif input.substr(i, i+7).cmpIgnoreCase("saturday") == 0:
+      i.inc 8
+    else:
+      result = false
+  of h, H:
+    parsed.hour = takeInt(1..2)
+    result = parsed.hour in HourRange
+  of hh, HH:
+    parsed.hour = takeInt(2..2)
+    result = parsed.hour in HourRange
+  of m:
+    parsed.minute = takeInt(1..2)
+    result = parsed.hour in MinuteRange
+  of mm:
+    parsed.minute = takeInt(2..2)
+    result = parsed.hour in MinuteRange
+  of M:
+    let month = takeInt(1..2)
+    result = month in 1..12
+    parsed.month = some(month)
+  of MM:
+    let month = takeInt(2..2)
+    result = month in 1..12
+    parsed.month = some(month)
+  of MMM:
+    case input.substr(i, i+2).toLowerAscii()
+    of "jan": parsed.month = some(1)
+    of "feb": parsed.month = some(2)
+    of "mar": parsed.month = some(3)
+    of "apr": parsed.month = some(4)
+    of "may": parsed.month = some(5)
+    of "jun": parsed.month = some(6)
+    of "jul": parsed.month = some(7)
+    of "aug": parsed.month = some(8)
+    of "sep": parsed.month = some(9)
+    of "oct": parsed.month = some(10)
+    of "nov": parsed.month = some(11)
+    of "dec": parsed.month = some(12)
+    else:
+      result = false
+    if result:
+      i.inc 3
+  of MMMM:
+    if input.substr(i, i+6).cmpIgnoreCase("january") == 0:
+      parsed.month = some(1)
+      i.inc 7
+    elif input.substr(i, i+7).cmpIgnoreCase("february") == 0:
+      parsed.month = some(2)
+      i.inc 8
+    elif input.substr(i, i+4).cmpIgnoreCase("march") == 0:
+      parsed.month = some(3)
+      i.inc 5
+    elif input.substr(i, i+4).cmpIgnoreCase("april") == 0:
+      parsed.month = some(4)
+      i.inc 5
+    elif input.substr(i, i+2).cmpIgnoreCase("may") == 0:
+      parsed.month = some(5)
+      i.inc 3
+    elif input.substr(i, i+3).cmpIgnoreCase("june") == 0:
+      parsed.month = some(6)
+      i.inc 4
+    elif input.substr(i, i+3).cmpIgnoreCase("july") == 0:
+      parsed.month = some(7)
+      i.inc 4
+    elif input.substr(i, i+5).cmpIgnoreCase("august") == 0:
+      parsed.month = some(8)
+      i.inc 6
+    elif input.substr(i, i+8).cmpIgnoreCase("september") == 0:
+      parsed.month = some(9)
+      i.inc 9
+    elif input.substr(i, i+6).cmpIgnoreCase("october") == 0:
+      parsed.month = some(10)
+      i.inc 7
+    elif input.substr(i, i+7).cmpIgnoreCase("november") == 0:
+      parsed.month = some(11)
+      i.inc 8
+    elif input.substr(i, i+7).cmpIgnoreCase("december") == 0:
+      parsed.month = some(12)
+      i.inc 8
+    else:
+      result = false
+  of s:
+    parsed.second = takeInt(1..2)
+  of ss:
+    parsed.second = takeInt(2..2)
+  of fff, ffffff, fffffffff:
+    let len = ($pattern).len
+    let v = takeInt(len..len)
+    parsed.nanosecond = v * 10^(9 - len)
+    result = parsed.nanosecond in NanosecondRange
+  of t:
+    case input[i]:
+    of 'P':
+      parsed.amPm = apPm
+    of 'A':
+      parsed.amPm = apAm
+    else:
+      result = false
+    i.inc 1
+  of tt:
+    if input.substr(i, i+1).cmpIgnoreCase("AM") == 0:
+      parsed.amPm = apAM
+      i.inc 2
+    elif input.substr(i, i+1).cmpIgnoreCase("PM") == 0:
+      parsed.amPm = apPm
+      i.inc 2
+    else:
+      result = false
+  of yy:
+    # Assumes current century
+    var year = takeInt(2..2)
+    var thisCen = now().year div 100
+    parsed.year = some(thisCen*100 + year)
+    result = year > 0
+  of yyyy:
+    let year =
+      if input[i] in { '+', '-' }:
+        takeInt(4..high(int))
+      else:
+        takeInt(4..4)
+    result = year > 0
+    parsed.year = some(year)
+  of YYYY:
+    let year = takeInt(1..high(int))
+    parsed.year = some(year)
+    result = year > 0
+  of uuuu:
+    let year =
+      if input[i] in { '+', '-' }:
+        takeInt(4..high(int))
       else:
-        formatToken(dt, currentF, result)
-        dec(i) # Move position back to re-process the character separately.
-        currentF = ""
+        takeInt(4..4)
+    parsed.year = some(year)
+  of UUUU:
+    parsed.year = some(takeInt(1..high(int)))
+  of z, zz, zzz, zzzz:
+    case input[i]
+    of '+', '-':
+      let sign = if input[i] == '-': 1 else: -1
+      i.inc
+      var offset = 0
+      case pattern
+      of z:
+        offset = takeInt(1..2) * -3600
+      of zz:
+        offset = takeInt(2..2) * -3600
+      of zzz:
+        offset.inc takeInt(2..2) * 3600
+        if input[i] != ':':
+          return false
+        i.inc
+        offset.inc takeInt(2..2) * 60
+      of zzzz:
+        offset.inc takeInt(2..2) * 3600
+        if input[i] != ':':
+          return false
+        i.inc
+        offset.inc takeInt(2..2) * 60
+        if input[i] != ':':
+          return false
+        i.inc
+        offset.inc takeInt(2..2)
+      else: assert false
+      parsed.utcOffset = some(offset * sign)
+    of 'Z':
+      parsed.utcOffset = some(0)
+      i.inc
+    else:
+      result = false
+  of g:
+    if input.substr(i, i+1).cmpIgnoreCase("BC") == 0:
+      parsed.era = eraBc
+      i.inc 2
+    elif input.substr(i, i+1).cmpIgnoreCase("AD") == 0:
+      parsed.era = eraAd
+      i.inc 2
+    else:
+      result = false
+  of y, yyy, yyyyy:
+    raise newException(ValueError,
+                      &"The pattern '{pattern}' is only valid for formatting")
+  of Lit: assert false # Can't happen
+
+proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat,
+                input: string): DateTime =
+  var month = mJan
+  var year: int
+  var monthday: int
+  # `now()` is an expensive call, so we avoid it when possible
+  (year, month, monthday) =
+    if p.year.isNone or p.month.isNone or p.monthday.isNone:
+      let n = now()
+      (p.year.get(n.year),
+        p.month.get(n.month.int).Month,
+        p.monthday.get(n.monthday))
+    else:
+      (p.year.get(), p.month.get().Month, p.monthday.get())
+
+  year =
+    case p.era
+    of eraUnknown:
+      year
+    of eraBc:
+      if year < 1:
+        raiseParseException(f, input,
+          "Expected year to be positive " &
+          "(use 'UUUU' or 'uuuu' for negative years).")
+      -year + 1
+    of eraAd:
+      if year < 1:
+        raiseParseException(f, input,
+          "Expected year to be positive " &
+          "(use 'UUUU' or 'uuuu' for negative years).")
+      year
+
+  let hour =
+    case p.amPm
+    of apUnknown:
+      p.hour
+    of apAm:
+      if p.hour notin 1..12:
+        raiseParseException(f, input,
+          "AM/PM time must be in the interval 1..12")
+      if p.hour == 12: 0 else: p.hour
+    of apPm:
+      if p.hour notin 1..12:
+        raiseParseException(f, input,
+          "AM/PM time must be in the interval 1..12")
+      if p.hour == 12: p.hour else: p.hour + 12
+  let minute = p.minute
+  let second = p.second
+  let nanosecond = p.nanosecond
+
+  if monthday > getDaysInMonth(month, year):
+    raiseParseException(f, input,
+      $year & "-" & ord(month).intToStr(2) &
+      "-" & $monthday & " is not a valid date")
+
+  result = DateTime(
+    year: year, month: month, monthday: monthday,
+    hour: hour, minute: minute, second: second, nanosecond: nanosecond
+  )
 
-    inc(i)
-  formatToken(dt, currentF, result)
+  if p.utcOffset.isNone:
+    # No timezone parsed - assume timezone is `zone`
+    result = initDateTime(zone.zoneInfoFromTz(result.toAdjTime), zone)
+  else:
+    # Otherwise convert to `zone`
+    result.utcOffset = p.utcOffset.get()
+    result = result.toTime.inZone(zone)
 
-proc format*(time: Time, f: string, zone: Timezone = local()): string {.tags: [].} =
-  ## Converts a `Time` value to a string representation. It will use format from
-  ## ``format(dt: DateTime, f: string)``.
+proc format*(f: TimeFormat, dt: DateTime): string {.raises: [].} =
+  ## Format ``dt`` using the format specified by ``f``.
+  runnableExamples:
+    let f = initTimeFormat("yyyy-MM-dd")
+    let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
+    doAssert "2000-01-01" == f.format(dt)
+  var idx = 0
+  while idx <= f.patterns.high:
+    case f.patterns[idx].FormatPattern
+    of Lit:
+      idx.inc
+      let len = f.patterns[idx]
+      for i in 1'u8..len:
+        idx.inc
+        result.add f.patterns[idx].char
+      idx.inc
+    else:
+      formatPattern(dt, f.patterns[idx].FormatPattern, result = result)
+      idx.inc
+
+proc format*(dt: DateTime, f: string): string =
+  ## Shorthand for constructing a ``TimeFormat`` and using it to format ``dt``.
+  ##
+  ## See `Parsing and formatting dates`_ for documentation of the
+  ## ``format`` argument.
+  runnableExamples:
+    let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
+    doAssert "2000-01-01" == format(dt, "yyyy-MM-dd")
+  let dtFormat = initTimeFormat(f)
+  result = dtFormat.format(dt)
+
+proc format*(dt: DateTime, format: static[string]): string {.raises: [].} =
+  ## Overload that validates ``format`` at compile time.
+  const f = initTimeFormat(format)
+  result = f.format(dt)
+
+proc format*(time: Time, format: string, zone: Timezone = local()): string {.tags: [].} =
+  ## Shorthand for constructing a ``TimeFormat`` and using it to format
+  ## ``time``. Will use the timezone specified by ``zone``.
+  ##
+  ## See `Parsing and formatting dates`_ for documentation of the
+  ## ``format`` argument.
   runnableExamples:
     var dt = initDateTime(01, mJan, 1970, 00, 00, 00, utc())
     var tm = dt.toTime()
     doAssert format(tm, "yyyy-MM-dd'T'HH:mm:ss", utc()) == "1970-01-01T00:00:00"
-  time.inZone(zone).format(f)
+  time.inZone(zone).format(format)
+
+proc format*(time: Time, format: static[string],
+             zone: Timezone = local()): string {.tags: [].} =
+  ## Overload that validates ``format`` at compile time.
+  const f = initTimeFormat(format)
+  result = f.format(time.inZone(zone))
+
+proc parse*(f: TimeFormat, input: string, zone: Timezone = local()): DateTime =
+  ## Parses ``input`` as a ``DateTime`` using the format specified by ``f``.
+  ## If no UTC offset was parsed, then ``input`` is assumed to be specified in
+  ## the ``zone`` timezone. If a UTC offset was parsed, the result will be
+  ## converted to the ``zone`` timezone.
+  runnableExamples:
+    let f = initTimeFormat("yyyy-MM-dd")
+    let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
+    doAssert dt == f.parse("2000-01-01", utc())
+  var inpIdx = 0 # Input index
+  var patIdx = 0 # Pattern index
+  var parsed: ParsedTime
+  while inpIdx <= input.high and patIdx <= f.patterns.high:
+    let pattern = f.patterns[patIdx].FormatPattern
+    case pattern
+    of Lit:
+      patIdx.inc
+      let len = f.patterns[patIdx]
+      patIdx.inc
+      for _ in 1'u8..len:
+        if input[inpIdx] != f.patterns[patIdx].char:
+          raiseParseException(f, input,
+                              "Unexpected character: " & input[inpIdx])
+        inpIdx.inc
+        patIdx.inc
+    else:
+      if not parsePattern(input, pattern, inpIdx, parsed):
+        raiseParseException(f, input, &"Failed on pattern '{pattern}'")
+      patIdx.inc
+
+  if inpIdx <= input.high:
+    raiseParseException(f, input,
+                        "Parsing ended but there was still input remaining")
+
+  if patIdx <= f.patterns.high:
+    raiseParseException(f, input,
+                        "Parsing ended but there was still patterns remaining")
+
+  result = toDateTime(parsed, zone, f, input)
+
+proc parse*(input, format: string, tz: Timezone = local()): DateTime =
+  ## Shorthand for constructing a ``TimeFormat`` and using it to parse
+  ## ``input`` as a ``DateTime``.
+  ##
+  ## See `Parsing and formatting dates`_ for documentation of the
+  ## ``format`` argument.
+  runnableExamples:
+    let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
+    doAssert dt == parse("2000-01-01", "yyyy-MM-dd", utc())
+  let dtFormat = initTimeFormat(format)
+  result = dtFormat.parse(input, tz)
+
+proc parse*(input: string, format: static[string], zone: Timezone = local()): DateTime =
+  ## Overload that validates ``format`` at compile time.
+  const f = initTimeFormat(format)
+  result = f.parse(input, zone)
+
+proc parseTime*(input, format: string, zone: Timezone): Time =
+  ## Shorthand for constructing a ``TimeFormat`` and using it to parse
+  ## ``input`` as a ``DateTime``, then converting it a ``Time``.
+  ##
+  ## See `Parsing and formatting dates`_ for documentation of the
+  ## ``format`` argument.
+  runnableExamples:
+    let tStr = "1970-01-01T00:00:00+00:00"
+    doAssert parseTime(tStr, "yyyy-MM-dd'T'HH:mm:sszzz", utc()) == fromUnix(0)
+  parse(input, format, zone).toTime()
+
+proc parseTime*(input: string, format: static[string], zone: Timezone): Time =
+  ## Overload that validates ``format`` at compile time.
+  const f = initTimeFormat(format)
+  result = f.parse(input, zone).toTime()
+
+#
+# End of parse & format implementation
+#
 
 proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} =
   ## Converts a `DateTime` object to a string representation.
   ## It uses the format ``yyyy-MM-dd'T'HH-mm-sszzz``.
   runnableExamples:
     let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc())
-    doAssert $dt == "2000-01-01T12:00:00+00:00"
-  try:
-    result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") # todo: optimize this
-  except ValueError: assert false # cannot happen because format string is valid
+    doAssert $dt == "2000-01-01T12:00:00Z"
+  result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz")
 
 proc `$`*(time: Time): string {.tags: [], raises: [], benign.} =
   ## converts a `Time` value to a string representation. It will use the local
@@ -1628,328 +2218,6 @@ proc `$`*(time: Time): string {.tags: [], raises: [], benign.} =
 
 {.pop.}
 
-proc parseToken(dt: var DateTime; token, value: string; j: var int) =
-  ## Helper of the parse proc to parse individual tokens.
-
-  # Overwrite system.`[]` to raise a ValueError on index out of bounds.
-  proc `[]`[T, U](s: string, x: HSlice[T, U]): string =
-    if x.a >= s.len or x.b >= s.len:
-      raise newException(ValueError, "Value is missing required tokens, got: " &
-                         s)
-    return system.`[]`(s, x)
-
-  var sv: int
-  case token
-  of "d":
-    var pd = parseInt(value[j..j+1], sv)
-    dt.monthday = sv
-    j += pd
-  of "dd":
-    dt.monthday = value[j..j+1].parseInt()
-    j += 2
-  of "ddd":
-    case value[j..j+2].toLowerAscii()
-    of "sun": dt.weekday = dSun
-    of "mon": dt.weekday = dMon
-    of "tue": dt.weekday = dTue
-    of "wed": dt.weekday = dWed
-    of "thu": dt.weekday = dThu
-    of "fri": dt.weekday = dFri
-    of "sat": dt.weekday = dSat
-    else:
-      raise newException(ValueError,
-        "Couldn't parse day of week (ddd), got: " & value[j..j+2])
-    j += 3
-  of "dddd":
-    if value.len >= j+6 and value[j..j+5].cmpIgnoreCase("sunday") == 0:
-      dt.weekday = dSun
-      j += 6
-    elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("monday") == 0:
-      dt.weekday = dMon
-      j += 6
-    elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("tuesday") == 0:
-      dt.weekday = dTue
-      j += 7
-    elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("wednesday") == 0:
-      dt.weekday = dWed
-      j += 9
-    elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("thursday") == 0:
-      dt.weekday = dThu
-      j += 8
-    elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("friday") == 0:
-      dt.weekday = dFri
-      j += 6
-    elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("saturday") == 0:
-      dt.weekday = dSat
-      j += 8
-    else:
-      raise newException(ValueError,
-        "Couldn't parse day of week (dddd), got: " & value)
-  of "h", "H":
-    var pd = parseInt(value[j..j+1], sv)
-    dt.hour = sv
-    j += pd
-  of "hh", "HH":
-    dt.hour = value[j..j+1].parseInt()
-    j += 2
-  of "m":
-    var pd = parseInt(value[j..j+1], sv)
-    dt.minute = sv
-    j += pd
-  of "mm":
-    dt.minute = value[j..j+1].parseInt()
-    j += 2
-  of "M":
-    var pd = parseInt(value[j..j+1], sv)
-    dt.month = sv.Month
-    j += pd
-  of "MM":
-    var month = value[j..j+1].parseInt()
-    j += 2
-    dt.month = month.Month
-  of "MMM":
-    case value[j..j+2].toLowerAscii():
-    of "jan": dt.month = mJan
-    of "feb": dt.month = mFeb
-    of "mar": dt.month = mMar
-    of "apr": dt.month = mApr
-    of "may": dt.month = mMay
-    of "jun": dt.month = mJun
-    of "jul": dt.month = mJul
-    of "aug": dt.month = mAug
-    of "sep": dt.month = mSep
-    of "oct": dt.month = mOct
-    of "nov": dt.month = mNov
-    of "dec": dt.month = mDec
-    else:
-      raise newException(ValueError,
-        "Couldn't parse month (MMM), got: " & value)
-    j += 3
-  of "MMMM":
-    if value.len >= j+7 and value[j..j+6].cmpIgnoreCase("january") == 0:
-      dt.month = mJan
-      j += 7
-    elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("february") == 0:
-      dt.month = mFeb
-      j += 8
-    elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("march") == 0:
-      dt.month = mMar
-      j += 5
-    elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("april") == 0:
-      dt.month = mApr
-      j += 5
-    elif value.len >= j+3 and value[j..j+2].cmpIgnoreCase("may") == 0:
-      dt.month = mMay
-      j += 3
-    elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("june") == 0:
-      dt.month = mJun
-      j += 4
-    elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("july") == 0:
-      dt.month = mJul
-      j += 4
-    elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("august") == 0:
-      dt.month = mAug
-      j += 6
-    elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("september") == 0:
-      dt.month = mSep
-      j += 9
-    elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("october") == 0:
-      dt.month = mOct
-      j += 7
-    elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("november") == 0:
-      dt.month = mNov
-      j += 8
-    elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("december") == 0:
-      dt.month = mDec
-      j += 8
-    else:
-      raise newException(ValueError,
-        "Couldn't parse month (MMMM), got: " & value)
-  of "s":
-    var pd = parseInt(value[j..j+1], sv)
-    dt.second = sv
-    j += pd
-  of "ss":
-    dt.second = value[j..j+1].parseInt()
-    j += 2
-  of "t":
-    if value[j] == 'A' and dt.hour == 12:
-      dt.hour = 0
-    elif value[j] == 'P' and dt.hour > 0 and dt.hour < 12:
-      dt.hour += 12
-    j += 1
-  of "tt":
-    if value[j..j+1] == "AM" and dt.hour == 12:
-      dt.hour = 0
-    elif value[j..j+1] == "PM" and dt.hour > 0 and dt.hour < 12:
-      dt.hour += 12
-    j += 2
-  of "yy":
-    # Assumes current century
-    var year = value[j..j+1].parseInt()
-    var thisCen = now().year div 100
-    dt.year = thisCen*100 + year
-    j += 2
-  of "yyyy":
-    dt.year = value[j..j+3].parseInt()
-    j += 4
-  of "z":
-    dt.isDst = false
-    let ch = if j < value.len: value[j] else: '\0'
-    if ch == '+':
-      dt.utcOffset = 0 - parseInt($value[j+1]) * secondsInHour
-    elif ch == '-':
-      dt.utcOffset = parseInt($value[j+1]) * secondsInHour
-    elif ch == 'Z':
-      dt.utcOffset = 0
-      j += 1
-      return
-    else:
-      raise newException(ValueError,
-        "Couldn't parse timezone offset (z), got: " & ch)
-    j += 2
-  of "zz":
-    dt.isDst = false
-    let ch = if j < value.len: value[j] else: '\0'
-    if ch == '+':
-      dt.utcOffset = 0 - value[j+1..j+2].parseInt() * secondsInHour
-    elif ch == '-':
-      dt.utcOffset = value[j+1..j+2].parseInt() * secondsInHour
-    elif ch == 'Z':
-      dt.utcOffset = 0
-      j += 1
-      return
-    else:
-      raise newException(ValueError,
-        "Couldn't parse timezone offset (zz), got: " & ch)
-    j += 3
-  of "zzz":
-    dt.isDst = false
-    var factor = 0
-    let ch = if j < value.len: value[j] else: '\0'
-    if ch == '+': factor = -1
-    elif ch == '-': factor = 1
-    elif ch == 'Z':
-      dt.utcOffset = 0
-      j += 1
-      return
-    else:
-      raise newException(ValueError,
-        "Couldn't parse timezone offset (zzz), got: " & ch)
-    dt.utcOffset = factor * value[j+1..j+2].parseInt() * secondsInHour
-    j += 4
-    dt.utcOffset += factor * value[j..j+1].parseInt() * 60
-    j += 2
-  of "fff", "ffffff", "fffffffff":
-    var numStr = ""
-    let n = parseWhile(value[j..len(value) - 1], numStr, {'0'..'9'})
-    dt.nanosecond = parseInt(numStr) * (10 ^ (9 - n))
-    j += n
-  else:
-    # Ignore the token and move forward in the value string by the same length
-    j += token.len
-
-proc parse*(value, layout: string, zone: Timezone = local()): DateTime =
-  ## This procedure parses a date/time string using the standard format
-  ## identifiers as listed below. The procedure defaults information not provided
-  ## in the format string from the running program (month, year, etc).
-  ##
-  ## The return value will always be in the `zone` timezone. If no UTC offset was
-  ## parsed, then the input will be assumed to be specified in the `zone` timezone
-  ## already, so no timezone conversion will be done in that case.
-  ##
-  ## =======================  =================================================================================  ================================================
-  ## Specifier                Description                                                                        Example
-  ## =======================  =================================================================================  ================================================
-  ##    d                     Numeric value of the day of the month, it will be one or two digits long.          ``1/04/2012 -> 1``, ``21/04/2012 -> 21``
-  ##    dd                    Same as above, but always two digits.                                              ``1/04/2012 -> 01``, ``21/04/2012 -> 21``
-  ##    ddd                   Three letter string which indicates the day of the week.                           ``Saturday -> Sat``, ``Monday -> Mon``
-  ##    dddd                  Full string for the day of the week.                                               ``Saturday -> Saturday``, ``Monday -> Monday``
-  ##    h                     The hours in one digit if possible. Ranging from 0-12.                             ``5pm -> 5``, ``2am -> 2``
-  ##    hh                    The hours in two digits always. If the hour is one digit 0 is prepended.           ``5pm -> 05``, ``11am -> 11``
-  ##    H                     The hours in one digit if possible, randing from 0-24.                             ``5pm -> 17``, ``2am -> 2``
-  ##    HH                    The hours in two digits always. 0 is prepended if the hour is one digit.           ``5pm -> 17``, ``2am -> 02``
-  ##    m                     The minutes in 1 digit if possible.                                                ``5:30 -> 30``, ``2:01 -> 1``
-  ##    mm                    Same as above but always 2 digits, 0 is prepended if the minute is one digit.      ``5:30 -> 30``, ``2:01 -> 01``
-  ##    M                     The month in one digit if possible.                                                ``September -> 9``, ``December -> 12``
-  ##    MM                    The month in two digits always. 0 is prepended.                                    ``September -> 09``, ``December -> 12``
-  ##    MMM                   Abbreviated three-letter form of the month.                                        ``September -> Sep``, ``December -> Dec``
-  ##    MMMM                  Full month string, properly capitalized.                                           ``September -> September``
-  ##    s                     Seconds as one digit if possible.                                                  ``00:00:06 -> 6``
-  ##    ss                    Same as above but always two digits. 0 is prepended.                               ``00:00:06 -> 06``
-  ##    t                     ``A`` when time is in the AM. ``P`` when time is in the PM.
-  ##    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. ``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``
-  ##    fff/ffffff/fffffffff  for consistency with format - nanoseconds                                          ``1 -> 1 nanosecond``
-  ## =======================  =================================================================================  ================================================
-  ##
-  ## Other strings can be inserted by putting them in ``''``. For example
-  ## ``hh'->'mm`` will give ``01->56``.  The following characters can be
-  ## inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]``
-  ## ``,``. However you don't need to necessarily separate format specifiers, a
-  ## unambiguous format string like ``yyyyMMddhhmmss`` is valid too.
-  runnableExamples:
-    let tStr = "1970-01-01T00:00:00.0+00:00"
-    doAssert parse(tStr, "yyyy-MM-dd'T'HH:mm:ss.fffzzz") == fromUnix(0).utc
-
-  var i = 0 # pointer for format string
-  var j = 0 # pointer for value string
-  var token = ""
-  # Assumes current day of month, month and year, but time is reset to 00:00:00. Weekday will be reset after parsing.
-  var dt = now()
-  dt.hour = 0
-  dt.minute = 0
-  dt.second = 0
-  dt.nanosecond = 0
-  dt.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 i < layout.len:
-    case layout[i]
-    of ' ', '-', '/', ':', '\'', '(', ')', '[', ']', ',':
-      if token.len > 0:
-        parseToken(dt, token, value, j)
-      # Reset token
-      token = ""
-      # Skip separator and everything between single quotes
-      # These are literals in both the layout and the value string
-      if layout[i] == '\'':
-        inc(i)
-        while i < layout.len-1 and layout[i] != '\'':
-          inc(i)
-          inc(j)
-        inc(i)
-      else:
-        inc(i)
-        inc(j)
-    else:
-      # Check if the letter being added matches previous accumulated buffer.
-      if token.len == 0 or token[high(token)] == layout[i]:
-        token.add(layout[i])
-        inc(i)
-      else:
-        parseToken(dt, token, value, j)
-        token = ""
-
-  if i >= layout.len and token.len > 0:
-    parseToken(dt, token, value, j)
-  if dt.isDst:
-    # No timezone parsed - assume timezone is `zone`
-    result = initDateTime(zone.zoneInfoFromTz(dt.toAdjTime), zone)
-  else:
-    # Otherwise convert to `zone`
-    result = dt.toTime.inZone(zone)
-
-proc parseTime*(value, layout: string, zone: Timezone): Time =
-  ## Simple wrapper for parsing string to time
-  runnableExamples:
-    let tStr = "1970-01-01T00:00:00+00:00"
-    doAssert parseTime(tStr, "yyyy-MM-dd'T'HH:mm:sszzz", local()) == fromUnix(0)
-  parse(value, layout, zone).toTime()
-
 proc countLeapYears*(yearSpan: int): int =
   ## Returns the number of leap years spanned by a given number of years.
   ##
diff --git a/tests/js/ttimes.nim b/tests/js/ttimes.nim
index 63972dd76..bd599a7ae 100644
--- a/tests/js/ttimes.nim
+++ b/tests/js/ttimes.nim
@@ -36,8 +36,8 @@ let utcPlus2 = Timezone(zoneInfoFromUtc: staticZoneInfoFromUtc, zoneInfoFromTz:
 block timezoneTests:
   let dt = initDateTime(01, mJan, 2017, 12, 00, 00, utcPlus2)
   doAssert $dt == "2017-01-01T12:00:00+02:00"
-  doAssert $dt.utc == "2017-01-01T10:00:00+00:00"
+  doAssert $dt.utc == "2017-01-01T10:00:00Z"
   doAssert $dt.utc.inZone(utcPlus2) == $dt
 
-doAssert $initDateTime(01, mJan, 1911, 12, 00, 00, utc()) == "1911-01-01T12:00:00+00:00"
-doAssert $initDateTime(01, mJan, 0023, 12, 00, 00, utc()) == "0023-01-01T12:00:00+00:00"
\ No newline at end of file
+doAssert $initDateTime(01, mJan, 1911, 12, 00, 00, utc()) == "1911-01-01T12:00:00Z"
+doAssert $initDateTime(01, mJan, 0023, 12, 00, 00, utc()) == "0023-01-01T12:00:00Z"
\ No newline at end of file
diff --git a/tests/stdlib/ttimes.nim b/tests/stdlib/ttimes.nim
index 4ab3ba581..e3f61ff77 100644
--- a/tests/stdlib/ttimes.nim
+++ b/tests/stdlib/ttimes.nim
@@ -8,6 +8,24 @@ discard """
 import
   times, os, strutils, unittest
 
+proc staticTz(hours, minutes, seconds: int = 0): Timezone {.noSideEffect.} =
+  let offset = hours * 3600 + minutes * 60 + seconds
+
+  proc zoneInfoFromTz(adjTime: Time): ZonedTime {.locks: 0.} =
+    result.isDst = false
+    result.utcOffset = offset
+    result.adjTime = adjTime
+
+  proc zoneInfoFromUtc(time: Time): ZonedTime {.locks: 0.}=
+    result.isDst = false
+    result.utcOffset = offset
+    result.adjTime = fromUnix(time.toUnix - offset)
+
+  result.name = ""
+  result.zoneInfoFromTz = zoneInfoFromTz
+  result.zoneInfoFromUtc = zoneInfoFromUtc
+
+
 # $ date --date='@2147483647'
 # Tue 19 Jan 03:14:07 GMT 2038
 
@@ -19,25 +37,10 @@ proc checkFormat(t: DateTime, format, expected: string) =
     echo "actual  : ", actual
     doAssert false
 
-let t = fromUnix(2147483647).utc
-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")
-
-# issue 7620
-let t7620_am = parse("4/15/2017 12:01:02 AM +0", "M/d/yyyy' 'h:mm:ss' 'tt' 'z", utc())
-t7620_am.checkFormat("M/d/yyyy' 'h:mm:ss' 'tt' 'z", "4/15/2017 12:01:02 AM +0")
-let t7620_pm = parse("4/15/2017 12:01:02 PM +0", "M/d/yyyy' 'h:mm:ss' 'tt' 'z", utc())
-t7620_pm.checkFormat("M/d/yyyy' 'h:mm:ss' 'tt' 'z", "4/15/2017 12:01:02 PM +0")
-
-let t2 = fromUnix(160070789).utc # Mon 27 Jan 16:06:29 GMT 1975
+let t2 = fromUnix(160070789).utc() # 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")
+  "27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 Z Z Z")
 
 var t4 = fromUnix(876124714).utc # Mon  6 Oct 08:58:34 BST 1997
 t4.checkFormat("M MM MMM MMMM", "10 10 Oct October")
@@ -83,16 +86,16 @@ let seqB: seq[Time] = @[]
 doAssert seqA == seqB
 
 for tz in [
-    (0, "+0", "+00", "+00:00"), # UTC
-    (-3600, "+1", "+01", "+01:00"), # CET
-    (-39600, "+11", "+11", "+11:00"), # two digits
-    (-1800, "+0", "+00", "+00:30"), # half an hour
-    (7200, "-2", "-02", "-02:00"), # positive
-    (38700, "-10", "-10", "-10:45")]: # positive with three quaters hour
-  let ti = DateTime(month: mJan, monthday: 1, utcOffset: tz[0])
-  doAssert ti.format("z") == tz[1]
-  doAssert ti.format("zz") == tz[2]
-  doAssert ti.format("zzz") == tz[3]
+    (staticTz(seconds = 0), "+0", "+00", "+00:00"), # UTC
+    (staticTz(seconds = -3600), "+1", "+01", "+01:00"), # CET
+    (staticTz(seconds = -39600), "+11", "+11", "+11:00"), # two digits
+    (staticTz(seconds = -1800), "+0", "+00", "+00:30"), # half an hour
+    (staticTz(seconds = 7200), "-2", "-02", "-02:00"), # positive
+    (staticTz(seconds = 38700), "-10", "-10", "-10:45")]: # positive with three quaters hour
+  let dt = initDateTime(1, mJan, 2000, 00, 00, 00, tz[0])
+  doAssert dt.format("z") == tz[1]
+  doAssert dt.format("zz") == tz[2]
+  doAssert dt.format("zzz") == tz[3]
 
 block countLeapYears:
   # 1920, 2004 and 2020 are leap years, and should be counted starting at the following year
@@ -112,11 +115,9 @@ template parseTest(s, f, sExpected: string, ydExpected: int) =
   let
     parsed = s.parse(f, utc())
     parsedStr = $parsed
+  if parsedStr != sExpected:
+    echo "GOT ", parsedStr, " EXPECTED ", sExpected, " FORMAT ", f
   check parsedStr == sExpected
-  if parsed.yearday != ydExpected:
-    echo s
-    echo parsed.repr
-    echo parsed.yearday, " exp: ", ydExpected
   check(parsed.yearday == ydExpected)
 
 template parseTestExcp(s, f: string) =
@@ -130,51 +131,43 @@ template parseTestTimeOnly(s, f, sExpected: string) =
 # explicit timezone offsets in all tests.
 template runTimezoneTests() =
   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)
+      "dddd 'at' hh:mmtt 'on' MMM d, yyyy z", "2015-12-15T09:04:00Z", 348)
   # ANSIC       = "Mon Jan _2 15:04:05 2006"
   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)
+      "2006-01-12T15:04:05Z", 11)
   # UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
   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)
+      "2006-01-12T15:04:05Z", 11)
   # RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
   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
+      "2016-02-29T15:04:05Z", 59) # leap day
   # RFC822      = "02 Jan 06 15:04 MST"
   parseTest("12 Jan 16 15:04 +0", "dd MMM yy HH:mm z",
-      "2016-01-12T15:04:00+00:00", 11)
+      "2016-01-12T15:04:00Z", 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",
-      "2016-03-01T22:04:00+00:00", 60) # day after february in leap year
+      "2016-03-01T22:04:00Z", 60) # day after february in leap year
   # RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
   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)
+      "2006-01-12T15:04:05Z", 11)
   # RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
   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
+      "2015-03-01T15:04:05Z", 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",
-      "2006-01-12T22:04:05+00:00", 11)
+      "2006-01-12T22:04:05Z", 11)
   # RFC3339     = "2006-01-02T15:04:05Z07:00"
-  parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-ddTHH:mm:ssZzzz",
-      "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",
-      "2006-01-12T22:04:05+00:00", 11)
+      "2006-01-12T22:04:05Z", 11)
   # 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)
+      "yyyy-MM-dd'T'HH:mm:ss'.999999999Z'zzz", "2006-01-12T22:04:05Z", 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)
+        "2001-01-12T22:04:05Z", 11)
   # Kitchen     = "3:04PM"
   parseTestTimeOnly("3:04PM", "h:mmtt", "15:04:00")
-  #when not defined(testing):
-  #  echo "Kitchen: " & $s.parse(f)
-  #  var ti = timeToTimeInfo(getTime())
-  #  echo "Todays date after decoding: ", ti
-  #  var tint = timeToTimeInterval(getTime())
-  #  echo "Todays date after decoding to interval: ", tint
 
   # Bug with parse not setting DST properly if the current local DST differs from
   # the date being parsed. Need to test parse dates both in and out of DST. We
@@ -195,8 +188,8 @@ template runTimezoneTests() =
     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")
-    doAssert toTime(parsedJan).toUnix == 1451962800
-    doAssert toTime(parsedJul).toUnix == 1467342000
+    check toTime(parsedJan).toUnix == 1451962800
+    check toTime(parsedJul).toUnix == 1467342000
 
 suite "ttimes":
 
@@ -256,7 +249,7 @@ suite "ttimes":
       check $(dt + initDuration(days = 1)) == "2017-03-26T13:00:00+02:00"      
 
     test "datetime before epoch":
-      check $fromUnix(-2147483648).utc == "1901-12-13T20:45:52+00:00"
+      check $fromUnix(-2147483648).utc == "1901-12-13T20:45:52Z"
 
     test "adding/subtracting time across dst":
       putenv("TZ", "Europe/Stockholm")
@@ -319,6 +312,15 @@ suite "ttimes":
   test "incorrect inputs: timezone (zzz) 3":
     parseTestExcp("2018-02-19 16:30:00 +01:0", "yyyy-MM-dd hh:mm:ss zzz")
 
+  test "incorrect inputs: year (yyyy/uuuu)":
+    parseTestExcp("-0001", "yyyy")
+    parseTestExcp("-0001", "YYYY")
+    parseTestExcp("1", "yyyy")
+    parseTestExcp("12345", "yyyy")
+    parseTestExcp("1", "uuuu")
+    parseTestExcp("12345", "uuuu")
+    parseTestExcp("-1 BC", "UUUU g")
+
   test "dynamic timezone":
     proc staticOffset(offset: int): Timezone =
       proc zoneInfoFromTz(adjTime: Time): ZonedTime =
@@ -340,7 +342,7 @@ suite "ttimes":
     check dt.utcOffset == -9000
     check dt.isDst == false
     check $dt == "2000-01-01T12:00:00+02:30"
-    check $dt.utc == "2000-01-01T09:30:00+00:00"
+    check $dt.utc == "2000-01-01T09:30:00Z"
     check $dt.utc.inZone(tz) == $dt
 
   test "isLeapYear":
@@ -351,12 +353,12 @@ suite "ttimes":
 
   test "subtract months":
     var dt = initDateTime(1, mFeb, 2017, 00, 00, 00, utc())
-    check $(dt - initTimeInterval(months = 1)) == "2017-01-01T00:00:00+00:00"
+    check $(dt - initTimeInterval(months = 1)) == "2017-01-01T00:00:00Z"
     dt = initDateTime(15, mMar, 2017, 00, 00, 00, utc())
-    check $(dt - initTimeInterval(months = 1)) == "2017-02-15T00:00:00+00:00"
+    check $(dt - initTimeInterval(months = 1)) == "2017-02-15T00:00:00Z"
     dt = initDateTime(31, mMar, 2017, 00, 00, 00, utc())
     # This happens due to monthday overflow. It's consistent with Phobos.
-    check $(dt - initTimeInterval(months = 1)) == "2017-03-03T00:00:00+00:00"
+    check $(dt - initTimeInterval(months = 1)) == "2017-03-03T00:00:00Z"
 
   test "duration":
     let d = initDuration
@@ -384,11 +386,11 @@ suite "ttimes":
     discard initDateTime(1, mJan, -35_000, 12, 00, 00)
     discard initDateTime(1, mJan,  35_000, 12, 00, 00)
     # with duration/timeinterval
-    let dt = initDateTime(1, mJan,  35_000, 12, 00, 00, utc()) +
+    let dt = initDateTime(1, mJan, -35_000, 12, 00, 00, utc()) +
       initDuration(seconds = 1)
     check dt.second == 1
     let dt2 = dt + 35_001.years
-    check $dt2 == "0001-01-01T12:00:01+00:00"
+    check $dt2 == "0001-01-01T12:00:01Z"
 
   test "compare datetimes":
     var dt1 = now()
@@ -426,4 +428,90 @@ suite "ttimes":
     check (-1).fromWinTime.nanosecond == convert(Seconds, Nanoseconds, 1) - 100
     check -1.fromWinTime.toWinTime == -1
     # One nanosecond is discarded due to differences in time resolution
-    check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100 
\ No newline at end of file
+    check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100
+    check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100
+
+  test "issue 7620":
+    let layout = "M/d/yyyy' 'h:mm:ss' 'tt' 'z"
+    let t7620_am = parse("4/15/2017 12:01:02 AM +0", layout, utc())
+    check t7620_am.format(layout) == "4/15/2017 12:01:02 AM Z"
+    let t7620_pm = parse("4/15/2017 12:01:02 PM +0", layout, utc())
+    check t7620_pm.format(layout) == "4/15/2017 12:01:02 PM Z"
+
+  test "format":
+    var dt = initDateTime(1, mJan, -0001,
+                          17, 01, 02, 123_456_789,
+                          staticTz(hours = 1, minutes = 2, seconds = 3))
+    check dt.format("d") == "1"
+    check dt.format("dd") == "01"
+    check dt.format("ddd") == "Fri"
+    check dt.format("dddd") == "Friday"
+    check dt.format("h") == "5"
+    check dt.format("hh") == "05"
+    check dt.format("H") == "17"
+    check dt.format("HH") == "17"
+    check dt.format("m") == "1"
+    check dt.format("mm") == "01"
+    check dt.format("M") == "1"
+    check dt.format("MM") == "01"
+    check dt.format("MMM") == "Jan"
+    check dt.format("MMMM") == "January"
+    check dt.format("s") == "2"
+    check dt.format("ss") == "02"
+    check dt.format("t") == "P"
+    check dt.format("tt") == "PM"
+    check dt.format("yy") == "02"
+    check dt.format("yyyy") == "0002"
+    check dt.format("YYYY") == "2"
+    check dt.format("uuuu") == "-0001"
+    check dt.format("UUUU") == "-1"
+    check dt.format("z") == "-1"
+    check dt.format("zz") == "-01"
+    check dt.format("zzz") == "-01:02"
+    check dt.format("zzzz") == "-01:02:03"
+    check dt.format("g") == "BC"
+
+    check dt.format("fff") == "123"
+    check dt.format("ffffff") == "123456"
+    check dt.format("fffffffff") == "123456789"
+    dt.nanosecond = 1
+    check dt.format("fff") == "000"
+    check dt.format("ffffff") == "000000"
+    check dt.format("fffffffff") == "000000001"
+
+    dt.year = 12345
+    check dt.format("yyyy") == "+12345"
+    check dt.format("uuuu") == "+12345"
+    dt.year = -12345
+    check dt.format("yyyy") == "+12346"
+    check dt.format("uuuu") == "-12345"
+
+    expect ValueError:
+      discard initTimeFormat("'")
+
+    expect ValueError:
+      discard initTimeFormat("'foo")
+
+    expect ValueError:
+      discard initTimeFormat("foo'")
+
+  test "parse":
+    check $parse("20180101", "yyyyMMdd", utc()) == "2018-01-01T00:00:00Z"
+    parseTestExcp("+120180101", "yyyyMMdd")
+
+    check parse("1", "YYYY", utc()).year == 1
+    check parse("1 BC", "YYYY g", utc()).year == 0
+    check parse("0001 BC", "yyyy g", utc()).year == 0
+    check parse("+12345 BC", "yyyy g", utc()).year == -12344
+    check parse("1 AD", "YYYY g", utc()).year == 1
+    check parse("0001 AD", "yyyy g", utc()).year == 1
+    check parse("+12345 AD", "yyyy g", utc()).year == 12345
+
+    check parse("-1", "UUUU", utc()).year == -1
+    check parse("-0001", "uuuu", utc()).year == -1
+
+    discard parse("foobar", "'foobar'")
+    discard parse("foo'bar", "'foo''''bar'")
+    discard parse("'", "''")
+
+    parseTestExcp("2000 A", "yyyy g")