#
#
#            Nimrod's Runtime Library
#        (c) Copyright 2012 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#


## This module contains routines and types for dealing with time.
## This module is available for the JavaScript target.

{.push debugger:off.} # the user does not want to trace a part
                      # of the standard library!

import
  strutils

include "system/inclrtl"

type
  TMonth* = enum ## represents a month
    mJan, mFeb, mMar, mApr, mMay, mJun, mJul, mAug, mSep, mOct, mNov, mDec
  TWeekDay* = enum ## represents a weekday
    dMon, dTue, dWed, dThu, dFri, dSat, dSun

var
  timezone {.importc, header: "<time.h>".}: int
  tzname {.importc, header: "<time.h>" .}: array[0..1, cstring]

when defined(posix) and not defined(JS):
  type
    TTimeImpl {.importc: "time_t", header: "<sys/time.h>".} = int
    TTime* = distinct TTimeImpl ## distinct type that represents a time
                                ## measured as number of seconds since the epoch
    
    Ttimeval {.importc: "struct timeval", header: "<sys/select.h>", 
               final, pure.} = object ## struct timeval
      tv_sec: int  ## Seconds. 
      tv_usec: int ## Microseconds. 
      
  # we cannot import posix.nim here, because posix.nim depends on times.nim.
  # Ok, we could, but I don't want circular dependencies. 
  # And gettimeofday() is not defined in the posix module anyway. Sigh.
  
  proc posix_gettimeofday(tp: var Ttimeval, unused: pointer = nil) {.
    importc: "gettimeofday", header: "<sys/time.h>".}

elif defined(windows):
  import winlean
  
  when defined(vcc):
    # newest version of Visual C++ defines time_t to be of 64 bits
    type TTimeImpl {.importc: "time_t", header: "<time.h>".} = int64
  else:
    type TTimeImpl {.importc: "time_t", header: "<time.h>".} = int32
  
  type
    TTime* = distinct TTimeImpl

elif defined(JS):
  type
    TTime* {.final, importc.} = object
      getDay: proc (): int {.tags: [].}
      getFullYear: proc (): int {.tags: [].}
      getHours: proc (): int {.tags: [].}
      getMilliseconds: proc (): int {.tags: [].}
      getMinutes: proc (): int {.tags: [].}
      getMonth: proc (): int {.tags: [].}
      getSeconds: proc (): int {.tags: [].}
      getTime: proc (): int {.tags: [].}
      getTimezoneOffset: proc (): int {.tags: [].}
      getDate: proc (): int {.tags: [].}
      getUTCDate: proc (): int {.tags: [].}
      getUTCFullYear: proc (): int {.tags: [].}
      getUTCHours: proc (): int {.tags: [].}
      getUTCMilliseconds: proc (): int {.tags: [].}
      getUTCMinutes: proc (): int {.tags: [].}
      getUTCMonth: proc (): int {.tags: [].}
      getUTCSeconds: proc (): int {.tags: [].}
      getUTCDay: proc (): int {.tags: [].}
      getYear: proc (): int {.tags: [].}
      parse: proc (s: cstring): TTime {.tags: [].}
      setDate: proc (x: int) {.tags: [].}
      setFullYear: proc (x: int) {.tags: [].}
      setHours: proc (x: int) {.tags: [].}
      setMilliseconds: proc (x: int) {.tags: [].}
      setMinutes: proc (x: int) {.tags: [].}
      setMonth: proc (x: int) {.tags: [].}
      setSeconds: proc (x: int) {.tags: [].}
      setTime: proc (x: int) {.tags: [].}
      setUTCDate: proc (x: int) {.tags: [].}
      setUTCFullYear: proc (x: int) {.tags: [].}
      setUTCHours: proc (x: int) {.tags: [].}
      setUTCMilliseconds: proc (x: int) {.tags: [].}
      setUTCMinutes: proc (x: int) {.tags: [].}
      setUTCMonth: proc (x: int) {.tags: [].}
      setUTCSeconds: proc (x: int) {.tags: [].}
      setYear: proc (x: int) {.tags: [].}
      toGMTString: proc (): cstring {.tags: [].}
      toLocaleString: proc (): cstring {.tags: [].}

