times.Timezone changes (#8527)

* Use floorDiv in times.nim

* New implementation of times.Timezone

* Tweak doc comments

* Fix typo
This commit is contained in:
Oscar Nihlgård
2018-08-17 11:12:58 +02:00
committed by Andreas Rumpf
parent b77d910e4e
commit 33ed8f7e73
4 changed files with 153 additions and 91 deletions

View File

@@ -40,6 +40,10 @@
- ``proc `-`*(a, b: Time): int64`` in the ``times`` module has changed return type
to ``times.Duration`` in order to support higher time resolutions.
The proc is no longer deprecated.
- The ``times.Timezone`` is now an immutable ref-type that must be initialized
with an explicit constructor (``newTimezone``).
- ``posix.Timeval.tv_sec`` has changed type to ``posix.Time``.
- ``math.`mod` `` for floats now behaves the same as ``mod`` for integers

View File

@@ -276,20 +276,22 @@ type
FixedTimeUnit* = range[Nanoseconds..Weeks] ## Subrange of ``TimeUnit`` that only includes units of fixed duration.
## These are the units that can be represented by a ``Duration``.
Timezone* = object ## Timezone interface for supporting ``DateTime``'s of arbritary timezones.
## The ``times`` module only supplies implementations for the systems local time and UTC.
## The members ``zoneInfoFromUtc`` and ``zoneInfoFromTz`` should not be accessed directly
## and are only exported so that ``Timezone`` can be implemented by other modules.
zoneInfoFromUtc*: proc (time: Time): ZonedTime {.tags: [], raises: [], benign.}
zoneInfoFromTz*: proc (adjTime: Time): ZonedTime {.tags: [], raises: [], benign.}
name*: string ## The name of the timezone, f.ex 'Europe/Stockholm' or 'Etc/UTC'. Used for checking equality.
## Se also: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone* = ref object ## \
## Timezone interface for supporting ``DateTime``'s of arbritary
## timezones. The ``times`` module only supplies implementations for the
## systems local time and UTC.
zonedTimeFromTimeImpl: proc (x: Time): ZonedTime
{.tags: [], raises: [], benign.}
zonedTimeFromAdjTimeImpl: proc (x: Time): ZonedTime
{.tags: [], raises: [], benign.}
name: string
ZonedTime* = object ## Represents a zoned instant in time that is not associated with any calendar.
## This type is only used for implementing timezones.
adjTime*: Time ## Time adjusted to a timezone.
utcOffset*: int ## Offset from UTC in seconds.
## The point in time represented by ``ZonedTime`` is ``adjTime + utcOffset.seconds``.
ZonedTime* = object ## Represents a point in time with an associated
## UTC offset and DST flag. This type is only used for
## implementing timezones.
time*: Time ## The point in time being represented.
utcOffset*: int ## The offset in seconds west of UTC,
## including any offset due to DST.
isDst*: bool ## Determines whether DST is in effect.
DurationParts* = array[FixedTimeUnit, int64] # Array of Duration parts starts
@@ -343,10 +345,9 @@ proc normalize[T: Duration|Time](seconds, nanoseconds: int64): T =
result.nanosecond = nanosecond.int
# Forward declarations
proc utcZoneInfoFromUtc(time: Time): ZonedTime {.tags: [], raises: [], benign .}
proc utcZoneInfoFromTz(adjTime: Time): ZonedTime {.tags: [], raises: [], benign .}
proc localZoneInfoFromUtc(time: Time): ZonedTime {.tags: [], raises: [], benign .}
proc localZoneInfoFromTz(adjTime: Time): ZonedTime {.tags: [], raises: [], benign .}
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.}
@@ -493,7 +494,7 @@ proc fromEpochDay(epochday: int64): tuple[monthday: MonthdayRange, month: Month,
proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): YeardayRange {.tags: [], raises: [], benign .} =
## Returns the day of the year.
## Equivalent with ``initDateTime(day, month, year).yearday``.
## Equivalent with ``initDateTime(monthday, month, year, 0, 0, 0).yearday``.
assertValidDate monthday, month, year
const daysUntilMonth: array[Month, int] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
const daysUntilMonthLeap: array[Month, int] = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]
@@ -505,11 +506,11 @@ proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): YeardayRan
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(day, month, year).weekday``.
## Equivalent with ``initDateTime(monthday, month, year, 0, 0, 0).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 = (if days >= 0: days else: days - 6) div 7
let weeks = floorDiv(days, 7)
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.
@@ -759,15 +760,14 @@ proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} =
seconds.inc dt.hour * secondsInHour
seconds.inc dt.minute * 60
seconds.inc dt.second
# The code above ignores the UTC offset of `timeInfo`,
# so we need to compensate for that here.
seconds.inc dt.utcOffset
result = initTime(seconds, dt.nanosecond)
proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime =
## Create a new ``DateTime`` using ``ZonedTime`` in the specified timezone.
let s = zt.adjTime.seconds
let epochday = (if s >= 0: s else: s - (secondsInDay - 1)) div secondsInDay
let adjTime = zt.time - initDuration(seconds = zt.utcOffset)
let s = adjTime.seconds
let epochday = floorDiv(s, secondsInDay)
var rem = s - epochday * secondsInDay
let hour = rem div secondsInHour
rem = rem - hour * secondsInHour
@@ -784,7 +784,7 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime =
hour: hour,
minute: minute,
second: second,
nanosecond: zt.adjTime.nanosecond,
nanosecond: zt.time.nanosecond,
weekday: getDayOfWeek(d, m, y),
yearday: getDayOfYear(d, m, y),
isDst: zt.isDst,
@@ -792,14 +792,55 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime =
utcOffset: zt.utcOffset
)
proc inZone*(time: Time, zone: Timezone): DateTime {.tags: [], raises: [], benign.} =
## Break down ``time`` into a ``DateTime`` using ``zone`` as the timezone.
let zoneInfo = zone.zoneInfoFromUtc(time)
result = initDateTime(zoneInfo, zone)
proc newTimezone*(
name: string,
zonedTimeFromTimeImpl: proc (time: Time): ZonedTime {.tags: [], raises: [], benign.},
zonedTimeFromAdjTimeImpl: proc (adjTime: Time): ZonedTime {.tags: [], raises: [], benign.}
): Timezone =
## Create a new ``Timezone``.
##
## ``zonedTimeFromTimeImpl`` and ``zonedTimeFromAdjTimeImpl`` is used
## as the underlying implementations for ``zonedTimeFromTime`` and
## ``zonedTimeFromAdjTime``.
##
## If possible, the name parameter should match the name used in the
## tz database. If the timezone doesn't exist in the tz database, or if the
## timezone name is unknown, then any string that describes the timezone
## unambiguously can be used. Note that the timezones name is used for
## checking equality!
runnableExamples:
proc utcTzInfo(time: Time): ZonedTime =
ZonedTime(utcOffset: 0, isDst: false, time: time)
let utc = newTimezone("Etc/UTC", utcTzInfo, utcTzInfo)
Timezone(
name: name,
zonedTimeFromTimeImpl: zonedTimeFromTimeImpl,
zonedTimeFromAdjTimeImpl: zonedTimeFromAdjTimeImpl
)
proc inZone*(dt: DateTime, zone: Timezone): DateTime {.tags: [], raises: [], benign.} =
## Convert ``dt`` into a ``DateTime`` using ``zone`` as the timezone.
dt.toTime.inZone(zone)
proc name*(zone: Timezone): string =
## The name of the timezone.
##
## If possible, the name will be the name used in the tz database.
## 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.
##
## See also: https://en.wikipedia.org/wiki/Tz_database
zone.name
proc zonedTimeFromTime*(zone: Timezone, time: Time): ZonedTime =
## Returns the ``ZonedTime`` for some point in time.
zone.zonedTimeFromTimeImpl(time)
proc zonedTimeFromAdjTime*(zone: TimeZone, adjTime: Time): ZonedTime =
## Returns the ``ZonedTime`` for some local time.
##
## Note that the ``Time`` argument does not represent a point in time, it
## represent a local time! E.g if ``adjTime`` is ``fromUnix(0)``, it should be
## interpreted as 1970-01-01T00:00:00 in the ``zone`` timezone, not in UTC.
zone.zonedTimeFromAdjTimeImpl(adjTime)
proc `$`*(zone: Timezone): string =
## Returns the name of the timezone.
@@ -807,8 +848,20 @@ 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()
zone1.name == zone2.name
proc inZone*(time: Time, zone: Timezone): DateTime {.tags: [], raises: [], benign.} =
## Convert ``time`` into a ``DateTime`` using ``zone`` as the timezone.
result = initDateTime(zone.zonedTimeFromTime(time), zone)
proc inZone*(dt: DateTime, zone: Timezone): DateTime {.tags: [], raises: [], benign.} =
## Returns a ``DateTime`` representing the same point in time as ``dt`` but
## using ``zone`` as the timezone.
dt.toTime.inZone(zone)
proc toAdjTime(dt: DateTime): Time =
let epochDay = toEpochday(dt.monthday, dt.month, dt.year)
var seconds = epochDay * secondsInDay
@@ -843,14 +896,14 @@ when defined(JS):
proc getYear(js: JsDate): int {.tags: [], raises: [], benign, importcpp.}
proc setFullYear(js: JsDate, year: int): void {.tags: [], raises: [], benign, importcpp.}
proc localZoneInfoFromUtc(time: Time): ZonedTime =
proc localZonedTimeFromTime(time: Time): ZonedTime =
let jsDate = newDate(time.seconds.float * 1000)
let offset = jsDate.getTimezoneOffset() * secondsInMin
result.adjTime = time - initDuration(seconds = offset)
result.time = time
result.utcOffset = offset
result.isDst = false
proc localZoneInfoFromTz(adjTime: Time): ZonedTime =
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime =
let utcDate = newDate(adjTime.seconds.float * 1000)
let localDate = newDate(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(),
utcDate.getUTCHours(), utcDate.getUTCMinutes(), utcDate.getUTCSeconds(), 0)
@@ -861,8 +914,8 @@ when defined(JS):
if utcDate.getUTCFullYear() in 0 .. 99:
localDate.setFullYear(utcDate.getUTCFullYear())
result.adjTime = adjTime
result.utcOffset = localDate.getTimezoneOffset() * secondsInMin
result.time = adjTime + initDuration(seconds = result.utcOffset)
result.isDst = false
else:
@@ -915,13 +968,13 @@ else:
return ((unix - tm.toAdjUnix).int, tm.isdst > 0)
return (0, false)
proc localZoneInfoFromUtc(time: Time): ZonedTime =
proc localZonedTimeFromTime(time: Time): ZonedTime =
let (offset, dst) = getLocalOffsetAndDst(time.seconds)
result.adjTime = time - initDuration(seconds = offset)
result.time = time
result.utcOffset = offset
result.isDst = dst
proc localZoneInfoFromTz(adjTime: Time): ZonedTime =
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime =
var adjUnix = adjTime.seconds
let past = adjUnix - secondsInDay
let (pastOffset, _) = getLocalOffsetAndDst(past)
@@ -943,31 +996,34 @@ else:
# as a result of offset changes (normally due to dst)
let utcUnix = adjTime.seconds + utcOffset
let (finalOffset, dst) = getLocalOffsetAndDst(utcUnix)
result.adjTime = initTime(utcUnix - finalOffset, adjTime.nanosecond)
result.time = initTime(utcUnix, adjTime.nanosecond)
result.utcOffset = finalOffset
result.isDst = dst
proc utcZoneInfoFromUtc(time: Time): ZonedTime =
result.adjTime = time
result.utcOffset = 0
result.isDst = false
proc utcTzInfo(time: Time): ZonedTime =
ZonedTime(utcOffset: 0, isDst: false, time: time)
proc utcZoneInfoFromTz(adjTime: Time): ZonedTime =
utcZoneInfoFromUtc(adjTime) # adjTime == time since we are in UTC
var utcInstance {.threadvar.}: Timezone
var localInstance {.threadvar.}: Timezone
proc utc*(): TimeZone =
## Get the ``Timezone`` implementation for the UTC timezone.
runnableExamples:
doAssert now().utc.timezone == utc()
doAssert utc().name == "Etc/UTC"
Timezone(zoneInfoFromUtc: utcZoneInfoFromUtc, zoneInfoFromTz: utcZoneInfoFromTz, name: "Etc/UTC")
if utcInstance.isNil:
utcInstance = newTimezone("Etc/UTC", utcTzInfo, utcTzInfo)
result = utcInstance
proc local*(): TimeZone =
## Get the ``Timezone`` implementation for the local timezone.
runnableExamples:
doAssert now().timezone == local()
doAssert local().name == "LOCAL"
Timezone(zoneInfoFromUtc: localZoneInfoFromUtc, zoneInfoFromTz: localZoneInfoFromTz, name: "LOCAL")
if localInstance.isNil:
localInstance = newTimezone("LOCAL", localZonedTimeFromTime,
localZonedTimeFromAdjTime)
result = localInstance
proc utc*(dt: DateTime): DateTime =
## Shorthand for ``dt.inZone(utc())``.
@@ -1233,7 +1289,7 @@ proc initDateTime*(monthday: MonthdayRange, month: Month, year: int,
second: second,
nanosecond: nanosecond
)
result = initDateTime(zone.zoneInfoFromTz(dt.toAdjTime), zone)
result = initDateTime(zone.zonedTimeFromAdjTime(dt.toAdjTime), zone)
proc initDateTime*(monthday: MonthdayRange, month: Month, year: int,
hour: HourRange, minute: MinuteRange, second: SecondRange,
@@ -1263,16 +1319,15 @@ proc `+`*(dt: DateTime, interval: TimeInterval): DateTime =
let (adjDur, absDur) = evaluateInterval(dt, interval)
if adjDur != DurationZero:
var zInfo = dt.timezone.zoneInfoFromTz(dt.toAdjTime + adjDur)
var zt = dt.timezone.zonedTimeFromAdjTime(dt.toAdjTime + adjDur)
if absDur != DurationZero:
let offsetDur = initDuration(seconds = zInfo.utcOffset)
zInfo = dt.timezone.zoneInfoFromUtc(zInfo.adjTime + offsetDur + absDur)
result = initDateTime(zInfo, dt.timezone)
zt = dt.timezone.zonedTimeFromTime(zt.time + absDur)
result = initDateTime(zt, dt.timezone)
else:
result = initDateTime(zInfo, dt.timezone)
result = initDateTime(zt, dt.timezone)
else:
var zInfo = dt.timezone.zoneInfoFromUtc(dt.toTime + absDur)
result = initDateTime(zInfo, dt.timezone)
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
@@ -1319,7 +1374,7 @@ proc `<=` * (a, b: DateTime): bool =
return a.toTime <= b.toTime
proc `==`*(a, b: DateTime): bool =
## Returns true if ``a == b``, that is if both dates represent the same point in datetime.
## Returns true if ``a == b``, that is if both dates represent the same point in time.
return a.toTime == b.toTime
@@ -2065,7 +2120,7 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat,
if p.utcOffset.isNone:
# No timezone parsed - assume timezone is `zone`
result = initDateTime(zone.zoneInfoFromTz(result.toAdjTime), zone)
result = initDateTime(zone.zonedTimeFromAdjTime(result.toAdjTime), zone)
else:
# Otherwise convert to `zone`
result.utcOffset = p.utcOffset.get()
@@ -2347,7 +2402,7 @@ proc fromSeconds*(since1970: int64): Time {.tags: [], raises: [], benign, deprec
proc toSeconds*(time: Time): float {.tags: [], raises: [], benign, deprecated.} =
## Returns the time in seconds since the unix epoch.
##
## **Deprecated since v0.18.0:** use ``fromUnix`` instead
## **Deprecated since v0.18.0:** use ``toUnix`` instead
time.seconds.float + time.nanosecond / convert(Seconds, Nanoseconds, 1)
proc getLocalTime*(time: Time): DateTime {.tags: [], raises: [], benign, deprecated.} =
@@ -2390,13 +2445,14 @@ proc timeInfoToTime*(dt: DateTime): Time {.tags: [], benign, deprecated.} =
when defined(JS):
var start = getTime()
proc getStartMilsecs*(): int {.deprecated, tags: [TimeEffect], benign.} =
## get the milliseconds from the start of the program.
## **Deprecated since v0.8.10:** use ``epochTime`` or ``cpuTime`` instead.
let dur = getTime() - start
result = (convert(Seconds, Milliseconds, dur.seconds) +
convert(Nanoseconds, Milliseconds, dur.nanosecond)).int
else:
proc getStartMilsecs*(): int {.deprecated, tags: [TimeEffect], benign.} =
## get the milliseconds from the start of the program.
##
## **Deprecated since v0.8.10:** use ``epochTime`` or ``cpuTime`` instead.
when defined(macosx):
result = toInt(toFloat(int(getClock())) / (toFloat(clocksPerSec) / 1000.0))
else:
@@ -2417,7 +2473,7 @@ proc getDayOfWeek*(day, month, year: int): WeekDay {.tags: [], raises: [], beni
proc getDayOfWeekJulian*(day, month, year: int): WeekDay {.deprecated.} =
## Returns the day of the week enum from day, month and year,
## according to the Julian calendar.
## **Deprecated since v0.18.0:**
## **Deprecated since v0.18.0**
# Day & month start from one.
let
a = (14 - month) div 12
@@ -2425,3 +2481,23 @@ proc getDayOfWeekJulian*(day, month, year: int): WeekDay {.deprecated.} =
m = month + (12*a) - 2
d = (5 + day + y + (y div 4) + (31*m) div 12) mod 7
result = d.WeekDay
proc adjTime*(zt: ZonedTime): Time
{.deprecated: "Use zt.time instead".} =
## **Deprecated since v0.19.0:** use the ``time`` field instead.
zt.time - initDuration(seconds = zt.utcOffset)
proc `adjTime=`*(zt: var ZonedTime, adjTime: Time)
{.deprecated: "Use zt.time instead".} =
## **Deprecated since v0.19.0:** use the ``time`` field instead.
zt.time = adjTime + initDuration(seconds = zt.utcOffset)
proc zoneInfoFromUtc*(zone: Timezone, time: Time): ZonedTime
{.deprecated: "Use zonedTimeFromTime instead".} =
## **Deprecated since v0.19.0:** use ``zonedTimeFromTime`` instead.
zone.zonedTimeFromTime(time)
proc zoneInfoFromTz*(zone: Timezone, adjTime: Time): ZonedTime
{.deprecated: "Use zonedTimeFromAdjTime instead".} =
## **Deprecated since v0.19.0:** use the ``zonedTimeFromAdjTime`` instead.
zone.zonedTimeFromAdjTime(adjTime)

View File

@@ -21,17 +21,17 @@ doAssert b - a == initDuration(seconds = 500_000_000)
# Because we can't change the timezone JS uses, we define a simple static timezone for testing.
proc staticZoneInfoFromUtc(time: Time): ZonedTime =
proc zonedTimeFromTime(time: Time): ZonedTime =
result.utcOffset = -7200
result.isDst = false
result.adjTime = time + 7200.seconds
result.time = time
proc staticZoneInfoFromTz(adjTime: Time): ZonedTIme =
proc zonedTimeFromAdjTime(adjTime: Time): ZonedTIme =
result.utcOffset = -7200
result.isDst = false
result.adjTime = adjTime
result.time = adjTime + initDuration(seconds = -7200)
let utcPlus2 = Timezone(zoneInfoFromUtc: staticZoneInfoFromUtc, zoneInfoFromTz: staticZoneInfoFromTz, name: "")
let utcPlus2 = newTimezone("", zonedTimeFromTime, zonedTimeFromAdjTime)
block timezoneTests:
let dt = initDateTime(01, mJan, 2017, 12, 00, 00, utcPlus2)

View File

@@ -11,20 +11,17 @@ import
proc staticTz(hours, minutes, seconds: int = 0): Timezone {.noSideEffect.} =
let offset = hours * 3600 + minutes * 60 + seconds
proc zoneInfoFromTz(adjTime: Time): ZonedTime {.locks: 0.} =
proc zonedTimeFromAdjTime(adjTime: Time): ZonedTime {.locks: 0.} =
result.isDst = false
result.utcOffset = offset
result.adjTime = adjTime
result.time = adjTime + initDuration(seconds = offset)
proc zoneInfoFromUtc(time: Time): ZonedTime {.locks: 0.}=
proc zonedTimeFromTime(time: Time): ZonedTime {.locks: 0.}=
result.isDst = false
result.utcOffset = offset
result.adjTime = fromUnix(time.toUnix - offset)
result.name = ""
result.zoneInfoFromTz = zoneInfoFromTz
result.zoneInfoFromUtc = zoneInfoFromUtc
result.time = time
newTimezone("", zonedTimeFromTime, zonedTImeFromAdjTime)
# $ date --date='@2147483647'
# Tue 19 Jan 03:14:07 GMT 2038
@@ -322,22 +319,7 @@ suite "ttimes":
parseTestExcp("-1 BC", "UUUU g")
test "dynamic timezone":
proc staticOffset(offset: int): Timezone =
proc zoneInfoFromTz(adjTime: Time): ZonedTime =
result.isDst = false
result.utcOffset = offset
result.adjTime = adjTime
proc zoneInfoFromUtc(time: Time): ZonedTime =
result.isDst = false
result.utcOffset = offset
result.adjTime = fromUnix(time.toUnix - offset)
result.name = ""
result.zoneInfoFromTz = zoneInfoFromTz
result.zoneInfoFromUtc = zoneInfoFromUtc
let tz = staticOffset(-9000)
let tz = staticTz(seconds = -9000)
let dt = initDateTime(1, mJan, 2000, 12, 00, 00, tz)
check dt.utcOffset == -9000
check dt.isDst == false