Skip to content

Commit

Permalink
LocalDateTime PoC
Browse files Browse the repository at this point in the history
I messed up GIT history while rebasing, so squashing into one commit
  • Loading branch information
justingrant committed Jun 28, 2020
1 parent 76da5bc commit 9fe4bc5
Show file tree
Hide file tree
Showing 40 changed files with 10,613 additions and 349 deletions.
2 changes: 2 additions & 0 deletions docs/cookbook/absoluteFromLegacyDate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ const legacyDate = new Date('1970-01-01T00:00:01Z');

// Convert legacy Date to Temporal.Absolute
const absolute = Temporal.Absolute.fromEpochMilliseconds(legacyDate.getTime());
const ldt = Temporal.LocalDateTime.from({ absolute, timeZone: Temporal.now.timeZone() });

assert(absolute instanceof Temporal.Absolute);
assert(ldt instanceof Temporal.LocalDateTime);
98 changes: 43 additions & 55 deletions docs/cookbook/getBusinessOpenStateText.mjs
Original file line number Diff line number Diff line change
@@ -1,76 +1,50 @@
// LocalDateTime POC notes
// - This sample's original implementation had a DST bug #698 that would have
// been easier to prevent using `LocalDateTime`.
// - The result will be easier to work with because it has its time zone already
// baked in.

