We could still define minutes, hours, etc like physics does:
year = 31557600 sec (julian)
day = 86400 sec
etc
As you've rightly pointed out the absence of those constants won't stop people from doing + 24*3600
kind of math.
We could still define minutes, hours, etc like physics does:
year = 31557600 sec (julian)
day = 86400 sec
etc
As you've rightly pointed out the absence of those constants won't stop people from doing + 24*3600
kind of math.
we could add days(_:)
, weeks(_:)
, etc. to a specific set of UnixDuration
/UnixInstant
/UnixClock
types, because when people do things like 24 * 3600
, they’re usually working with unix seconds where this kind of math is safe.
but it shouldn’t be on Duration
.
Obviously we should add .beats(_:)
as well.
I’m not too worried about leap seconds (so “minutes” and “hours” would be okay with me), but I am worried about Summer Time changes, in which some days have 25 hours and some 23 (and maybe more unusual changes in locales I don’t know about). In this case advancing by .days(1)
wouldn’t go from 11:00 to 10:59:59; it could go from 11:00 to 12:00. That’s a bit more of a mess.
There are certainly reasonable uses for “day” as a duration meaning “24 hours (of 60 minutes of 60 seconds)”, but I think it’s reasonable to omit it from the standard library—or perhaps include it but marked unavailable, so that people are reminded to think about time changes.
I’m surprised that this would cause any concern about defining the duration of a “day” in the standard library. Wouldn’t we define a day to be the amount of time between two consecutive calendar days in a single time zone? Leap seconds (which are a result of the earth’s rotational velocity being slightly variable) make sense to me as a potentially fatal obstacle to adding the definition of the duration of a day to the standard library, but the arbitrary political decision to sometimes change the time zone of a certain region seems like it would be irrelevant given the “in the same time zone” clause of the definition of the duration of a day (a definition which I admittedly just made up, but which I struggle to imagine is fundamentally wrong).
The concern is that people would try to use “now plus one day” to form an instant representing “tomorrow at the same time” for the purpose of, e.g., setting an alarm. If you get that wrong by a second due to leap seconds it’s probably not so bad, but getting it wrong by an hour could definitely have consequences. Users should use calendar-aware APIs for calculations that deal in “days” as a unit of time. You can define things however you want in the docs, but if we use the term “days” we need to confront the fact that “day” can signify a significantly different duration depending on the context.
To be clear, I’m talking about the transitions between “Standard Time” and “Daylight Time” (or “Daylight Saving Time”, or “Summer Time”), which are modeled as separate time zones in the ISO 8601 sense, but which (I think) people don’t usually think of as “time zone transitions”.
Fair enough. I think I’m convinced that the term “day” is overloaded enough that it’s good that the Duration
type doesn’t try to tell you exactly how long it is. If someone is coding an app that controls grow lights for plants and is therefore interested in the biological definition of a day and not the political one, they can easily add their own extension.
If a hour or day duration were added as API, it should probably be called unixHours
and unixDays
to ensure they can be easily distinguished from calendar aware API.
Leap seconds will also screw up the millisecond delta between two “days”.
This suggestion seems to be a clear API and could be used starting from minutes
, resulting in the existing names until seconds
, then continuing with unixMinutes
, unixHours
, unixDays
, and unixWeeks
. Thanks to the unix
prefix developers would immediately understand that something is different about these compared to seconds
and below.
If the documentation additionally mentions that these durations do not support leap seconds or summer time/winter time adjustments and should therefore not be used for calculating "the same time at a different day" and if the docs even mentioned how to do that correctly using a Calendar
, then this added API surface could even help prevent current misusage by doing the calculation manually by the developer.
Additionally, we could also add functions named minutes
, hours
, days
, and weeks
(and even months
and years
) that take a calendar system as a parameter and together with their unix
counterparts give developers full flexibility while at the same time communicating that anything above seconds is not exact anymore.
This would make calendar-based time calculation more discoverable. The docs could even state something like "calling this is equivalent to Calendar...
" teaching them about the other API right when they need to learn it. I'm a big fan of meeting people where they are, even if that means we have two ways of doing the same thing – as long as the "convenience" APIs simply call into the "source" ones. And I think it's very common and natural that people want to call something like Date.now.advanced(by: .days(3, calendar: .current))
rather than having to start with Calendar.current...
. This expresses the main intention first (moving the date by a certain duration) and the calendar becomes just a required implementation detail (a parameter), which I feel is more intuitive than the current API. Why not allow both?
just to be clear, this isn’t a “precision” issue, it’s a correctness issue. assuming that 24 * 3600
seconds equals 1
day is a bit like assuming that 4 * 7
days equals 1
month. as a first approximation, it’s pretty good, its useful for reasoning about month-level timeframes, and for some months like february in some years, it is correct. but in general, you don’t want to be doing
january14th.advanced(by: .days(28))
and expect to get february 14th as the result.
what could possibly happen in one measly second? (not trying to be flippant here either, a lot of this stuff is really highly leveraged and hedging the right amount at the wrong time can make a big splash.)
Your point about correctness is a good one, I agree with you. See my last comment: µPitch: add `Duration.nanoseconds(_:)` - #31 by Jeehut
i just feel that having general days(_:)
, weeks(_:)
, etc. as API on Duration
leads people to think that a “day” is a duration (like a second), and it is not.
here’s an analogy: 99% of the time a Character
consists of one UTF-8 byte, so we are often tempted to think of Character
s as having a stride of 1. but not all Character
s have a stride of 1 (in UTF-8), some of them have multi-byte encodings.
said another way, individual instances of Character
have byte lengths, but Character
as a type does not have a length, and it does not make sense to do something like
let a:String.Index
let b:String.Index = a.advanced(by: .characters(5))
this can be done in the context of a particular position in a particular string. but it cannot be done abstractly on a String.Index
value itself.
in my opinion, calendars are like unicode strings, and days are like unicode characters.
A day definitely is a duration. When I tweet "2 days until WWDC" I'm definitely talking about a duration. If that duration spans exactly 2 * 24 * 60 * 60 seconds is a different question. But it certainly is a duration.
I as the developer want clarity of use and correctness at the same time. If I write let duration = .days(3)
and then advance a date by that duration, the intention is pretty clear. But what I learned in this thread is that the result might not be what I was expecting, it might be off by up to an hour and a second. This I totally understand. But the consequence should not be "let's not define .days
at all then" because that will just lead to people writing code like .seconds(3 * 24 * 60 * 60)
.
What I want is a clear and correct API. Not providing one doesn't communicate anything to a developer other than "we don't help you here".
What I suggest is to provide APIs that are explicit, clear, and correct. If you have other suggestions how to achieve that, I'm happy to hear. But if you don't want to remove the Duration
type entirely, people will write manual calculation code. This is just the reality. And we can help them.
how about something like:
extension Calendar
{
func advance(date:Date,
by scalar:Int,
of unit:(some CalendarUnit).Type)
}
let calendar:Calendar = ...
let today:Calendar.Date = calendar.today()
let wwdc:Calendar.Date = calendar.advance(date: today,
by: 2,
of: Calendar.Days.self)
?
From my perspective Date
is more like a String
, and Duration
like String.Index
(when used as an offset). A Calendar
to me is basically the Encoding of a String
.
how can can a Date
(a point) be a String
(a “line”)? for me,
Date
is a String.Index
,String
, andDuration
is a UTF-8 distance.This would be analogous to:
let encoding: Encoding = ...
let text: Encoding.String = encoding.string("Hello, World!")
let worldSuffix: Encoding.String = encoding.suffix(string: text, startOffset: 7)
Not very simple to use, I think. Seems quite verbose for such a common thing. The encoding should be an implementation detail and the intention should be front and center, which is the String
, not the Encoding
. Same for Date
which should be front and center rather than Calendar
like you suggest. IMHO
You're right, I'm sorry, I wasn't precise. What I wanted to say is that a Range<Date>
is like a String
.
Hmm ... what I find really confusing about this analogy is that I can create any String I want, like "Hello, World!". But there are just a few well defined calendars and a user typically has a specific calendar set as their .current
. There's no such thing as String.current
. Doesn't even make sense. This is the big difference for me. And that's why to me Calendar.current
feels more like Encoding.UTF8
. There are different ones to choose from, but I don't create my own. And there's a "default" one.