type
  TTimeInfo* = object of TObject ## represents a time in different parts
    second*: range[0..61]     ## The number of seconds after the minute,
                              ## normally in the range 0 to 59, but can
                              ## be up to 61 to allow for leap seconds.
    minute*: range[0..59]     ## The number of minutes after the hour,
                              ## in the range 0 to 59.
    hour*: range[0..23]       ## The number of hours past midnight,
                              ## in the range 0 to 23.
    monthday*: range[1..31]   ## The day of the month, in the range 1 to 31.
    month*: TMonth            ## The current month.
    year*: range[-10_000..10_000] ## The current year.
    weekday*: TWeekDay        ## The current day of the week.
    yearday*: range[0..365]   ## The number of days since January 1,
                              ## in the range 0 to 365.
                              ## Always 0 if the target is JS.
    isDST*: bool              ## Determines whether DST is in effect. Always
                              ## ``False`` if time is UTC.
    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.

  ## I make some assumptions about the data in here. Either
  ## everything should be positive or everything negative. Zero is
  ## fine too. Mixed signs will lead to unexpected results.
  TTimeInterval* {.pure.} = object ## a time interval
    miliseconds*: int ## The number of miliseconds
    seconds*: int     ## The number of seconds
    minutes*: int     ## The number of minutes
    hours*: int       ## The number of hours
    days*: int        ## The number of days
    months*: int      ## The number of months
    years*: int       ## The number of years

proc getTime*(): TTime {.tags: [FTime].}
  ## gets the current calendar time as a UNIX epoch value (number of seconds
  ## elapsed since 1970) with integer precission. Use epochTime for higher
  ## resolution.
proc getLocalTime*(t: TTime): TTimeInfo {.tags: [FTime].}
  ## converts the calendar time `t` to broken-time representation,
  ## expressed relative to the user's specified time zone.
proc getGMTime*(t: TTime): TTimeInfo {.tags: [FTime].}
  ## converts the calendar time `t` to broken-down time representation,
  ## expressed in Coordinated Universal Time (UTC).

proc TimeInfoToTime*(timeInfo: TTimeInfo): TTime {.tags: [].}
  ## 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
  ## them from the other information in the broken-down time structure.

proc fromSeconds*(since1970: float): TTime {.tags: [].}
  ## Takes a float which contains the number of seconds since the unix epoch and
  ## returns a time object.

proc fromSeconds*(since1970: int|int64): TTime = fromSeconds(float(since1970))
  ## Takes an int which contains the number of seconds since the unix epoch and
  ## returns a time object.

proc toSeconds*(time: TTime): float {.tags: [].}
  ## Returns the time in seconds since the unix epoch.

proc `$` *(timeInfo: TTimeInfo): string {.tags: [].}
  ## converts a `TTimeInfo` object to a string representation.
proc `$` *(time: TTime): string {.tags: [].}
  ## converts a calendar time to a string representation.

proc `-`*(a, b: TTime): int64 {.
  rtl, extern: "ntDiffTime".}
  ## computes the difference of two calendar times. Result is in seconds.

proc `<`*(a, b: TTime): bool {.
  rtl, extern: "ntLtTime".} = 
  ## returns true iff ``a < b``, that is iff a happened before b.
  result = a - b < 0
  
proc `<=` * (a, b: TTime): bool {.
  rtl, extern: "ntLeTime".}= 
  ## returns true iff ``a <= b``.
  result = a - b <= 0

proc `==`*(a, b: TTime): bool {.rtl, extern: "ntEqTime".} =
  ## 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: [FTime].}
    ## 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: [FTime].}
  ## returns the offset of the local (non-DST) timezone in seconds west of UTC.