/**
* Compare the given time to the business hours of a business located in a
* particular time zone, and return a string indicating whether the business is
* open, closed, opening soon, or closing soon. The length of "soon" can be
* controlled using the `soonWindow` parameter.
*
* @param {Temporal.Absolute} now - Time at which to consider whether the
* business is open
* @param {Temporal.TimeZone} timeZone - Time zone in which the business is
* located
* @param {Temporal.LocalDateTime} now - Date and Time at which to consider
* whether the business is open
* @param {(Object|null)[]} businessHours - Array of length 7 indicating
* business hours during the week
* @param {Temporal.Time} businessHours[].open - Time at which the business
* opens
* @param {Temporal.Time} businessHours[].close - Time at which the business
* closes
* closes. If this time is smaller than the `open` time, it means that the
* business hours wrap around midnight, so this time represents the closing
* time on the next day.
* @param {Temporal.Duration} soonWindow - Length of time before the opening or
* closing time during which the business should be considered "opening soon"
* or "closing soon"
* @returns {string} "open", "closed", "opening soon", or "closing soon"
*/
function getBusinessOpenStateText(now, timeZone, businessHours, soonWindow) {
function inRange(abs, start, end) {
return Temporal.Absolute.compare(abs, start) >= 0 && Temporal.Absolute.compare(abs, end) < 0;
}

const dateTime = now.inTimeZone(timeZone);
const weekday = dateTime.dayOfWeek % 7; // convert to 0-based, for array indexing
function getBusinessOpenStateText(now, businessHours, soonWindow) {
const inRange = (localDateTime, start, end) =>
Temporal.LocalDateTime.compare(localDateTime, start) >= 0 && Temporal.LocalDateTime.compare(localDateTime, end) < 0;

// Because of times wrapping around at midnight, we may need to consider
// yesterday's and tomorrow's hours as well
const today = dateTime.getDate();
const yesterday = today.minus({ days: 1 });
const tomorrow = today.plus({ days: 1 });

// Push any of the businessHours that overlap today's date into an array,
// that we will subsequently check. Convert the businessHours Times into
// DateTimes so that they no longer wrap around.
const businessHoursOverlappingToday = [];
const yesterdayHours = businessHours[(weekday + 6) % 7];
if (yesterdayHours) {
const { open, close } = yesterdayHours;
if (Temporal.Time.compare(close, open) < 0) {
businessHoursOverlappingToday.push({
open: yesterday.withTime(open).inTimeZone(timeZone),
close: today.withTime(close).inTimeZone(timeZone)
});
for (const delta of [-1, 0, 1]) {
const index = (now.dayOfWeek + 7 + delta) % 7; // convert to 0-based, for array indexing
if (!businessHours[index]) continue;
const openDate = now.toDate()[delta < 0 ? 'minus' : 'plus']({ days: Math.abs(delta) });
const { open: openTime, close: closeTime } = businessHours[index];
const open = now.with({ ...openDate.getFields(), ...openTime.getFields() });
const isWrap = Temporal.Time.compare(closeTime, openTime) < 0;
const closeDate = isWrap ? openDate.plus({ days: 1 }) : openDate;
const close = now.with({ ...closeDate.getFields(), ...closeTime.getFields() });
if (inRange(now, open, close)) {
return Temporal.LocalDateTime.compare(now, close.minus(soonWindow)) >= 0 ? 'closing soon' : 'open';
}
if (inRange(now.plus(soonWindow), open, close)) return 'opening soon';
}
const todayHours = businessHours[weekday];
if (todayHours) {
const { open, close } = todayHours;
const todayOrTomorrow = Temporal.Time.compare(close, open) >= 0 ? today : tomorrow;
businessHoursOverlappingToday.push({
open: today.withTime(open).inTimeZone(timeZone),
close: todayOrTomorrow.withTime(close).inTimeZone(timeZone)
});
}

// Check if any of the candidate business hours include the given time
const soon = now.plus(soonWindow);
let openNow = false;
let openSoon = false;
for (const { open, close } of businessHoursOverlappingToday) {
openNow = openNow || inRange(now, open, close);
openSoon = openSoon || inRange(soon, open, close);
}

if (openNow) {
if (!openSoon) return 'closing soon';
return 'open';
}
if (openSoon) return 'opening soon';
return 'closed';
}

Expand All @@ -86,8 +60,22 @@ const businessHours = [
/* Sat */ { open: Temporal.Time.from('11:00'), close: Temporal.Time.from('02:00') }
];

const now = Temporal.Absolute.from('2019-04-07T00:00+01:00[Europe/Berlin]');
const tz = Temporal.TimeZone.from('Europe/Berlin');
// This ISO string is intentionally conflicting, because the real TZ offset for
// that date is +02:00. The default behavior of from() on ISO strings is to
// assume that the offset is correct and the time zone definition has changed
// since the time was stored. (The user can get different behavior via the
// `prefer` option.)
const now = Temporal.LocalDateTime.from('2019-04-07T00:00+01:00[Europe/Berlin]');
assert.equal(now.toString(), '2019-04-07T01:00+02:00[Europe/Berlin]');
const soonWindow = Temporal.Duration.from({ minutes: 30 });
const saturdayNightState = getBusinessOpenStateText(now, tz, businessHours, soonWindow);
const saturdayNightState = getBusinessOpenStateText(now, businessHours, soonWindow);
assert.equal(saturdayNightState, 'open');

const lastCall = now.plus({ minutes: 50 });
assert.equal(lastCall.toString(), '2019-04-07T01:50+02:00[Europe/Berlin]');
const lastCallState = getBusinessOpenStateText(lastCall, businessHours, soonWindow);
assert.equal(lastCallState, 'closing soon');

const tuesdayEarly = now.plus({ days: 2, hours: 6 });
const tuesdayEarlyState = getBusinessOpenStateText(tuesdayEarly, businessHours, soonWindow);
assert.equal(tuesdayEarlyState, 'closed');
10 changes: 5 additions & 5 deletions docs/cookbook/getInstantBeforeOldRecord.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
* Retrieve a time at which to give advance notice of a record that is
* potentially about to be broken.
*
* @param {Temporal.Absolute} start - Start time of the event
* @param {Temporal.LocalDateTime} start - Start of the event
* @param {Temporal.Duration} previousRecord - Existing record to be broken
* @param {Temporal.Duration} noticeWindow - Advance notice time
* @returns {Temporal.Absolute} Time at which to give advance notice of breaking
* the record
* @returns {Temporal.LocalDateTime} Time at which to give advance notice of
* breaking the record
*/
function getInstantBeforeOldRecord(start, previousRecord, noticeWindow) {
return start.plus(previousRecord).minus(noticeWindow);
}

// Start of the men's 10000 meters at the Rio de Janeiro 2016 Olympic Games
const raceStart = Temporal.Absolute.from('2016-08-13T21:27-03:00[America/Sao_Paulo]');
const raceStart = Temporal.LocalDateTime.from('2016-08-13T21:27-03:00[America/Sao_Paulo]');
// Kenenisa Bekele's world record set in 2005
const record = Temporal.Duration.from({ minutes: 26, seconds: 17, milliseconds: 530 });
const noticeWindow = Temporal.Duration.from({ minutes: 1 });
// Time to send a "hurry up, can you finish the race in 1 minute?" push
// notification to all the runners
const reminderAt = getInstantBeforeOldRecord(raceStart, record, noticeWindow);

assert.equal(reminderAt.toString(), '2016-08-14T00:52:17.530Z');
assert.equal(reminderAt.toString(), '2016-08-13T21:52:17.530-03:00[America/Sao_Paulo]');
19 changes: 13 additions & 6 deletions docs/cookbook/getLocalizedArrival.mjs
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// LocalDateTime POC notes
// - Existing code is fine-- no DST issues.
// - This function will break around DST transitions if the caller passes a
// date/time duration instead of the expected absolute duration.
// - The result will be easier to work with because it has its time zone already
// baked in.

/**
* Given a localized departure time and a flight duration, get a local arrival
* time in the destination time zone.
*
* @param {string} parseableDeparture - Departure time with time zone
* @param {Temporal.Duration} flightTime - Duration of the flight
* @param {Temporal.Duration} flightTime - Absolute duration of the flight
* @param {Temporal.TimeZone} destinationTimeZone - Time zone in which the
* flight's destination is located
* @returns {Temporal.DateTime} Local arrival time
* @returns {Temporal.LocalDateTime} Local arrival time
*/
function getLocalizedArrival(parseableDeparture, flightTime, destinationTimeZone) {
const departure = Temporal.Absolute.from(parseableDeparture);
const arrival = departure.plus(flightTime);
return arrival.inTimeZone(destinationTimeZone);
const departure = Temporal.LocalDateTime.from(parseableDeparture);
const arrival = departure.plus(flightTime, { durationKind: 'absolute' });
return arrival.with({ timeZone: destinationTimeZone });
}

const arrival = getLocalizedArrival(
'2020-03-08T11:55:00+08:00[Asia/Hong_Kong]',
Temporal.Duration.from({ minutes: 775 }),
'America/Los_Angeles'
);
assert.equal(arrival.toString(), '2020-03-08T09:50');
assert.equal(arrival.toString(), '2020-03-08T09:50-07:00[America/Los_Angeles]');
14 changes: 14 additions & 0 deletions docs/cookbook/getParseableZonedStringAtInstant.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
// LocalDateTime POC notes
// - TODO: should the string representation of an "offset time zone" include
// the offset in brackets, or have nothing in brackets? If "nothing", then
// should `LocalDateTime.from` also accept bracket-less strings when parsing?

const absoluteTime = Temporal.Absolute.from('2020-01-03T10:41:51Z');

const result = absoluteTime.toString('Europe/Paris');

assert.equal(result, '2020-01-03T11:41:51+01:00[Europe/Paris]');
assert.equal(Temporal.Absolute.compare(absoluteTime, Temporal.Absolute.from(result)), 0);

const ldt = Temporal.LocalDateTime.from({ absolute: absoluteTime, timeZone: 'Europe/Paris' });
assert.equal(ldt.toString(), '2020-01-03T11:41:51+01:00[Europe/Paris]');
assert.equal(Temporal.Absolute.compare(absoluteTime, ldt.absolute), 0);
assert.equal(ldt.toDateTime().toString(), '2020-01-03T11:41:51');

// With an offset:

const result2 = absoluteTime.toString('-07:00');
const ldt2 = Temporal.LocalDateTime.from({ absolute: absoluteTime, timeZone: '-07:00' });

assert.equal(result2, '2020-01-03T03:41:51-07:00');
assert.equal(ldt2.toString(), '2020-01-03T03:41:51-07:00[-07:00]');

// With a Temporal.TimeZone object:

const timeZone = Temporal.TimeZone.from('Asia/Seoul');
const result3 = absoluteTime.toString(timeZone);
const ldt3 = Temporal.LocalDateTime.from({ absolute: absoluteTime, timeZone: 'Asia/Seoul' });

assert.equal(result3, '2020-01-03T19:41:51+09:00[Asia/Seoul]');
assert.equal(ldt3.toString(), '2020-01-03T19:41:51+09:00[Asia/Seoul]');
24 changes: 4 additions & 20 deletions docs/cookbook/getParseableZonedStringWithLocalTimeInOtherZone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,17 @@
* Takes a local date and time in one time zone, and serializes it to a string
* expressing the local date and time in another time zone.
*
* If `sourceDateTime` doesn't exist in `sourceTimeZone`, or exists twice, then
* an error will be thrown by default.
* Usually this is what you want. (FIXME: but is it?)
* Use `sourceDisambiguationPolicy` to change this behaviour.
*
* @param {Temporal.DateTime} sourceDateTime - The local date and time
* @param {Temporal.TimeZone} sourceTimeZone - The time zone for
* `sourceDateTime`
* @param {Temporal.LocalDateTime} source - The local date and time
* @param {Temporal.TimeZone} targetTimeZone - The time zone for the
* return value
* @param {string} [sourceDisambiguationPolicy=reject] - what to do when
* `sourceDateTime` is ambiguous
* @returns {string} String indicating the time with time zone designation
*/
function getParseableZonedStringWithLocalTimeInOtherZone(
sourceDateTime,
sourceTimeZone,
targetTimeZone,
sourceDisambiguationPolicy = 'reject'
) {
let instant = sourceDateTime.inTimeZone(sourceTimeZone, { disambiguation: sourceDisambiguationPolicy });
return instant.toString(targetTimeZone);
function getParseableZonedStringWithLocalTimeInOtherZone(source, targetTimeZone) {
return source.with({ timeZone: targetTimeZone }).toString();
}

const result = getParseableZonedStringWithLocalTimeInOtherZone(
Temporal.DateTime.from('2020-01-09T00:00'),
Temporal.TimeZone.from('America/Chicago'),
Temporal.LocalDateTime.from('2020-01-09T00:00-06:00[America/Chicago]'),
Temporal.TimeZone.from('America/Los_Angeles')
);
// On this date, when it's midnight in Chicago, it's 10 PM the previous night in LA
Expand Down
6 changes: 3 additions & 3 deletions docs/cookbook/getTripDurationInHrMinSec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
* @returns {Temporal.Duration} A duration with units no larger than hours
*/
function getTripDurationInHrMinSec(parseableDeparture, parseableArrival) {
const departure = Temporal.Absolute.from(parseableDeparture);
const arrival = Temporal.Absolute.from(parseableArrival);
return arrival.difference(departure, { largestUnit: 'hours' });
const departure = Temporal.LocalDateTime.from(parseableDeparture);
const arrival = Temporal.LocalDateTime.from(parseableArrival);
return arrival.difference(departure, { largestUnit: 'hours', durationKind: 'absolute' });
}

const flightTime = getTripDurationInHrMinSec(
Expand Down
17 changes: 8 additions & 9 deletions docs/cookbook/getUtcOffsetDifferenceSecondsAtInstant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@
* Returns the number of seconds' difference between the UTC offsets of two time
* zones, at a particular moment in time.
*
* @param {Temporal.Absolute} instant - A moment in time
* @param {Temporal.TimeZone} sourceTimeZone - A time zone to examine
* @param {Temporal.localDateTime} instant - A moment in the source time zone
* @param {Temporal.TimeZone} targetTimeZone - A second time zone to examine
* @returns {number} The number of seconds difference between the time zones'
* UTC offsets
* UTC offsets at that moment.
*/
function getUtcOffsetDifferenceSecondsAtInstant(instant, sourceTimeZone, targetTimeZone) {
const sourceOffsetNs = sourceTimeZone.getOffsetNanosecondsFor(instant);
const targetOffsetNs = targetTimeZone.getOffsetNanosecondsFor(instant);
return (targetOffsetNs - sourceOffsetNs) / 1e9;
function getUtcOffsetDifferenceSecondsAtInstant(source, targetTimeZone) {
const target = source.with({ timeZone: targetTimeZone });
return (target.timeZoneOffsetNanoseconds - source.timeZoneOffsetNanoseconds) / 1e9;
}

const instant = Temporal.Absolute.from('2020-01-09T00:00Z');
const nyc = Temporal.TimeZone.from('America/New_York');
const ldtNyc = Temporal.LocalDateTime.from({ absolute: instant, timeZone: nyc });
const chicago = Temporal.TimeZone.from('America/Chicago');

// At this instant, Chicago is 3600 seconds earlier than New York
assert.equal(getUtcOffsetDifferenceSecondsAtInstant(instant, nyc, chicago), -3600);
// At this instant, Chicago's local time is 3600 seconds earlier than New York
assert.equal(getUtcOffsetDifferenceSecondsAtInstant(ldtNyc, chicago), -3600);
7 changes: 4 additions & 3 deletions docs/cookbook/getUtcOffsetSecondsAtInstant.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const instant = Temporal.Absolute.from('2020-01-09T00:00Z');
const nyc = Temporal.TimeZone.from('America/New_York');
const ldt = Temporal.LocalDateTime.from({ absolute: '2020-01-09T00:00Z', timeZone: 'America/New_York' });

nyc.getOffsetNanosecondsFor(instant) / 1e9; // => -18000
ldt.timeZoneOffsetNanoseconds / 1e9; // => -18000

assert.equal(ldt.timeZoneOffsetNanoseconds / 1e9, '-18000');
7 changes: 4 additions & 3 deletions docs/cookbook/getUtcOffsetStringAtInstant.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const instant = Temporal.Absolute.from('2020-01-09T00:00Z');
const nyc = Temporal.TimeZone.from('America/New_York');
const ldt = Temporal.LocalDateTime.from({ absolute: '2020-01-09T00:00Z', timeZone: 'America/New_York' });

nyc.getOffsetStringFor(instant); // => -05:00
ldt.timeZoneOffsetString; // => '-05:00'

assert.equal(ldt.timeZoneOffsetString, '-05:00');
18 changes: 10 additions & 8 deletions docs/cookbook/localTimeForFutureEvents.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@ const tc39meetings = [

// To follow the meetings remotely from Tokyo, calculate the times you would
// need to join:
const localTimeZone = Temporal.TimeZone.from('Asia/Tokyo');
const localTimes = tc39meetings.map(({ dateTime, timeZone }) => {
return Temporal.DateTime.from(dateTime).inTimeZone(timeZone, { disambiguation: 'reject' }).inTimeZone(localTimeZone);
return Temporal.LocalDateTime.from({ ...Temporal.DateTime.from(dateTime).getFields(), timeZone }).with(
{ timeZone: 'Asia/Tokyo' },
{ disambiguation: 'reject' }
);
});

assert.deepEqual(
localTimes.map((dt) => dt.toString()),
[
'2019-01-29T02:00',
'2019-03-26T23:00',
'2019-06-04T17:00',
'2019-07-24T02:00',
'2019-10-01T23:00',
'2019-12-04T03:00'
'2019-01-29T02:00+09:00[Asia/Tokyo]',
'2019-03-26T23:00+09:00[Asia/Tokyo]',
'2019-06-04T17:00+09:00[Asia/Tokyo]',
'2019-07-24T02:00+09:00[Asia/Tokyo]',
'2019-10-01T23:00+09:00[Asia/Tokyo]',
'2019-12-04T03:00+09:00[Asia/Tokyo]'
]
);
Loading

0 comments on commit 9fe4bc5

Please sign in to comment.