@pipobscure and I had a long conversation at the TC39 dinner, and looped in @fabalbon, @sffc, and @littledan after the meeting. Here is an unorganized list of the resulting conclusions of relevance to this proposal.
getSystemTimeZone function or systemTimeZone Symbol).OffsetDateTime.fromString rejects input with a bracketed time zone, since it only cares about offset and cannot necessarily verify consistency (e.g., because the implementation doesn't have time zone data). For example, OffsetDateTime.fromString("2019-04-02T22:58:36.123456789-04:00[America/New_York]") does not result in a successful parse.ZonedDateTime.fromString _requires_ a bracketed time zone and consistency between that value and the offset. For example, ZonedDateTime.fromString("2019-04-02T22:58:36.123456789-05:00[America/New_York]") does not result in a successful parse because the UTC offset and time zone are inconsistent at the given instant.Date, fromString functions accept reduced precision values, e.g. CivilTime.fromString("10:23") and OffsetDateTime.fromString("2019-04-03T02:30Z") and maybe CivilDate.fromString("2019-04").{milli,micro,nano}Second are consistently treated as distinct integer-valued elements in all types and signatures, and always representing time since the rollover of the immediately larger element.plus(Duration [, disambiguationRule])/minus(Duration [, disambiguationRule]), and—if we include it—difference($compatibleType [, largestElementInResult])). plus/minus cast input to Duration; difference (name TBD) _returns_ a Duration.plus({months: 1.5}).zoned.instant), or of inheritance (zoned.epochSeconds/zoned.epochMilliseconds/etc.)?offsetSeconds and offsetString, as shown below? If so, which should be used when "with"-extending CivilDateTime and Instant to include an offset (e.g., withOffset(number) vs. withOffset(string) vs. withOffsetSeconds(number) vs. withOffsetString(string))?CivilYearMonth.prototype.withDay(number), CivilMonthDay.prototype.withYear(number), {CivilDateTime,Instant}.prototype.withOffset*(number|string), and withTimeZone(string).new Duration({ nanoseconds: Number.MAX_SAFE_INTEGER }) is only 104 days.withZone? https://github.com/tc39/proposal-temporal/issues/84#issuecomment-497245032class diagram source
CivilYearMonth <|--> CivilDate : withDay({day})
CivilMonthDay <|--> CivilDate : withYear({year})
Duration --> Duration : plus({year, month, day, hour, minute, …})
CivilDate <|--> CivilDateTime : withTime({hour, minute, second, …})
CivilTime <|--> CivilDateTime : withDate({year, month, day})
CivilDateTime <|--> OffsetDateTime : withOffsetSeconds({offsetSeconds})
Instant o--> OffsetDateTime : withOffsetSeconds({offsetSeconds})
OffsetDateTime <|--> ZonedDateTime : withTimeZone({timeZone})
CivilDateTime --> ZonedDateTime : withTimeZone({timeZone})
Instant --> ZonedDateTime : withTimeZone({timeZone})
CivilYearMonth : Number year
CivilYearMonth : Number month
CivilMonthDay : Number month
CivilMonthDay : Number day
CivilDate : Number weekOfYear
CivilDate : Number dayOfWeek
CivilDate : Number dayOfYear
CivilTime : Number hour
CivilTime : Number minute
CivilTime : Number second
CivilTime : Number millisecond
CivilTime : Number microsecond
CivilTime : Number nanosecond
OffsetDateTime : Instant instant
OffsetDateTime : Number offsetSeconds
OffsetDateTime : String offsetString
ZonedDateTime : String timeZone
Instant : Number epochSeconds
Instant : Number epochMilliseconds
Instant : BigInt epochMicroseconds
Instant : BigInt epochNanoseconds
Duration : Number years
Duration : Number months
Duration : Number days
Duration : Number hours
Duration : Number minutes
Duration : Number seconds
Duration : Number milliseconds
Duration : Number microseconds
Duration : Number nanoseconds
Thanks for the summary. From a couple conversations afterwards:
Intl.DateTimeFormat at length, but later, @domenic, pointed out that this isn't really compatible with the idea of lazy-loading built-in modules. So, now I'm thinking that this would make more sense as a separate formatter, e.g., the export from std:temporal could be called IntlTemporalFormat, and work generally the same as Intl.DateTimeFormat (even share options processing spec text, except for ignoring the timeZone). As we discussed previously, the idea of having a single IntlTemporalFormat instance supporting several different Temporal data types seems right. Internally, there would be one pattern per Temporal type that could be formatted. The types that are supported would initially be CivilYearMonth, CivilMonthDay, CivilDate, CivilTime, CivilDateTime, and ZonedDateTime.Date.now, and so it can be higher precision) into a separate module. @erights and I discussed this as an alternative to the System object for such things. It could be good for mocking in tests, e.g., in conjunction with import maps or Node module hooks, so you don't have to remap the whole module, only these parts. As a name, I'd suggest something like std:temporal/now, with two exports: now() returning an Instant, and timeZone() returning an IANA timezone string.The above diagram seems like a great explanation to me, but I worry that it makes things look more complicated than they really are to someone who's not as deep in this problem space. I hope we can work in the next months on developing intuitive explanations and documentation. The intuition is pretty straightforward: You look at the data you have about the date/time, and then find the type that logically represents that. As your diagram explains, the methods are generally analogous between different types that contain subsets of each others' data.
Is there a need to support CivilDate[Time] vs OffsetDate[Time]? Could they be collapsed into 1, considering OffsetDateTime with an offset of 0 is the same as CivilDateTime? I understand that there are some conceptual boundaries here (e.g. OffsetDate doesn't make much sense) but it seems as @littledan points out that the amount of classes can become confusing. Another way to frame this could be that CivilDateTime could simply have an Number offset property which defaults to 0, forgoing any need for Offset*.
Is there a need to support
CivilDate[Time]vsOffsetDate[Time]? Could they be collapsed into 1, consideringOffsetDateTimewith an offset of0is the same asCivilDateTime?
That collapsing doesn't work, because OffsetDateTime with offset 0 is _not_ the same as CivilDateTime—the former has a fixed position on the UTC timeline, while the former exists in an abstract calendar space divorced from it.
I understand that there are some conceptual boundaries here (e.g.
OffsetDatedoesn't make much sense) but it seems as @littledan points out that the amount of classes can become confusing. Another way to frame this could be thatCivilDateTimecould simply have anNumber offsetproperty which defaults to0, forgoing any need forOffset*.
I made the same argument about collapsing together ZonedDateTime and Instant by means of nullable offsetSeconds/offsetString and timeZone, but was ultimately convinced by the corresponding complexity in method implementation (e.g., having to use different output formats in serialization based upon the presence vs. absence of certain data) that separation was better. I now believe that each class in the above diagram carries its own weight.
it is as complicated as it looks. i don't think this gives a better user-experience than Date. in fact, its less optimal in solving many ux-workflow problems, because Date at least grounds everything in tangible utc-time -- none of this [low-level] abstract-calendar CivilDateTime conversion cr*p.
perhaps the scope of this proposal should be narrowed to focus only on utc-time (_and standardizing timezone-conversion to/from utc in javascript_), which is were most real-world painpoints dealing with datetime arise.
I made a new issue to split off the Intl discussion.
I don't think type.difference(compatibleType [, largestElementInResult]) is necessary. It seems like that could instead be a specific constructor of the Duration type. ex. new Duration(type, compatibleType [, largestElementInResult]).
@yay295 agreed. It’s actually something we can take from the types.
Are you both saying the same thing? @Yay295 seems to be suggesting the removal of a difference method in favor of just the Duration constructor, which a) doesn't make sense because one would expect Duration to be constructed from Duration-like data (e.g., {days: 4}) rather than from a pair of other entities, and b) would call into question the with<Element(s)> methods, which are largely also just wrappers around a different-class constructor (instant#withZone(timeZone) is basically just return new ZonedDateTime(this, timeZone), Date#withTime(timeLike) is basically just return new DateTime(this.year, this.month, this.day, timeLike.hour, timeLike.minute, …), etc.). If we have any such convenience methods at all, then difference should make the cut.
No. It’s just that Duration should be creatable and we don’t need “largest unit” argument, since it can be derived from the type.
So yes difference is still key!
Are you in NYC tonight? Drinks & chat?
The difference between the difference method and the with<Element(s)> methods is that the difference method returns a completely different type while the with<Element(s)> methods return what is effectively a superclass of the current type.
Having Duration(type, compatibleType [, largestElementInResult]) does not preclude a different constructor that could take "Duration-like data". new Graduation({days: 4}) would be the way to create a specific duration without context, while new Duration(type, compatibleType) would be the way to create a duration from the difference of two other types. I suppose Duration.fromDifference(type, compatibleType) could be another way to write it if you want to go with the factory-style approach.
As for them being convenience methods,
new Duration(a,b) is a difference of two characters
a.difference(b), but the best you can do with a with<Element(s)> method
new ZonedDateTime(a,b)
a.withTimeZone(b) is a difference of five characters, and it only gets worse
new CivilDate(a.year, a.month, b.day)
a.withDay(b). Of course
Duration.fromDifference(a,b) vs
a.difference(b) would be a more appreciable convenience.
That said, I know there is precedence for odd methods like this (ex. Java's .equals), so I'm not trying to absolutely push for this to change.
I agree that largestElementInResult is unnecessary, though perhaps the opposite would be useful as a method. Basically a Math.round method for Durations. So if you got the difference between two times, but don't need/want nanosecond precision, you could round it to the nearest second instead.
I would also argue that the plus/minus methods shouldn't cast their input to a Duration. If I have a specific date and add five days to it, I would expect to get a new date that is five days later than my starting date, not... a Duration of five days? What Duration is this actually supposed to return?
I would also argue that the plus/minus methods shouldn't cast their input to a Duration. If I have a specific date and add five days to it, I would expect to get a new date that is five days later than my starting date, not... a Duration of five days? What Duration is this actually supposed to return?
this matches what i'd expect; "plus" and "minus" should produce the same type; something that produces a duration would be like "fromNow" or "untilNow" or something. What does the current spec do?
This conversation feels very weird. For one thing there seem to be lots of misunderstandings of what methods do and what they take:
obj.difference(other) - takes an object of the same type and returns a Duration
obj.plus(duration) - takes something that’s like a Duration (Duration, object with the fields of a Duration, ISO-8601 duration). It returns something of the same type as the original object
obj.minus(duration) - same input/output types as .plus()
Now as to a Duration constructor that accepts two objects if the same of the same type: I think that’s a really bad anti-pattern. (Convince me of why it’s a good idea in a new issue, if you want)
I misread how plus/minus work, so that's my bad; it does what I thought it should.
obj.difference(other) - takes an object of the same type and returns a Duration
This was not clear before. The first post here only says it takes a compatibleType, not specifically the same type. I can see how getting the time difference between a CivilYearMonth with a CivilMonthDay wouldn't work, but it seems like you should be able to get the time difference between a CivilYearMonth and a CivilDate.
How would that work? A Temporal.YearMonth has no day. You can however do
date.getYearMonth().difference(yearMonth)
so it’s easy to do, you just have to be explicit
let date = new CivilDate(2019, 10, 1);
let year_month = new CivilYearMonth(2019, 11);
console.assert(date.difference(year_month).months === 1);
Assuming items are compared by their start. This doesn't seem to be specified though. Does it compare the starts, the ends, the time between the start and end, or the time between the end and start? These all give different answers, and actually it might be useful to have that as an option as to what range it returns. In this way it's possible to compare any two types as long as their largest unit is the same.
Also, I just noticed this:
Duration objects are produced by subtracting two temporal object from each other using the
minus()method.
So according to the current documentation, plus and minus do not return the same type as the type they are called on. Though also here:
Creates a new
OffsetDateTimeobject by subtracting values to its members.
So it seems like it needs some cleaning.
Absolutely: cleaning required!
(The canonical is currently the polyfill, because we are trying to work kinks out which is easiest in code)
A YearMonth represents the entire month. So the ym.difference(date) would require us to choose which end to take the difference from. We don’t want to make this kind if arbitrary choice.
So instead be explicit:
ym.withDay(1).difference(date)
or
ym.withDay(31).difference(date)
or
date.getYearMonth().difference(ym)
all are explicit and clear. So the mixing of objects for difference() is not justified given that it implies a host of unclear assumptions.
also the minus() from that part of the documentation stems from a time when we thought the could be negative Durations. However plus/minus actually require different algorithms.
So there now are:
obj.difference(obj) => duration
obj.plus(duration) => obj
obj.minus(duration) => obj
A YearMonth represents the entire month.
So would
let a = new CivilYearMonth(2019, 1);
let b = new CivilYearMonth(2020, 1);
console.log(a.difference(b).months);
log 11 then, if it's getting the time from the end of the a to the start of b?
Nope: 0 because a.difference(b).years === 1
All of this is dealt with