proc getStartMilsecs*(): int {.deprecated, tags: [FTime].}
  ## get the miliseconds from the start of the program. **Deprecated since
  ## version 0.8.10.** Use ``epochTime`` or ``cpuTime`` instead.

proc initInterval*(miliseconds, seconds, minutes, hours, days, months, 
                   years: int = 0): TTimeInterval =
  ## creates a new ``TTimeInterval``.
  result.miliseconds = miliseconds
  result.seconds = seconds
  result.minutes = minutes
  result.hours = hours
  result.days = days
  result.months = months
  result.years = years

proc isLeapYear(year: int): bool =
  if year mod 400 == 0:
    return true
  elif year mod 100 == 0: 
    return false
  elif year mod 4 == 0: 
    return true
  else:
    return false

proc getDaysInMonth(month: TMonth, year: int): int =
  # http://www.dispersiondesign.com/articles/time/number_of_days_in_a_month
  case month 
  of mFeb: result = if isLeapYear(year): 29 else: 28
  of mApr, mJun, mSep, mNov: result = 30
  else: result = 31

proc toSeconds(a: TTimeInfo, interval: TTimeInterval): float =
  ## Calculates how many seconds the interval is worth by adding up
  ## all the fields

  var anew = a
  var newinterv = interval
  result = 0
  
  newinterv.months += interval.years * 12
  var curMonth = anew.month
  for mth in 1 .. newinterv.months:
    result += float(getDaysInMonth(curMonth, anew.year) * 24 * 60 * 60)
    if curMonth == mDec:
      curMonth = mJan
      anew.year.inc()
    else:
      curMonth.inc()
  result += float(newinterv.days * 24 * 60 * 60)
  result += float(newinterv.minutes * 60 * 60)
  result += float(newinterv.seconds)
  result += newinterv.miliseconds / 1000

proc `+`*(a: TTimeInfo, interval: TTimeInterval): TTimeInfo =
  ## adds ``interval`` time.
  ##
  ## **Note:** This has been only briefly tested and it may not be
  ## very accurate.
  let t = toSeconds(TimeInfoToTime(a))
  let secs = toSeconds(a, interval)
  if a.tzname == "UTC":
    result = getGMTime(fromSeconds(t + secs))
  else:
    result = getLocalTime(fromSeconds(t + secs))

proc `-`*(a: TTimeInfo, interval: TTimeInterval): TTimeInfo =
  ## subtracts ``interval`` time.
  ##
  ## **Note:** This has been only briefly tested, it is inaccurate especially
  ## when you subtract so much that you reach the Julian calendar.
  let t = toSeconds(TimeInfoToTime(a))
  let secs = toSeconds(a, interval)
  if a.tzname == "UTC":
    result = getGMTime(fromSeconds(t - secs))
  else:
    result = getLocalTime(fromSeconds(t - secs))

when not defined(JS):  
  proc epochTime*(): float {.rtl, extern: "nt$1", tags: [FTime].}
    ## gets time after the UNIX epoch (1970) in seconds. It is a float
    ## because sub-second resolution is likely to be supported (depending 
    ## on the hardware/OS).

  proc cpuTime*(): float {.rtl, extern: "nt$1", tags: [FTime].}
    ## gets time spent that the CPU spent to run the current process in
    ## seconds. This may be more useful for benchmarking than ``epochTime``.
    ## However, it may measure the real time instead (depending on the OS).
    ## The value of the result has no meaning. 
    ## To generate useful timing values, take the difference between 
    ## the results of two ``cpuTime`` calls:
    ##
    ## .. code-block:: nimrod
    ##   var t0 = cpuTime()
    ##   doWork()
    ##   echo "CPU time [s] ", cpuTime() - t0

