diff options
Diffstat (limited to 'lib/pure/times.nim')
-rw-r--r-- | lib/pure/times.nim | 2670 |
1 files changed, 1380 insertions, 1290 deletions
diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 4a33197e2..e59153455 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -8,20 +8,20 @@ # ##[ - The ``times`` module contains routines and types for dealing with time using + The `times` module contains routines and types for dealing with time using the `proleptic Gregorian calendar<https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar>`_. It's also available for the `JavaScript target <backends.html#backends-the-javascript-target>`_. - Although the ``times`` module supports nanosecond time resolution, the - resolution used by ``getTime()`` depends on the platform and backend + Although the `times` module supports nanosecond time resolution, the + resolution used by `getTime()` depends on the platform and backend (JS is limited to millisecond precision). Examples ======== - .. code-block:: nim - import times, os + ```nim + import std/[times, os] # Simple benchmarking let time = cpuTime() sleep(100) # Replace this with something to be timed @@ -37,138 +37,150 @@ # Arithmetic using TimeInterval echo "One year from now : ", now() + 1.years echo "One month from now : ", now() + 1.months + ``` Parsing and Formatting Dates ============================ - The ``DateTime`` type can be parsed and formatted using the different - ``parse`` and ``format`` procedures. - - .. code-block:: nim + The `DateTime` type can be parsed and formatted using the different + `parse` and `format` procedures. + ```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 one digit if possible. | ``5:30 -> 30`` - | ``2:01 -> 1`` - ``mm`` Same as above but always two 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 if the month value is one digit. | ``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 if the second is one digit. | ``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 at least 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 at least 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. | ``UTC+7 -> +7`` - | ``UTC-5 -> -5`` - ``zz`` Same as above but with leading 0. | ``UTC+7 -> +07`` - | ``UTC-5 -> -05`` - ``zzz`` Same as above but with ``:mm`` where *mm* represents minutes. | ``UTC+7 -> +07:00`` - | ``UTC-5 -> -05:00`` - ``zzzz`` Same as above but with ``:ss`` where *ss* represents seconds. | ``UTC+7 -> +07:00:00`` - | ``UTC-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 ``''``. + =========== ================================================================================= ============================================== + 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` + `GG` The last two digits of the Iso Week-Year | `30/12/2012 -> 13` + `GGGG` The Iso week-calendar year padded to four digits | `30/12/2012 -> 2013` + `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 one digit if possible. | `5:30 -> 30` + | `2:01 -> 1` + `mm` Same as above but always two 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 if the month value is one digit. | `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 if the second is one digit. | `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 at least 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 at least 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` + `V` The Iso Week-Number as one or two digits | `3/2/2012 -> 5` + | `1/4/2012 -> 13` + `VV` The Iso Week-Number as two digits always. 0 is prepended if one digit. | `3/2/2012 -> 05` + | `1/4/2012 -> 13` + `z` Displays the timezone offset from UTC. | `UTC+7 -> +7` + | `UTC-5 -> -5` + `zz` Same as above but with leading 0. | `UTC+7 -> +07` + | `UTC-5 -> -05` + `zzz` Same as above but with `:mm` where *mm* represents minutes. | `UTC+7 -> +07:00` + | `UTC-5 -> -05:00` + `ZZZ` Same as above but with `mm` where *mm* represents minutes. | `UTC+7 -> +0700` + | `UTC-5 -> -0500` + `zzzz` Same as above but with `:ss` where *ss* represents seconds. | `UTC+7 -> +07:00:00` + | `UTC-5 -> -05:00:00` + `ZZZZ` Same as above but with `ss` where *ss* represents seconds. | `UTC+7 -> +070000` + | `UTC-5 -> -050000` + `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`. In addition to spaces, + 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, as an - unambiguous format string like ``yyyyMMddhhmmss`` is also valid (although + unambiguous format string like `yyyyMMddhhmmss` is also valid (although only for years in the range 1..9999). Duration vs TimeInterval ============================ - The ``times`` module exports two similar types that are both used to + The `times` module exports two similar types that are both used to represent some amount of time: `Duration <#Duration>`_ and `TimeInterval <#TimeInterval>`_. This section explains how they differ and when one should be preferred over the - other (short answer: use ``Duration`` unless support for months and years is + other (short answer: use `Duration` unless support for months and years is needed). Duration ---------------------------- - A ``Duration`` represents a duration of time stored as seconds and - nanoseconds. A ``Duration`` is always fully normalized, so -``initDuration(hours = 1)`` and ``initDuration(minutes = 60)`` are equivalent. + A `Duration` represents a duration of time stored as seconds and + nanoseconds. A `Duration` is always fully normalized, so + `initDuration(hours = 1)` and `initDuration(minutes = 60)` are equivalent. - Arithmetic with a ``Duration`` is very fast, especially when used with the - ``Time`` type, since it only involves basic arithmetic. Because ``Duration`` + Arithmetic with a `Duration` is very fast, especially when used with the + `Time` type, since it only involves basic arithmetic. Because `Duration` is more performant and easier to understand it should generally preferred. TimeInterval ---------------------------- - A ``TimeInterval`` represents an amount of time expressed in calendar + A `TimeInterval` represents an amount of time expressed in calendar units, for example "1 year and 2 days". Since some units cannot be normalized (the length of a year is different for leap years for example), - the ``TimeInterval`` type uses separate fields for every unit. The - ``TimeInterval``'s returned from this module generally don't normalize + the `TimeInterval` type uses separate fields for every unit. The + `TimeInterval`'s returned from this module generally don't normalize **anything**, so even units that could be normalized (like seconds, milliseconds and so on) are left untouched. - Arithmetic with a ``TimeInterval`` can be very slow, because it requires + Arithmetic with a `TimeInterval` can be very slow, because it requires timezone information. - Since it's slower and more complex, the ``TimeInterval`` type should be + Since it's slower and more complex, the `TimeInterval` type should be avoided unless the program explicitly needs the features it offers that - ``Duration`` doesn't have. + `Duration` doesn't have. How long is a day? ---------------------------- It should be especially noted that the handling of days differs between - ``TimeInterval`` and ``Duration``. The ``Duration`` type always treats a day - as exactly 86400 seconds. For ``TimeInterval``, it's more complex. + `TimeInterval` and `Duration`. The `Duration` type always treats a day + as exactly 86400 seconds. For `TimeInterval`, it's more complex. As an example, consider the amount of time between these two timestamps, both in the same timezone: @@ -182,8 +194,8 @@ timezones that use daylight savings time. Because of this change, the amount of time that has passed is actually 25 hours. - The ``TimeInterval`` type uses calendar units, and will say that exactly one - day has passed. The ``Duration`` type on the other hand normalizes everything + The `TimeInterval` type uses calendar units, and will say that exactly one + day has passed. The `Duration` type on the other hand normalizes everything to seconds, and will therefore say that 90000 seconds has passed, which is the same as 25 hours. @@ -192,12 +204,17 @@ * `monotimes module <monotimes.html>`_ ]## -import strutils, math, options +import std/[strutils, math, options] +import std/private/since include "system/inclrtl" +when defined(nimPreviewSlimSystem): + import std/assertions + + when defined(js): - import jscore + import std/jscore # This is really bad, but overflow checks are broken badly for # ints on the JS backend. See #6752. @@ -221,28 +238,19 @@ when defined(js): {.pop.} elif defined(posix): - import posix + import std/posix type CTime = posix.Time - when not defined(freebsd) and not defined(netbsd) and not defined(openbsd): - var timezone {.importc, header: "<time.h>".}: int - when not defined(valgrind_workaround_10121): - tzset() - when defined(macosx): proc gettimeofday(tp: var Timeval, unused: pointer = nil) - {.importc: "gettimeofday", header: "<sys/time.h>".} + {.importc: "gettimeofday", header: "<sys/time.h>", sideEffect.} elif defined(windows): - import winlean, std/time_t - - type CTime = time_t.Time - - # visual c's c runtime exposes these under a different name - var timezone {.importc: "_timezone", header: "<time.h>".}: int + import std/winlean, std/time_t type + CTime = time_t.Time Tm {.importc: "struct tm", header: "<time.h>", final, pure.} = object tm_sec*: cint ## Seconds [0,60]. tm_min*: cint ## Minutes [0,59]. @@ -254,12 +262,12 @@ elif defined(windows): tm_yday*: cint ## Day of year [0,365]. tm_isdst*: cint ## Daylight Savings flag. - proc localtime(a1: var CTime): ptr Tm {.importc, header: "<time.h>".} + proc localtime(a1: var CTime): ptr Tm {.importc, header: "<time.h>", sideEffect.} type - Month* = enum ## Represents a month. Note that the enum starts at ``1``, - ## so ``ord(month)`` will give the month number in the - ## range ``1..12``. + Month* = enum ## Represents a month. Note that the enum starts at `1`, + ## so `ord(month)` will give the month number in the + ## range `1..12`. mJan = (1, "January") mFeb = "February" mMar = "March" @@ -282,78 +290,53 @@ type dSat = "Saturday" dSun = "Sunday" -when defined(nimHasStyleChecks): - {.push styleChecks: off.} - -type - DateTimeLocale* = object - MMM*: array[mJan..mDec, string] - MMMM*: array[mJan..mDec, string] - ddd*: array[dMon..dSun, string] - dddd*: array[dMon..dSun, string] - -when defined(nimHasStyleChecks): - {.pop.} - type MonthdayRange* = range[1..31] HourRange* = range[0..23] MinuteRange* = range[0..59] - SecondRange* = range[0..60] + SecondRange* = range[0..60] ## \ + ## Includes the value 60 to allow for a leap second. Note however + ## that the `second` of a `DateTime` will never be a leap second. YeardayRange* = range[0..365] NanosecondRange* = range[0..999_999_999] + IsoWeekRange* = range[1 .. 53] + ## An ISO 8601 calendar week number. + IsoYear* = distinct int + ## An ISO 8601 calendar year number. + ## + ## .. warning:: The ISO week-based year can correspond to the following or previous year from 29 December to January 3. + Time* = object ## Represents a point in time. seconds: int64 nanosecond: NanosecondRange DateTime* = object of RootObj ## \ - ## Represents a time in different parts. Although this type can represent - ## leap seconds, they are generally not supported in this module. They are - ## not ignored, but the ``DateTime``'s returned by procedures in this - ## module will never have a leap second. - ## - ## **Warning**: even though the fields of ``DateTime`` are exported, - ## they should never be mutated directly. Doing so is unsafe and will - ## result in the ``DateTime`` ending up in an invalid state. - ## - ## Instead of mutating the fields directly, use the `Duration <#Duration>`_ - ## and `TimeInterval <#TimeInterval>`_ types for arithmetic and use the - ## `initDateTime proc <#initDateTime,MonthdayRange,Month,int,HourRange,MinuteRange,SecondRange,NanosecondRange,Timezone>`_ - ## for changing a specific field. - nanosecond*: NanosecondRange ## The number of nanoseconds after the second, - ## in the range 0 to 999_999_999. - second*: SecondRange ## The number of seconds after the minute, - ## normally in the range 0 to 59, but can - ## be up to 60 to allow for a leap second. - minute*: MinuteRange ## The number of minutes after the hour, - ## in the range 0 to 59. - hour*: HourRange ## The number of hours past midnight, - ## in the range 0 to 23. - monthday*: MonthdayRange ## The day of the month, in the range 1 to 31. - month*: Month ## The month. - year*: int ## The year, using astronomical year numbering - ## (meaning that before year 1 is year 0, - ## then year -1 and so on). - weekday*: WeekDay ## The day of the week. - yearday*: YeardayRange ## The number of days since January 1, - ## in the range 0 to 365. - isDst*: bool ## Determines whether DST is in effect. - ## Always false for the JavaScript backend. - timezone*: Timezone ## The timezone represented as an implementation - ## of ``Timezone``. - utcOffset*: int ## The offset in seconds west of UTC, including - ## any offset due to DST. Note that the sign of - ## this number is the opposite of the one in a - ## formatted offset string like ``+01:00`` (which - ## would be equivalent to the UTC offset - ## ``-3600``). + ## Represents a time in different parts. Although this type can represent + ## leap seconds, they are generally not supported in this module. They are + ## not ignored, but the `DateTime`'s returned by procedures in this + ## module will never have a leap second. + nanosecond: NanosecondRange + second: SecondRange + minute: MinuteRange + hour: HourRange + monthdayZero: int + monthZero: int + year: int + weekday: WeekDay + yearday: YeardayRange + isDst: bool + timezone: Timezone + utcOffset: int Duration* = object ## Represents a fixed duration of time, meaning a duration ## that has constant length independent of the context. ## - ## To create a new ``Duration``, use `initDuration proc + ## To create a new `Duration`, use `initDuration ## <#initDuration,int64,int64,int64,int64,int64,int64,int64,int64>`_. + ## Instead of trying to access the private attributes, use + ## `inSeconds <#inSeconds,Duration>`_ for converting to seconds and + ## `inNanoseconds <#inNanoseconds,Duration>`_ for converting to nanoseconds. seconds: int64 nanosecond: NanosecondRange @@ -362,23 +345,23 @@ type Weeks, Months, Years FixedTimeUnit* = range[Nanoseconds..Weeks] ## \ - ## Subrange of ``TimeUnit`` that only includes units of fixed duration. - ## These are the units that can be represented by a ``Duration``. + ## Subrange of `TimeUnit` that only includes units of fixed duration. + ## These are the units that can be represented by a `Duration`. TimeInterval* = object ## \ ## Represents a non-fixed duration of time. Can be used to add and ## subtract non-fixed time units from a `DateTime <#DateTime>`_ or ## `Time <#Time>`_. ## - ## Create a new ``TimeInterval`` with `initTimeInterval proc + ## Create a new `TimeInterval` with `initTimeInterval proc ## <#initTimeInterval,int,int,int,int,int,int,int,int,int,int>`_. ## - ## Note that ``TimeInterval`` doesn't represent a fixed duration of time, + ## Note that `TimeInterval` doesn't represent a fixed duration of time, ## since the duration of some units depend on the context (e.g a year ## can be either 365 or 366 days long). The non-fixed time units are ## years, months, days and week. ## - ## Note that ``TimeInterval``'s returned from the ``times`` module are + ## Note that `TimeInterval`'s returned from the `times` module are ## never normalized. If you want to normalize a time unit, ## `Duration <#Duration>`_ should be used instead. nanoseconds*: int ## The number of nanoseconds @@ -394,8 +377,8 @@ type Timezone* = ref object ## \ ## Timezone interface for supporting `DateTime <#DateTime>`_\s of arbitrary - ## timezones. The ``times`` module only supplies implementations for the - ## systems local time and UTC. + ## timezones. The `times` module only supplies implementations for the + ## system's local time and UTC. zonedTimeFromTimeImpl: proc (x: Time): ZonedTime {.tags: [], raises: [], benign.} zonedTimeFromAdjTimeImpl: proc (x: Time): ZonedTime @@ -412,7 +395,6 @@ type DurationParts* = array[FixedTimeUnit, int64] # Array of Duration parts starts TimeIntervalParts* = array[TimeUnit, int] # Array of Duration parts starts - TimesMutableTypes = DateTime | Time | Duration | TimeInterval const secondsInMin = 60 @@ -434,15 +416,21 @@ const unitWeights: array[FixedTimeUnit, int64] = [ 7 * secondsInDay * 1e9.int64, ] -const DefaultLocale* = DateTimeLocale( - MMM: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", - "Nov", "Dec"], - MMMM: ["January", "February", "March", "April", "May", "June", "July", - "August", "September", "October", "November", "December"], - ddd: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], - dddd: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", - "Sunday"], -) +when (NimMajor, NimMinor) >= (1, 4): + # Newer versions of Nim don't track defects + {.pragma: parseFormatRaises, raises: [TimeParseError, TimeFormatParseError].} + {.pragma: parseRaises, raises: [TimeParseError].} +else: + # Still track when using older versions + {.pragma: parseFormatRaises, raises: [TimeParseError, TimeFormatParseError, Defect].} + {.pragma: parseRaises, raises: [TimeParseError, Defect].} + + +# +# Helper procs +# + +{.pragma: operator, rtl, noSideEffect, benign.} proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T {.inline.} = @@ -459,8 +447,8 @@ proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T proc normalize[T: Duration|Time](seconds, nanoseconds: int64): T = ## Normalize a (seconds, nanoseconds) pair and return it as either - ## a ``Duration`` or ``Time``. A normalized ``Duration|Time`` has a - ## positive nanosecond part in the range ``NanosecondRange``. + ## a `Duration` or `Time`. A normalized `Duration|Time` has a + ## positive nanosecond part in the range `NanosecondRange`. result.seconds = seconds + convert(Nanoseconds, Seconds, nanoseconds) var nanosecond = nanoseconds mod convert(Seconds, Nanoseconds, 1) if nanosecond < 0: @@ -468,177 +456,15 @@ proc normalize[T: Duration|Time](seconds, nanoseconds: int64): T = result.seconds -= 1 result.nanosecond = nanosecond.int -# Forward declarations -proc utcTzInfo(time: Time): ZonedTime - {.tags: [], raises: [], benign.} -proc localZonedTimeFromTime(time: Time): ZonedTime - {.tags: [], raises: [], benign.} -proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime - {.tags: [], raises: [], benign.} -proc initTime*(unix: int64, nanosecond: NanosecondRange): Time - {.tags: [], raises: [], benign, noSideEffect.} - -proc nanosecond*(time: Time): NanosecondRange = - ## Get the fractional part of a ``Time`` as the number - ## of nanoseconds of the second. - time.nanosecond - -proc initDuration*(nanoseconds, microseconds, milliseconds, - seconds, minutes, hours, days, weeks: int64 = 0): Duration = - ## Create a new `Duration <#Duration>`_. - runnableExamples: - let dur = initDuration(seconds = 1, milliseconds = 1) - doAssert dur.milliseconds == 1 - doAssert dur.seconds == 1 - - let seconds = convert(Weeks, Seconds, weeks) + - convert(Days, Seconds, days) + - convert(Minutes, Seconds, minutes) + - convert(Hours, Seconds, hours) + - convert(Seconds, Seconds, seconds) + - convert(Milliseconds, Seconds, milliseconds) + - convert(Microseconds, Seconds, microseconds) + - convert(Nanoseconds, Seconds, nanoseconds) - let nanoseconds = (convert(Milliseconds, Nanoseconds, milliseconds mod 1000) + - convert(Microseconds, Nanoseconds, microseconds mod 1_000_000) + - nanoseconds mod 1_000_000_000).int - # Nanoseconds might be negative so we must normalize. - result = normalize[Duration](seconds, nanoseconds) - -template convert(dur: Duration, unit: static[FixedTimeUnit]): int64 = - # The correction is required due to how durations are normalized. - # For example,` initDuration(nanoseconds = -1)` is stored as - # { seconds = -1, nanoseconds = 999999999 }. - when unit == Nanoseconds: - dur.seconds * 1_000_000_000 + dur.nanosecond - else: - let correction = dur.seconds < 0 and dur.nanosecond > 0 - when unit >= Seconds: - convert(Seconds, unit, dur.seconds + ord(correction)) - else: - if correction: - convert(Seconds, unit, dur.seconds + 1) - - convert(Nanoseconds, unit, - convert(Seconds, Nanoseconds, 1) - dur.nanosecond) - else: - convert(Seconds, unit, dur.seconds) + - convert(Nanoseconds, unit, dur.nanosecond) - -proc inWeeks*(dur: Duration): int64 = - ## Convert the duration to the number of whole weeks. - runnableExamples: - let dur = initDuration(days = 8) - doAssert dur.inWeeks == 1 - dur.convert(Weeks) - -proc inDays*(dur: Duration): int64 = - ## Convert the duration to the number of whole days. - runnableExamples: - let dur = initDuration(hours = -50) - doAssert dur.inDays == -2 - dur.convert(Days) - -proc inHours*(dur: Duration): int64 = - ## Convert the duration to the number of whole hours. - runnableExamples: - let dur = initDuration(minutes = 60, days = 2) - doAssert dur.inHours == 49 - dur.convert(Hours) - -proc inMinutes*(dur: Duration): int64 = - ## Convert the duration to the number of whole minutes. - runnableExamples: - let dur = initDuration(hours = 2, seconds = 10) - doAssert dur.inMinutes == 120 - dur.convert(Minutes) - -proc inSeconds*(dur: Duration): int64 = - ## Convert the duration to the number of whole seconds. - runnableExamples: - let dur = initDuration(hours = 2, milliseconds = 10) - doAssert dur.inSeconds == 2 * 60 * 60 - dur.convert(Seconds) - -proc inMilliseconds*(dur: Duration): int64 = - ## Convert the duration to the number of whole milliseconds. - runnableExamples: - let dur = initDuration(seconds = -2) - doAssert dur.inMilliseconds == -2000 - dur.convert(Milliseconds) - -proc inMicroseconds*(dur: Duration): int64 = - ## Convert the duration to the number of whole microseconds. - runnableExamples: - let dur = initDuration(seconds = -2) - doAssert dur.inMicroseconds == -2000000 - dur.convert(Microseconds) - -proc inNanoseconds*(dur: Duration): int64 = - ## Convert the duration to the number of whole nanoseconds. - runnableExamples: - let dur = initDuration(seconds = -2) - doAssert dur.inNanoseconds == -2000000000 - dur.convert(Nanoseconds) - -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:00Z" - initTime(unix, 0) - -proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} = - ## Convert ``t`` to a unix timestamp (seconds since ``1970-01-01T00:00:00Z``). - ## See also `toUnixFloat` for subsecond resolution. - runnableExamples: - doAssert fromUnix(0).toUnix() == 0 - t.seconds - -proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideEffect.} = - ## Convert a unix timestamp in seconds to a `Time`; same as `fromUnix` - ## but with subsecond resolution. - runnableExamples: - doAssert fromUnixFloat(123456.0) == fromUnixFloat(123456) - doAssert fromUnixFloat(-123456.0) == fromUnixFloat(-123456) - let secs = seconds.floor - let nsecs = (seconds - secs) * 1e9 - initTime(secs.int64, nsecs.NanosecondRange) - -proc toUnixFloat(t: Time): float {.benign, tags: [], raises: [].} = - ## Same as `toUnix` but using subsecond resolution. - runnableExamples: - let t = getTime() - # `<` because of rounding errors - doAssert abs(t.toUnixFloat().fromUnixFloat - t) < initDuration(nanoseconds = 1000) - t.seconds.float + t.nanosecond / convert(Seconds, Nanoseconds, 1) - -since((1, 1)): - export fromUnixFloat - export toUnixFloat - -proc fromWinTime*(win: int64): Time = - ## Convert a Windows file time (100-nanosecond intervals since - ## ``1601-01-01T00:00:00Z``) to a ``Time``. - const hnsecsPerSec = convert(Seconds, Nanoseconds, 1) div 100 - let nanos = floorMod(win, hnsecsPerSec) * 100 - let seconds = floorDiv(win - epochDiff, hnsecsPerSec) - result = initTime(seconds, nanos) - -proc toWinTime*(t: Time): int64 = - ## Convert ``t`` to a Windows file time (100-nanosecond intervals - ## since ``1601-01-01T00:00:00Z``). - result = t.seconds * rateDiff + epochDiff + t.nanosecond div 100 - proc isLeapYear*(year: int): bool = - ## Returns true if ``year`` is a leap year. + ## Returns true if `year` is a leap year. runnableExamples: doAssert isLeapYear(2000) doAssert not isLeapYear(1900) year mod 4 == 0 and (year mod 100 != 0 or year mod 400 == 0) proc getDaysInMonth*(month: Month, year: int): int = - ## Get the number of days in ``month`` of ``year``. + ## Get the number of days in `month` of `year`. # http://www.dispersiondesign.com/articles/time/number_of_days_in_a_month runnableExamples: doAssert getDaysInMonth(mFeb, 2000) == 29 @@ -648,13 +474,6 @@ proc getDaysInMonth*(month: Month, year: int): int = of mApr, mJun, mSep, mNov: result = 30 else: result = 31 -proc getDaysInYear*(year: int): int = - ## Get the number of days in a ``year`` - runnableExamples: - doAssert getDaysInYear(2000) == 366 - doAssert getDaysInYear(2001) == 365 - result = 365 + (if isLeapYear(year): 1 else: 0) - proc assertValidDate(monthday: MonthdayRange, month: Month, year: int) {.inline.} = assert monthday <= getDaysInMonth(month, year), @@ -698,7 +517,7 @@ proc fromEpochDay(epochday: int64): proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): YeardayRange {.tags: [], raises: [], benign.} = ## Returns the day of the year. - ## Equivalent with ``initDateTime(monthday, month, year, 0, 0, 0).yearday``. + ## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).yearday`. runnableExamples: doAssert getDayOfYear(1, mJan, 2000) == 0 doAssert getDayOfYear(10, mJan, 2000) == 9 @@ -718,7 +537,7 @@ proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay {.tags: [], raises: [], benign.} = ## Returns the day of the week enum from day, month and year. - ## Equivalent with ``initDateTime(monthday, month, year, 0, 0, 0).weekday``. + ## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).weekday`. runnableExamples: doAssert getDayOfWeek(13, mJun, 1990) == dWed doAssert $getDayOfWeek(13, mJun, 1990) == "Wednesday" @@ -726,13 +545,91 @@ proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay assertValidDate monthday, month, year # 1970-01-01 is a Thursday, we adjust to the previous Monday let days = toEpochDay(monthday, month, year) - 3 - let weeks = floorDiv(days, 7) + let weeks = floorDiv(days, 7'i64) let wd = days - weeks * 7 # The value of d is 0 for a Sunday, 1 for a Monday, 2 for a Tuesday, etc. # so we must correct for the WeekDay type. result = if wd == 0: dSun else: WeekDay(wd - 1) -{.pragma: operator, rtl, noSideEffect, benign.} +proc getDaysInYear*(year: int): int = + ## Get the number of days in a `year` + runnableExamples: + doAssert getDaysInYear(2000) == 366 + doAssert getDaysInYear(2001) == 365 + result = 365 + (if isLeapYear(year): 1 else: 0) + +proc `==`*(a, b: IsoYear): bool {.borrow.} +proc `$`*(p: IsoYear): string {.borrow.} + +proc getWeeksInIsoYear*(y: IsoYear): IsoWeekRange {.since: (1, 5).} = + ## Returns the number of weeks in the specified ISO 8601 week-based year, which can be + ## either 53 or 52. + runnableExamples: + assert getWeeksInIsoYear(IsoYear(2019)) == 52 + assert getWeeksInIsoYear(IsoYear(2020)) == 53 + + var y = int(y) + + # support negative years + y = if y < 0: 400 + y mod 400 else: y + + # source: https://webspace.science.uu.nl/~gent0113/calendar/isocalendar.htm + let p = (y + (y div 4) - (y div 100) + (y div 400)) mod 7 + let y1 = y - 1 + let p1 = (y1 + (y1 div 4) - (y1 div 100) + (y1 div 400)) mod 7 + if p == 4 or p1 == 3: 53 else: 52 + +proc getIsoWeekAndYear*(dt: DateTime): + tuple[isoweek: IsoWeekRange, isoyear: IsoYear] {.since: (1, 5).} = + ## Returns the ISO 8601 week and year. + ## + ## .. warning:: The ISO week-based year can correspond to the following or previous year from 29 December to January 3. + runnableExamples: + assert getIsoWeekAndYear(initDateTime(21, mApr, 2018, 00, 00, 00)) == (isoweek: 16.IsoWeekRange, isoyear: 2018.IsoYear) + block: + let (w, y) = getIsoWeekAndYear(initDateTime(30, mDec, 2019, 00, 00, 00)) + assert w == 01.IsoWeekRange + assert y == 2020.IsoYear + assert getIsoWeekAndYear(initDateTime(13, mSep, 2020, 00, 00, 00)) == (isoweek: 37.IsoWeekRange, isoyear: 2020.IsoYear) + block: + let (w, y) = getIsoWeekAndYear(initDateTime(2, mJan, 2021, 00, 00, 00)) + assert w.int > 52 + assert w.int < 54 + assert y.int mod 100 == 20 + + # source: https://webspace.science.uu.nl/~gent0113/calendar/isocalendar.htm + var w = (dt.yearday.int - dt.weekday.int + 10) div 7 + if w < 1: + (isoweek: getWeeksInIsoYear(IsoYear(dt.year - 1)), isoyear: IsoYear(dt.year - 1)) + elif (w > getWeeksInIsoYear(IsoYear(dt.year))): + (isoweek: IsoWeekRange(1), isoyear: IsoYear(dt.year + 1)) + else: + (isoweek: IsoWeekRange(w), isoyear: IsoYear(dt.year)) + +proc stringifyUnit(value: int | int64, unit: TimeUnit): string = + ## Stringify time unit with it's name, lowercased + let strUnit = $unit + result = "" + result.addInt value + result.add ' ' + if abs(value) != 1: + result.add(strUnit.toLowerAscii()) + else: + result.add(strUnit[0..^2].toLowerAscii()) + +proc humanizeParts(parts: seq[string]): string = + ## Make date string parts human-readable + result = "" + if parts.len == 0: + result.add "0 nanoseconds" + elif parts.len == 1: + result = parts[0] + elif parts.len == 2: + result = parts[0] & " and " & parts[1] + else: + for i in 0..high(parts)-1: + result.add parts[i] & ", " + result.add "and " & parts[high(parts)] template subImpl[T: Duration|Time](a: Duration|Time, b: Duration|Time): T = normalize[T](a.seconds - b.seconds, a.nanosecond - b.nanosecond) @@ -750,21 +647,122 @@ template lqImpl(a: Duration|Time, b: Duration|Time): bool = template eqImpl(a: Duration|Time, b: Duration|Time): bool = a.seconds == b.seconds and a.nanosecond == b.nanosecond -const DurationZero* = initDuration() ## \ + +# +# Duration +# + +const DurationZero* = Duration() ## \ ## Zero value for durations. Useful for comparisons. - ## - ## .. code-block:: nim - ## + ## ```nim ## doAssert initDuration(seconds = 1) > DurationZero ## doAssert initDuration(seconds = 0) == DurationZero + ## ``` + +proc initDuration*(nanoseconds, microseconds, milliseconds, + seconds, minutes, hours, days, weeks: int64 = 0): Duration = + ## Create a new `Duration <#Duration>`_. + runnableExamples: + let dur = initDuration(seconds = 1, milliseconds = 1) + doAssert dur.inMilliseconds == 1001 + doAssert dur.inSeconds == 1 + + let seconds = convert(Weeks, Seconds, weeks) + + convert(Days, Seconds, days) + + convert(Minutes, Seconds, minutes) + + convert(Hours, Seconds, hours) + + convert(Seconds, Seconds, seconds) + + convert(Milliseconds, Seconds, milliseconds) + + convert(Microseconds, Seconds, microseconds) + + convert(Nanoseconds, Seconds, nanoseconds) + let nanoseconds = (convert(Milliseconds, Nanoseconds, milliseconds mod 1000) + + convert(Microseconds, Nanoseconds, microseconds mod 1_000_000) + + nanoseconds mod 1_000_000_000).int + # Nanoseconds might be negative so we must normalize. + result = normalize[Duration](seconds, nanoseconds) + +template convert(dur: Duration, unit: static[FixedTimeUnit]): int64 = + # The correction is required due to how durations are normalized. + # For example,` initDuration(nanoseconds = -1)` is stored as + # { seconds = -1, nanoseconds = 999999999 }. + when unit == Nanoseconds: + dur.seconds * 1_000_000_000 + dur.nanosecond + else: + let correction = dur.seconds < 0 and dur.nanosecond > 0 + when unit >= Seconds: + convert(Seconds, unit, dur.seconds + ord(correction)) + else: + if correction: + convert(Seconds, unit, dur.seconds + 1) - + convert(Nanoseconds, unit, + convert(Seconds, Nanoseconds, 1) - dur.nanosecond) + else: + convert(Seconds, unit, dur.seconds) + + convert(Nanoseconds, unit, dur.nanosecond) + +proc inWeeks*(dur: Duration): int64 = + ## Converts the duration to the number of whole weeks. + runnableExamples: + let dur = initDuration(days = 8) + doAssert dur.inWeeks == 1 + dur.convert(Weeks) + +proc inDays*(dur: Duration): int64 = + ## Converts the duration to the number of whole days. + runnableExamples: + let dur = initDuration(hours = -50) + doAssert dur.inDays == -2 + dur.convert(Days) + +proc inHours*(dur: Duration): int64 = + ## Converts the duration to the number of whole hours. + runnableExamples: + let dur = initDuration(minutes = 60, days = 2) + doAssert dur.inHours == 49 + dur.convert(Hours) + +proc inMinutes*(dur: Duration): int64 = + ## Converts the duration to the number of whole minutes. + runnableExamples: + let dur = initDuration(hours = 2, seconds = 10) + doAssert dur.inMinutes == 120 + dur.convert(Minutes) + +proc inSeconds*(dur: Duration): int64 = + ## Converts the duration to the number of whole seconds. + runnableExamples: + let dur = initDuration(hours = 2, milliseconds = 10) + doAssert dur.inSeconds == 2 * 60 * 60 + dur.convert(Seconds) + +proc inMilliseconds*(dur: Duration): int64 = + ## Converts the duration to the number of whole milliseconds. + runnableExamples: + let dur = initDuration(seconds = -2) + doAssert dur.inMilliseconds == -2000 + dur.convert(Milliseconds) + +proc inMicroseconds*(dur: Duration): int64 = + ## Converts the duration to the number of whole microseconds. + runnableExamples: + let dur = initDuration(seconds = -2) + doAssert dur.inMicroseconds == -2000000 + dur.convert(Microseconds) + +proc inNanoseconds*(dur: Duration): int64 = + ## Converts the duration to the number of whole nanoseconds. + runnableExamples: + let dur = initDuration(seconds = -2) + doAssert dur.inNanoseconds == -2000000000 + dur.convert(Nanoseconds) proc toParts*(dur: Duration): DurationParts = ## Converts a duration into an array consisting of fixed time units. ## ## Each value in the array gives information about a specific unit of - ## time, for example ``result[Days]`` gives a count of days. + ## time, for example `result[Days]` gives a count of days. ## - ## This procedure is useful for converting ``Duration`` values to strings. + ## This procedure is useful for converting `Duration` values to strings. runnableExamples: var dp = toParts(initDuration(weeks = 2, days = 1)) doAssert dp[Days] == 1 @@ -793,33 +791,8 @@ proc toParts*(dur: Duration): DurationParts = result[unit] = quantity -proc stringifyUnit(value: int | int64, unit: TimeUnit): string = - ## Stringify time unit with it's name, lowercased - let strUnit = $unit - result = "" - result.add($value) - result.add(" ") - if abs(value) != 1: - result.add(strUnit.toLowerAscii()) - else: - result.add(strUnit[0..^2].toLowerAscii()) - -proc humanizeParts(parts: seq[string]): string = - ## Make date string parts human-readable - result = "" - if parts.len == 0: - result.add "0 nanoseconds" - elif parts.len == 1: - result = parts[0] - elif parts.len == 2: - result = parts[0] & " and " & parts[1] - else: - for i in 0..high(parts)-1: - result.add parts[i] & ", " - result.add "and " & parts[high(parts)] - proc `$`*(dur: Duration): string = - ## Human friendly string representation of a ``Duration``. + ## Human friendly string representation of a `Duration`. runnableExamples: doAssert $initDuration(seconds = 2) == "2 seconds" doAssert $initDuration(weeks = 1, days = 2) == "1 week and 2 days" @@ -859,9 +832,9 @@ proc `-`*(a: Duration): Duration {.operator, extern: "ntReverseDuration".} = proc `<`*(a, b: Duration): bool {.operator, extern: "ntLtDuration".} = ## Note that a duration can be negative, - ## so even if ``a < b`` is true ``a`` might + ## so even if `a < b` is true `a` might ## represent a larger absolute duration. - ## Use ``abs(a) < abs(b)`` to compare the absolute + ## Use `abs(a) < abs(b)` to compare the absolute ## duration. runnableExamples: doAssert initDuration(seconds = 1) < initDuration(seconds = 2) @@ -896,6 +869,15 @@ proc `*`*(a: Duration, b: int64): Duration {.operator, doAssert initDuration(minutes = 45) * 3 == initDuration(hours = 2, minutes = 15) b * a +proc `+=`*(d1: var Duration, d2: Duration) = + d1 = d1 + d2 + +proc `-=`*(dt: var Duration, ti: Duration) = + dt = dt - ti + +proc `*=`*(a: var Duration, b: int) = + a = a * b + proc `div`*(a: Duration, b: int64): Duration {.operator, extern: "ntDivDuration".} = ## Integer division for durations. @@ -909,11 +891,113 @@ proc `div`*(a: Duration, b: int64): Duration {.operator, let carryOver = convert(Seconds, Nanoseconds, a.seconds mod b) normalize[Duration](a.seconds div b, (a.nanosecond + carryOver) div b) +proc high*(typ: typedesc[Duration]): Duration = + ## Get the longest representable duration. + initDuration(seconds = high(int64), nanoseconds = high(NanosecondRange)) + +proc low*(typ: typedesc[Duration]): Duration = + ## Get the longest representable duration of negative direction. + initDuration(seconds = low(int64)) + +proc abs*(a: Duration): Duration = + runnableExamples: + doAssert initDuration(milliseconds = -1500).abs == + initDuration(milliseconds = 1500) + initDuration(seconds = abs(a.seconds), nanoseconds = -a.nanosecond) + +# +# Time +# + proc initTime*(unix: int64, nanosecond: NanosecondRange): Time = ## Create a `Time <#Time>`_ from a unix timestamp and a nanosecond part. result.seconds = unix result.nanosecond = nanosecond +proc nanosecond*(time: Time): NanosecondRange = + ## Get the fractional part of a `Time` as the number + ## of nanoseconds of the second. + time.nanosecond + +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:00Z" + initTime(unix, 0) + +proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} = + ## Convert `t` to a unix timestamp (seconds since `1970-01-01T00:00:00Z`). + ## See also `toUnixFloat` for subsecond resolution. + runnableExamples: + doAssert fromUnix(0).toUnix() == 0 + t.seconds + +proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideEffect.} = + ## Convert a unix timestamp in seconds to a `Time`; same as `fromUnix` + ## but with subsecond resolution. + runnableExamples: + doAssert fromUnixFloat(123456.0) == fromUnixFloat(123456) + doAssert fromUnixFloat(-123456.0) == fromUnixFloat(-123456) + let secs = seconds.floor + let nsecs = (seconds - secs) * 1e9 + initTime(secs.int64, nsecs.NanosecondRange) + +proc toUnixFloat(t: Time): float {.benign, tags: [], raises: [].} = + ## Same as `toUnix` but using subsecond resolution. + runnableExamples: + let t = getTime() + # `<` because of rounding errors + doAssert abs(t.toUnixFloat().fromUnixFloat - t) < initDuration(nanoseconds = 1000) + t.seconds.float + t.nanosecond / convert(Seconds, Nanoseconds, 1) + +since((1, 1)): + export fromUnixFloat + export toUnixFloat + + +proc fromWinTime*(win: int64): Time = + ## Convert a Windows file time (100-nanosecond intervals since + ## `1601-01-01T00:00:00Z`) to a `Time`. + const hnsecsPerSec = convert(Seconds, Nanoseconds, 1) div 100 + let nanos = floorMod(win, hnsecsPerSec) * 100 + let seconds = floorDiv(win - epochDiff, hnsecsPerSec) + result = initTime(seconds, nanos) + +proc toWinTime*(t: Time): int64 = + ## Convert `t` to a Windows file time (100-nanosecond intervals + ## since `1601-01-01T00:00:00Z`). + result = t.seconds * rateDiff + epochDiff + t.nanosecond div 100 + +proc getTimeImpl(typ: typedesc[Time]): Time = + discard "implemented in the vm" + +proc getTime*(): Time {.tags: [TimeEffect], benign.} = + ## Gets the current time as a `Time` with up to nanosecond resolution. + when nimvm: + result = getTimeImpl(Time) + else: + when defined(js): + let millis = newDate().getTime() + let seconds = convert(Milliseconds, Seconds, millis) + let nanos = convert(Milliseconds, Nanoseconds, + millis mod convert(Seconds, Milliseconds, 1).int) + result = initTime(seconds, nanos) + elif defined(macosx): + var a {.noinit.}: Timeval + gettimeofday(a) + result = initTime(a.tv_sec.int64, + convert(Microseconds, Nanoseconds, a.tv_usec.int)) + elif defined(posix): + var ts {.noinit.}: Timespec + discard clock_gettime(CLOCK_REALTIME, ts) + result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) + elif defined(windows): + var f {.noinit.}: FILETIME + getSystemTimeAsFileTime(f) + result = fromWinTime(rdFileTime(f)) + proc `-`*(a, b: Time): Duration {.operator, extern: "ntDiffTime".} = ## Computes the duration between two points in time. runnableExamples: @@ -922,53 +1006,155 @@ proc `-`*(a, b: Time): Duration {.operator, extern: "ntDiffTime".} = subImpl[Duration](a, b) proc `+`*(a: Time, b: Duration): Time {.operator, extern: "ntAddTime".} = - ## Add a duration of time to a ``Time``. + ## Add a duration of time to a `Time`. runnableExamples: doAssert (fromUnix(0) + initDuration(seconds = 1)) == fromUnix(1) addImpl[Time](a, b) proc `-`*(a: Time, b: Duration): Time {.operator, extern: "ntSubTime".} = - ## Subtracts a duration of time from a ``Time``. + ## Subtracts a duration of time from a `Time`. runnableExamples: doAssert (fromUnix(0) - initDuration(seconds = 1)) == fromUnix(-1) subImpl[Time](a, b) proc `<`*(a, b: Time): bool {.operator, extern: "ntLtTime".} = - ## Returns true if ``a < b``, that is if ``a`` happened before ``b``. + ## Returns true if `a < b`, that is if `a` happened before `b`. runnableExamples: doAssert initTime(50, 0) < initTime(99, 0) ltImpl(a, b) proc `<=`*(a, b: Time): bool {.operator, extern: "ntLeTime".} = - ## Returns true if ``a <= b``. + ## Returns true if `a <= b`. lqImpl(a, b) proc `==`*(a, b: Time): bool {.operator, extern: "ntEqTime".} = - ## Returns true if ``a == b``, that is if both times represent the same point in time. + ## Returns true if `a == b`, that is if both times represent the same point in time. eqImpl(a, b) +proc `+=`*(t: var Time, b: Duration) = + t = t + b + +proc `-=`*(t: var Time, b: Duration) = + t = t - b + proc high*(typ: typedesc[Time]): Time = initTime(high(int64), high(NanosecondRange)) proc low*(typ: typedesc[Time]): Time = - initTime(low(int64), 0) + initTime(0, 0) -proc high*(typ: typedesc[Duration]): Duration = - ## Get the longest representable duration. - initDuration(seconds = high(int64), nanoseconds = high(NanosecondRange)) - -proc low*(typ: typedesc[Duration]): Duration = - ## Get the longest representable duration of negative direction. - initDuration(seconds = low(int64)) +# +# DateTime & Timezone +# -proc abs*(a: Duration): Duration = - runnableExamples: - doAssert initDuration(milliseconds = -1500).abs == - initDuration(milliseconds = 1500) - initDuration(seconds = abs(a.seconds), nanoseconds = -a.nanosecond) +template assertDateTimeInitialized(dt: DateTime) = + assert dt.monthdayZero != 0, "Uninitialized datetime" + +proc nanosecond*(dt: DateTime): NanosecondRange {.inline.} = + ## The number of nanoseconds after the second, + ## in the range 0 to 999_999_999. + assertDateTimeInitialized(dt) + dt.nanosecond + +proc second*(dt: DateTime): SecondRange {.inline.} = + ## The number of seconds after the minute, + ## in the range 0 to 59. + assertDateTimeInitialized(dt) + dt.second + +proc minute*(dt: DateTime): MinuteRange {.inline.} = + ## The number of minutes after the hour, + ## in the range 0 to 59. + assertDateTimeInitialized(dt) + dt.minute + +proc hour*(dt: DateTime): HourRange {.inline.} = + ## The number of hours past midnight, + ## in the range 0 to 23. + assertDateTimeInitialized(dt) + dt.hour + +proc monthday*(dt: DateTime): MonthdayRange {.inline.} = + ## The day of the month, in the range 1 to 31. + assertDateTimeInitialized(dt) + # 'cast' to avoid extra range check + cast[MonthdayRange](dt.monthdayZero) + +proc month*(dt: DateTime): Month = + ## The month as an enum, the ordinal value + ## is in the range 1 to 12. + assertDateTimeInitialized(dt) + # 'cast' to avoid extra range check + cast[Month](dt.monthZero) + +proc year*(dt: DateTime): int {.inline.} = + ## The year, using astronomical year numbering + ## (meaning that before year 1 is year 0, + ## then year -1 and so on). + assertDateTimeInitialized(dt) + dt.year + +proc weekday*(dt: DateTime): WeekDay {.inline.} = + ## The day of the week as an enum, the ordinal + ## value is in the range 0 (monday) to 6 (sunday). + assertDateTimeInitialized(dt) + dt.weekday + +proc yearday*(dt: DateTime): YeardayRange {.inline.} = + ## The number of days since January 1, + ## in the range 0 to 365. + assertDateTimeInitialized(dt) + dt.yearday + +proc isDst*(dt: DateTime): bool {.inline.} = + ## Determines whether DST is in effect. + ## Always false for the JavaScript backend. + assertDateTimeInitialized(dt) + dt.isDst + +proc timezone*(dt: DateTime): Timezone {.inline.} = + ## The timezone represented as an implementation + ## of `Timezone`. + assertDateTimeInitialized(dt) + dt.timezone + +proc utcOffset*(dt: DateTime): int {.inline.} = + ## The offset in seconds west of UTC, including + ## any offset due to DST. Note that the sign of + ## this number is the opposite of the one in a + ## formatted offset string like `+01:00` (which + ## would be equivalent to the UTC offset + ## `-3600`). + assertDateTimeInitialized(dt) + dt.utcOffset + +proc isInitialized(dt: DateTime): bool = + # Returns true if `dt` is not the (invalid) default value for `DateTime`. + runnableExamples: + doAssert now().isInitialized + doAssert not default(DateTime).isInitialized + dt.monthZero != 0 + +since((1, 3)): + export isInitialized + +proc isLeapDay*(dt: DateTime): bool {.since: (1, 1).} = + ## Returns whether `t` is a leap day, i.e. Feb 29 in a leap year. This matters + ## as it affects time offset calculations. + runnableExamples: + let dt = dateTime(2020, mFeb, 29, 00, 00, 00, 00, utc()) + doAssert dt.isLeapDay + doAssert dt+1.years-1.years != dt + let dt2 = dateTime(2020, mFeb, 28, 00, 00, 00, 00, utc()) + doAssert not dt2.isLeapDay + doAssert dt2+1.years-1.years == dt2 + doAssertRaises(Exception): discard dateTime(2021, mFeb, 29, 00, 00, 00, 00, utc()) + assertDateTimeInitialized dt + dt.year.isLeapYear and dt.month == mFeb and dt.monthday == 29 proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = - ## Converts a ``DateTime`` to a ``Time`` representing the same point in time. + ## Converts a `DateTime` to a `Time` representing the same point in time. + assertDateTimeInitialized dt let epochDay = toEpochDay(dt.monthday, dt.month, dt.year) var seconds = epochDay * secondsInDay seconds.inc dt.hour * secondsInHour @@ -978,7 +1164,7 @@ proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = result = initTime(seconds, dt.nanosecond) proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime = - ## Create a new ``DateTime`` using ``ZonedTime`` in the specified timezone. + ## Create a new `DateTime` using `ZonedTime` in the specified timezone. let adjTime = zt.time - initDuration(seconds = zt.utcOffset) let s = adjTime.seconds let epochday = floorDiv(s, secondsInDay) @@ -993,8 +1179,8 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime = DateTime( year: y, - month: m, - monthday: d, + monthZero: m.int, + monthdayZero: d, hour: hour, minute: minute, second: second, @@ -1013,11 +1199,11 @@ proc newTimezone*( zonedTimeFromAdjTimeImpl: proc (adjTime: Time): ZonedTime {.tags: [], raises: [], benign.} ): owned Timezone = - ## Create a new ``Timezone``. + ## Create a new `Timezone`. ## - ## ``zonedTimeFromTimeImpl`` and ``zonedTimeFromAdjTimeImpl`` is used - ## as the underlying implementations for ``zonedTimeFromTime`` and - ## ``zonedTimeFromAdjTime``. + ## `zonedTimeFromTimeImpl` and `zonedTimeFromAdjTimeImpl` is used + ## as the underlying implementations for `zonedTimeFromTime` and + ## `zonedTimeFromAdjTime`. ## ## If possible, the name parameter should match the name used in the ## tz database. If the timezone doesn't exist in the tz database, or if the @@ -1041,48 +1227,48 @@ proc name*(zone: Timezone): string = ## If the timezone doesn't exist in the tz database, or if the timezone ## name is unknown, then any string that describes the timezone ## unambiguously might be used. For example, the string "LOCAL" is used - ## for the systems local timezone. + ## for the system's local timezone. ## ## See also: https://en.wikipedia.org/wiki/Tz_database zone.name proc zonedTimeFromTime*(zone: Timezone, time: Time): ZonedTime = - ## Returns the ``ZonedTime`` for some point in time. + ## Returns the `ZonedTime` for some point in time. zone.zonedTimeFromTimeImpl(time) proc zonedTimeFromAdjTime*(zone: Timezone, adjTime: Time): ZonedTime = - ## Returns the ``ZonedTime`` for some local time. + ## Returns the `ZonedTime` for some local time. ## - ## Note that the ``Time`` argument does not represent a point in time, it - ## represent a local time! E.g if ``adjTime`` is ``fromUnix(0)``, it should be - ## interpreted as 1970-01-01T00:00:00 in the ``zone`` timezone, not in UTC. + ## Note that the `Time` argument does not represent a point in time, it + ## represent a local time! E.g if `adjTime` is `fromUnix(0)`, it should be + ## interpreted as 1970-01-01T00:00:00 in the `zone` timezone, not in UTC. zone.zonedTimeFromAdjTimeImpl(adjTime) proc `$`*(zone: Timezone): string = ## Returns the name of the timezone. - zone.name + if zone != nil: result = zone.name proc `==`*(zone1, zone2: Timezone): bool = - ## Two ``Timezone``'s are considered equal if their name is equal. + ## Two `Timezone`'s are considered equal if their name is equal. + runnableExamples: + doAssert local() == local() + doAssert local() != utc() if system.`==`(zone1, zone2): return true if zone1.isNil or zone2.isNil: return false - - runnableExamples: - doAssert local() == local() - doAssert local() != utc() zone1.name == zone2.name proc inZone*(time: Time, zone: Timezone): DateTime {.tags: [], raises: [], benign.} = - ## Convert ``time`` into a ``DateTime`` using ``zone`` as the timezone. + ## Convert `time` into a `DateTime` using `zone` as the timezone. result = initDateTime(zone.zonedTimeFromTime(time), zone) proc inZone*(dt: DateTime, zone: Timezone): DateTime {.tags: [], raises: [], benign.} = - ## Returns a ``DateTime`` representing the same point in time as ``dt`` but - ## using ``zone`` as the timezone. + ## Returns a `DateTime` representing the same point in time as `dt` but + ## using `zone` as the timezone. + assertDateTimeInitialized dt dt.toTime.inZone(zone) proc toAdjTime(dt: DateTime): Time = @@ -1094,14 +1280,14 @@ proc toAdjTime(dt: DateTime): Time = result = initTime(seconds, dt.nanosecond) when defined(js): - proc localZonedTimeFromTime(time: Time): ZonedTime = + proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} = let jsDate = newDate(time.seconds * 1000) let offset = jsDate.getTimezoneOffset() * secondsInMin result.time = time result.utcOffset = offset result.isDst = false - proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime = + proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} = let utcDate = newDate(adjTime.seconds * 1000) let localDate = newDate(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), utcDate.getUTCHours(), utcDate.getUTCMinutes(), @@ -1148,13 +1334,13 @@ else: return ((a.int64 - tm.toAdjUnix).int, tm.tm_isdst > 0) return (0, false) - proc localZonedTimeFromTime(time: Time): ZonedTime = + proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} = let (offset, dst) = getLocalOffsetAndDst(time.seconds) result.time = time result.utcOffset = offset result.isDst = dst - proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime = + proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} = var adjUnix = adjTime.seconds let past = adjUnix - secondsInDay let (pastOffset, _) = getLocalOffsetAndDst(past) @@ -1187,7 +1373,7 @@ var utcInstance {.threadvar.}: Timezone var localInstance {.threadvar.}: Timezone proc utc*(): Timezone = - ## Get the ``Timezone`` implementation for the UTC timezone. + ## Get the `Timezone` implementation for the UTC timezone. runnableExamples: doAssert now().utc.timezone == utc() doAssert utc().name == "Etc/UTC" @@ -1196,7 +1382,7 @@ proc utc*(): Timezone = result = utcInstance proc local*(): Timezone = - ## Get the ``Timezone`` implementation for the local timezone. + ## Get the `Timezone` implementation for the local timezone. runnableExamples: doAssert now().timezone == local() doAssert local().name == "LOCAL" @@ -1206,272 +1392,42 @@ proc local*(): Timezone = result = localInstance proc utc*(dt: DateTime): DateTime = - ## Shorthand for ``dt.inZone(utc())``. + ## Shorthand for `dt.inZone(utc())`. dt.inZone(utc()) proc local*(dt: DateTime): DateTime = - ## Shorthand for ``dt.inZone(local())``. + ## Shorthand for `dt.inZone(local())`. dt.inZone(local()) proc utc*(t: Time): DateTime = - ## Shorthand for ``t.inZone(utc())``. + ## Shorthand for `t.inZone(utc())`. t.inZone(utc()) proc local*(t: Time): DateTime = - ## Shorthand for ``t.inZone(local())``. + ## Shorthand for `t.inZone(local())`. t.inZone(local()) -proc getTime*(): Time {.tags: [TimeEffect], benign.} = - ## Gets the current time as a ``Time`` with up to nanosecond resolution. - when defined(js): - let millis = newDate().getTime() - let seconds = convert(Milliseconds, Seconds, millis) - let nanos = convert(Milliseconds, Nanoseconds, - millis mod convert(Seconds, Milliseconds, 1).int) - result = initTime(seconds, nanos) - elif defined(macosx): - var a: Timeval - gettimeofday(a) - result = initTime(a.tv_sec.int64, - convert(Microseconds, Nanoseconds, a.tv_usec.int)) - elif defined(posix): - var ts: Timespec - discard clock_gettime(CLOCK_REALTIME, ts) - result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) - elif defined(windows): - var f: FILETIME - getSystemTimeAsFileTime(f) - result = fromWinTime(rdFileTime(f)) - proc now*(): DateTime {.tags: [TimeEffect], benign.} = - ## Get the current time as a ``DateTime`` in the local timezone. + ## Get the current time as a `DateTime` in the local timezone. + ## Shorthand for `getTime().local`. ## - ## Shorthand for ``getTime().local``. + ## .. warning:: Unsuitable for benchmarking, use `monotimes.getMonoTime` or + ## `cpuTime` instead, depending on the use case. getTime().local -proc initTimeInterval*(nanoseconds, microseconds, milliseconds, - seconds, minutes, hours, - days, weeks, months, years: int = 0): TimeInterval = - ## Creates a new `TimeInterval <#TimeInterval>`_. - ## - ## This proc doesn't perform any normalization! For example, - ## ``initTimeInterval(hours = 24)`` and ``initTimeInterval(days = 1)`` are - ## not equal. - ## - ## You can also use the convenience procedures called ``milliseconds``, - ## ``seconds``, ``minutes``, ``hours``, ``days``, ``months``, and ``years``. - runnableExamples: - let day = initTimeInterval(hours = 24) - let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) - doAssert $(dt + day) == "2000-01-02T12:00:00Z" - doAssert initTimeInterval(hours = 24) != initTimeInterval(days = 1) - result.nanoseconds = nanoseconds - result.microseconds = microseconds - result.milliseconds = milliseconds - result.seconds = seconds - result.minutes = minutes - result.hours = hours - result.days = days - result.weeks = weeks - result.months = months - result.years = years - -proc `+`*(ti1, ti2: TimeInterval): TimeInterval = - ## Adds two ``TimeInterval`` objects together. - result.nanoseconds = ti1.nanoseconds + ti2.nanoseconds - result.microseconds = ti1.microseconds + ti2.microseconds - result.milliseconds = ti1.milliseconds + ti2.milliseconds - result.seconds = ti1.seconds + ti2.seconds - result.minutes = ti1.minutes + ti2.minutes - result.hours = ti1.hours + ti2.hours - result.days = ti1.days + ti2.days - result.weeks = ti1.weeks + ti2.weeks - result.months = ti1.months + ti2.months - result.years = ti1.years + ti2.years - -proc `-`*(ti: TimeInterval): TimeInterval = - ## Reverses a time interval - runnableExamples: - let day = -initTimeInterval(hours = 24) - doAssert day.hours == -24 - - result = TimeInterval( - nanoseconds: -ti.nanoseconds, - microseconds: -ti.microseconds, - milliseconds: -ti.milliseconds, - seconds: -ti.seconds, - minutes: -ti.minutes, - hours: -ti.hours, - days: -ti.days, - weeks: -ti.weeks, - months: -ti.months, - years: -ti.years - ) - -proc `-`*(ti1, ti2: TimeInterval): TimeInterval = - ## Subtracts TimeInterval ``ti1`` from ``ti2``. - ## - ## Time components are subtracted one-by-one, see output: - runnableExamples: - let ti1 = initTimeInterval(hours = 24) - let ti2 = initTimeInterval(hours = 4) - doAssert (ti1 - ti2) == initTimeInterval(hours = 20) - - result = ti1 + (-ti2) - -proc getDateStr*(dt = now()): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = - ## Gets the current local date as a string of the format ``YYYY-MM-DD``. - runnableExamples: - echo getDateStr(now() - 1.months) - result = $dt.year & '-' & intToStr(ord(dt.month), 2) & - '-' & intToStr(dt.monthday, 2) - -proc getClockStr*(dt = now()): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = - ## Gets the current local clock time as a string of the format ``HH:mm:ss``. - runnableExamples: - echo getClockStr(now() - 1.hours) - result = intToStr(dt.hour, 2) & ':' & intToStr(dt.minute, 2) & - ':' & intToStr(dt.second, 2) - -proc toParts*(ti: TimeInterval): TimeIntervalParts = - ## Converts a ``TimeInterval`` into an array consisting of its time units, - ## starting with nanoseconds and ending with years. - ## - ## This procedure is useful for converting ``TimeInterval`` values to strings. - ## E.g. then you need to implement custom interval printing - runnableExamples: - var tp = toParts(initTimeInterval(years = 1, nanoseconds = 123)) - doAssert tp[Years] == 1 - doAssert tp[Nanoseconds] == 123 - - var index = 0 - for name, value in fieldPairs(ti): - result[index.TimeUnit()] = value - index += 1 - -proc `$`*(ti: TimeInterval): string = - ## Get string representation of ``TimeInterval``. - runnableExamples: - doAssert $initTimeInterval(years = 1, nanoseconds = 123) == - "1 year and 123 nanoseconds" - doAssert $initTimeInterval() == "0 nanoseconds" - - var parts: seq[string] = @[] - var tiParts = toParts(ti) - for unit in countdown(Years, Nanoseconds): - if tiParts[unit] != 0: - parts.add(stringifyUnit(tiParts[unit], unit)) - - result = humanizeParts(parts) - -proc nanoseconds*(nanos: int): TimeInterval {.inline.} = - ## TimeInterval of ``nanos`` nanoseconds. - initTimeInterval(nanoseconds = nanos) - -proc microseconds*(micros: int): TimeInterval {.inline.} = - ## TimeInterval of ``micros`` microseconds. - initTimeInterval(microseconds = micros) - -proc milliseconds*(ms: int): TimeInterval {.inline.} = - ## TimeInterval of ``ms`` milliseconds. - initTimeInterval(milliseconds = ms) - -proc seconds*(s: int): TimeInterval {.inline.} = - ## TimeInterval of ``s`` seconds. - ## - ## ``echo getTime() + 5.seconds`` - initTimeInterval(seconds = s) - -proc minutes*(m: int): TimeInterval {.inline.} = - ## TimeInterval of ``m`` minutes. - ## - ## ``echo getTime() + 5.minutes`` - initTimeInterval(minutes = m) - -proc hours*(h: int): TimeInterval {.inline.} = - ## TimeInterval of ``h`` hours. - ## - ## ``echo getTime() + 2.hours`` - initTimeInterval(hours = h) - -proc days*(d: int): TimeInterval {.inline.} = - ## TimeInterval of ``d`` days. - ## - ## ``echo getTime() + 2.days`` - initTimeInterval(days = d) - -proc weeks*(w: int): TimeInterval {.inline.} = - ## TimeInterval of ``w`` weeks. - ## - ## ``echo getTime() + 2.weeks`` - initTimeInterval(weeks = w) - -proc months*(m: int): TimeInterval {.inline.} = - ## TimeInterval of ``m`` months. - ## - ## ``echo getTime() + 2.months`` - initTimeInterval(months = m) - -proc years*(y: int): TimeInterval {.inline.} = - ## TimeInterval of ``y`` years. - ## - ## ``echo getTime() + 2.years`` - initTimeInterval(years = y) - -proc evaluateInterval(dt: DateTime, interval: TimeInterval): - tuple[adjDur, absDur: Duration] = - ## Evaluates how many nanoseconds the interval is worth - ## in the context of ``dt``. - ## The result in split into an adjusted diff and an absolute diff. - var months = interval.years * 12 + interval.months - var curYear = dt.year - var curMonth = dt.month - # Subtracting - if months < 0: - for mth in countdown(-1 * months, 1): - if curMonth == mJan: - curMonth = mDec - curYear.dec - else: - curMonth.dec() - let days = getDaysInMonth(curMonth, curYear) - result.adjDur = result.adjDur - initDuration(days = days) - # Adding - else: - for mth in 1 .. months: - let days = getDaysInMonth(curMonth, curYear) - result.adjDur = result.adjDur + initDuration(days = days) - if curMonth == mDec: - curMonth = mJan - curYear.inc - else: - curMonth.inc() - - result.adjDur = result.adjDur + initDuration( - days = interval.days, - weeks = interval.weeks) - result.absDur = initDuration( - nanoseconds = interval.nanoseconds, - microseconds = interval.microseconds, - milliseconds = interval.milliseconds, - seconds = interval.seconds, - minutes = interval.minutes, - hours = interval.hours) - -proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, - hour: HourRange, minute: MinuteRange, second: SecondRange, - nanosecond: NanosecondRange, - zone: Timezone = local()): DateTime = +proc dateTime*(year: int, month: Month, monthday: MonthdayRange, + hour: HourRange = 0, minute: MinuteRange = 0, second: SecondRange = 0, + nanosecond: NanosecondRange = 0, + zone: Timezone = local()): DateTime = ## Create a new `DateTime <#DateTime>`_ in the specified timezone. runnableExamples: - let dt1 = initDateTime(30, mMar, 2017, 00, 00, 00, 00, utc()) - doAssert $dt1 == "2017-03-30T00:00:00Z" + assert $dateTime(2017, mMar, 30, zone = utc()) == "2017-03-30T00:00:00Z" assertValidDate monthday, month, year let dt = DateTime( - monthday: monthday, + monthdayZero: monthday, year: year, - month: month, + monthZero: month.int, hour: hour, minute: minute, second: second, @@ -1481,57 +1437,24 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, hour: HourRange, minute: MinuteRange, second: SecondRange, - zone: Timezone = local()): DateTime = + nanosecond: NanosecondRange, + zone: Timezone = local()): DateTime {.deprecated: "use `dateTime`".} = ## Create a new `DateTime <#DateTime>`_ in the specified timezone. - runnableExamples: - let dt1 = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) - doAssert $dt1 == "2017-03-30T00:00:00Z" - initDateTime(monthday, month, year, hour, minute, second, 0, zone) - - -proc `+`*(dt: DateTime, interval: TimeInterval): DateTime = - ## Adds ``interval`` to ``dt``. Components from ``interval`` are added - ## in the order of their size, i.e. first the ``years`` component, then the - ## ``months`` component and so on. The returned ``DateTime`` will have the - ## same timezone as the input. - ## - ## Note that when adding months, monthday overflow is allowed. This means that - ## if the resulting month doesn't have enough days it, the month will be - ## incremented and the monthday will be set to the number of days overflowed. - ## So adding one month to `31 October` will result in `31 November`, which - ## will overflow and result in `1 December`. - runnableExamples: - let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) - 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:00Z" - let (adjDur, absDur) = evaluateInterval(dt, interval) - - if adjDur != DurationZero: - var zt = dt.timezone.zonedTimeFromAdjTime(dt.toAdjTime + adjDur) - if absDur != DurationZero: - zt = dt.timezone.zonedTimeFromTime(zt.time + absDur) - result = initDateTime(zt, dt.timezone) - else: - result = initDateTime(zt, dt.timezone) - else: - var zt = dt.timezone.zonedTimeFromTime(dt.toTime + absDur) - result = initDateTime(zt, dt.timezone) - -proc `-`*(dt: DateTime, interval: TimeInterval): DateTime = - ## Subtract ``interval`` from ``dt``. Components from ``interval`` are - ## subtracted in the order of their size, i.e. first the ``years`` component, - ## then the ``months`` 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:00Z" + runnableExamples("--warning:deprecated:off"): + assert $initDateTime(30, mMar, 2017, 00, 00, 00, 00, utc()) == "2017-03-30T00:00:00Z" + dateTime(year, month, monthday, hour, minute, second, nanosecond, zone) - dt + (-interval) +proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, + hour: HourRange, minute: MinuteRange, second: SecondRange, + zone: Timezone = local()): DateTime {.deprecated: "use `dateTime`".} = + ## Create a new `DateTime <#DateTime>`_ in the specified timezone. + runnableExamples("--warning:deprecated:off"): + assert $initDateTime(30, mMar, 2017, 00, 00, 00, utc()) == "2017-03-30T00:00:00Z" + dateTime(year, month, monthday, hour, minute, second, 0, zone) proc `+`*(dt: DateTime, dur: Duration): DateTime = runnableExamples: - let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) + let dt = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) let dur = initDuration(hours = 5) doAssert $(dt + dur) == "2017-03-30T05:00:00Z" @@ -1539,206 +1462,97 @@ proc `+`*(dt: DateTime, dur: Duration): DateTime = proc `-`*(dt: DateTime, dur: Duration): DateTime = runnableExamples: - let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) + let dt = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) let dur = initDuration(days = 5) doAssert $(dt - dur) == "2017-03-25T00:00:00Z" (dt.toTime - dur).inZone(dt.timezone) proc `-`*(dt1, dt2: DateTime): Duration = - ## Compute the duration between ``dt1`` and ``dt2``. + ## Compute the duration between `dt1` and `dt2`. runnableExamples: - let dt1 = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) - let dt2 = initDateTime(25, mMar, 2017, 00, 00, 00, utc()) + let dt1 = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) + let dt2 = dateTime(2017, mMar, 25, 00, 00, 00, 00, utc()) doAssert dt1 - dt2 == initDuration(days = 5) dt1.toTime - dt2.toTime proc `<`*(a, b: DateTime): bool = - ## Returns true if ``a`` happened before ``b``. + ## Returns true if `a` happened before `b`. return a.toTime < b.toTime proc `<=`*(a, b: DateTime): bool = - ## Returns true if ``a`` happened before or at the same time as ``b``. + ## Returns true if `a` happened before or at the same time as `b`. return a.toTime <= b.toTime proc `==`*(a, b: DateTime): bool = - ## Returns true if ``a`` and ``b`` represent the same point in time. - return a.toTime == b.toTime - -proc isStaticInterval(interval: TimeInterval): bool = - interval.years == 0 and interval.months == 0 and - interval.days == 0 and interval.weeks == 0 - -proc evaluateStaticInterval(interval: TimeInterval): Duration = - assert interval.isStaticInterval - initDuration(nanoseconds = interval.nanoseconds, - microseconds = interval.microseconds, - milliseconds = interval.milliseconds, - seconds = interval.seconds, - minutes = interval.minutes, - hours = interval.hours) + ## Returns true if `a` and `b` represent the same point in time. + if not a.isInitialized: not b.isInitialized + elif not b.isInitialized: false + else: a.toTime == b.toTime -proc between*(startDt, endDt: DateTime): TimeInterval = - ## Gives the difference between ``startDt`` and ``endDt`` as a - ## ``TimeInterval``. The following guarantees about the result is given: - ## - ## - All fields will have the same sign. - ## - If `startDt.timezone == endDt.timezone`, it is guaranteed that - ## `startDt + between(startDt, endDt) == endDt`. - ## - If `startDt.timezone != endDt.timezone`, then the result will be - ## equivalent to `between(startDt.utc, endDt.utc)`. - runnableExamples: - var a = initDateTime(25, mMar, 2015, 12, 0, 0, utc()) - var b = initDateTime(1, mApr, 2017, 15, 0, 15, utc()) - var ti = initTimeInterval(years = 2, weeks = 1, hours = 3, seconds = 15) - doAssert between(a, b) == ti - doAssert between(a, b) == -between(b, a) - - if startDt.timezone != endDt.timezone: - return between(startDt.utc, endDt.utc) - elif endDt < startDt: - return -between(endDt, startDt) - - type Date = tuple[year, month, monthday: int] - var startDate: Date = (startDt.year, startDt.month.ord, startDt.monthday) - var endDate: Date = (endDt.year, endDt.month.ord, endDt.monthday) - - # Subtract one day from endDate if time of day is earlier than startDay - # The subtracted day will be counted by fixed units (hour and lower) - # at the end of this proc - if (endDt.hour, endDt.minute, endDt.second, endDt.nanosecond) < - (startDt.hour, startDt.minute, startDt.second, startDt.nanosecond): - if endDate.month == 1 and endDate.monthday == 1: - endDate.year.dec - endDate.monthday = 31 - endDate.month = 12 - elif endDate.monthday == 1: - endDate.month.dec - endDate.monthday = getDaysInMonth(endDate.month.Month, endDate.year) - else: - endDate.monthday.dec - - # Years - result.years.inc endDate.year - startDate.year - 1 - if (startDate.month, startDate.monthday) <= (endDate.month, endDate.monthday): - result.years.inc - startDate.year.inc result.years - - # Months - if startDate.year < endDate.year: - result.months.inc 12 - startDate.month # Move to dec - if endDate.month != 1 or (startDate.monthday <= endDate.monthday): - result.months.inc - startDate.year = endDate.year - startDate.month = 1 - else: - startDate.month = 12 - if startDate.year == endDate.year: - if (startDate.monthday <= endDate.monthday): - result.months.inc endDate.month - startDate.month - startDate.month = endDate.month - elif endDate.month != 1: - let month = endDate.month - 1 - let daysInMonth = getDaysInMonth(month.Month, startDate.year) - if daysInMonth < startDate.monthday: - if startDate.monthday - daysInMonth < endDate.monthday: - result.months.inc endDate.month - startDate.month - 1 - startDate.month = endDate.month - startDate.monthday = startDate.monthday - daysInMonth - else: - result.months.inc endDate.month - startDate.month - 2 - startDate.month = endDate.month - 2 - else: - result.months.inc endDate.month - startDate.month - 1 - startDate.month = endDate.month - 1 - - # Days - # This means that start = dec and end = jan - if startDate.year < endDate.year: - result.days.inc 31 - startDate.monthday + endDate.monthday - startDate = endDate - else: - while startDate.month < endDate.month: - let daysInMonth = getDaysInMonth(startDate.month.Month, startDate.year) - result.days.inc daysInMonth - startDate.monthday + 1 - startDate.month.inc - startDate.monthday = 1 - result.days.inc endDate.monthday - startDate.monthday - result.weeks = result.days div 7 - result.days = result.days mod 7 - startDate = endDate +proc `+=`*(a: var DateTime, b: Duration) = + a = a + b - # Handle hours, minutes, seconds, milliseconds, microseconds and nanoseconds - let newStartDt = initDateTime(startDate.monthday, startDate.month.Month, - startDate.year, startDt.hour, startDt.minute, startDt.second, - startDt.nanosecond, startDt.timezone) - let dur = endDt - newStartDt - let parts = toParts(dur) - # There can still be a full day in `parts` since `Duration` and `TimeInterval` - # models days differently. - result.hours = parts[Hours].int + parts[Days].int * 24 - result.minutes = parts[Minutes].int - result.seconds = parts[Seconds].int - result.milliseconds = parts[Milliseconds].int - result.microseconds = parts[Microseconds].int - result.nanoseconds = parts[Nanoseconds].int +proc `-=`*(a: var DateTime, b: Duration) = + a = a - b -proc `+`*(time: Time, interval: TimeInterval): Time = - ## Adds `interval` to `time`. - ## If `interval` contains any years, months, weeks or days the operation - ## is performed in the local timezone. +proc getDateStr*(dt = now()): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = + ## Gets the current local date as a string of the format `YYYY-MM-dd`. runnableExamples: - let tm = fromUnix(0) - doAssert tm + 5.seconds == fromUnix(5) - - if interval.isStaticInterval: - time + evaluateStaticInterval(interval) - else: - toTime(time.local + interval) + echo getDateStr(now() - 1.months) + assertDateTimeInitialized dt + result = newStringOfCap(10) # len("YYYY-MM-dd") == 10 + result.addInt dt.year + result.add '-' + result.add intToStr(dt.monthZero, 2) + result.add '-' + result.add intToStr(dt.monthday, 2) -proc `-`*(time: Time, interval: TimeInterval): Time = - ## Subtracts `interval` from Time `time`. - ## If `interval` contains any years, months, weeks or days the operation - ## is performed in the local timezone. +proc getClockStr*(dt = now()): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = + ## Gets the current local clock time as a string of the format `HH:mm:ss`. runnableExamples: - let tm = fromUnix(5) - doAssert tm - 5.seconds == fromUnix(0) + echo getClockStr(now() - 1.hours) + assertDateTimeInitialized dt + result = newStringOfCap(8) # len("HH:mm:ss") == 8 + result.add intToStr(dt.hour, 2) + result.add ':' + result.add intToStr(dt.minute, 2) + result.add ':' + result.add intToStr(dt.second, 2) - if interval.isStaticInterval: - time - evaluateStaticInterval(interval) - else: - toTime(time.local - interval) -proc `+=`*[T, U: TimesMutableTypes](a: var T, b: U) = - ## Modify ``a`` in place by adding ``b``. - runnableExamples: - var tm = fromUnix(0) - tm += initDuration(seconds = 1) - doAssert tm == fromUnix(1) - a = a + b +# +# Iso week forward declarations +# -proc `-=`*[T, U: TimesMutableTypes](a: var T, b: U) = - ## Modify ``a`` in place by subtracting ``b``. - runnableExamples: - var tm = fromUnix(5) - tm -= initDuration(seconds = 5) - doAssert tm == fromUnix(0) - a = a - b +proc initDateTime*(weekday: WeekDay, isoweek: IsoWeekRange, isoyear: IsoYear, + hour: HourRange, minute: MinuteRange, second: SecondRange, + nanosecond: NanosecondRange, + zone: Timezone = local()): DateTime {.gcsafe, raises: [], tags: [], since: (1, 5).} -proc `*=`*[T: TimesMutableTypes, U](a: var T, b: U) = - # Mutable type is often multiplied by number - runnableExamples: - var dur = initDuration(seconds = 1) - dur *= 5 - doAssert dur == initDuration(seconds = 5) - a = a * b +proc initDateTime*(weekday: WeekDay, isoweek: IsoWeekRange, isoyear: IsoYear, + hour: HourRange, minute: MinuteRange, second: SecondRange, + zone: Timezone = local()): DateTime {.gcsafe, raises: [], tags: [], since: (1, 5).} # -# Parse & format implementation +# TimeFormat # +when defined(nimHasStyleChecks): + {.push styleChecks: off.} + +type + DateTimeLocale* = object + MMM*: array[mJan..mDec, string] + MMMM*: array[mJan..mDec, string] + ddd*: array[dMon..dSun, string] + dddd*: array[dMon..dSun, string] + +when defined(nimHasStyleChecks): + {.pop.} + type AmPm = enum apUnknown, apAm, apPm @@ -1752,6 +1566,9 @@ type year: Option[int] month: Option[int] monthday: Option[int] + isoyear: Option[int] + yearweek: Option[int] + weekday: Option[WeekDay] utcOffset: Option[int] # '0' as default for these work fine @@ -1766,45 +1583,59 @@ type FormatPattern {.pure.} = enum d, dd, ddd, dddd + GG, GGGG h, hh, H, HH m, mm, M, MM, MMM, MMMM s, ss fff, ffffff, fffffffff t, tt - y, yy, yyy, yyyy, yyyyy + yy, yyyy YYYY uuuu UUUU + V, VV z, zz, zzz, zzzz + ZZZ, ZZZZ g # This is a special value used to mark literal format values. - # See the doc comment for ``TimeFormat.patterns``. + # See the doc comment for `TimeFormat.patterns`. Lit TimeFormat* = object ## Represents a format for parsing and printing ## time types. ## - ## To create a new ``TimeFormat`` use `initTimeFormat proc + ## To create a new `TimeFormat` use `initTimeFormat proc ## <#initTimeFormat,string>`_. 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 + ## 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]``. + ## be encoded as `@[Lit.byte, 3.byte, 'f'.byte, 'o'.byte, 'o'.byte]`. formatStr: string TimeParseError* = object of ValueError ## \ - ## Raised when parsing input using a ``TimeFormat`` fails. + ## Raised when parsing input using a `TimeFormat` fails. TimeFormatParseError* = object of ValueError ## \ - ## Raised when parsing a ``TimeFormat`` string fails. + ## Raised when parsing a `TimeFormat` string fails. -const FormatLiterals = {' ', '-', '/', ':', '(', ')', '[', ']', ','} +const + DefaultLocale* = DateTimeLocale( + MMM: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", + "Nov", "Dec"], + MMMM: ["January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"], + ddd: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + dddd: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", + "Sunday"], + ) + + FormatLiterals = {' ', '-', '/', ':', '(', ')', '[', ']', ',', '.'} proc `$`*(f: TimeFormat): string = - ## Returns the format string that was used to construct ``f``. + ## Returns the format string that was used to construct `f`. runnableExamples: let f = initTimeFormat("yyyy-MM-dd") doAssert $f == "yyyy-MM-dd" @@ -1891,6 +1722,8 @@ proc stringToPattern(str: string): FormatPattern = of "dd": result = dd of "ddd": result = ddd of "dddd": result = dddd + of "GG": result = GG + of "GGGG": result = GGGG of "h": result = h of "hh": result = hh of "H": result = H @@ -1908,18 +1741,19 @@ proc stringToPattern(str: string): FormatPattern = 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 "V": result = V + of "VV": result = VV of "z": result = z of "zz": result = zz of "zzz": result = zzz of "zzzz": result = zzzz + of "ZZZ": result = ZZZ + of "ZZZZ": result = ZZZZ of "g": result = g else: raise newException(TimeFormatParseError, "'" & str & "' is not a valid pattern") @@ -1928,7 +1762,7 @@ 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. + ## `format` argument. runnableExamples: let f = initTimeFormat("yyyy-MM-dd") doAssert "2000-01-01" == "2000-01-01".parse(f).format(f) @@ -1963,6 +1797,10 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, result.add loc.ddd[dt.weekday] of dddd: result.add loc.dddd[dt.weekday] + of GG: + result.add (dt.getIsoWeekAndYear.isoyear.int mod 100).intToStr(2) + of GGGG: + result.add $dt.getIsoWeekAndYear.isoyear of h: result.add( if dt.hour == 0: "12" @@ -2005,20 +1843,14 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, 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) @@ -2032,7 +1864,11 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, result.add '+' & $year of UUUU: result.add $dt.year - of z, zz, zzz, zzzz: + of V: + result.add $dt.getIsoWeekAndYear.isoweek + of VV: + result.add dt.getIsoWeekAndYear.isoweek.intToStr(2) + of z, zz, zzz, zzzz, ZZZ, ZZZZ: if dt.timezone != nil and dt.timezone.name == "Etc/UTC": result.add 'Z' else: @@ -2043,16 +1879,18 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, result.add $(absOffset div 3600) of zz: result.add (absOffset div 3600).intToStr(2) - of zzz: + of zzz, ZZZ: let h = (absOffset div 3600).intToStr(2) let m = ((absOffset div 60) mod 60).intToStr(2) - result.add h & ":" & m - of zzzz: + let sep = if pattern == zzz: ":" else: "" + result.add h & sep & m + of zzzz, 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 + let sep = if pattern == zzzz: ":" else: "" + result.add h & sep & m & sep & s else: assert false of g: result.add if dt.year < 1: "BC" else: "AD" @@ -2061,7 +1899,7 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, proc parsePattern(input: string, pattern: FormatPattern, i: var int, parsed: var ParsedTime, loc: DateTimeLocale): bool = template takeInt(allowedWidth: Slice[int], allowSign = false): int = - var sv: int + var sv = 0 var pd = parseInt(input, sv, i, allowedWidth.b, allowSign) if pd < allowedWidth.a: return false @@ -2084,18 +1922,30 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, result = monthday in MonthdayRange of ddd: result = false - for v in loc.ddd: + for d, v in loc.ddd: if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + parsed.weekday = some(d.WeekDay) result = true i.inc v.len break of dddd: result = false - for v in loc.dddd: + for d, v in loc.dddd: if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + parsed.weekday = some(d.WeekDay) result = true i.inc v.len break + of GG: + # Assumes current century + var isoyear = takeInt(2..2) + var thisCen = now().year div 100 + parsed.isoyear = some(thisCen*100 + isoyear) + result = isoyear > 0 + of GGGG: + let isoyear = takeInt(1..high(int)) + parsed.isoyear = some(isoyear) + result = isoyear > 0 of h, H: parsed.hour = takeInt(1..2) result = parsed.hour in HourRange @@ -2186,7 +2036,15 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, parsed.year = some(year) of UUUU: parsed.year = some(takeInt(1..high(int), allowSign = true)) - of z, zz, zzz, zzzz: + of V: + let yearweek = takeInt(1..2) + parsed.yearweek = some(yearweek) + result = yearweek in IsoWeekRange + of VV: + let yearweek = takeInt(2..2) + parsed.yearweek = some(yearweek) + result = yearweek in IsoWeekRange + of z, zz, zzz, zzzz, ZZZ, ZZZZ: case input[i] of '+', '-': let sign = if input[i] == '-': 1 else: -1 @@ -2197,21 +2055,24 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, offset = takeInt(1..2) * 3600 of zz: offset = takeInt(2..2) * 3600 - of zzz: + of zzz, ZZZ: offset.inc takeInt(2..2) * 3600 - if input[i] != ':': - return false - i.inc + if pattern == zzz: + if input[i] != ':': + return false + i.inc offset.inc takeInt(2..2) * 60 - of zzzz: + of zzzz, ZZZZ: offset.inc takeInt(2..2) * 3600 - if input[i] != ':': - return false - i.inc + if pattern == zzzz: + if input[i] != ':': + return false + i.inc offset.inc takeInt(2..2) * 60 - if input[i] != ':': - return false - i.inc + if pattern == zzzz: + if input[i] != ':': + return false + i.inc offset.inc takeInt(2..2) else: assert false parsed.utcOffset = some(offset * sign) @@ -2229,25 +2090,13 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, i.inc 2 else: result = false - of y, yyy, yyyyy: - raiseAssert "Pattern is invalid for parsing: " & $pattern - of Lit: doAssert false, "Can't happen" + of Lit: raiseAssert "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()) - + var year = p.year.get(0) + var month = p.month.get(1).Month + var monthday = p.monthday.get(1) year = case p.era of eraUnknown: @@ -2288,26 +2137,55 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat, $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 - ) - if p.utcOffset.isNone: # No timezone parsed - assume timezone is `zone` - result = initDateTime(zone.zonedTimeFromAdjTime(result.toAdjTime), zone) + result = dateTime(year, month, monthday, hour, minute, second, nanosecond, zone) else: # Otherwise convert to `zone` - result.utcOffset = p.utcOffset.get() - result = result.toTime.inZone(zone) + result = (dateTime(year, month, monthday, hour, minute, second, nanosecond, utc()).toTime + + initDuration(seconds = p.utcOffset.get())).inZone(zone) + +proc toDateTimeByWeek(p: ParsedTime, zone: Timezone, f: TimeFormat, + input: string): DateTime = + var isoyear = p.isoyear.get(0) + var yearweek = p.yearweek.get(1) + var weekday = p.weekday.get(dMon) + + if p.amPm != apUnknown: + raiseParseException(f, input, "Parsing iso weekyear dates does not support am/pm") + + if p.year.isSome: + raiseParseException(f, input, "Use iso-year GG or GGGG as year with iso week number") + + if p.month.isSome: + raiseParseException(f, input, "Use either iso week number V or VV or month") + + if p.monthday.isSome: + raiseParseException(f, input, "Use weekday ddd or dddd as day with with iso week number") + + if p.isoyear.isNone: + raiseParseException(f, input, "Need iso-year with week number") + + let hour = p.hour + let minute = p.minute + let second = p.second + let nanosecond = p.nanosecond + + if p.utcOffset.isNone: + result = initDateTime(weekday, yearweek.IsoWeekRange, isoyear.IsoYear, hour, minute, second, nanosecond, zone) + else: + result = (initDateTime(weekday, yearweek.IsoWeekRange, isoyear.IsoYear, hour, minute, second, nanosecond, zone).toTime + + initDuration(seconds = p.utcOffset.get())).inZone(zone) proc format*(dt: DateTime, f: TimeFormat, loc: DateTimeLocale = DefaultLocale): string {.raises: [].} = - ## Format ``dt`` using the format specified by ``f``. + ## 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()) + let dt = dateTime(2000, mJan, 01, 00, 00, 00, 00, utc()) doAssert "2000-01-01" == dt.format(f) + assertDateTimeInitialized dt + result = "" var idx = 0 while idx <= f.patterns.high: case f.patterns[idx].FormatPattern @@ -2324,61 +2202,56 @@ proc format*(dt: DateTime, f: TimeFormat, proc format*(dt: DateTime, f: string, loc: DateTimeLocale = DefaultLocale): string {.raises: [TimeFormatParseError].} = - ## Shorthand for constructing a ``TimeFormat`` and using it to format ``dt``. + ## Shorthand for constructing a `TimeFormat` and using it to format `dt`. ## ## See `Parsing and formatting dates`_ for documentation of the - ## ``format`` argument. + ## `format` argument. runnableExamples: - let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) + let dt = dateTime(2000, mJan, 01, 00, 00, 00, 00, utc()) doAssert "2000-01-01" == format(dt, "yyyy-MM-dd") let dtFormat = initTimeFormat(f) result = dt.format(dtFormat, loc) proc format*(dt: DateTime, f: static[string]): string {.raises: [].} = - ## Overload that validates ``format`` at compile time. + ## Overload that validates `format` at compile time. const f2 = initTimeFormat(f) result = dt.format(f2) -proc formatValue*(result: var string; value: DateTime, specifier: string) = +proc formatValue*(result: var string; value: DateTime | Time, specifier: string) = ## adapter for strformat. Not intended to be called directly. result.add format(value, if specifier.len == 0: "yyyy-MM-dd'T'HH:mm:sszzz" else: specifier) proc format*(time: Time, f: string, zone: Timezone = local()): string {.raises: [TimeFormatParseError].} = - ## Shorthand for constructing a ``TimeFormat`` and using it to format - ## ``time``. Will use the timezone specified by ``zone``. + ## 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 - ## ``f`` argument. + ## `f` argument. runnableExamples: - var dt = initDateTime(01, mJan, 1970, 00, 00, 00, utc()) + var dt = dateTime(1970, mJan, 01, 00, 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) proc format*(time: Time, f: static[string], zone: Timezone = local()): string {.raises: [].} = - ## Overload that validates ``f`` at compile time. + ## Overload that validates `f` at compile time. const f2 = initTimeFormat(f) result = time.inZone(zone).format(f2) -template formatValue*(result: var string; value: Time, specifier: string) = - ## adapter for ``strformat``. Not intended to be called directly. - result.add format(value, specifier) - proc parse*(input: string, f: TimeFormat, zone: Timezone = local(), - loc: DateTimeLocale = DefaultLocale): DateTime - {.raises: [TimeParseError, Defect].} = - ## 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. + loc: DateTimeLocale = DefaultLocale): DateTime {.parseRaises.} = + ## 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. ## - ## Month and day names from the passed in ``loc`` are used. + ## Month and day names from the passed in `loc` are used. runnableExamples: let f = initTimeFormat("yyyy-MM-dd") - let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) + let dt = dateTime(2000, mJan, 01, 00, 00, 00, 00, utc()) doAssert dt == "2000-01-01".parse(f, utc()) var inpIdx = 0 # Input index var patIdx = 0 # Pattern index @@ -2409,136 +2282,551 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local(), raiseParseException(f, input, "Parsing ended but there was still patterns remaining") - result = toDateTime(parsed, zone, f, input) + if parsed.yearweek.isSome: + result = toDateTimeByWeek(parsed, zone, f, input) + elif parsed.isoyear.isSome: + raiseParseException(f, input, "Iso year GG or GGGG require iso week V or VV") + else: + result = toDateTime(parsed, zone, f, input) proc parse*(input, f: string, tz: Timezone = local(), - loc: DateTimeLocale = DefaultLocale): DateTime - {.raises: [TimeParseError, TimeFormatParseError, Defect].} = - ## Shorthand for constructing a ``TimeFormat`` and using it to parse - ## ``input`` as a ``DateTime``. + loc: DateTimeLocale = DefaultLocale): DateTime {.parseFormatRaises.} = + ## Shorthand for constructing a `TimeFormat` and using it to parse + ## `input` as a `DateTime`. ## ## See `Parsing and formatting dates`_ for documentation of the - ## ``f`` argument. + ## `f` argument. runnableExamples: - let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) + let dt = dateTime(2000, mJan, 01, 00, 00, 00, 00, utc()) doAssert dt == parse("2000-01-01", "yyyy-MM-dd", utc()) let dtFormat = initTimeFormat(f) result = input.parse(dtFormat, tz, loc = loc) proc parse*(input: string, f: static[string], zone: Timezone = local(), - loc: DateTimeLocale = DefaultLocale): - DateTime {.raises: [TimeParseError, Defect].} = - ## Overload that validates ``f`` at compile time. + loc: DateTimeLocale = DefaultLocale): DateTime {.parseRaises.} = + ## Overload that validates `f` at compile time. const f2 = initTimeFormat(f) result = input.parse(f2, zone, loc = loc) -proc parseTime*(input, f: string, zone: Timezone): Time - {.raises: [TimeParseError, TimeFormatParseError, Defect].} = - ## Shorthand for constructing a ``TimeFormat`` and using it to parse - ## ``input`` as a ``DateTime``, then converting it a ``Time``. +proc parseTime*(input, f: string, zone: Timezone): Time {.parseFormatRaises.} = + ## 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. + ## `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, f, zone).toTime() proc parseTime*(input: string, f: static[string], zone: Timezone): Time - {.raises: [TimeParseError, Defect].} = - ## Overload that validates ``format`` at compile time. + {.parseRaises.} = + ## Overload that validates `format` at compile time. const f2 = initTimeFormat(f) result = input.parse(f2, 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``. + ## It uses the format `yyyy-MM-dd'T'HH:mm:sszzz`. runnableExamples: - let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) + let dt = dateTime(2000, mJan, 01, 12, 00, 00, 00, utc()) doAssert $dt == "2000-01-01T12:00:00Z" - result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") + doAssert $default(DateTime) == "Uninitialized DateTime" + if not dt.isInitialized: + result = "Uninitialized DateTime" + else: + 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 - ## time zone and use the format ``yyyy-MM-dd'T'HH:mm:sszzz``. + ## time zone and use the format `yyyy-MM-dd'T'HH:mm:sszzz`. runnableExamples: - let dt = initDateTime(01, mJan, 1970, 00, 00, 00, local()) + let dt = dateTime(1970, mJan, 01, 00, 00, 00, 00, local()) let tm = dt.toTime() doAssert $tm == "1970-01-01T00:00:00" & format(dt, "zzz") $time.local -proc countLeapYears*(yearSpan: int): int - {.deprecated.} = - ## Returns the number of leap years spanned by a given number of years. +# +# TimeInterval +# + +proc initTimeInterval*(nanoseconds, microseconds, milliseconds, + seconds, minutes, hours, + days, weeks, months, years: int = 0): TimeInterval = + ## Creates a new `TimeInterval <#TimeInterval>`_. + ## + ## This proc doesn't perform any normalization! For example, + ## `initTimeInterval(hours = 24)` and `initTimeInterval(days = 1)` are + ## not equal. + ## + ## You can also use the convenience procedures called `milliseconds`, + ## `seconds`, `minutes`, `hours`, `days`, `months`, and `years`. + runnableExamples: + let day = initTimeInterval(hours = 24) + let dt = dateTime(2000, mJan, 01, 12, 00, 00, 00, utc()) + doAssert $(dt + day) == "2000-01-02T12:00:00Z" + doAssert initTimeInterval(hours = 24) != initTimeInterval(days = 1) + result.nanoseconds = nanoseconds + result.microseconds = microseconds + result.milliseconds = milliseconds + result.seconds = seconds + result.minutes = minutes + result.hours = hours + result.days = days + result.weeks = weeks + result.months = months + result.years = years + +proc `+`*(ti1, ti2: TimeInterval): TimeInterval = + ## Adds two `TimeInterval` objects together. + result.nanoseconds = ti1.nanoseconds + ti2.nanoseconds + result.microseconds = ti1.microseconds + ti2.microseconds + result.milliseconds = ti1.milliseconds + ti2.milliseconds + result.seconds = ti1.seconds + ti2.seconds + result.minutes = ti1.minutes + ti2.minutes + result.hours = ti1.hours + ti2.hours + result.days = ti1.days + ti2.days + result.weeks = ti1.weeks + ti2.weeks + result.months = ti1.months + ti2.months + result.years = ti1.years + ti2.years + +proc `-`*(ti: TimeInterval): TimeInterval = + ## Reverses a time interval + runnableExamples: + let day = -initTimeInterval(hours = 24) + doAssert day.hours == -24 + + result = TimeInterval( + nanoseconds: -ti.nanoseconds, + microseconds: -ti.microseconds, + milliseconds: -ti.milliseconds, + seconds: -ti.seconds, + minutes: -ti.minutes, + hours: -ti.hours, + days: -ti.days, + weeks: -ti.weeks, + months: -ti.months, + years: -ti.years + ) + +proc `-`*(ti1, ti2: TimeInterval): TimeInterval = + ## Subtracts TimeInterval `ti1` from `ti2`. + ## + ## Time components are subtracted one-by-one, see output: + runnableExamples: + let ti1 = initTimeInterval(hours = 24) + let ti2 = initTimeInterval(hours = 4) + doAssert (ti1 - ti2) == initTimeInterval(hours = 20) + + result = ti1 + (-ti2) + +proc `+=`*(a: var TimeInterval, b: TimeInterval) = + a = a + b + +proc `-=`*(a: var TimeInterval, b: TimeInterval) = + a = a - b + +proc isStaticInterval(interval: TimeInterval): bool = + interval.years == 0 and interval.months == 0 and + interval.days == 0 and interval.weeks == 0 + +proc evaluateStaticInterval(interval: TimeInterval): Duration = + assert interval.isStaticInterval + initDuration(nanoseconds = interval.nanoseconds, + microseconds = interval.microseconds, + milliseconds = interval.milliseconds, + seconds = interval.seconds, + minutes = interval.minutes, + hours = interval.hours) + +proc between*(startDt, endDt: DateTime): TimeInterval = + ## Gives the difference between `startDt` and `endDt` as a + ## `TimeInterval`. The following guarantees about the result is given: + ## + ## - All fields will have the same sign. + ## - If `startDt.timezone == endDt.timezone`, it is guaranteed that + ## `startDt + between(startDt, endDt) == endDt`. + ## - If `startDt.timezone != endDt.timezone`, then the result will be + ## equivalent to `between(startDt.utc, endDt.utc)`. + runnableExamples: + var a = dateTime(2015, mMar, 25, 12, 0, 0, 00, utc()) + var b = dateTime(2017, mApr, 1, 15, 0, 15, 00, utc()) + var ti = initTimeInterval(years = 2, weeks = 1, hours = 3, seconds = 15) + doAssert between(a, b) == ti + doAssert between(a, b) == -between(b, a) + + if startDt.timezone != endDt.timezone: + return between(startDt.utc, endDt.utc) + elif endDt < startDt: + return -between(endDt, startDt) + + type Date = tuple[year, month, monthday: int] + var startDate: Date = (startDt.year, startDt.month.ord, startDt.monthday) + var endDate: Date = (endDt.year, endDt.month.ord, endDt.monthday) + + # Subtract one day from endDate if time of day is earlier than startDay + # The subtracted day will be counted by fixed units (hour and lower) + # at the end of this proc + if (endDt.hour, endDt.minute, endDt.second, endDt.nanosecond) < + (startDt.hour, startDt.minute, startDt.second, startDt.nanosecond): + if endDate.month == 1 and endDate.monthday == 1: + endDate.year.dec + endDate.monthday = 31 + endDate.month = 12 + elif endDate.monthday == 1: + endDate.month.dec + endDate.monthday = getDaysInMonth(endDate.month.Month, endDate.year) + else: + endDate.monthday.dec + + # Years + result.years = endDate.year - startDate.year - 1 + if (startDate.month, startDate.monthday) <= (endDate.month, endDate.monthday): + result.years.inc + startDate.year.inc result.years + + # Months + if startDate.year < endDate.year: + result.months.inc 12 - startDate.month # Move to dec + if endDate.month != 1 or (startDate.monthday <= endDate.monthday): + result.months.inc + startDate.year = endDate.year + startDate.month = 1 + else: + startDate.month = 12 + if startDate.year == endDate.year: + if (startDate.monthday <= endDate.monthday): + result.months.inc endDate.month - startDate.month + startDate.month = endDate.month + elif endDate.month != 1: + let month = endDate.month - 1 + let daysInMonth = getDaysInMonth(month.Month, startDate.year) + if daysInMonth < startDate.monthday: + if startDate.monthday - daysInMonth < endDate.monthday: + result.months.inc endDate.month - startDate.month - 1 + startDate.month = endDate.month + startDate.monthday = startDate.monthday - daysInMonth + else: + result.months.inc endDate.month - startDate.month - 2 + startDate.month = endDate.month - 2 + else: + result.months.inc endDate.month - startDate.month - 1 + startDate.month = endDate.month - 1 + + # Days + # This means that start = dec and end = jan + if startDate.year < endDate.year: + result.days.inc 31 - startDate.monthday + endDate.monthday + startDate = endDate + else: + while startDate.month < endDate.month: + let daysInMonth = getDaysInMonth(startDate.month.Month, startDate.year) + result.days.inc daysInMonth - startDate.monthday + 1 + startDate.month.inc + startDate.monthday = 1 + result.days.inc endDate.monthday - startDate.monthday + result.weeks = result.days div 7 + result.days = result.days mod 7 + startDate = endDate + + # Handle hours, minutes, seconds, milliseconds, microseconds and nanoseconds + let newStartDt = dateTime(startDate.year, startDate.month.Month, + startDate.monthday, startDt.hour, startDt.minute, startDt.second, + startDt.nanosecond, startDt.timezone) + let dur = endDt - newStartDt + let parts = toParts(dur) + # There can still be a full day in `parts` since `Duration` and `TimeInterval` + # models days differently. + result.hours = parts[Hours].int + parts[Days].int * 24 + result.minutes = parts[Minutes].int + result.seconds = parts[Seconds].int + result.milliseconds = parts[Milliseconds].int + result.microseconds = parts[Microseconds].int + result.nanoseconds = parts[Nanoseconds].int + +proc toParts*(ti: TimeInterval): TimeIntervalParts = + ## Converts a `TimeInterval` into an array consisting of its time units, + ## starting with nanoseconds and ending with years. + ## + ## This procedure is useful for converting `TimeInterval` values to strings. + ## E.g. then you need to implement custom interval printing + runnableExamples: + var tp = toParts(initTimeInterval(years = 1, nanoseconds = 123)) + doAssert tp[Years] == 1 + doAssert tp[Nanoseconds] == 123 + + var index = 0 + for name, value in fieldPairs(ti): + result[index.TimeUnit()] = value + index += 1 + +proc `$`*(ti: TimeInterval): string = + ## Get string representation of `TimeInterval`. + runnableExamples: + doAssert $initTimeInterval(years = 1, nanoseconds = 123) == + "1 year and 123 nanoseconds" + doAssert $initTimeInterval() == "0 nanoseconds" + + var parts: seq[string] = @[] + var tiParts = toParts(ti) + for unit in countdown(Years, Nanoseconds): + if tiParts[unit] != 0: + parts.add(stringifyUnit(tiParts[unit], unit)) + + result = humanizeParts(parts) + +proc nanoseconds*(nanos: int): TimeInterval {.inline.} = + ## TimeInterval of `nanos` nanoseconds. + initTimeInterval(nanoseconds = nanos) + +proc microseconds*(micros: int): TimeInterval {.inline.} = + ## TimeInterval of `micros` microseconds. + initTimeInterval(microseconds = micros) + +proc milliseconds*(ms: int): TimeInterval {.inline.} = + ## TimeInterval of `ms` milliseconds. + initTimeInterval(milliseconds = ms) + +proc seconds*(s: int): TimeInterval {.inline.} = + ## TimeInterval of `s` seconds. ## - ## **Note:** For leap years, start date is assumed to be 1 AD. - ## counts the number of leap years up to January 1st of a given year. - ## Keep in mind that if specified year is a leap year, the leap day - ## has not happened before January 1st of that year. + ## `echo getTime() + 5.seconds` + initTimeInterval(seconds = s) + +proc minutes*(m: int): TimeInterval {.inline.} = + ## TimeInterval of `m` minutes. ## - ## **Deprecated since v0.20.0**. - (yearSpan - 1) div 4 - (yearSpan - 1) div 100 + (yearSpan - 1) div 400 + ## `echo getTime() + 5.minutes` + initTimeInterval(minutes = m) -proc countDays*(yearSpan: int): int - {.deprecated.} = - ## Returns the number of days spanned by a given number of years. +proc hours*(h: int): TimeInterval {.inline.} = + ## TimeInterval of `h` hours. ## - ## **Deprecated since v0.20.0**. - (yearSpan - 1) * 365 + countLeapYears(yearSpan) + ## `echo getTime() + 2.hours` + initTimeInterval(hours = h) -proc countYears*(daySpan: int): int - {.deprecated.} = - ## Returns the number of years spanned by a given number of days. +proc days*(d: int): TimeInterval {.inline.} = + ## TimeInterval of `d` days. ## - ## **Deprecated since v0.20.0**. - ((daySpan - countLeapYears(daySpan div 365)) div 365) + ## `echo getTime() + 2.days` + initTimeInterval(days = d) + +proc weeks*(w: int): TimeInterval {.inline.} = + ## TimeInterval of `w` weeks. + ## + ## `echo getTime() + 2.weeks` + initTimeInterval(weeks = w) + +proc months*(m: int): TimeInterval {.inline.} = + ## TimeInterval of `m` months. + ## + ## `echo getTime() + 2.months` + initTimeInterval(months = m) + +proc years*(y: int): TimeInterval {.inline.} = + ## TimeInterval of `y` years. + ## + ## `echo getTime() + 2.years` + initTimeInterval(years = y) + +proc evaluateInterval(dt: DateTime, interval: TimeInterval): + tuple[adjDur, absDur: Duration] = + ## Evaluates how many nanoseconds the interval is worth + ## in the context of `dt`. + ## The result in split into an adjusted diff and an absolute diff. + var months = interval.years * 12 + interval.months + var curYear = dt.year + var curMonth = dt.month + result = default(tuple[adjDur, absDur: Duration]) + # Subtracting + if months < 0: + for mth in countdown(-1 * months, 1): + if curMonth == mJan: + curMonth = mDec + curYear.dec + else: + curMonth.dec() + let days = getDaysInMonth(curMonth, curYear) + result.adjDur = result.adjDur - initDuration(days = days) + # Adding + else: + for mth in 1 .. months: + let days = getDaysInMonth(curMonth, curYear) + result.adjDur = result.adjDur + initDuration(days = days) + if curMonth == mDec: + curMonth = mJan + curYear.inc + else: + curMonth.inc() + + result.adjDur = result.adjDur + initDuration( + days = interval.days, + weeks = interval.weeks) + result.absDur = initDuration( + nanoseconds = interval.nanoseconds, + microseconds = interval.microseconds, + milliseconds = interval.milliseconds, + seconds = interval.seconds, + minutes = interval.minutes, + hours = interval.hours) -proc countYearsAndDays*(daySpan: int): tuple[years: int, days: int] - {.deprecated.} = - ## Returns the number of years spanned by a given number of days and the - ## remainder as days. +proc `+`*(dt: DateTime, interval: TimeInterval): DateTime = + ## Adds `interval` to `dt`. Components from `interval` are added + ## in the order of their size, i.e. first the `years` component, then the + ## `months` component and so on. The returned `DateTime` will have the + ## same timezone as the input. ## - ## **Deprecated since v0.20.0**. - let days = daySpan - countLeapYears(daySpan div 365) - result.years = days div 365 - result.days = days mod 365 - -proc toTimeInterval*(time: Time): TimeInterval - {.deprecated: "Use `between` instead".} = - ## Converts a Time to a TimeInterval. To be used when diffing times. + ## Note that when adding months, monthday overflow is allowed. This means that + ## if the resulting month doesn't have enough days it, the month will be + ## incremented and the monthday will be set to the number of days overflowed. + ## So adding one month to `31 October` will result in `31 November`, which + ## will overflow and result in `1 December`. + runnableExamples: + let dt = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) + 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:00Z" + let (adjDur, absDur) = evaluateInterval(dt, interval) + + if adjDur != DurationZero: + var zt = dt.timezone.zonedTimeFromAdjTime(dt.toAdjTime + adjDur) + if absDur != DurationZero: + zt = dt.timezone.zonedTimeFromTime(zt.time + absDur) + result = initDateTime(zt, dt.timezone) + else: + result = initDateTime(zt, dt.timezone) + else: + var zt = dt.timezone.zonedTimeFromTime(dt.toTime + absDur) + result = initDateTime(zt, dt.timezone) + +proc `-`*(dt: DateTime, interval: TimeInterval): DateTime = + ## Subtract `interval` from `dt`. Components from `interval` are + ## subtracted in the order of their size, i.e. first the `years` component, + ## then the `months` component and so on. The returned `DateTime` will + ## have the same timezone as the input. + runnableExamples: + let dt = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) + doAssert $(dt - 5.days) == "2017-03-25T00:00:00Z" + + dt + (-interval) + +proc `+`*(time: Time, interval: TimeInterval): Time = + ## Adds `interval` to `time`. + ## If `interval` contains any years, months, weeks or days the operation + ## is performed in the local timezone. + runnableExamples: + let tm = fromUnix(0) + doAssert tm + 5.seconds == fromUnix(5) + + if interval.isStaticInterval: + time + evaluateStaticInterval(interval) + else: + toTime(time.local + interval) + +proc `-`*(time: Time, interval: TimeInterval): Time = + ## Subtracts `interval` from Time `time`. + ## If `interval` contains any years, months, weeks or days the operation + ## is performed in the local timezone. + runnableExamples: + let tm = fromUnix(5) + doAssert tm - 5.seconds == fromUnix(0) + + if interval.isStaticInterval: + time - evaluateStaticInterval(interval) + else: + toTime(time.local - interval) + +proc `+=`*(a: var DateTime, b: TimeInterval) = + a = a + b + +proc `-=`*(a: var DateTime, b: TimeInterval) = + a = a - b + +proc `+=`*(t: var Time, b: TimeInterval) = + t = t + b + +proc `-=`*(t: var Time, b: TimeInterval) = + t = t - b + +# +# Iso week +# + +proc initDateTime*(weekday: WeekDay, isoweek: IsoWeekRange, isoyear: IsoYear, + hour: HourRange, minute: MinuteRange, second: SecondRange, + nanosecond: NanosecondRange, + zone: Timezone = local()): DateTime {.raises: [], tags: [], since: (1, 5).} = + ## Create a new `DateTime <#DateTime>`_ from a weekday and an ISO 8601 week number and year + ## in the specified timezone. ## - ## **Deprecated since version 0.20.0:** Use the `between proc - ## <#between,DateTime,DateTime>`_ instead. + ## .. warning:: The ISO week-based year can correspond to the following or previous year from 29 December to January 3. runnableExamples: - let a = fromUnix(10) - let b = fromUnix(1_500_000_000) - let ti = b.toTimeInterval() - a.toTimeInterval() - doAssert a + ti == b - var dt = time.local - initTimeInterval(dt.nanosecond, 0, 0, dt.second, dt.minute, dt.hour, - dt.monthday, 0, dt.month.ord - 1, dt.year) + assert initDateTime(21, mApr, 2018, 00, 00, 00) == initDateTime(dSat, 16, 2018.IsoYear, 00, 00, 00) + assert initDateTime(30, mDec, 2019, 00, 00, 00) == initDateTime(dMon, 01, 2020.IsoYear, 00, 00, 00) + assert initDateTime(13, mSep, 2020, 00, 00, 00) == initDateTime(dSun, 37, 2020.IsoYear, 00, 00, 00) + assert initDateTime(2, mJan, 2021, 00, 00, 00) == initDateTime(dSat, 53, 2020.IsoYear, 00, 00, 00) + + # source https://webspace.science.uu.nl/~gent0113/calendar/isocalendar.htm + let d = isoweek * 7 + weekday.int - initDateTime(4, mJan, isoyear.int, 00, 00, 00, zone).weekday.int - 4 + initDateTime(1, mJan, isoyear.int, hour, minute, second, nanosecond, zone) + initTimeInterval(days=d) + +proc initDateTime*(weekday: WeekDay, isoweek: IsoWeekRange, isoyear: IsoYear, + hour: HourRange, minute: MinuteRange, second: SecondRange, + zone: Timezone = local()): DateTime {.raises: [], tags: [], since: (1, 5).} = + initDateTime(weekday, isoweek, isoyear, hour, minute, second, 0, zone) + +# +# Other +# + +proc epochTime*(): float {.tags: [TimeEffect].} = + ## 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). + ## + ## `getTime` should generally be preferred over this proc. + ## + ## .. warning:: Unsuitable for benchmarking (but still better than `now`), + ## use `monotimes.getMonoTime` or `cpuTime` instead, depending on the use case. + when defined(js): + result = newDate().getTime() / 1000 + elif defined(macosx): + var a {.noinit.}: Timeval + gettimeofday(a) + result = toBiggestFloat(a.tv_sec.int64) + toBiggestFloat( + a.tv_usec)*0.00_0001 + elif defined(posix): + var ts {.noinit.}: Timespec + discard clock_gettime(CLOCK_REALTIME, ts) + result = toBiggestFloat(ts.tv_sec.int64) + + toBiggestFloat(ts.tv_nsec.int64) / 1_000_000_000 + elif defined(windows): + var f {.noinit.}: winlean.FILETIME + 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".} when not defined(js): type Clock {.importc: "clock_t".} = distinct int proc getClock(): Clock - {.importc: "clock", header: "<time.h>", tags: [TimeEffect], used.} + {.importc: "clock", header: "<time.h>", tags: [TimeEffect], used, sideEffect.} var clocksPerSec {.importc: "CLOCKS_PER_SEC", nodecl, used.}: int proc cpuTime*(): float {.tags: [TimeEffect].} = - ## gets time spent that the CPU spent to run the current process in - ## seconds. This may be more useful for benchmarking than ``epochTime``. + ## 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: + ## the results of two `cpuTime` calls: runnableExamples: var t0 = cpuTime() # some useless work here (calculate fibonacci) @@ -2547,6 +2835,8 @@ when not defined(js): fib.add(fib[^1] + fib[^2]) echo "CPU time [s] ", cpuTime() - t0 echo "Fib is [s] ", fib + ## When the flag `--benchmarkVM` is passed to the compiler, this proc is + ## also available at compile time when defined(posix) and not defined(osx) and declared(CLOCK_THREAD_CPUTIME_ID): # 'clocksPerSec' is a compile-time constant, possibly a # rather awful one, so use clock_gettime instead @@ -2557,243 +2847,43 @@ when not defined(js): else: result = toFloat(int(getClock())) / toFloat(clocksPerSec) - proc epochTime*(): float {.tags: [TimeEffect].} = - ## 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). - ## - ## ``getTime`` should generally be preferred over this proc. - when defined(macosx): - var a: Timeval - gettimeofday(a) - result = toBiggestFloat(a.tv_sec.int64) + toBiggestFloat( - a.tv_usec)*0.00_0001 - elif defined(posix): - var ts: Timespec - discard clock_gettime(CLOCK_REALTIME, ts) - result = toBiggestFloat(ts.tv_sec.int64) + - toBiggestFloat(ts.tv_nsec.int64) / 1_000_000_000 - elif defined(windows): - var f: winlean.FILETIME - 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".} - -when defined(js): - proc epochTime*(): float {.tags: [TimeEffect].} = - newDate().getTime() / 1000 -# Deprecated procs +# +# Deprecations +# -proc weeks*(dur: Duration): int64 - {.inline, deprecated: "Use `inWeeks` instead".} = - ## Number of whole weeks represented by the duration. - ## - ## **Deprecated since version v0.20.0**: Use the `inWeeks proc - ## <#inWeeks,Duration>`_ instead. - runnableExamples: - let dur = initDuration(weeks = 1, days = 2, hours = 3, minutes = 4) - doAssert dur.weeks == 1 - dur.inWeeks +proc `nanosecond=`*(dt: var DateTime, value: NanosecondRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.nanosecond = value -proc days*(dur: Duration): int64 - {.inline, deprecated: "Use `inDays` instead".} = - ## Number of whole days represented by the duration. - ## - ## **Deprecated since version v0.20.0**: Use the `inDays proc - ## <#inDays,Duration>`_ instead. - runnableExamples: - let dur = initDuration(weeks = 1, days = 2, hours = 3, minutes = 4) - doAssert dur.days == 9 - dur.inDays +proc `second=`*(dt: var DateTime, value: SecondRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.second = value -proc hours*(dur: Duration): int64 - {.inline, deprecated: "Use `inHours` instead".} = - ## Number of whole hours represented by the duration. - ## - ## **Deprecated since version v0.20.0**: Use the `inHours proc - ## <#inHours,Duration>`_ instead. - runnableExamples: - let dur = initDuration(days = 1, hours = 2, minutes = 3) - doAssert dur.hours == 26 - dur.inHours +proc `minute=`*(dt: var DateTime, value: MinuteRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.minute = value -proc minutes*(dur: Duration): int64 - {.inline, deprecated: "Use `inMinutes` instead".} = - ## Number of whole minutes represented by the duration. - ## - ## **Deprecated since version v0.20.0**: Use the `inMinutes proc - ## <#inMinutes,Duration>`_ instead. - runnableExamples: - let dur = initDuration(days = 1, hours = 2, minutes = 3) - doAssert dur.minutes == 1563 - dur.inMinutes +proc `hour=`*(dt: var DateTime, value: HourRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.hour = value -proc seconds*(dur: Duration): int64 - {.inline, deprecated: "Use `inSeconds` instead".} = - ## Number of whole seconds represented by the duration. - ## - ## **Deprecated since version v0.20.0**: Use the `inSeconds proc - ## <#inSeconds,Duration>`_ instead. - runnableExamples: - let dur = initDuration(minutes = 10, seconds = 30) - doAssert dur.seconds == 630 - dur.inSeconds +proc `monthdayZero=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.monthdayZero = value -proc milliseconds*(dur: Duration): int {.inline, deprecated.} = - ## Number of whole milliseconds represented by the **fractional** - ## part of the duration. - ## - ## **Deprecated since version v0.20.0**. - runnableExamples: - let dur = initDuration(minutes = 5, seconds = 6, milliseconds = 7, - microseconds = 8, nanoseconds = 9) - doAssert dur.milliseconds == 7 - result = convert(Nanoseconds, Milliseconds, dur.nanosecond) +proc `monthZero=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.monthZero = value -proc microseconds*(dur: Duration): int {.inline, deprecated.} = - ## Number of whole microseconds represented by the **fractional** - ## part of the duration. - ## - ## **Deprecated since version v0.20.0**. - runnableExamples: - let dur = initDuration(minutes = 5, seconds = 6, milliseconds = 7, - microseconds = 8, nanoseconds = 9) - doAssert dur.microseconds == 7008 - result = convert(Nanoseconds, Microseconds, dur.nanosecond) +proc `year=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.year = value -proc nanoseconds*(dur: Duration): NanosecondRange {.inline, deprecated.} = - ## Number of whole microseconds represented by the **fractional** - ## part of the duration. - ## - ## **Deprecated since version v0.20.0**. - runnableExamples: - let dur = initDuration(minutes = 5, seconds = 6, milliseconds = 7, - microseconds = 8, nanoseconds = 9) - doAssert dur.nanoseconds == 7008009 - dur.nanosecond +proc `weekday=`*(dt: var DateTime, value: WeekDay) {.deprecated: "Deprecated since v1.3.1".} = + dt.weekday = value -proc fractional*(dur: Duration): Duration {.inline, deprecated.} = - ## The fractional part of `dur`, as a duration. - ## - ## **Deprecated since version v0.20.0**. - runnableExamples: - let dur = initDuration(minutes = 5, seconds = 6, milliseconds = 7, - microseconds = 8, nanoseconds = 9) - doAssert dur.fractional == initDuration(milliseconds = 7, microseconds = 8, - nanoseconds = 9) - initDuration(nanoseconds = dur.nanosecond) +proc `yearday=`*(dt: var DateTime, value: YeardayRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.yearday = value -when not defined(js): - proc unixTimeToWinTime*(time: CTime): int64 - {.deprecated: "Use toWinTime instead".} = - ## Converts a UNIX `Time` (``time_t``) to a Windows file time - ## - ## **Deprecated:** use ``toWinTime`` instead. - result = int64(time) * rateDiff + epochDiff +proc `isDst=`*(dt: var DateTime, value: bool) {.deprecated: "Deprecated since v1.3.1".} = + dt.isDst = value - proc winTimeToUnixTime*(time: int64): CTime - {.deprecated: "Use fromWinTime instead".} = - ## Converts a Windows time to a UNIX `Time` (``time_t``) - ## - ## **Deprecated:** use ``fromWinTime`` instead. - result = CTime((time - epochDiff) div rateDiff) - -proc initInterval*(seconds, minutes, hours, days, months, years: int = 0): - TimeInterval {.deprecated.} = - ## **Deprecated since v0.18.0:** use ``initTimeInterval`` instead. - initTimeInterval(0, 0, 0, seconds, minutes, hours, days, 0, months, years) - -proc fromSeconds*(since1970: float): Time - {.tags: [], raises: [], benign, deprecated: "Use fromUnixFloat or fromUnix".} = - ## Takes a float which contains the number of seconds since the unix epoch and - ## returns a time object. - ## - ## **Deprecated since v0.18.0:** use ``fromUnix`` instead - fromUnixFloat(since1970) +proc `timezone=`*(dt: var DateTime, value: Timezone) {.deprecated: "Deprecated since v1.3.1".} = + dt.timezone = value -proc fromSeconds*(since1970: int64): Time - {.tags: [], raises: [], benign, deprecated.} = - ## Takes an int which contains the number of seconds since the unix epoch and - ## returns a time object. - ## - ## **Deprecated since v0.18.0:** use ``fromUnix`` instead - fromUnix(since1970) - -proc toSeconds*(time: Time): float - {.tags: [], raises: [], benign, deprecated: "Use toUnixFloat or toUnix".} = - ## Returns the time in seconds since the unix epoch, with subsecond resolution. - toUnixFloat(time) - -proc getLocalTime*(time: Time): DateTime - {.tags: [], raises: [], benign, deprecated.} = - ## Converts the calendar time `time` to broken-time representation, - ## expressed relative to the user's specified time zone. - ## - ## **Deprecated since v0.18.0:** use ``local`` instead - time.local - -proc getGMTime*(time: Time): DateTime - {.tags: [], raises: [], benign, deprecated.} = - ## Converts the calendar time `time` to broken-down time representation, - ## expressed in Coordinated Universal Time (UTC). - ## - ## **Deprecated since v0.18.0:** use ``utc`` instead - time.utc - -proc getTimezone*(): int - {.tags: [TimeEffect], raises: [], benign, deprecated.} = - ## Returns the offset of the local (non-DST) timezone in seconds west of UTC. - ## - ## **Deprecated since v0.18.0:** use ``now().utcOffset`` to get the current - ## utc offset (including DST). - when defined(js): - return newDate().getTimezoneOffset() * 60 - elif defined(freebsd) or defined(netbsd) or defined(openbsd): - # This is wrong since it will include DST offsets, but the behavior has - # always been wrong for bsd and the proc is deprecated so lets ignore it. - return now().utcOffset - else: - return timezone - -proc getDayOfWeek*(day, month, year: int): WeekDay - {.tags: [], raises: [], benign, deprecated.} = - ## **Deprecated since v0.18.0:** use - ## ``getDayOfWeek(monthday: MonthdayRange; month: Month; year: int)`` instead. - getDayOfWeek(day, month.Month, year) - -proc getDayOfWeekJulian*(day, month, year: int): WeekDay {.deprecated.} = - ## Returns the day of the week enum from day, month and year, - ## according to the Julian calendar. - ## **Deprecated since v0.18.0** - # Day & month start from one. - let - a = (14 - month) div 12 - y = year - a - m = month + (12*a) - 2 - d = (5 + day + y + (y div 4) + (31*m) div 12) mod 7 - result = d.WeekDay - -proc adjTime*(zt: ZonedTime): Time - {.deprecated: "Use zt.time instead".} = - ## **Deprecated since v0.19.0:** use the ``time`` field instead. - zt.time - initDuration(seconds = zt.utcOffset) - -proc `adjTime=`*(zt: var ZonedTime, adjTime: Time) - {.deprecated: "Use zt.time instead".} = - ## **Deprecated since v0.19.0:** use the ``time`` field instead. - zt.time = adjTime + initDuration(seconds = zt.utcOffset) - -proc zoneInfoFromUtc*(zone: Timezone, time: Time): ZonedTime - {.deprecated: "Use zonedTimeFromTime instead".} = - ## **Deprecated since v0.19.0:** use ``zonedTimeFromTime`` instead. - zone.zonedTimeFromTime(time) - -proc zoneInfoFromTz*(zone: Timezone, adjTime: Time): ZonedTime - {.deprecated: "Use zonedTimeFromAdjTime instead".} = - ## **Deprecated since v0.19.0:** use the ``zonedTimeFromAdjTime`` instead. - zone.zonedTimeFromAdjTime(adjTime) +proc `utcOffset=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.utcOffset = value |