diff options
author | Oscar NihlgÄrd <oscarnihlgard@gmail.com> | 2020-04-21 17:07:37 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-21 17:07:37 +0200 |
commit | 218cbf0e090cb8de1b859fcd4f9c54eec69e5437 (patch) | |
tree | 583f7c7a7cd3008d05da17b237b1e66452994c6c | |
parent | 7828199827cac4056a008d679cb5c45ff3d34f6e (diff) | |
download | Nim-218cbf0e090cb8de1b859fcd4f9c54eec69e5437.tar.gz |
Times refactorings (#13949)
-rw-r--r-- | lib/pure/times.nim | 1531 |
1 files changed, 778 insertions, 753 deletions
diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 2313afbff..23854fea7 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -283,19 +283,6 @@ 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[0..31] ## 0 represents an invalid day of the month @@ -414,7 +401,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 @@ -436,15 +422,11 @@ 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"], -) +# +# Helper procs +# + +{.pragma: operator, rtl, noSideEffect, benign.} proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T {.inline.} = @@ -470,168 +452,6 @@ 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. runnableExamples: @@ -639,19 +459,6 @@ proc isLeapYear*(year: int): bool = doAssert not isLeapYear(1900) year mod 4 == 0 and (year mod 100 != 0 or year mod 400 == 0) -proc isLeapDay*(t: DateTime): bool {.since: (1,1).} = - ## returns whether `t` is a leap day, ie, Feb 29 in a leap year. This matters - ## as it affects time offset calculations. - runnableExamples: - let t = initDateTime(29, mFeb, 2020, 00, 00, 00, utc()) - doAssert t.isLeapDay - doAssert t+1.years-1.years != t - let t2 = initDateTime(28, mFeb, 2020, 00, 00, 00, utc()) - doAssert not t2.isLeapDay - doAssert t2+1.years-1.years == t2 - doAssertRaises(Exception): discard initDateTime(29, mFeb, 2021, 00, 00, 00, utc()) - t.year.isLeapYear and t.month == mFeb and t.monthday == 29 - proc getDaysInMonth*(month: Month, year: int): int = ## Get the number of days in ``month`` of ``year``. # http://www.dispersiondesign.com/articles/time/number_of_days_in_a_month @@ -663,13 +470,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 > 0 and monthday <= getDaysInMonth(month, year), @@ -747,7 +547,37 @@ proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay # 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 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)] template subImpl[T: Duration|Time](a: Duration|Time, b: Duration|Time): T = normalize[T](a.seconds - b.seconds, a.nanosecond - b.nanosecond) @@ -765,7 +595,12 @@ 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 @@ -773,6 +608,103 @@ const DurationZero* = initDuration() ## \ ## 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.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 toParts*(dur: Duration): DurationParts = ## Converts a duration into an array consisting of fixed time units. ## @@ -808,31 +740,6 @@ 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``. runnableExamples: @@ -911,6 +818,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. @@ -924,11 +840,106 @@ 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 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 `-`*(a, b: Time): Duration {.operator, extern: "ntDiffTime".} = ## Computes the duration between two points in time. runnableExamples: @@ -962,25 +973,34 @@ proc `==`*(a, b: Time): bool {.operator, extern: "ntEqTime".} = ## 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) -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 = +proc isLeapDay*(t: DateTime): bool {.since: (1,1).} = + ## returns whether `t` is a leap day, ie, Feb 29 in a leap year. This matters + ## as it affects time offset calculations. runnableExamples: - doAssert initDuration(milliseconds = -1500).abs == - initDuration(milliseconds = 1500) - initDuration(seconds = abs(a.seconds), nanoseconds = -a.nanosecond) + let t = initDateTime(29, mFeb, 2020, 00, 00, 00, utc()) + doAssert t.isLeapDay + doAssert t+1.years-1.years != t + let t2 = initDateTime(28, mFeb, 2020, 00, 00, 00, utc()) + doAssert not t2.isLeapDay + doAssert t2+1.years-1.years == t2 + doAssertRaises(Exception): discard initDateTime(29, mFeb, 2021, 00, 00, 00, utc()) + t.year.isLeapYear and t.month == mFeb and t.monthday == 29 proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = ## Converts a ``DateTime`` to a ``Time`` representing the same point in time. @@ -1109,14 +1129,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(), @@ -1163,13 +1183,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) @@ -1236,243 +1256,12 @@ proc local*(t: Time): DateTime = ## 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. ## ## Shorthand for ``getTime().local``. 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, @@ -1503,47 +1292,6 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, 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" - - dt + (-interval) - proc `+`*(dt: DateTime, dur: Duration): DateTime = runnableExamples: let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) @@ -1587,177 +1335,42 @@ proc `==`*(a, b: DateTime): bool = elif b.isDefault: false else: 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) - -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) + result = $dt.year & '-' & intToStr(ord(dt.month), 2) & + '-' & 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) - - if interval.isStaticInterval: - time - evaluateStaticInterval(interval) - else: - toTime(time.local - interval) + echo getClockStr(now() - 1.hours) + result = intToStr(dt.hour, 2) & ':' & intToStr(dt.minute, 2) & + ':' & intToStr(dt.second, 2) -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 +# +# TimeFormat +# -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 +when defined(nimHasStyleChecks): + {.push styleChecks: off.} -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 +type + DateTimeLocale* = object + MMM*: array[mJan..mDec, string] + MMMM*: array[mJan..mDec, string] + ddd*: array[dMon..dSun, string] + dddd*: array[dMon..dSun, string] -# -# Parse & format implementation -# +when defined(nimHasStyleChecks): + {.pop.} type AmPm = enum @@ -1821,7 +1434,18 @@ type TimeFormatParseError* = object of ValueError ## \ ## 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``. @@ -2460,10 +2084,6 @@ proc parseTime*(input: string, f: static[string], zone: Timezone): 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``. @@ -2481,6 +2101,478 @@ proc `$`*(time: Time): string {.tags: [], raises: [], benign.} = doAssert $tm == "1970-01-01T00:00:00" & format(dt, "zzz") $time.local +# +# 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 = 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 `+=`*(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 = 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 + + # 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 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 `+`*(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" + + 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 + +# +# 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. + 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 + elif defined(js): + result = newDate().getTime() / 1000 + 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, 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``. + ## 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: + runnableExamples: + var t0 = cpuTime() + # some useless work here (calculate fibonacci) + var fib = @[0, 1, 1] + for i in 1..10: + fib.add(fib[^1] + fib[^2]) + echo "CPU time [s] ", cpuTime() - t0 + echo "Fib is [s] ", fib + 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 + var ts: Timespec + discard clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) + result = toFloat(ts.tv_sec.int) + + toFloat(ts.tv_nsec.int) / 1_000_000_000 + else: + result = toFloat(int(getClock())) / toFloat(clocksPerSec) + +# +# Deprecations +# + proc countLeapYears*(yearSpan: int): int {.deprecated.} = ## Returns the number of leap years spanned by a given number of years. @@ -2532,73 +2624,6 @@ proc toTimeInterval*(time: Time): TimeInterval initTimeInterval(dt.nanosecond, 0, 0, dt.second, dt.minute, dt.hour, dt.monthday, 0, dt.month.ord - 1, dt.year) -when not defined(js): - type - Clock {.importc: "clock_t".} = distinct int - - proc getClock(): Clock - {.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``. - ## 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: - runnableExamples: - var t0 = cpuTime() - # some useless work here (calculate fibonacci) - var fib = @[0, 1, 1] - for i in 1..10: - fib.add(fib[^1] + fib[^2]) - echo "CPU time [s] ", cpuTime() - t0 - echo "Fib is [s] ", fib - 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 - var ts: Timespec - discard clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) - result = toFloat(ts.tv_sec.int) + - toFloat(ts.tv_nsec.int) / 1_000_000_000 - 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 - proc weeks*(dur: Duration): int64 {.inline, deprecated: "Use `inWeeks` instead".} = ## Number of whole weeks represented by the duration. |