when not defined(JS):
  # C wrapper:
  type
    structTM {.importc: "struct tm", final.} = object
      second {.importc: "tm_sec".},
        minute {.importc: "tm_min".},
        hour {.importc: "tm_hour".},
        monthday {.importc: "tm_mday".},
        month {.importc: "tm_mon".},
        year {.importc: "tm_year".},
        weekday {.importc: "tm_wday".},
        yearday {.importc: "tm_yday".},
        isdst {.importc: "tm_isdst".}: cint
  
    PTimeInfo = ptr structTM
    PTime = ptr TTime
  
    TClock {.importc: "clock_t".} = distinct int
  
  proc localtime(timer: PTime): PTimeInfo {.
    importc: "localtime", header: "<time.h>", tags: [].}
  proc gmtime(timer: PTime): PTimeInfo {.
    importc: "gmtime", header: "<time.h>", tags: [].}
  proc timec(timer: PTime): TTime {.
    importc: "time", header: "<time.h>", tags: [].}
  proc mktime(t: structTM): TTime {.
    importc: "mktime", header: "<time.h>", tags: [].}
  proc asctime(tblock: structTM): CString {.
    importc: "asctime", header: "<time.h>", tags: [].}
  proc ctime(time: PTime): CString {.
    importc: "ctime", header: "<time.h>", tags: [].}
  #  strftime(s: CString, maxsize: int, fmt: CString, t: tm): int {.
  #    importc: "strftime", header: "<time.h>".}
  proc clock(): TClock {.importc: "clock", header: "<time.h>", tags: [FTime].}
  proc difftime(a, b: TTime): float {.importc: "difftime", header: "<time.h>", 
    tags: [].}
  
  var
    clocksPerSec {.importc: "CLOCKS_PER_SEC", nodecl.}: int
    
  # our own procs on top of that:
  proc tmToTimeInfo(tm: structTM, local: bool): TTimeInfo =
    const
      weekDays: array [0..6, TWeekDay] = [
        dSun, dMon, dTue, dWed, dThu, dFri, dSat]
    result.second = int(tm.second)
    result.minute = int(tm.minute)
    result.hour = int(tm.hour)
    result.monthday = int(tm.monthday)
    result.month = TMonth(tm.month)
    result.year = tm.year + 1900'i32
    result.weekday = weekDays[int(tm.weekDay)]
    result.yearday = int(tm.yearday)
    result.isDST = tm.isDST > 0
    if local:
      if result.isDST:
        result.tzname = getTzname().DST
      else:
        result.tzname = getTzname().nonDST
    else:
      result.tzname = "UTC"
    
    result.timezone = if local: getTimezone() else: 0
  
  proc timeInfoToTM(t: TTimeInfo): structTM =
    const
      weekDays: array [TWeekDay, int8] = [1'i8,2'i8,3'i8,4'i8,5'i8,6'i8,0'i8]
    result.second = t.second
    result.minute = t.minute
    result.hour = t.hour
    result.monthday = t.monthday
    result.month = ord(t.month)
    result.year = t.year - 1900
    result.weekday = weekDays[t.weekDay]
    result.yearday = t.yearday
    result.isdst = if t.isDST: 1 else: 0
  
  when not defined(useNimRtl):
    proc `-` (a, b: TTime): int64 =
      return toBiggestInt(difftime(a, b))
  
  proc getStartMilsecs(): int =
    #echo "clocks per sec: ", clocksPerSec, "clock: ", int(clock())
    #return clock() div (clocksPerSec div 1000)
    when defined(macosx):
      result = toInt(toFloat(int(clock())) / (toFloat(clocksPerSec) / 1000.0))
    else:
      result = int(clock()) div (clocksPerSec div 1000)
    when false:
      var a: Ttimeval
      posix_gettimeofday(a)
      result = a.tv_sec * 1000'i64 + a.tv_usec div 1000'i64
      #echo "result: ", result
    
  proc getTime(): TTime = return timec(nil)
  proc getLocalTime(t: TTime): TTimeInfo =
    var a = t
    result = tmToTimeInfo(localtime(addr(a))[], true)
    # copying is needed anyway to provide reentrancity; thus
    # the conversion is not expensive
  
  proc getGMTime(t: TTime): TTimeInfo =
    var a = t
    result = tmToTimeInfo(gmtime(addr(a))[], false)
    # copying is needed anyway to provide reentrancity; thus
    # the conversion is not expensive
  
  proc TimeInfoToTime(timeInfo: TTimeInfo): TTime =
    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: TTimeInfo): string =
    # BUGFIX: asctime returns a newline at the end!
    var p = asctime(timeInfoToTM(timeInfo))
    result = toStringTillNL(p)
  
  proc `$`(time: TTime): string =
    # BUGFIX: ctime returns a newline at the end!
    var a = time
    return toStringTillNL(ctime(addr(a)))

  const
    epochDiff = 116444736000000000'i64
    rateDiff = 10000000'i64 # 100 nsecs

  proc unixTimeToWinTime*(t: TTime): int64 = 
    ## converts a UNIX `TTime` (``time_t``) to a Windows file time
    result = int64(t) * rateDiff + epochDiff
    
  proc winTimeToUnixTime*(t: int64): TTime = 
    ## converts a Windows time to a UNIX `TTime` (``time_t``)
    result = TTime((t - epochDiff) div rateDiff)
 
  proc getTzname(): tuple[nonDST, DST: string] =
    return ($tzname[0], $tzname[1])
  
  proc getTimezone(): int =
    return timezone

  proc fromSeconds(since1970: float): TTime = TTime(since1970)

  proc toSeconds(time: TTime): float = float(time)

  when not defined(useNimRtl):
    proc epochTime(): float = 
      when defined(posix):
        var a: Ttimeval
        posix_gettimeofday(a)
        result = toFloat(a.tv_sec) + toFloat(a.tv_usec)*0.00_0001
      elif defined(windows):
        var f: winlean.TFiletime
        GetSystemTimeAsFileTime(f)
        var i64 = rdFileTime(f) - epochDiff
        var secs = i64 div rateDiff
        var subsecs = i64 mod rateDiff
        result = toFloat(int(secs)) + toFloat(int(subsecs)) * 0.0000001
      else:
        {.error: "unknown OS".}
      
    proc cpuTime(): float = 
      result = toFloat(int(clock())) / toFloat(clocksPerSec)
    
