Skip to content

Commit

Permalink
Add startOfDay prop; fix DST-related props + tests
Browse files Browse the repository at this point in the history
* Per tc39#737, adds property to get first valid time at or after midnight
* Fixed bug in `hoursInDay` property getter
* FIxed docs for `isTimeZoneOffsetTransition` property
* Added DST-focused tests for all three properties
* Also added tests for `toLocalDateTime` methods on other types
  • Loading branch information
justingrant committed Sep 14, 2020
1 parent 82e1843 commit c81189a
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 19 deletions.
36 changes: 29 additions & 7 deletions polyfill/lib/localdatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,11 @@ export class LocalDateTime {
* midnight and the next day's midnight.
*/
get hoursInDay() {
const today = this.toDate().toDateTime(new Temporal.Time());
const tomorrow = today.plus({ days: 1 });
// TODO: add tests for Azores timezone on midnight of a DST transition
const todayAbs = today.toAbsolute(this._tz);
const tomorrowAbs = tomorrow.toAbsolute(this._tz);
const todayDate = this.toDate();
const today = LocalDateTime.from({ ...todayDate.getFields(), timeZone: this.timeZone });
const tomorrow = LocalDateTime.from({ ...todayDate.plus({ days: 1 }).getFields(), timeZone: this.timeZone });
const todayAbs = today.toAbsolute();
const tomorrowAbs = tomorrow.toAbsolute();
const diff = tomorrowAbs.difference(todayAbs, { largestUnit: 'hours' });
const hours =
diff.hours +
Expand All @@ -447,11 +447,33 @@ export class LocalDateTime {
}

/**
* True if this `Temporal.LocalDateTime` instance falls exactly on a DST
* Returns a new `Temporal.LocalDateTime` instance representing the first
* valid time during the current calendar day and time zone of `this`.
*
* The local time of the result is almost always `00:00`, but in rare cases it
* could be a later time e.g. if DST starts at midnight in a time zone. For
* example:
* ```
* const ldt = Temporal.LocalDateTime.from('2015-10-18T12:00-02:00[America/Sao_Paulo]');
* ldt.startOfDay; // => 2015-10-18T01:00-02:00[America/Sao_Paulo]
* ```
*/
get startOfDay() {
const date = this.toDate();
const ldt = LocalDateTime.from({ ...date.getFields(), timeZone: this.timeZone });
return ldt;
}

/**
* True if this `Temporal.LocalDateTime` instance is immediately after a DST
* transition or other change in time zone offset, false otherwise.
*
* "Immediately after" means that subtracting one nanosecond would yield a
* `Temporal.LocalDateTime` instance that has a different value for
* `timeZoneOffsetNanoseconds`.
*
* To calculate if a DST transition happens on the same day (but not
* necessarily at the same time), use `.getDayDuration()`.
* necessarily at the same time), use `.hoursInDay() !== 24`.
* */
get isTimeZoneOffsetTransition() {
const oneNsBefore = this.minus({ nanoseconds: 1 });
Expand Down
21 changes: 19 additions & 2 deletions polyfill/lib/poc/LocalDateTime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,28 @@ export declare class LocalDateTime {
*/
get hoursInDay(): number;
/**
* True if this `Temporal.LocalDateTime` instance falls exactly on a DST
* Returns a new `Temporal.LocalDateTime` instance representing the first
* valid time during the current calendar day and time zone of `this`.
*
* The local time of the result is almost always `00:00`, but in rare cases it
* could be a later time e.g. if DST starts at midnight in a time zone. For
* example:
* ```
* const ldt = Temporal.LocalDateTime.from('2015-10-18T12:00-02:00[America/Sao_Paulo]');
* ldt.startOfDay; // => 2015-10-18T01:00-02:00[America/Sao_Paulo]
* ```
*/
get startOfDay(): LocalDateTime;
/**
* True if this `Temporal.LocalDateTime` instance is immediately after a DST
* transition or other change in time zone offset, false otherwise.
*
* "Immediately after" means that subtracting one nanosecond would yield a
* `Temporal.LocalDateTime` instance that has a different value for
* `timeZoneOffsetNanoseconds`.
*
* To calculate if a DST transition happens on the same day (but not
* necessarily at the same time), use `.getDayDuration()`.
* necessarily at the same time), use `.hoursInDay() !== 24`.
* */
get isTimeZoneOffsetTransition(): boolean;
/**
Expand Down
1 change: 1 addition & 0 deletions polyfill/lib/poc/LocalDateTime.nocomments.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export declare class LocalDateTime {
get calendar(): Temporal.CalendarProtocol;
toDateTime(): Temporal.DateTime;
get hoursInDay(): number;
get startOfDay(): LocalDateTime;
get isTimeZoneOffsetTransition(): boolean;
get timeZoneOffsetNanoseconds(): number;
get timeZoneOffsetString(): string;
Expand Down
103 changes: 103 additions & 0 deletions polyfill/lib/poc/LocalDateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,109 @@ describe('LocalDateTime', () => {
const hourBeforeDstStart = LocalDateTime.from({ ...new DateTime(2020, 3, 8, 1).getFields(), timeZone: tz });
const dayBeforeDstStart = LocalDateTime.from({ ...new DateTime(2020, 3, 7, 2, 30).getFields(), timeZone: tz });

describe('toLocalDateTime() on other Temporal objects', () => {
it('Date.toLocalDateTime() works', () => {
const date = Temporal.Date.from('2020-01-01');
const time = Temporal.Time.from('12:00');
const ldt = date.toLocalDateTime('America/Los_Angeles', time);
equal(ldt.toString(), '2020-01-01T12:00-08:00[America/Los_Angeles]');
});
it('Date.toLocalDateTime() works with time omitted', () => {
const date = Temporal.Date.from('2020-01-01');
const ldt = date.toLocalDateTime('America/Los_Angeles');
equal(ldt.toString(), '2020-01-01T00:00-08:00[America/Los_Angeles]');
});
it('Date.toLocalDateTime() works with disambiguation option', () => {
const date = Temporal.Date.from('2020-03-08');
const time = Temporal.Time.from('02:00');
const ldt = date.toLocalDateTime('America/Los_Angeles', time, { disambiguation: 'earlier' });
equal(ldt.toString(), '2020-03-08T01:00-08:00[America/Los_Angeles]');
});
it('Time.toLocalDateTime works', () => {
const date = Temporal.Date.from('2020-01-01');
const time = Temporal.Time.from('12:00');
const ldt = time.toLocalDateTime('America/Los_Angeles', date);
equal(ldt.toString(), '2020-01-01T12:00-08:00[America/Los_Angeles]');
});
it('Time.toLocalDateTime() works with disambiguation option', () => {
const date = Temporal.Date.from('2020-03-08');
const time = Temporal.Time.from('02:00');
const ldt = time.toLocalDateTime('America/Los_Angeles', date, { disambiguation: 'earlier' });
equal(ldt.toString(), '2020-03-08T01:00-08:00[America/Los_Angeles]');
});
it('Absolute.toLocalDateTime works', () => {
const abs = Temporal.Absolute.from('2020-01-01T00:00-08:00');
const ldt = abs.toLocalDateTime('America/Los_Angeles');
equal(ldt.toString(), '2020-01-01T00:00-08:00[America/Los_Angeles]');
});
it('DateTime.toLocalDateTime works', () => {
const dt = Temporal.DateTime.from('2020-01-01T00:00');
const ldt = dt.toLocalDateTime('America/Los_Angeles');
equal(ldt.toString(), '2020-01-01T00:00-08:00[America/Los_Angeles]');
});
it('DateTime.toLocalDateTime works with disambiguation option', () => {
const dt = Temporal.DateTime.from('2020-03-08T02:00');
const ldt = dt.toLocalDateTime('America/Los_Angeles', { disambiguation: 'earlier' });
equal(ldt.toString(), '2020-03-08T01:00-08:00[America/Los_Angeles]');
});
});

describe('properties around DST', () => {
it('hoursInDay works with DST start', () => {
equal(hourBeforeDstStart.hoursInDay, 23);
});
it('hoursInDay works with non-DST days', () => {
equal(dayBeforeDstStart.hoursInDay, 24);
});
it('hoursInDay works with DST end', () => {
const dstEnd = LocalDateTime.from('2020-11-01T01:00-08:00[America/Los_Angeles]');
equal(dstEnd.hoursInDay, 25);
});
it('hoursInDay works when day starts at 1:00 due to DST start at midnight', () => {
const ldt = Temporal.LocalDateTime.from('2015-10-18T12:00:00-02:00[America/Sao_Paulo]');
equal(ldt.hoursInDay, 23);
});
it('startOfDay works', () => {
const start = dayBeforeDstStart.startOfDay;
equal(start.toDate().toString(), dayBeforeDstStart.toDate().toString());
equal('00:00', start.toTime().toString());
});
it('startOfDay works when day starts at 1:00 due to DST start at midnight', () => {
const ldt = LocalDateTime.from('2015-10-18T12:00:00-02:00[America/Sao_Paulo]');
const start = ldt.startOfDay;
equal('01:00', start.toTime().toString());
});

const dayAfterSamoaDateLineChange = LocalDateTime.from('2011-12-31T22:00+14:00[Pacific/Apia]');
const dayBeforeSamoaDateLineChange = LocalDateTime.from('2011-12-29T22:00-10:00[Pacific/Apia]');
it('startOfDay works after Samoa date line change', () => {
const start = dayAfterSamoaDateLineChange.startOfDay;
equal('00:00', start.toTime().toString());
});
it('hoursInDay works after Samoa date line change', () => {
equal(dayAfterSamoaDateLineChange.hoursInDay, 24);
});
it('hoursInDay works before Samoa date line change', () => {
equal(dayBeforeSamoaDateLineChange.hoursInDay, 24);
});

it('isTimeZoneOffsetTransition normally returns false', () => {
equal(hourBeforeDstStart.isTimeZoneOffsetTransition, false);
});
it('isTimeZoneOffsetTransition returns true at a DST start transition', () => {
const dstStart = hourBeforeDstStart.plus({ hours: 1 });
equal(dstStart.isTimeZoneOffsetTransition, true);
});
it('isTimeZoneOffsetTransition returns true at a DST end transition', () => {
const dstEnd = LocalDateTime.from('2020-11-01T01:00-08:00[America/Los_Angeles]');
equal(dstEnd.isTimeZoneOffsetTransition, true);
});
it('isTimeZoneOffsetTransition returns true right after Samoa date line change', () => {
const rightAfterSamoaDateLineChange = LocalDateTime.from('2011-12-31T00:00+14:00[Pacific/Apia]');
equal(rightAfterSamoaDateLineChange.isTimeZoneOffsetTransition, true);
});
});

describe('math around DST', () => {
it('add 1 hour to get to DST start', () => {
const added = hourBeforeDstStart.plus({ hours: 1 });
Expand Down
36 changes: 29 additions & 7 deletions polyfill/lib/poc/LocalDateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,11 @@ export class LocalDateTime {
* midnight and the next day's midnight.
*/
get hoursInDay(): number {
const today = this.toDate().toDateTime(new Temporal.Time());
const tomorrow = today.plus({ days: 1 });
// TODO: add tests for Azores timezone on midnight of a DST transition
const todayAbs = today.toAbsolute(this._tz);
const tomorrowAbs = tomorrow.toAbsolute(this._tz);
const todayDate = this.toDate();
const today = LocalDateTime.from({ ...todayDate.getFields(), timeZone: this.timeZone });
const tomorrow = LocalDateTime.from({ ...todayDate.plus({ days: 1 }).getFields(), timeZone: this.timeZone });
const todayAbs = today.toAbsolute();
const tomorrowAbs = tomorrow.toAbsolute();
const diff = tomorrowAbs.difference(todayAbs, { largestUnit: 'hours' });
const hours =
diff.hours +
Expand All @@ -529,11 +529,33 @@ export class LocalDateTime {
}

/**
* True if this `Temporal.LocalDateTime` instance falls exactly on a DST
* Returns a new `Temporal.LocalDateTime` instance representing the first
* valid time during the current calendar day and time zone of `this`.
*
* The local time of the result is almost always `00:00`, but in rare cases it
* could be a later time e.g. if DST starts at midnight in a time zone. For
* example:
* ```
* const ldt = Temporal.LocalDateTime.from('2015-10-18T12:00-02:00[America/Sao_Paulo]');
* ldt.startOfDay; // => 2015-10-18T01:00-02:00[America/Sao_Paulo]
* ```
*/
get startOfDay(): LocalDateTime {
const date = this.toDate();
const ldt = LocalDateTime.from({ ...date.getFields(), timeZone: this.timeZone });
return ldt;
}

/**
* True if this `Temporal.LocalDateTime` instance is immediately after a DST
* transition or other change in time zone offset, false otherwise.
*
* "Immediately after" means that subtracting one nanosecond would yield a
* `Temporal.LocalDateTime` instance that has a different value for
* `timeZoneOffsetNanoseconds`.
*
* To calculate if a DST transition happens on the same day (but not
* necessarily at the same time), use `.getDayDuration()`.
* necessarily at the same time), use `.hoursInDay() !== 24`.
* */
get isTimeZoneOffsetTransition(): boolean {
const oneNsBefore = this.minus({ nanoseconds: 1 });
Expand Down
23 changes: 20 additions & 3 deletions polyfill/poc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export namespace Temporal {
): Temporal.Duration;
toLocalDateTime(
tzLike: TimeZoneProtocol | string,
temporalTime: Temporal.Time,
temporalTime?: Temporal.Time,
options?: ToAbsoluteOptions
): Temporal.LocalDateTime;
toDateTime(temporalTime: Temporal.Time): Temporal.DateTime;
Expand Down Expand Up @@ -973,11 +973,28 @@ export namespace Temporal {
*/
get hoursInDay(): number;
/**
* True if this `Temporal.LocalDateTime` instance falls exactly on a DST
* Returns a new `Temporal.LocalDateTime` instance representing the first
* valid time during the current calendar day and time zone of `this`.
*
* The local time of the result is almost always `00:00`, but in rare cases it
* could be a later time e.g. if DST starts at midnight in a time zone. For
* example:
* ```
* const ldt = Temporal.LocalDateTime.from('2015-10-18T12:00-02:00[America/Sao_Paulo]');
* ldt.startOfDay; // => 2015-10-18T01:00-02:00[America/Sao_Paulo]
* ```
*/
get startOfDay(): LocalDateTime;
/**
* True if this `Temporal.LocalDateTime` instance is immediately after a DST
* transition or other change in time zone offset, false otherwise.
*
* "Immediately after" means that subtracting one nanosecond would yield a
* `Temporal.LocalDateTime` instance that has a different value for
* `timeZoneOffsetNanoseconds`.
*
* To calculate if a DST transition happens on the same day (but not
* necessarily at the same time), use `.getDayDuration()`.
* necessarily at the same time), use `.hoursInDay() !== 24`.
* */
get isTimeZoneOffsetTransition(): boolean;
/**
Expand Down
Loading

0 comments on commit c81189a

Please sign in to comment.