Comments (64)
At a certain point, a lot of this becomes subjective. For example, the difference that @timrwood describes is similar to the difference between JSR310 and Time4J. Both models work, it's really just about which model we want to bake in to the language. We can make objective statements about pros and cons of each model, but we shouldn't be trying to figure out which one is more correct than the other. That would be futile.
from proposal-temporal.
API-wise, instead of a I would prefer b below:
// a)
let i = new Instant(1514764800000);
let zi = new ZonedInstant(1514764800000, 'America/Los_Angeles');
// b)
let i = new Instant(1514764800000);
let zi = new Instant(1514764800000, {timeZone: 'America/Los_Angeles'});
from proposal-temporal.
I would say that two ZonedInstants
in different time zones that reflect the same point in time are unequal - that's not necessarily what the user wants. So basically, what @apaprocki said.
from proposal-temporal.
There's massive value to the code clarity you get in differentiating between a point on the global timeline with no context, and a point for which we have local context. I'll have to come up with some larger code samples to clarify.
from proposal-temporal.
Because in practice both are so similar that is simpler to have one type?
It would be good if @maggiepint could proceed with her examples #42 (comment)
from proposal-temporal.
It seems like the above line of reasoning argues for a separate zoneless Instant class because defining rules for comparing zoned instant values is difficult, but a) those issues remain even with the separation, and b) so do issues around comparing zoned- to non-zoned instants, which then cross class boundaries. In other words, I don't think they hold water.
But for the record (and in anticipation of that other issue, which I believe is dependent upon this one), I would prefer operator-based relational comparison of instants (zoned or otherwise) to be strictly mathematical by time-since-epoch (e.g., zi2 < zi1
and zi3 >= zi4
and zi3 <= zi4
).
from proposal-temporal.
@timrwood one of the aims I have for these Objects is that they are neutral in terms of culture. (i.e. methods specific to a calendar do not belong on Instant
/ ZonedInstant
/ Zone
)
CivilDateTime (et al) are specific to a calendar and way of time-keeping. As such, I don't think Zoned
or Instant
should have any knowledge of them.
The consequence is that:
zoned.toCivilDateTime(); // BAD
That means it would mean for your proposal:
let civil = CivilDateTime.fromZoneInstant(zone, instant);
let instant = civil.withZone(zoned);
let zone = civil.withInstant(instant); // find the timezone where this Instant is that CivilDateTime
from proposal-temporal.
Passing things around as a string is harder to validate (both for correctness of the data, and correctness of "this is the right kind of thing"), harder to type for those using a type system, and harder to reason about.
from proposal-temporal.
Sure. It would give abilities like:
TimeZone.isValid('America/New_York') // true
TimeZone.isValid('America/Seattle') // false
TimeZone.isValid('-12:34') // true
TimeZone.validTimeZoneIds // ['SYSTEM', 'UTC', 'America/New_York', 'Asia/Tokyo', ...]
However, the last one is interesting because we are also allowing arbitrary time zone offsets like shown above it, and those wouldn't be in the list.
Either way, I think we can work on this for stage 3. It certainly could be a standalone object - it doesn't have to be passed as parameter. In other words,
const zdt = new ZonedDateTime('America/New_York', 2018, 1, 2, 3, 4, 5);
vs
const zdt = new ZonedDateTime(new TimeZone('America/New_York'), 2018, 1, 2, 3, 4, 5);
even if we have a TimeZone
object for validation and listing, I think I still prefer the first one for constructing a zdt.
from proposal-temporal.
valueOf
is only invoked when @@toPrimitive is undefined, but if it is invoked and throws, then the exception will propagate up: http://jsbin.com/vozipotufo/edit?js,console
from proposal-temporal.
var i = new Instant(1514764800000);
var zi = i.withZone('UTC');
var d = zi.toCivilDate(); // 2018-01-01
vs
var i = new Instant(1514764800000);
var d = i.toCivilDate(); // 2018-01-01
Ok, that makes sense, but is that always what we want to do?
var i = new Instant(1514764800000);
var zi = i.withZone('America/Los_Angeles');
var d = zi.toCivilDate(); // 2017-12-31
Without an interim object, we'd have two choices:
-
var i = new Instant(1514764800000); var d = i.toCivilDate('America/Los_Angeles'); // 2017-12-31
-
var zi = new ZonedInstant(1514764800000, 'America/Los_Angeles'); var d = zi.toCivilDate(); // 2017-12-31
So assuming you meant the second option, the downside is that two ZonedInstant
objects might have the same instant, but different time zones. In that case it would be difficult to apply equality and comparison operations to them.
var zi1 = new ZonedInstant(1514764800000, 'UTC'); // "2018-01-01T00:00:00Z"
var zi2 = new ZonedInstant(1514764800000, 'America/Los_Angeles'); // "2017-12-31T16:00:00-08:00"
How does one equate or compare these two objects?
With a separate Instant
object, we can offer an API that allows comparison and equality. We could then prohibit comparison operations on ZonedInstant
(always false, or throw, etc. TBD) but still allow equality if both timestamp and time zone matched.
from proposal-temporal.
Also, WRT option 1, I'd say that is not acceptable because it prohibits retaining time zone or offset information in the result of parsing a string that might contain such information.
from proposal-temporal.
We could prohibit comparison operations on ZonedInstance whenever the zone isn't "UTC" just as easily, could we not?
from proposal-temporal.
@mj1856 can you show me an example how to use temporal to check if date A in timezone X is equal to date B in timezone Y? For example, how to assert that 1:10PM in America/Los_Angeles means the same instant that 5:10PM in America/Sao_Paulo (both if we consider Jul 27, 2017 as date)?
from proposal-temporal.
@rxaviers For that specific comparison, you'd want a.toInstant() === b.toInstant()
. Personally, I wouldn't want to see equality on ZonedInstant
that implicitly coerces to UTC.
from proposal-temporal.
@apaprocki to be clear, you wouldn't use ===
, since the return values would be different objects. Right?
from proposal-temporal.
Note that Date
equality for either ==
or ===
doesn't work either. Should Instant
?
All below are true:
new Date(0) != new Date(0)
new Date(0) !== new Date(0)
+new Date(0) == +new Date(0)
+new Date(0) === +new Date(0)
new Date(0) <= new Date(0)
new Date(0) >= new Date(0)
new Date(0) < new Date(1)
new Date(1) > new Date(0)
from proposal-temporal.
Let's not operator overload :-)
from proposal-temporal.
@rxaviers - RE the two options you gave, I'd prefer a
, given the time zone is required. This is also being discussed in #41
from proposal-temporal.
@mj1856 how do I get a zoned date in the user's environment date please? (sorry if this was already discussed somewhere else)
from proposal-temporal.
What two options did I give?
from proposal-temporal.
@domenic - sorry, meant @rxaviers
from proposal-temporal.
Or did you mean that there would only be Instant
and time zone would be optional?
from proposal-temporal.
@mj1856 that seems much more reasonable to me than having two "types".
from proposal-temporal.
@mj1856 yes, your last comment :); specially depending on your answer for #42 (comment)
from proposal-temporal.
@maggiepint since timezone and "type" would only be present when constructing the instance (there's no type annotations elsewhere that would be relevant), I'd love to see the code examples for that.
from proposal-temporal.
@maggiepint me too, thanks.
from proposal-temporal.
As a strawman, you could replace ZonedInstant
with TimeZone
and make the user keep track:
const instant = new Instant(1514764800000);
const timeZone = new TimeZone('America/Los_Angeles');
// whichever you prefer, or both:
const civilDateFromTimeZone = timeZone.toCivilDate(instant); // 2017-12-31
const civilDateFromInstant = instant.toCivilDate(timeZone); // 2017-12-31
Of course, instant.toCivilDate('America/Los_Angeles')
as mentioned also works fine (unless there's some perf issue?).
One problem with this would be than you can't directly .plus()
it, would it ever be the case that instant.plus(x).toCivilDate(z)
would give different results to instant.toCivilDate(z).plus(x)
? Would that be problematic?
I'm guessing ZonedInstant
basically exists for having the thing to toString()
? This is a bigger issue, for sure, and would probably be enough for me to lean towards keeping it, but it should probably be trying to dovetail with Intl.DateTimeFormat
in some form, which itself handles timezone conversions. E.g.:
const format = new Intl.DateTimeFormat('en-AU', {
hour: 'numeric', minute: 'numeric', second: 'numeric',
timeZone: 'Australia/Sydney',
timeZoneName: 'short'
});
new Instant(1514764800000).toString(format); '11:00:00 AM AEDT'
I'm pretty sure you wouldn't want to combine the other format data into a ZonedFormattedInstant
, so maybe ZonedInstant
is not needed either?
I mention this largely because it's a bit of a weird concept for me to have a combined instant in time and a time zone: in my head time zones are what you use to convert between instants and local date and times!
from proposal-temporal.
Just to keep track... On #84 (comment), @gibson042 said:
I dispute that a separate type is critical for conveying that semantic (as opposed to e.g. nullable time zone data on a single Instant type), but the semantic itself does seem to matter and is not captured by setting time zone to UTC.
from proposal-temporal.
In terms of API, would it make sense to consider something like the below as an alternative to having two types?
const instant = new Instant(0);
instant.toString(); // "1970-01-01T00:00:00.000000000Z"
instant.timeZone; // null
instant instanceof Instant; // true
// Equivalent to new Instant(0, {timeZone: "UTC"});
const utcInstant = instant.withZone('UTC');
utcInstant.toString(); // "1970-01-01T00:00:00.000000000+00:00"
utcInstant.timeZone; // "UTC"
utcInstant instanceof Instant; // true
With respect to comparison, it could throw if comparing Instant
instances on different time zones, but compare normally if they are the same.
from proposal-temporal.
Why would ducktyping the value of a property be a better design than two distinct types, when "the zone doesn't matter" is a distinct thing from "it has a zone"?
from proposal-temporal.
It may be simpler from a technical perspective, but I think it's more complex from a mental model perspective, because people don't tend to think that a fundamental semantic of an object is represented by the value of a single property on it (and when they do, bugs often abound).
from proposal-temporal.
Time value is certainly fundamental to the model of an instant, but is time zone or even UTC offset? They're arguably more like decoration. Time zone can obviously influence comparison class methods, but in my opinion should have no effects on the operation of <
, >
, <=
, or >=
operators—instants, with or without zone information, are fundamentally temporal.
I'm open to persuasion on the merits of a separate class, but find the code samples currently in this issue to be unconvincing. What is an example that would be more difficult, complex, or confusing with zone-optional Instant as opposed to zoneless Instant and zone-mandatory ZonedInstant?
from proposal-temporal.
const zi1 = ZonedInstant.fromString("2018‑01-01T00:00:00-08:00[America/Los_Angeles]");
const zi2 = ZonedInstant.fromString("2018‑01-01T02:00:00-05:00[America/New_York]");
By their instants, zi2
is before zi1
by one hour. By their local times, zi1 is before
zi2` by two hours. Simply sorting the strings or their local times will give a different result than sorting their instants.
I tend to agree that if we have to choose one or the other, we chose the instant. That is one of the benefits of calling it a "zoned instant". However there are cases that are less clear:
const zi3 = ZonedInstant.fromString("2018‑01-01T00:00:00-08:00[America/Los_Angeles]");
const zi4 = ZonedInstant.fromString("2018‑01-01T03:00:00-05:00[America/New_York]");
Both zi3
and zi4
have the same instant, even though the two objects aren't "equal". So which comes first in a sort? What does comparing them return? It's much less clear what to do.
from proposal-temporal.
Instant (and ZonedInstant) should compare by their instants, otherwise user could transform them to Civil (or Gregorian, or Local, or whatever) and use that to compare by their local/unzoned times. Right?
from proposal-temporal.
Yes, but having a separate Instant
type eliminates any potential ambiguity, such as when zi3
and zi4
above are representing the same instant in two different zones.
(Even that they should be compared by their instants is not something APIs in other languages agree on, by the way.)
from proposal-temporal.
Thanks. Should we isolate Instant
and ZonedInstant
comparison behavior in a separate issue?
from proposal-temporal.
I kind of talked around this earlier, but I don't think "ZonedInstant" is a well defined term. Time zones get updated all the time, so if you have an ZonedInstant that describes a zoned local time that now refers to a new point in time, is it really an instant?
If ZonedInstant was renamed to e.g. Timestamp, meaning "the time described by this string" vs Instant meaning "this point in time", does that help?
from proposal-temporal.
@simonbuchan first things first :). Let's focus this issue on identifying the reasons for a separate class. If the separate class is indeed needed, we can discuss (preferably elsewhere) its naming. Sounds reasonable?
from proposal-temporal.
Summary so far...
We need a separate class because:
- Comparison between instants with timezone context is ambiguous
- Counter arguments:
- The ambiquity remains even with two classes (we need an example here)
- To avoid ambiguity comparison of instants (zoned or otherwise) should be strictly mathematical by time-since-epoch
- We need to define comparison
- Counter arguments:
- Subjective or unclear statements that I am ignoring (feel free to correct me or clarify)
Single class (timezone as option/decorator) API proposal
Related conclusions:
- No timezone !== UTC timezone.
- Is it because of leap seconds?
from proposal-temporal.
Subjective statements weigh heavily and can’t be ignored; this is language design, not a math problem. Separately, the claim “to avoid ambiguity ... should be” is quite subjective.
from proposal-temporal.
Above, "ambiguity" is very well defined by @mj1856 in his example. And "to avoid it" using "mathematical by time-since-epoch" is another well defined statement. There's no subjective here. This can be detailed if needed.
from proposal-temporal.
Subjective statements are always debatable. In the other hand, objective statements aren't.
from proposal-temporal.
Here's my debate to that objective statement: #42 (comment) was a subjective statement of preference; being well-defined isn't the point. #42 (comment) isn't a counter argument to having two types, it's an argument for it.
Separately, the debatability of statements doesn't mean subjective statements aren't still weighed heavily in the standardization process.
from proposal-temporal.
I think the main use case of ZonedInstant
is to convert back and forth between an Instant
and a DateTime
in a specific time zone.
What if we dropped the Instant
part of ZonedInstant
and instead had a Zone
object that knew how to convert an Instant
to a DateTime
and back? Here is how that could change the example in the readme.
let chicagoZone = new Zone('America/Chicago')
let sydneyZone = new Zone('Australia/Sydney')
let dateTimeInChicago = new CivilDateTime(2000, 12, 31, 23, 59)
let instant = chicagoZone.instantAt(dateTimeInChicago)
let dateTimeInSydney = sydneyZone.dateTimeAt(instant)
dateTimeInChicago.toString() // 2000-12-31T23:59:00.000000000
dateTimeInSydney.toString() // 2001-01-01T16:59:00.000000000
from proposal-temporal.
"Simpler mental model perspective" - this is based on preference, what could be simpler for you, could be confusing for me. 🤷♂️ - @ljharb sorry, I won't debate preferences
from proposal-temporal.
@timrwood, thanks for the example, but it's not clear to me how it helps define the need for both classes (instead of one). Using today's spec your example would be written as the below, right?
let dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
// Instant considering dateTime is based in Chicago:
let instant = dateTime.withZone('America/Chicago');
let dateTimeInSydney = instant.withZone('Australia/Sydney').toLocalDateTime();
// ... or alternatively:
// let dateTimeInSydney = new ZonedInstant(instant, 'Australia/Sydney').toCivilDateTime();
dateTime.toString(); // "2000-12-31T23:59:00.000000000"
dateTimeInSydney.toString(); // "2001-01-01T16:59:00.000000000"
If instead we follow the single class proposal above, nothing changes...
from proposal-temporal.
(More on this difference in the Time4J docs)
from proposal-temporal.
@rxaviers, you seem to have missed the point of my comment, sorry. I don't care what the name is except for what it implies about the concept, and I think the concept of "instant + time zone" is wrong, which is what is causing the disagreement in this thread.
My suggestion was to say "do I still want to compare by point in time if this type doesn't have instant in the name?"
Given your example above, I would not expect CivilDateTime#withZone()
to return a ZonedInstant
, but rather, perhaps, ZonedDateTime
, and for that to have a #toInstant()
that is comparable for point in time comparisons. I would guess ZonedDateTime
would be either incomparable or lexicographically compared (roughly, compare iso strings)
Does that make it clearer?
from proposal-temporal.
@simonbuchan thanks for the clarification. Therefore, I understand you're challenging where "timezone" should be: In *Instant*
or in *DateTime*
? My opinion: (a) I agree your question have impact to what we are discussing here, (b) but anyway, I encourage we don't mix topics but discuss this separately (perhaps reusing #84). Sounds reasonable?
from proposal-temporal.
@simonbuchan - Would you please restate your specific objection with "instant + time zone" is wrong? I think I'm following you, just want to be super clear. Thanks.
from proposal-temporal.
@timrwood and @simonbuchan have pitched similar proposals here
and here that are interesting alternatives.
I'm not completely opposed, but the questions I have with that model are:
- Why do we need an object at all? Why not pass the time zone id as a string?
- How would operations like
plus
work accross DST transitions? - How would one obtain a time zone offset or a datetimeoffset like string?
from proposal-temporal.
To my mind, the debate on whether we need ZonedInstant
at all is one of those things that will boil down to developer preference every time, and developer preference will vary across everyone.
At the end of the day, there is a lot of gravity toward a ZonedInstant
type object (regardless of name), in Java, and in several other datetime libraries around. That being said, I feel like existing forward momentum to have the object overrides other concerns.
from proposal-temporal.
I will note tho; if we had a Zone
object, then i wouldn't feel the need to have "ZonedFoo" objects - or at least, those objects could have a nullable/Zone zone
field, and I think i'd be content with that.
from proposal-temporal.
While a Zone
(or TimeZone
) object might add some validation, I don't think it's absolutely required. Anywhere we need a time zone, we can pass as a string.
We're not talking about having a variety of "zoned" objects. There is only one, and it is currently ZonedInstant
, and might possibly be renamed to ZonedDateTime
, but having a Zone
object doesn't necessarily solve all of the problems such that we could remove the other ZonedInstant
/ZonedDateTime
object. If anything, we'd have to replace it with an OffsetDateTime
object, and then we'd be passing zone objects around to various functions. I'm not sure the trade off is worth it.
from proposal-temporal.
A TimeZone object would be capable of normalizing input and/or providing validation in a way that strings would not, without also needing to create an arbitrary date and/or time to associate with it (and implicitly get the zone validation in the process).
from proposal-temporal.
The ambiquity remains even with two classes (we need an example here)
val = 1538058600000000n;
unzoned = new Instant(val);
zoned1 = new ZonedInstant(val, 'Pacific/Auckland');
zoned2 = new ZonedInstant(val, 'America/New_York');
[
zoned1 > unzoned,
zoned1 < unzoned,
zoned2 > unzoned,
zoned2 < unzoned,
zoned1 > zoned2,
zoned1 < zoned2
];
// → [?, ?, ?, ?, ?, ?]
from proposal-temporal.
We are removing valueOf
, so >
and <
will not work. We thus have ability to create our own comparison operators if/when necessary.
from proposal-temporal.
@mj1856 since toString will still be there, <
and >
would work fine as string comparisons.
(agreed that even with a TimeZone object, we'd want to accept strings or TimeZone objects anywhere in the API that took either)
from proposal-temporal.
Except that a) string comparison is only useful for comparing temporal instants that use exactly the same time zone designator, and b) abstract relational comparison strongly prefers numbers anyway (toString
is only invoked after failing to find both @@toPrimitive and valueOf
).
from proposal-temporal.
The hint isn't helpful unless we make valueOf
or Symbol.toPrimitive
throw (which is an option).
from proposal-temporal.
@ljharb - Thanks for clarifying that point. So if valueOf
throws, then string comparison does not happen? I assume there is an order of precedence?
from proposal-temporal.
I opened #92 for discussion around relational comparison, even though it is somewhat intertwined with this discussion.
from proposal-temporal.
For better or worse, things seem to have stabilized around keeping ZonedInstant a distinct class from Instant (and also adding one for TimeZone). We can reopen or start a new issue if someone wants to raise this banner again.
from proposal-temporal.
Related Issues (20)
- Consistent argument order for "timeZoneRec" and "calendarRec" HOT 1
- DifferencePlainDateTimeWithRounding + DifferenceZonedDateTimeWithRounding: Align argument order for "resolvedOptions" HOT 1
- DifferenceZonedDateTimeWithRounding: precalculatedPlainDateTime can be undefined HOT 1
- Temporal.Duration.prototype.total: Unnecessary assertion for roundRecord.[[Total]] not being UNSET HOT 1
- Temporal.Duration.prototype.{round,total}: Is CreateTemporalDuration really fallible when creating from balanced valid Duration?
- NudgeToCalendarUnit: CreateTemporalDate is fallible HOT 5
- NudgeToZonedTime: Bad named character reference for "≠"
- NudgeToZonedTime: Incorrect assertion for user-defined time zones or calendars HOT 1
- NudgeToDayOrTime: "remainder" not converted to Normalized Time Duration Record
- BubbleRelativeDuration: Wrong operator for loop condition HOT 1
- AddDate: CreateTemporalDate is fallible
- does `ZonedDateTime.from` with `offset: reject` always throw when the datetime is in a DST transition gap? if so, is that intended?
- NudgeToCalendarUnit: Wrong roundedSign for (roundedUnit - total == 0)? HOT 1
- NudgeToDayOrTime: Incorrect assertion in step 1
- Polyfill: Buddhist, Ethiopic-Amete-Alem, Indian, Islamic, and Persian calendar return non-undefind era HOT 3
- CalendarFieldDescriptors: Require eraYear to be alway positive? HOT 8
- Polyfill: Allow alternate era codes
- RoundRelativeDuration: Disallow edge case in roundingIncrement within year/month HOT 5
- SystemDateTime, SystemZonedDateTime: "calendarLike" argument is always constant string "iso8601"
- Polyfill : undefined weekOfYear and yearOfWeek with calendarId = "islamic-civil" HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from proposal-temporal.