elif defined(JS):
  proc newDate(): TTime {.importc: "new Date".}
  proc internGetTime(): TTime {.importc: "new Date", tags: [].}
  
  proc newDate(value: float): TTime {.importc: "new Date".}
  proc newDate(value: string): TTime {.importc: "new Date".}
  proc getTime(): TTime =
    # Warning: This is something different in JS.
    return newDate()

  const
    weekDays: array [0..6, TWeekDay] = [
      dSun, dMon, dTue, dWed, dThu, dFri, dSat]
  
  proc getLocalTime(t: TTime): TTimeInfo =
    result.second = t.getSeconds()
    result.minute = t.getMinutes()
    result.hour = t.getHours()
    result.monthday = t.getDate()
    result.month = TMonth(t.getMonth())
    result.year = t.getFullYear()
    result.weekday = weekDays[t.getDay()]
    result.yearday = 0

  proc getGMTime(t: TTime): TTimeInfo =
    result.second = t.getUTCSeconds()
    result.minute = t.getUTCMinutes()
    result.hour = t.getUTCHours()
    result.monthday = t.getUTCDate()
    result.month = TMonth(t.getUTCMonth())
    result.year = t.getUTCFullYear()
    result.weekday = weekDays[t.getUTCDay()]
    result.yearday = 0
  
  proc TimeInfoToTime*(timeInfo: TTimeInfo): TTime =
    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: TTimeInfo): string = return $(TimeInfoToTIme(timeInfo))
  proc `$`(time: TTime): string = return $time.toLocaleString()
    
  proc `-` (a, b: TTime): int64 = 
    return a.getTime() - b.getTime()
  
  var
    startMilsecs = getTime()
  
  proc getStartMilsecs(): int =
    ## get the miliseconds from the start of the program
    return int(getTime() - startMilsecs)

  proc valueOf(time: TTime): float {.importcpp: "getTime", tags:[]}

  proc fromSeconds(since1970: float): TTime = result = newDate(since1970)

  proc toSeconds(time: TTime): float = result = time.valueOf() / 1000

  proc getTimezone(): int = result = newDate().getTimezoneOffset()

