diff options
Diffstat (limited to 'lib/pure/times.nim')
-rw-r--r-- | lib/pure/times.nim | 428 |
1 files changed, 318 insertions, 110 deletions
diff --git a/lib/pure/times.nim b/lib/pure/times.nim index e763b17bb..e59153455 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -20,7 +20,7 @@ Examples ======== - .. code-block:: nim + ```nim import std/[times, os] # Simple benchmarking let time = cpuTime() @@ -37,6 +37,7 @@ # Arithmetic using TimeInterval echo "One year from now : ", now() + 1.years echo "One month from now : ", now() + 1.months + ``` Parsing and Formatting Dates ============================ @@ -44,10 +45,10 @@ The `DateTime` type can be parsed and formatted using the different `parse` and `format` procedures. - .. code-block:: nim - + ```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. @@ -62,6 +63,8 @@ | `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` @@ -104,6 +107,10 @@ | `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` @@ -124,9 +131,10 @@ =========== ================================================================================= ============================================== 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 `''`. + `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 @@ -196,13 +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. @@ -226,7 +238,7 @@ when defined(js): {.pop.} elif defined(posix): - import posix + import std/posix type CTime = posix.Time @@ -235,7 +247,7 @@ elif defined(posix): {.importc: "gettimeofday", header: "<sys/time.h>", sideEffect.} elif defined(windows): - import winlean, std/time_t + import std/winlean, std/time_t type CTime = time_t.Time @@ -288,6 +300,13 @@ type 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 @@ -313,8 +332,11 @@ type 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 @@ -356,7 +378,7 @@ 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. + ## system's local time and UTC. zonedTimeFromTimeImpl: proc (x: Time): ZonedTime {.tags: [], raises: [], benign.} zonedTimeFromAdjTimeImpl: proc (x: Time): ZonedTime @@ -394,6 +416,16 @@ const unitWeights: array[FixedTimeUnit, int64] = [ 7 * secondsInDay * 1e9.int64, ] +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 # @@ -485,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 @@ -505,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" @@ -513,7 +545,7 @@ 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. @@ -526,12 +558,60 @@ proc getDaysInYear*(year: int): int = 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.add($value) - result.add(" ") + result.addInt value + result.add ' ' if abs(value) != 1: result.add(strUnit.toLowerAscii()) else: @@ -574,11 +654,10 @@ template eqImpl(a: Duration|Time, b: Duration|Time): bool = 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 = @@ -622,56 +701,56 @@ template convert(dur: Duration, unit: static[FixedTimeUnit]): int64 = convert(Nanoseconds, unit, dur.nanosecond) proc inWeeks*(dur: Duration): int64 = - ## Convert the duration to the number of whole weeks. + ## 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 = - ## Convert the duration to the number of whole days. + ## 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 = - ## Convert the duration to the number of whole hours. + ## 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 = - ## Convert the duration to the number of whole minutes. + ## 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 = - ## Convert the duration to the number of whole seconds. + ## 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 = - ## Convert the duration to the number of whole milliseconds. + ## 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 = - ## Convert the duration to the number of whole microseconds. + ## 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 = - ## Convert the duration to the number of whole nanoseconds. + ## Converts the duration to the number of whole nanoseconds. runnableExamples: let dur = initDuration(seconds = -2) doAssert dur.inNanoseconds == -2000000000 @@ -877,6 +956,7 @@ 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`. @@ -890,27 +970,33 @@ proc toWinTime*(t: Time): int64 = ## 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 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)) + 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. @@ -955,7 +1041,7 @@ proc high*(typ: typedesc[Time]): Time = initTime(high(int64), high(NanosecondRange)) proc low*(typ: typedesc[Time]): Time = - initTime(low(int64), 0) + initTime(0, 0) # # DateTime & Timezone @@ -1056,13 +1142,13 @@ 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 = initDateTime(29, mFeb, 2020, 00, 00, 00, utc()) + let dt = dateTime(2020, mFeb, 29, 00, 00, 00, 00, utc()) doAssert dt.isLeapDay doAssert dt+1.years-1.years != dt - let dt2 = initDateTime(28, mFeb, 2020, 00, 00, 00, utc()) + 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 initDateTime(29, mFeb, 2021, 00, 00, 00, utc()) + 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 @@ -1141,7 +1227,7 @@ 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 @@ -1329,14 +1415,13 @@ proc now*(): DateTime {.tags: [TimeEffect], benign.} = ## `cpuTime` instead, depending on the use case. getTime().local -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( @@ -1352,16 +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) + 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) + +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" @@ -1369,7 +1462,7 @@ 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" @@ -1378,8 +1471,8 @@ proc `-`*(dt: DateTime, dur: Duration): DateTime = proc `-`*(dt1, dt2: DateTime): Duration = ## 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) @@ -1406,20 +1499,42 @@ proc `-=`*(a: var DateTime, b: Duration) = a = a - b 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`. + ## Gets the current local date as a string of the format `YYYY-MM-dd`. runnableExamples: echo getDateStr(now() - 1.months) assertDateTimeInitialized dt - result = $dt.year & '-' & intToStr(dt.monthZero, 2) & - '-' & intToStr(dt.monthday, 2) + 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 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) assertDateTimeInitialized dt - result = intToStr(dt.hour, 2) & ':' & intToStr(dt.minute, 2) & - ':' & intToStr(dt.second, 2) + 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) + + +# +# Iso week forward declarations +# + +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 initDateTime*(weekday: WeekDay, isoweek: IsoWeekRange, isoyear: IsoYear, + hour: HourRange, minute: MinuteRange, second: SecondRange, + zone: Timezone = local()): DateTime {.gcsafe, raises: [], tags: [], since: (1, 5).} # # TimeFormat @@ -1451,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 @@ -1465,6 +1583,7 @@ type FormatPattern {.pure.} = enum d, dd, ddd, dddd + GG, GGGG h, hh, H, HH m, mm, M, MM, MMM, MMMM s, ss @@ -1474,6 +1593,7 @@ type YYYY uuuu UUUU + V, VV z, zz, zzz, zzzz ZZZ, ZZZZ g @@ -1512,7 +1632,7 @@ const "Sunday"], ) - FormatLiterals = {' ', '-', '/', ':', '(', ')', '[', ']', ','} + FormatLiterals = {' ', '-', '/', ':', '(', ')', '[', ']', ',', '.'} proc `$`*(f: TimeFormat): string = ## Returns the format string that was used to construct `f`. @@ -1602,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 @@ -1624,6 +1746,8 @@ proc stringToPattern(str: string): FormatPattern = 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 @@ -1673,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" @@ -1736,6 +1864,10 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, result.add '+' & $year of UUUU: result.add $dt.year + 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' @@ -1790,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 @@ -1892,6 +2036,14 @@ 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 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 '+', '-': @@ -1938,7 +2090,7 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, i.inc 2 else: result = false - of Lit: doAssert false, "Can't happen" + of Lit: raiseAssert "Can't happen" proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat, input: string): DateTime = @@ -1987,10 +2139,42 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat, if p.utcOffset.isNone: # No timezone parsed - assume timezone is `zone` - result = initDateTime(monthday, month, year, hour, minute, second, nanosecond, zone) + result = dateTime(year, month, monthday, hour, minute, second, nanosecond, zone) else: # Otherwise convert to `zone` - result = (initDateTime(monthday, month, year, hour, minute, second, nanosecond, utc()).toTime + + 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, @@ -1998,7 +2182,7 @@ proc format*(dt: DateTime, f: TimeFormat, ## 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 = "" @@ -2023,7 +2207,7 @@ proc format*(dt: DateTime, f: string, loc: DateTimeLocale = DefaultLocale): stri ## See `Parsing and formatting dates`_ for documentation of the ## `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) @@ -2033,7 +2217,7 @@ proc format*(dt: DateTime, f: static[string]): string {.raises: [].} = 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) @@ -2046,7 +2230,7 @@ proc format*(time: Time, f: string, zone: Timezone = local()): string ## See `Parsing and formatting dates`_ for documentation of the ## `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) @@ -2057,13 +2241,8 @@ proc format*(time: Time, f: static[string], zone: Timezone = local()): string 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].} = + 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 @@ -2072,7 +2251,7 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local(), ## 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 @@ -2103,31 +2282,33 @@ 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].} = + 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. 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].} = + 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].} = +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`. ## @@ -2139,7 +2320,7 @@ proc parseTime*(input, f: string, zone: Timezone): Time parse(input, f, zone).toTime() proc parseTime*(input: string, f: static[string], zone: Timezone): Time - {.raises: [TimeParseError, Defect].} = + {.parseRaises.} = ## Overload that validates `format` at compile time. const f2 = initTimeFormat(f) result = input.parse(f2, zone).toTime() @@ -2148,7 +2329,7 @@ proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} = ## Converts a `DateTime` object to a string representation. ## It uses the format `yyyy-MM-dd'T'HH:mm:sszzz`. runnableExamples: - let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) + let dt = dateTime(2000, mJan, 01, 12, 00, 00, 00, utc()) doAssert $dt == "2000-01-01T12:00:00Z" doAssert $default(DateTime) == "Uninitialized DateTime" if not dt.isInitialized: @@ -2160,7 +2341,7 @@ 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`. 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 @@ -2182,7 +2363,7 @@ proc initTimeInterval*(nanoseconds, microseconds, milliseconds, ## `seconds`, `minutes`, `hours`, `days`, `months`, and `years`. runnableExamples: let day = initTimeInterval(hours = 24) - let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) + 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 @@ -2268,8 +2449,8 @@ proc between*(startDt, endDt: DateTime): TimeInterval = ## - 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 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) @@ -2349,8 +2530,8 @@ proc between*(startDt, endDt: DateTime): TimeInterval = 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, + 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) @@ -2501,7 +2682,7 @@ proc `+`*(dt: DateTime, interval: TimeInterval): DateTime = ## 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()) + 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" @@ -2524,7 +2705,7 @@ proc `-`*(dt: DateTime, interval: TimeInterval): DateTime = ## 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()) + let dt = dateTime(2017, mMar, 30, 00, 00, 00, 00, utc()) doAssert $(dt - 5.days) == "2017-03-25T00:00:00Z" dt + (-interval) @@ -2568,6 +2749,33 @@ 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. + ## + ## .. warning:: The ISO week-based year can correspond to the following or previous year from 29 December to January 3. + runnableExamples: + 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 # @@ -2580,7 +2788,9 @@ proc epochTime*(): float {.tags: [TimeEffect].} = ## ## .. warning:: Unsuitable for benchmarking (but still better than `now`), ## use `monotimes.getMonoTime` or `cpuTime` instead, depending on the use case. - when defined(macosx): + when defined(js): + result = newDate().getTime() / 1000 + elif defined(macosx): var a {.noinit.}: Timeval gettimeofday(a) result = toBiggestFloat(a.tv_sec.int64) + toBiggestFloat( @@ -2597,8 +2807,6 @@ proc epochTime*(): float {.tags: [TimeEffect].} = 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".} |