5

I’m struggling with JavaScript’s proposed new Temporal API. What I am trying to do should be straight-forward, yet I fail to find a convincing solution. I must be missing something.

The task is as follows: instantiate an object representation of an UTC datetime from variables for year, month, day, hour and minute.

My thinking is as follows:

  • we are talking UTC so I need a Temporal.Instant;
  • new Temporal.Instant() requires the timestamp in nanoseconds so that doesn’t work;
  • Temporal.Instant.from() requires a ISO datetime string, which would require me to generate a properly formatted piece of text from the five variables I have — this is possible but a bit of a hack and kinda defeating the purpose of using a datetime library;
  • Temporal.PlainDateTime.from() has the right design, as it accepts an object like { year, month, day, hour, minute };
  • so then all we need to do is creating an Instant from this PlainDateTime. This does not seem to be possible though? Other than through — once again — a datetime string or a timestamp in ns…?

This is silly! The use case here is super basic, and yet it’s not obvious (to me) at all how to address it.

I was expecting to be able to simply do something like: Temporal.Instant.from({ year, month, day, hour, minute });

Now the best I can come up with is: Temporal.Instant.from(year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0') + 'T' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + 'Z'); //

Please tell me I’m bigtime overlooking something.

Justin Grant
  • 44,807
  • 15
  • 124
  • 208
Tim Molendijk
  • 1,016
  • 1
  • 10
  • 14
  • I don't see what's wrong with `Temporal.Instant.from()`. If you want to hardcode an instant, it seems to be exactly the way to go. – Bergi Oct 23 '22 at 01:35
  • You might use a *zonedDateTime* with timezone "UTC" or "Etc/GMT+0". Convert to an instant using *zonedDateTime.epochNanoseconds*. But there's not much difference between building an ISO string for the instant you want and building one object to convert it to another object. You could even use `Date.UTC(...)*1e3` to get nanoseconds since epoch (where `...` is values parsed from the input timestamp). – RobG Oct 23 '22 at 11:44
  • @Bergi Fair point. But let’s assume I don’t want to hardcode the datetime, but instead I have variables for year, month, day, hour, minute. – Tim Molendijk Oct 23 '22 at 11:44

2 Answers2

6

Your PlainDateTime represents "a calendar date and wall-clock time that does not carry time zone information". To convert it to an exact time, you need to supply a timezone, using the toZonedDateTime method. By then ignoring calendar and timezone via the toInstant method, you can get the desired Instant instance.

So there's a few ways to achieve this:

  • Create a PlainDateTime from an object, convert it to an instant by assuming UTC timezone:

    Temporal.PlainDateTime.from({year, month, day, hour, minute}).toZonedDateTime("UTC").toInstant()
    
  • Create a PlainDateTime using the constructor, convert it to an instant by assuming UTC timezone:

    new Temporal.PlainDateTime(year, month, day, hour, minute).toZonedDateTime("UTC").toInstant()
    
  • Create a ZonedDateTime directly from an object, providing the timezone in there, then convert it:

    Temporal.ZonedDateTime.from({timeZone: 'UTC', year, month, day, hour, minute}).toInstant()
    
  • Instead of going via a zoned datetime, you can also get the instant that a TimeZone instance ascribes to a PlainDateTime object:

    Temporal.TimeZone.from("UTC").getInstantFor(Temporal.PlainDateTime.from({year, month, day, hour, minute}))
    new Temporal.TimeZone("UTC").getInstantFor(new Temporal.PlainDateTime(year, month, day, hour, minute))
    
  • If you wanted to hardcode the instant in your code, you could also directly create it from an ISO string:

    Temporal.Instant.from("2022-10-23T02:50Z")
    
  • If you are open to including the old Date methods, you could also use Date.UTC to compute the millisecond value for the instant - beware zero-based months:

    Temporal.Instant.fromEpochMilliseconds(Date.UTC(year, month-1, day, hour, minute));
    

Try them for yourselves with your particular example:

const year = 2022;
const month = 10;
const day = 23;
const hour = 2;
const minute = 50;

log(Temporal.PlainDateTime.from({year, month, day, hour, minute}).toZonedDateTime("UTC").toInstant());
log(new Temporal.PlainDateTime(year, month, day, hour, minute).toZonedDateTime("UTC").toInstant());
log(Temporal.ZonedDateTime.from({timeZone: 'UTC', year, month, day, hour, minute}).toInstant());
log(Temporal.TimeZone.from("UTC").getInstantFor(Temporal.PlainDateTime.from({year, month, day, hour, minute})));
log(new Temporal.TimeZone("UTC").getInstantFor(new Temporal.PlainDateTime(year, month, day, hour, minute)));
log(Temporal.Instant.from("2022-10-23T02:50Z"));
log(Temporal.Instant.fromEpochMilliseconds(Date.UTC(year, month-1, day, hour, minute)));
<script src="https://tc39.es/proposal-temporal/docs/playground.js"></script>
<script>function log(instant) { console.log(instant.epochSeconds); }</script>
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • This one is not too bad: `Temporal.ZonedDateTime.from({timeZone: 'UTC', year, month, day, hour, minute}).toInstant()` – Tim Molendijk Oct 23 '22 at 15:51
  • Although the “UTC” aspect is effectively alluded to twice, which is not the most elegant, but ah well. I suppose I’m asking for too much here. :) – Tim Molendijk Oct 23 '22 at 15:58
  • What do you mean by "*alluded to twice*"? – Bergi Oct 23 '22 at 16:00
  • Well, first I specify it when creating the `ZonedDateTime`: `Temporal.ZonedDateTime.from({timeZone: 'UTC', year, month, day, hour, minute})`. And then I basically specify it again by converting it into an `Instant`: `.toInstant()`. I think the thing that confuses me is that as far as I understand `Instant` is simply a datetime in UTC, which makes me expect the APIs on `ZonedDateTime` and `Instant` to be very similar. But they aren’t. They are actually very different. (Different constructor, no `add` or `subtract`, etc.) – Tim Molendijk Oct 23 '22 at 18:11
  • 1
    No, an instant is not a datetime in utc. There's no date, there's no time, there's no timezone - it's just a timestamp. On which you could add/subtract absolute durations (in milliseconds), but which you can't do any calendar-based math. The docs write: "*An `Instant` is a single point in time (called "exact time"), with a precision in nanoseconds. No time zone or calendar information is present.*" Maybe this is not what you actually want? – Bergi Oct 23 '22 at 19:56
  • The above comment is correct. If your data model includes years, months, days, or a time zone even if it's UTC, then Instant is not the type you should be using. It's only for timestamps. – ptomato Oct 23 '22 at 20:14
  • 1
    There's intentionally no method that converts directly between PlainDateTime and Instant, because we found that whenever we wanted to use it, it was almost always a sign that we were misusing either the Instant or PlainDateTime type and should have used ZonedDateTime instead... (If you really need it, you can go via an intermediate ZonedDateTime, or use TimeZone's getInstantFor/getPlainDateTimeFor methods) – ptomato Oct 23 '22 at 20:14
  • @ptomato Thanks, I had forgotten about `getInstantFor` - added to the answer – Bergi Oct 23 '22 at 20:42
  • Thanks for the explanations! I must admit that I find this concept of “timestamp that has no date or time” hard to grasp. A single point in UTC time has a year and a month, etc, even a time zone, just like any other time… ?? And a zoned datetime is no less exact than a timestamp?? – Tim Molendijk Oct 23 '22 at 22:28
  • @TimMolendijk A single point in UTC has date and and time *in UTC*. Yes, a `ZonedDateTime` no less exact than an `Instant`, they both can represent the same exact point in time, but the `ZonedDateTime` contains the used timezone as extra information (even if it's just "UTC" which you might consider as a default). But an `Instant` does not, it is part of many different date&times in different timezones, it just doesn't say where it belongs. – Bergi Oct 23 '22 at 22:33
  • @Bergi Well, `Instant` says that it belongs in UTC. Which I don’s see how it negates calendering operations. The data I am working with is explicitly in UTC, as regional time zones are irrelevant to it. So that would suggest that I should use `Instant`, would you agree? But at the same time, I have clear calendering requirements on this data, for example: I take a UTC timestamp from my data, and I want to generate the timestamp for the point in time that comes exactly one month later. So now suddenly that would suggest I should use `ZonedDateTime`. And that’s how I ended up posting here. :) – Tim Molendijk Oct 23 '22 at 22:56
  • @TimMolendijk "*`Instant` says that it belongs in UTC*" - no it doesn't, an instant does not belong to any particular timezone, that's the whole point. It's a nanosecond offset to some arbitrary (but agreed-on) fixed epoch, nothing more. – Bergi Oct 23 '22 at 23:03
  • @TimMolendijk "*The data I am working with is explicitly in UTC*" - that would suggest you should use `ZonedDateTime`s that explicitly always use the UTC timezone. It would not suggest the usage of `Instant`s, or stronger: it would suggest not to use `Instant`s - what you want are UTC dates. – Bergi Oct 23 '22 at 23:04
  • @Bergi Alright, clear. For your information, this is the article (and section) that I had based my view on: https://2ality.com/2021/06/temporal-api.html#the-core-classes-of-the-temporal-api. I suppose it is somewhat misleading. – Tim Molendijk Oct 23 '22 at 23:16
  • 1
    @TimMolendijk Ah, yes, sentences like "*Class `Instant` represents global exact time. Its time standard is UTC*" are definitely misleading. I would have preferred to write "*For some operations (such as `.toString()`), `Instant` internally uses an ISO-8601 calendar and the UTC time zone, but those are not stored in instances.*" Notice that [the timezone is an argument to the `toString` method](https://tc39.es/proposal-temporal/docs/instant.html#toString), UTC is just the default if not passed. – Bergi Oct 23 '22 at 23:44
0

The Date API will continue to exist forever and is pretty handy so it seems sensible to use it if it helps.

Date.UTC returns the number of milliseconds since the ECMAScript epoch. Temporal.instant requires nanoseconds, so given an input object with {year, month, day, etc.} the only hassle is to deal with the nanoseconds using BigInt, which is not hard:

// Object for 7 December 2022
let d = {
  year: 2022,
  month: 12,
  day: 7,
  hour: 3,
  minute: 24,
  second: 30,
  millisecond: 10,
  microsecond: 3,
  nanosecond: 500
}

// Convert to time value - nanoseconds since ECMAScript eopch
let timeValue = BigInt(Date.UTC(d.year, d.month-1, d.day, 
  d.hour, d.minute, d.second, d.millisecond)) * BigInt(1e6) +
  BigInt(d.microsecond * 1e3) + BigInt(d.nanosecond);

// 1670383470010003500
console.log(timeValue.toString());

// 2023-01-07T03:24:30.010Z
console.log(new Date(Number(timeValue/BigInt(1e6))).toISOString());

An instant can then be created as:

let instant = new Temporal.Instant(timeValue);
RobG
  • 142,382
  • 31
  • 172
  • 209