proc getDateStr*(): string {.rtl, extern: "nt$1", tags: [FTime].} =
  ## gets the current date as a string of the format ``YYYY-MM-DD``.
  var ti = getLocalTime(getTime())
  result = $ti.year & '-' & intToStr(ord(ti.month)+1, 2) &
    '-' & intToStr(ti.monthDay, 2)

proc getClockStr*(): string {.rtl, extern: "nt$1", tags: [FTime].} =
  ## gets the current clock time as a string of the format ``HH:MM:SS``.
  var ti = getLocalTime(getTime())
  result = intToStr(ti.hour, 2) & ':' & intToStr(ti.minute, 2) &
    ':' & intToStr(ti.second, 2)

proc `$`*(day: TWeekDay): string =
  ## stingify operator for ``TWeekDay``.
  const lookup: array[TWeekDay, string] = ["Monday", "Tuesday", "Wednesday",
     "Thursday", "Friday", "Saturday", "Sunday"]
  return lookup[day]

proc `$`*(m: TMonth): string =
  ## stingify operator for ``TMonth``.
  const lookup: array[TMonth, string] = ["January", "February", "March", 
      "April", "May", "June", "July", "August", "September", "October",
      "November", "December"]
  return lookup[m]

proc format*(info: TTimeInfo, f: string): string =
  ## This function formats `info` 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 ``:00``.                                                    ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00``
  ##    ZZZ      Displays the name of the timezone.                                                 ``GMT -> GMT``, ``EST -> EST``
  ## ==========  =================================================================================  ================================================
  ##
  ## Other strings can be inserted by putting them in ``''``. For example ``hh'->'mm`` will give ``01->56``.
  ## The following characters can be inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]`` ``,``

  result = ""
  var i = 0
  var currentF = ""
  while True:
    case f[i]
    of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',':
      case currentF
      of "d":
        result.add($info.monthday)
      of "dd":
        if info.monthday < 10:
          result.add("0")
        result.add($info.monthday)
      of "ddd":
        result.add(($info.weekday)[0 .. 2])
      of "dddd":
        result.add($info.weekday)
      of "h":
        result.add($(if info.hour > 12: info.hour - 12 else: info.hour))
      of "hh":
        let amerHour = if info.hour > 12: info.hour - 12 else: info.hour
        if amerHour < 10:
          result.add('0')
        result.add($amerHour)
      of "H":
        result.add($info.hour)
      of "HH":
        if info.hour < 10:
          result.add('0')
        result.add($info.hour)
      of "m":
        result.add($info.minute)
      of "mm":
        if info.minute < 10:
          result.add('0')
        result.add($info.minute)
      of "M":
        result.add($(int(info.month)+1))
      of "MM":
        if info.month < mOct:
          result.add('0')
        result.add($(int(info.month)+1))
      of "MMM":
        result.add(($info.month)[0..2])
      of "MMMM":
        result.add($info.month)
      of "s":
        result.add($info.second)
      of "ss":
        if info.second < 10:
          result.add('0')
        result.add($info.second)
      of "t":
        if info.hour >= 12:
          result.add('P')
        else: result.add('A')
      of "tt":
        if info.hour >= 12:
          result.add("PM")
        else: result.add("AM")
      of "y":
        var fr = ($info.year).len()-1
        if fr < 0: fr = 0
        result.add(($info.year)[fr .. ($info.year).len()-1])
      of "yy":
        var fr = ($info.year).len()-2
        if fr < 0: fr = 0
        var fyear = ($info.year)[fr .. ($info.year).len()-1]
        if fyear.len != 2: fyear = repeatChar(2-fyear.len(), '0') & fyear
        result.add(fyear)
      of "yyy":
        var fr = ($info.year).len()-3
        if fr < 0: fr = 0
        var fyear = ($info.year)[fr .. ($info.year).len()-1]
        if fyear.len != 3: fyear = repeatChar(3-fyear.len(), '0') & fyear
        result.add(fyear)
      of "yyyy":
        var fr = ($info.year).len()-4
        if fr < 0: fr = 0
        var fyear = ($info.year)[fr .. ($info.year).len()-1]
        if fyear.len != 4: fyear = repeatChar(4-fyear.len(), '0') & fyear
        result.add(fyear)
      of "yyyyy":
        var fr = ($info.year).len()-5
        if fr < 0: fr = 0
        var fyear = ($info.year)[fr .. ($info.year).len()-1]
        if fyear.len != 5: fyear = repeatChar(5-fyear.len(), '0') & fyear
        result.add(fyear)
      of "z":
        let hrs = (info.timezone div 60) div 60
        result.add($hrs)
      of "zz":
        let hrs = (info.timezone div 60) div 60
        
        result.add($hrs)
        if hrs.abs < 10:
          var atIndex = result.len-(($hrs).len-(if hrs < 0: 1 else: 0))
          result.insert("0", atIndex)
      of "zzz":
        let hrs = (info.timezone div 60) div 60
        
        result.add($hrs & ":00")
        if hrs.abs < 10:
          var atIndex = result.len-(($hrs & ":00").len-(if hrs < 0: 1 else: 0))
          result.insert("0", atIndex)
      of "ZZZ":
        result.add(info.tzname)
      of "":
        nil # Do nothing.
      else:
        raise newException(EInvalidValue, "Invalid format string: " & currentF)
      
      currentF = ""
      if f[i] == '\0': break
      
      if f[i] == '\'':
        inc(i) # Skip '
        while f[i] != '\'' and f.len-1 > i:
          result.add(f[i])
          inc(i)
      else: result.add(f[i])
      
    else: currentF.add(f[i])
    inc(i)

