diff options
-rw-r--r-- | changelog.md | 5 | ||||
-rw-r--r-- | lib/pure/times.nim | 256 | ||||
-rw-r--r-- | tests/closure/ttimeinfo.nim | 8 | ||||
-rw-r--r-- | tests/stdlib/ttimes.nim | 29 |
4 files changed, 209 insertions, 89 deletions
diff --git a/changelog.md b/changelog.md index 165518bd7..082fe5690 100644 --- a/changelog.md +++ b/changelog.md @@ -76,6 +76,11 @@ proc foo(x: int, y: int): auto {.noSideEffect.} = x + y ``` +- The fields of `times.DateTime` are now private, and are accessed with getters and deprecated setters. + +- The `times` module now handles the default value for `DateTime` more consistently. Most procs raise an assertion error when given + an uninitialized `DateTime`, the exceptions are `==` and `$` (which returns `"Uninitialized DateTime"`). The proc `times.isInitialized` + has been added which can be used to check if a `DateTime` has been initialized. ## Language changes - In newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this: diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 2b98e5971..1fff3d99b 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -252,7 +252,6 @@ type Month* = enum ## Represents a month. Note that the enum starts at ``1``, ## so ``ord(month)`` will give the month number in the ## range ``1..12``. - # mInvalid = (0, "Invalid") # intentionally left out so `items` works mJan = (1, "January") mFeb = "February" mMar = "March" @@ -276,11 +275,12 @@ type dSun = "Sunday" type - MonthdayRange* = range[0..31] - ## 0 represents an invalid day of the month + MonthdayRange* = range[1..31] HourRange* = range[0..23] MinuteRange* = range[0..59] - SecondRange* = range[0..60] + SecondRange* = range[0..60] ## \ + ## Includes the value 60 to allow for a leap second. Note however + ## that the `second` of a `DateTime` will never be a leap second. YeardayRange* = range[0..365] NanosecondRange* = range[0..999_999_999] @@ -289,46 +289,22 @@ type nanosecond: NanosecondRange DateTime* = object of RootObj ## \ - ## Represents a time in different parts. Although this type can represent - ## leap seconds, they are generally not supported in this module. They are - ## not ignored, but the ``DateTime``'s returned by procedures in this - ## module will never have a leap second. - ## - ## **Warning**: even though the fields of ``DateTime`` are exported, - ## they should never be mutated directly. Doing so is unsafe and will - ## result in the ``DateTime`` ending up in an invalid state. - ## - ## Instead of mutating the fields directly, use the `Duration <#Duration>`_ - ## and `TimeInterval <#TimeInterval>`_ types for arithmetic and use the - ## `initDateTime proc <#initDateTime,MonthdayRange,Month,int,HourRange,MinuteRange,SecondRange,NanosecondRange,Timezone>`_ - ## for changing a specific field. - nanosecond*: NanosecondRange ## The number of nanoseconds after the second, - ## in the range 0 to 999_999_999. - second*: SecondRange ## The number of seconds after the minute, - ## normally in the range 0 to 59, but can - ## be up to 60 to allow for a leap second. - minute*: MinuteRange ## The number of minutes after the hour, - ## in the range 0 to 59. - hour*: HourRange ## The number of hours past midnight, - ## in the range 0 to 23. - monthday*: MonthdayRange ## The day of the month, in the range 1 to 31. - month*: Month ## The month. - year*: int ## The year, using astronomical year numbering - ## (meaning that before year 1 is year 0, - ## then year -1 and so on). - weekday*: WeekDay ## The day of the week. - yearday*: YeardayRange ## The number of days since January 1, - ## in the range 0 to 365. - isDst*: bool ## Determines whether DST is in effect. - ## Always false for the JavaScript backend. - timezone*: Timezone ## The timezone represented as an implementation - ## of ``Timezone``. - utcOffset*: int ## The offset in seconds west of UTC, including - ## any offset due to DST. Note that the sign of - ## this number is the opposite of the one in a - ## formatted offset string like ``+01:00`` (which - ## would be equivalent to the UTC offset - ## ``-3600``). + ## Represents a time in different parts. Although this type can represent + ## leap seconds, they are generally not supported in this module. They are + ## not ignored, but the ``DateTime``'s returned by procedures in this + ## module will never have a leap second. + nanosecond: NanosecondRange + second: SecondRange + minute: MinuteRange + hour: HourRange + monthdayZero: int + monthZero: int + year: int + weekday: WeekDay + yearday: YeardayRange + isDst: bool + timezone: Timezone + utcOffset: int Duration* = object ## Represents a fixed duration of time, meaning a duration ## that has constant length independent of the context. @@ -464,7 +440,7 @@ proc getDaysInMonth*(month: Month, year: int): int = proc assertValidDate(monthday: MonthdayRange, month: Month, year: int) {.inline.} = - assert monthday > 0 and monthday <= getDaysInMonth(month, year), + assert monthday <= getDaysInMonth(month, year), $year & "-" & intToStr(ord(month), 2) & "-" & $monthday & " is not a valid date" @@ -981,21 +957,114 @@ proc low*(typ: typedesc[Time]): Time = # DateTime & Timezone # -proc isLeapDay*(t: DateTime): bool {.since: (1, 1).} = +template assertDateTimeInitialized(dt: DateTime) = + assert dt.monthdayZero != 0, "Uninitialized datetime" + +proc nanosecond*(dt: DateTime): NanosecondRange {.inline.} = + ## The number of nanoseconds after the second, + ## in the range 0 to 999_999_999. + assertDateTimeInitialized(dt) + dt.nanosecond + +proc second*(dt: DateTime): SecondRange {.inline.} = + ## The number of seconds after the minute, + ## in the range 0 to 59. + assertDateTimeInitialized(dt) + dt.second + +proc minute*(dt: DateTime): MinuteRange {.inline.} = + ## The number of minutes after the hour, + ## in the range 0 to 59. + assertDateTimeInitialized(dt) + dt.minute + +proc hour*(dt: DateTime): HourRange {.inline.} = + ## The number of hours past midnight, + ## in the range 0 to 23. + assertDateTimeInitialized(dt) + dt.hour + +proc monthday*(dt: DateTime): MonthdayRange {.inline.} = + ## The day of the month, in the range 1 to 31. + assertDateTimeInitialized(dt) + # 'cast' to avoid extra range check + cast[MonthdayRange](dt.monthdayZero) + +proc month*(dt: DateTime): Month = + ## The month as an enum, the ordinal value + ## is in the range 1 to 12. + assertDateTimeInitialized(dt) + # 'cast' to avoid extra range check + cast[Month](dt.monthZero) + +proc year*(dt: DateTime): int {.inline.} = + ## The year, using astronomical year numbering + ## (meaning that before year 1 is year 0, + ## then year -1 and so on). + assertDateTimeInitialized(dt) + dt.year + +proc weekday*(dt: DateTime): WeekDay {.inline.} = + ## The day of the week as an enum, the ordinal + ## value is in the range 0 (monday) to 6 (sunday). + assertDateTimeInitialized(dt) + dt.weekday + +proc yearday*(dt: DateTime): YeardayRange {.inline.} = + ## The number of days since January 1, + ## in the range 0 to 365. + assertDateTimeInitialized(dt) + dt.yearday + +proc isDst*(dt: DateTime): bool {.inline.} = + ## Determines whether DST is in effect. + ## Always false for the JavaScript backend. + assertDateTimeInitialized(dt) + dt.isDst + +proc timezone*(dt: DateTime): Timezone {.inline.} = + ## The timezone represented as an implementation + ## of ``Timezone``. + assertDateTimeInitialized(dt) + dt.timezone + +proc utcOffset*(dt: DateTime): int {.inline.} = + ## The offset in seconds west of UTC, including + ## any offset due to DST. Note that the sign of + ## this number is the opposite of the one in a + ## formatted offset string like ``+01:00`` (which + ## would be equivalent to the UTC offset + ## ``-3600``). + assertDateTimeInitialized(dt) + dt.utcOffset + +proc isInitialized(dt: DateTime): bool = + # Returns true if `dt` is not the (invalid) default value for `DateTime`. + runnableExamples: + doAssert now().isInitialized + doAssert not default(DateTime).isInitialized + dt.monthZero != 0 + +since((1, 3)): + export isInitialized + +proc isLeapDay*(dt: DateTime): bool {.since: (1, 1).} = ## returns whether `t` is a leap day, 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 + let dt = initDateTime(29, mFeb, 2020, 00, 00, 00, utc()) + doAssert dt.isLeapDay + doAssert dt+1.years-1.years != dt + let dt2 = initDateTime(28, mFeb, 2020, 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()) - t.year.isLeapYear and t.month == mFeb and t.monthday == 29 + assertDateTimeInitialized dt + dt.year.isLeapYear and dt.month == mFeb and dt.monthday == 29 proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = ## Converts a ``DateTime`` to a ``Time`` representing the same point in time. + assertDateTimeInitialized dt let epochDay = toEpochDay(dt.monthday, dt.month, dt.year) var seconds = epochDay * secondsInDay seconds.inc dt.hour * secondsInHour @@ -1020,8 +1089,8 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime = DateTime( year: y, - month: m, - monthday: d, + monthZero: m.int, + monthdayZero: d, hour: hour, minute: minute, second: second, @@ -1091,14 +1160,13 @@ proc `$`*(zone: Timezone): string = proc `==`*(zone1, zone2: Timezone): bool = ## Two ``Timezone``'s are considered equal if their name is equal. + runnableExamples: + doAssert local() == local() + doAssert local() != utc() if system.`==`(zone1, zone2): return true if zone1.isNil or zone2.isNil: return false - - runnableExamples: - doAssert local() == local() - doAssert local() != utc() zone1.name == zone2.name proc inZone*(time: Time, zone: Timezone): DateTime @@ -1110,6 +1178,7 @@ proc inZone*(dt: DateTime, zone: Timezone): DateTime {.tags: [], raises: [], benign.} = ## Returns a ``DateTime`` representing the same point in time as ``dt`` but ## using ``zone`` as the timezone. + assertDateTimeInitialized dt dt.toTime.inZone(zone) proc toAdjTime(dt: DateTime): Time = @@ -1265,9 +1334,9 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, assertValidDate monthday, month, year let dt = DateTime( - monthday: monthday, + monthdayZero: monthday, year: year, - month: month, + monthZero: month.int, hour: hour, minute: minute, second: second, @@ -1318,13 +1387,10 @@ proc `<=`*(a, b: DateTime): bool = ## Returns true if ``a`` happened before or at the same time as ``b``. return a.toTime <= b.toTime -proc isDefault[T](a: T): bool = - system.`==`(a, default(T)) - proc `==`*(a, b: DateTime): bool = ## Returns true if ``a`` and ``b`` represent the same point in time. - if a.isDefault: b.isDefault - elif b.isDefault: false + if not a.isInitialized: not b.isInitialized + elif not b.isInitialized: false else: a.toTime == b.toTime proc `+=`*(a: var DateTime, b: Duration) = @@ -1337,13 +1403,15 @@ 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) & + assertDateTimeInitialized dt + result = $dt.year & '-' & intToStr(dt.monthZero, 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) + assertDateTimeInitialized dt result = intToStr(dt.hour, 2) & ':' & intToStr(dt.minute, 2) & ':' & intToStr(dt.second, 2) @@ -1914,18 +1982,13 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat, $year & "-" & ord(month).intToStr(2) & "-" & $monthday & " is not a valid date") - result = DateTime( - year: year, month: month, monthday: monthday, - hour: hour, minute: minute, second: second, nanosecond: nanosecond - ) - if p.utcOffset.isNone: # No timezone parsed - assume timezone is `zone` - result = initDateTime(zone.zonedTimeFromAdjTime(result.toAdjTime), zone) + result = initDateTime(monthday, month, year, hour, minute, second, nanosecond, zone) else: # Otherwise convert to `zone` - result.utcOffset = p.utcOffset.get() - result = result.toTime.inZone(zone) + result = (initDateTime(monthday, month, year, hour, minute, second, nanosecond, utc()).toTime + + initDuration(seconds = p.utcOffset.get())).inZone(zone) proc format*(dt: DateTime, f: TimeFormat, loc: DateTimeLocale = DefaultLocale): string {.raises: [].} = @@ -1934,6 +1997,7 @@ proc format*(dt: DateTime, f: TimeFormat, let f = initTimeFormat("yyyy-MM-dd") let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) doAssert "2000-01-01" == dt.format(f) + assertDateTimeInitialized dt var idx = 0 while idx <= f.patterns.high: case f.patterns[idx].FormatPattern @@ -2082,7 +2146,11 @@ proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} = runnableExamples: let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) doAssert $dt == "2000-01-01T12:00:00Z" - result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") + doAssert $default(DateTime) == "Uninitialized DateTime" + if not dt.isInitialized: + result = "Uninitialized DateTime" + else: + result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") proc `$`*(time: Time): string {.tags: [], raises: [], benign.} = ## Converts a `Time` value to a string representation. It will use the local @@ -2721,3 +2789,39 @@ proc getGMTime*(time: Time): DateTime ## expressed in Coordinated Universal Time (UTC). # Deprecated since v0.18.0 time.utc + +proc `nanosecond=`*(dt: var DateTime, value: NanosecondRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.nanosecond = value + +proc `second=`*(dt: var DateTime, value: SecondRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.second = value + +proc `minute=`*(dt: var DateTime, value: MinuteRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.minute = value + +proc `hour=`*(dt: var DateTime, value: HourRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.hour = value + +proc `monthdayZero=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.monthdayZero = value + +proc `monthZero=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.monthZero = value + +proc `year=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.year = value + +proc `weekday=`*(dt: var DateTime, value: WeekDay) {.deprecated: "Deprecated since v1.3.1".} = + dt.weekday = value + +proc `yearday=`*(dt: var DateTime, value: YeardayRange) {.deprecated: "Deprecated since v1.3.1".} = + dt.yearday = value + +proc `isDst=`*(dt: var DateTime, value: bool) {.deprecated: "Deprecated since v1.3.1".} = + dt.isDst = value + +proc `timezone=`*(dt: var DateTime, value: Timezone) {.deprecated: "Deprecated since v1.3.1".} = + dt.timezone = value + +proc `utcOffset=`*(dt: var DateTime, value: int) {.deprecated: "Deprecated since v1.3.1".} = + dt.utcOffset = value diff --git a/tests/closure/ttimeinfo.nim b/tests/closure/ttimeinfo.nim index 7416c0d31..24d535cbf 100644 --- a/tests/closure/ttimeinfo.nim +++ b/tests/closure/ttimeinfo.nim @@ -1,7 +1,7 @@ discard """ output: ''' -@[2000-01-01T00:00:00+00:00, 2001-01-01T00:00:00+00:00, 2002-01-01T00:00:00+00:00, 2003-01-01T00:00:00+00:00, 2004-01-01T00:00:00+00:00, 2005-01-01T00:00:00+00:00, 2006-01-01T00:00:00+00:00, 2007-01-01T00:00:00+00:00, 2008-01-01T00:00:00+00:00, 2009-01-01T00:00:00+00:00, 2010-01-01T00:00:00+00:00, 2011-01-01T00:00:00+00:00, 2012-01-01T00:00:00+00:00, 2013-01-01T00:00:00+00:00, 2014-01-01T00:00:00+00:00, 2015-01-01T00:00:00+00:00] -@[2000-01-01T00:00:00+00:00, 2001-01-01T00:00:00+00:00, 2002-01-01T00:00:00+00:00, 2003-01-01T00:00:00+00:00, 2004-01-01T00:00:00+00:00, 2005-01-01T00:00:00+00:00, 2006-01-01T00:00:00+00:00, 2007-01-01T00:00:00+00:00, 2008-01-01T00:00:00+00:00, 2009-01-01T00:00:00+00:00, 2010-01-01T00:00:00+00:00, 2011-01-01T00:00:00+00:00, 2012-01-01T00:00:00+00:00, 2013-01-01T00:00:00+00:00, 2014-01-01T00:00:00+00:00, 2015-01-01T00:00:00+00:00] +@[2000-01-01T00:00:00Z, 2001-01-01T00:00:00Z, 2002-01-01T00:00:00Z, 2003-01-01T00:00:00Z, 2004-01-01T00:00:00Z, 2005-01-01T00:00:00Z, 2006-01-01T00:00:00Z, 2007-01-01T00:00:00Z, 2008-01-01T00:00:00Z, 2009-01-01T00:00:00Z, 2010-01-01T00:00:00Z, 2011-01-01T00:00:00Z, 2012-01-01T00:00:00Z, 2013-01-01T00:00:00Z, 2014-01-01T00:00:00Z, 2015-01-01T00:00:00Z] +@[2000-01-01T00:00:00Z, 2001-01-01T00:00:00Z, 2002-01-01T00:00:00Z, 2003-01-01T00:00:00Z, 2004-01-01T00:00:00Z, 2005-01-01T00:00:00Z, 2006-01-01T00:00:00Z, 2007-01-01T00:00:00Z, 2008-01-01T00:00:00Z, 2009-01-01T00:00:00Z, 2010-01-01T00:00:00Z, 2011-01-01T00:00:00Z, 2012-01-01T00:00:00Z, 2013-01-01T00:00:00Z, 2014-01-01T00:00:00Z, 2015-01-01T00:00:00Z] ''' """ @@ -12,11 +12,11 @@ import times # 1 proc f(n: int): DateTime = - DateTime(year: n, month: mJan, monthday: 1) + initDateTime(1, mJan, n, 0, 0, 0, utc()) echo toSeq(2000 || 2015).map(f) # 2 echo toSeq(2000 || 2015).map(proc (n: int): DateTime = - DateTime(year: n, month: mJan, monthday: 1) + initDateTime(1, mJan, n, 0, 0, 0, utc()) ) diff --git a/tests/stdlib/ttimes.nim b/tests/stdlib/ttimes.nim index 590018c6d..e6305b2d0 100644 --- a/tests/stdlib/ttimes.nim +++ b/tests/stdlib/ttimes.nim @@ -617,17 +617,28 @@ suite "ttimes": test "default DateTime": # https://github.com/nim-lang/RFCs/issues/211 var num = 0 for ai in Month: num.inc - doAssert num == 12 + check num == 12 var a: DateTime - doAssert a == DateTime.default - doAssert ($a).len > 0 # no crash - doAssert a.month.Month.ord == 0 - doAssert a.month.Month == cast[Month](0) - doAssert a.monthday == 0 - - doAssertRaises(AssertionDefect): discard getDayOfWeek(a.monthday, a.month, a.year) - doAssertRaises(AssertionDefect): discard a.toTime + check a == DateTime.default + check not a.isInitialized + check $a == "Uninitialized DateTime" + + expect(AssertionDefect): discard getDayOfWeek(a.monthday, a.month, a.year) + expect(AssertionDefect): discard a.toTime + expect(AssertionDefect): discard a.utc() + expect(AssertionDefect): discard a.local() + expect(AssertionDefect): discard a.inZone(utc()) + expect(AssertionDefect): discard a + initDuration(seconds = 1) + expect(AssertionDefect): discard a + initTimeInterval(seconds = 1) + expect(AssertionDefect): discard a.isLeapDay + expect(AssertionDefect): discard a < a + expect(AssertionDefect): discard a <= a + expect(AssertionDefect): discard getDateStr(a) + expect(AssertionDefect): discard getClockStr(a) + expect(AssertionDefect): discard a.format "yyyy" + expect(AssertionDefect): discard a.format initTimeFormat("yyyy") + expect(AssertionDefect): discard between(a, a) test "inX procs": doAssert initDuration(seconds = 1).inSeconds == 1 |