{.pop.}

when isMainModule:
  # $ date --date='@2147483647'
  # Tue 19 Jan 03:14:07 GMT 2038

  var t = getGMTime(fromSeconds(2147483647))
  echo t.format("ddd dd MMM hh:mm:ss ZZZ yyyy")
  assert t.format("ddd dd MMM hh:mm:ss ZZZ yyyy") == "Tue 19 Jan 03:14:07 UTC 2038"
  
  assert 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"
  
  var t2 = getGMTime(fromSeconds(160070789)) # Mon 27 Jan 16:06:29 GMT 1975
  assert 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"
  
  when not defined(JS) and sizeof(TTime) == 8:
    var t3 = getGMTime(fromSeconds(889067643645)) # Fri  7 Jun 19:20:45 BST 30143
    assert 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"
    assert t3.format(":,[]()-/") == ":,[]()-/" 
  
  var t4 = getGMTime(fromSeconds(876124714)) # Mon  6 Oct 08:58:34 BST 1997
  assert t4.format("M MM MMM MMMM") == "10 10 Oct October"
  
  # Interval tests
  assert((t4 - initInterval(years = 2)).format("yyyy") == "1995")
  assert((t4 - initInterval(years = 7, minutes = 34, seconds = 24)).format("yyyy mm ss") == "1990 24 10")
