Skip to content

Commit

Permalink
Merge branch 'main' into listview-alpha3
Browse files Browse the repository at this point in the history
  • Loading branch information
jluyau committed Aug 27, 2021
2 parents 0a01ab9 + 63e59f3 commit ebd4316
Show file tree
Hide file tree
Showing 216 changed files with 3,077 additions and 990 deletions.
93 changes: 76 additions & 17 deletions packages/@internationalized/date/src/CalendarDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {add, addTime, addZoned, cycleDate, cycleTime, cycleZoned, set, setTime, setZoned, subtract, subtractTime, subtractZoned} from './manipulation';
import {Calendar, CycleOptions, CycleTimeOptions, DateField, DateFields, Disambiguation, Duration, OverflowBehavior, TimeField, TimeFields} from './types';
import {AnyCalendarDate, AnyTime, Calendar, CycleOptions, CycleTimeOptions, DateField, DateFields, Disambiguation, Duration, TimeField, TimeFields} from './types';
import {compareDate, compareTime} from './queries';
import {dateTimeToString, dateToString, timeToString, zonedDateTimeToString} from './string';
import {GregorianCalendar} from './calendars/GregorianCalendar';
Expand All @@ -38,6 +38,10 @@ function shiftArgs(args: any[]) {
}

export class CalendarDate {
// This prevents TypeScript from allowing other types with the same fields to match.
// i.e. a ZonedDateTime should not be be passable to a parameter that expects CalendarDate.
// If that behavior is desired, use the AnyCalendarDate interface instead.
#type;
public readonly calendar: Calendar;
public readonly era: string;
public readonly year: number;
Expand Down Expand Up @@ -76,8 +80,8 @@ export class CalendarDate {
return subtract(this, duration);
}

set(fields: DateFields, behavior?: OverflowBehavior) {
return set(this, fields, behavior);
set(fields: DateFields) {
return set(this, fields);
}

cycle(field: DateField, amount: number, options?: CycleOptions) {
Expand All @@ -92,12 +96,15 @@ export class CalendarDate {
return dateToString(this);
}

compare(b: CalendarDate) {
compare(b: AnyCalendarDate) {
return compareDate(this, b);
}
}

export class Time {
// This prevents TypeScript from allowing other types with the same fields to match.
#type;

constructor(
public readonly hour: number = 0,
public readonly minute: number = 0,
Expand All @@ -117,8 +124,8 @@ export class Time {
return subtractTime(this, duration);
}

set(fields: TimeFields, behavior?: OverflowBehavior) {
return setTime(this, fields, behavior);
set(fields: TimeFields) {
return setTime(this, fields);
}

cycle(field: TimeField, amount: number, options?: CycleTimeOptions) {
Expand All @@ -129,12 +136,19 @@ export class Time {
return timeToString(this);
}

compare(b: Time) {
compare(b: AnyTime) {
return compareTime(this, b);
}
}

export class CalendarDateTime extends CalendarDate {
export class CalendarDateTime {
// This prevents TypeScript from allowing other types with the same fields to match.
#type;
public readonly calendar: Calendar;
public readonly era: string;
public readonly year: number;
public readonly month: number;
public readonly day: number;
public readonly hour: number;
public readonly minute: number;
public readonly second: number;
Expand All @@ -145,7 +159,16 @@ export class CalendarDateTime extends CalendarDate {
constructor(calendar: Calendar, era: string, year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number);
constructor(...args: any[]) {
let [calendar, era, year, month, day] = shiftArgs(args);
super(calendar, era, year, month, day);
this.calendar = calendar;
this.era = era;
this.year = year;
this.month = month;
this.day = day;

if (this.calendar.balanceDate) {
this.calendar.balanceDate(this);
}

this.hour = args.shift() || 0;
this.minute = args.shift() || 0;
this.second = args.shift() || 0;
Expand All @@ -160,8 +183,16 @@ export class CalendarDateTime extends CalendarDate {
}
}

set(fields: DateFields & TimeFields, behavior?: OverflowBehavior) {
return set(setTime(this, fields, behavior), fields, behavior);
add(duration: Duration) {
return add(this, duration);
}

subtract(duration: Duration) {
return subtract(this, duration);
}

set(fields: DateFields & TimeFields) {
return set(setTime(this, fields), fields);
}

cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions) {
Expand All @@ -176,11 +207,15 @@ export class CalendarDateTime extends CalendarDate {
}
}

toDate(timeZone: string) {
return toDate(this, timeZone);
}

toString() {
return dateTimeToString(this);
}

compare(b: CalendarDate | CalendarDateTime) {
compare(b: CalendarDate | CalendarDateTime | ZonedDateTime) {
let res = compareDate(this, b);
if (res === 0) {
return compareTime(this, toCalendarDateTime(b));
Expand All @@ -190,7 +225,18 @@ export class CalendarDateTime extends CalendarDate {
}
}

export class ZonedDateTime extends CalendarDateTime {
export class ZonedDateTime {
// This prevents TypeScript from allowing other types with the same fields to match.
#type;
public readonly calendar: Calendar;
public readonly era: string;
public readonly year: number;
public readonly month: number;
public readonly day: number;
public readonly hour: number;
public readonly minute: number;
public readonly second: number;
public readonly millisecond: number;
public readonly timeZone: string;
public readonly offset: number;

Expand All @@ -201,9 +247,22 @@ export class ZonedDateTime extends CalendarDateTime {
let [calendar, era, year, month, day] = shiftArgs(args);
let timeZone = args.shift();
let offset = args.shift();
super(calendar, era, year, month, day, ...args);
this.calendar = calendar;
this.era = era;
this.year = year;
this.month = month;
this.day = day;

if (this.calendar.balanceDate) {
this.calendar.balanceDate(this);
}

this.timeZone = timeZone;
this.offset = offset;
this.hour = args.shift() || 0;
this.minute = args.shift() || 0;
this.second = args.shift() || 0;
this.millisecond = args.shift() || 0;
}

copy(): ZonedDateTime {
Expand All @@ -222,8 +281,8 @@ export class ZonedDateTime extends CalendarDateTime {
return subtractZoned(this, duration);
}

set(fields: DateFields & TimeFields, behavior?: OverflowBehavior, disambiguation?: Disambiguation) {
return setZoned(this, fields, behavior, disambiguation);
set(fields: DateFields & TimeFields, disambiguation?: Disambiguation) {
return setZoned(this, fields, disambiguation);
}

cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions) {
Expand All @@ -244,6 +303,6 @@ export class ZonedDateTime extends CalendarDateTime {

compare(b: CalendarDate | CalendarDateTime | ZonedDateTime) {
// TODO: Is this a bad idea??
return this.toDate() - toZoned(b, this.timeZone).toDate();
return this.toDate().getTime() - toZoned(b, this.timeZone).toDate().getTime();
}
}
179 changes: 179 additions & 0 deletions packages/@internationalized/date/src/DateFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

let formatterCache = new Map<string, Intl.DateTimeFormat>();

interface ResolvedDateTimeFormatOptions extends Intl.ResolvedDateTimeFormatOptions {
hourCycle?: Intl.DateTimeFormatOptions['hourCycle']
}

export class DateFormatter implements Intl.DateTimeFormat {
private formatter: Intl.DateTimeFormat;
private options: Intl.DateTimeFormatOptions;
private resolvedHourCycle: Intl.DateTimeFormatOptions['hourCycle'];

constructor(locale: string, options: Intl.DateTimeFormatOptions = {}) {
this.formatter = getCachedDateFormatter(locale, options);
this.options = options;
}

format(value: Date): string {
return this.formatter.format(value);
}

formatToParts(value: Date): Intl.DateTimeFormatPart[] {
return this.formatter.formatToParts(value);
}

formatRange(start: Date, end: Date) {
// @ts-ignore
if (typeof this.formatter.formatRange === 'function') {
// @ts-ignore
return this.formatter.formatRange(start, end);
}

if (end < start) {
throw new RangeError('End date must be >= start date');
}

// Very basic fallback for old browsers.
return `${this.formatter.format(start)}${this.formatter.format(end)}`;
}

formatRangeToParts(start: Date, end: Date) {
// @ts-ignore
if (typeof this.formatter.formatRangeToParts === 'function') {
// @ts-ignore
return this.formatter.formatRangeToParts(start, end);
}

if (end < start) {
throw new RangeError('End date must be >= start date');
}

let startParts = this.formatter.formatToParts(start);
let endParts = this.formatter.formatToParts(end);
return [
...startParts.map(p => ({...p, source: 'startRange'})),
{type: 'literal', value: ' – ', source: 'shared'},
...endParts.map(p => ({...p, source: 'endRange'}))
];
}

resolvedOptions(): ResolvedDateTimeFormatOptions {
let resolvedOptions = this.formatter.resolvedOptions() as ResolvedDateTimeFormatOptions;
if (hasBuggyResolvedHourCycle()) {
if (!this.resolvedHourCycle) {
this.resolvedHourCycle = getResolvedHourCycle(resolvedOptions.locale, this.options);
}
resolvedOptions.hourCycle = this.resolvedHourCycle;
resolvedOptions.hour12 = this.resolvedHourCycle === 'h11' || this.resolvedHourCycle === 'h12';
}

return resolvedOptions;
}
}

// There are multiple bugs involving the hour12 and hourCycle options in various browser engines.
// - Chrome [1] (and the ECMA 402 spec [2]) resolve hour12: false in English and other locales to h24 (24:00 - 23:59)
// rather than h23 (00:00 - 23:59). Same can happen with hour12: true in French, which Chrome resolves to h11 (00:00 - 11:59)
// rather than h12 (12:00 - 11:59).
// - WebKit returns an incorrect hourCycle resolved option in the French locale due to incorrect parsing of 'h' literal
// in the resolved pattern. It also formats incorrectly when specifying the hourCycle option for the same reason. [3]
// [1] https://bugs.chromium.org/p/chromium/issues/detail?id=1045791
// [2] https://github.com/tc39/ecma402/issues/402
// [3] https://bugs.webkit.org/show_bug.cgi?id=229313

// https://github.com/unicode-org/cldr/blob/018b55eff7ceb389c7e3fc44e2f657eae3b10b38/common/supplemental/supplementalData.xml#L4774-L4802
const hour12Preferences = {
true: {
// Only Japanese uses the h11 style for 12 hour time. All others use h12.
ja: 'h11'
},
false: {
// All locales use h23 for 24 hour time. None use h24.
}
};

function getCachedDateFormatter(locale: string, options: Intl.DateTimeFormatOptions = {}): Intl.DateTimeFormat {
// Work around buggy hour12 behavior in Chrome / ECMA 402 spec by using hourCycle instead.
// Only apply the workaround if the issue is detected, because the hourCycle option is buggy in Safari.
if (typeof options.hour12 === 'boolean' && hasBuggyHour12Behavior()) {
options = {...options};
let pref = hour12Preferences[String(options.hour12)][locale.split('-')[0]];
let defaultHourCycle = options.hour12 ? 'h12' : 'h23';
options.hourCycle = pref ?? defaultHourCycle;
delete options.hour12;
}

let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : '');
if (formatterCache.has(cacheKey)) {
return formatterCache.get(cacheKey);
}

let numberFormatter = new Intl.DateTimeFormat(locale, options);
formatterCache.set(cacheKey, numberFormatter);
return numberFormatter;
}

let _hasBuggyHour12Behavior: boolean = null;
function hasBuggyHour12Behavior() {
if (_hasBuggyHour12Behavior == null) {
_hasBuggyHour12Behavior = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
hour12: false
}).format(new Date(2020, 2, 3, 0)) === '24';
}

return _hasBuggyHour12Behavior;
}

let _hasBuggyResolvedHourCycle: boolean = null;
function hasBuggyResolvedHourCycle() {
if (_hasBuggyResolvedHourCycle == null) {
_hasBuggyResolvedHourCycle = (new Intl.DateTimeFormat('fr', {
hour: 'numeric',
hour12: false
}).resolvedOptions() as ResolvedDateTimeFormatOptions).hourCycle === 'h12';
}

return _hasBuggyResolvedHourCycle;
}

function getResolvedHourCycle(locale: string, options: Intl.DateTimeFormatOptions) {
// Work around buggy results in resolved hourCycle and hour12 options in WebKit.
// Format the minimum possible hour and maximum possible hour in a day and parse the results.
locale = locale.replace(/(-u-)?-nu-[a-zA-Z0-9]+/, '');
locale += (locale.includes('-u-') ? '' : '-u') + '-nu-latn';
let formatter = getCachedDateFormatter(locale, options);

let min = parseInt(formatter.formatToParts(new Date(2020, 2, 3, 0)).find(p => p.type === 'hour').value, 10);
let max = parseInt(formatter.formatToParts(new Date(2020, 2, 3, 23)).find(p => p.type === 'hour').value, 10);

if (min === 0 && max === 23) {
return 'h23';
}

if (min === 24 && max === 23) {
return 'h24';
}

if (min === 0 && max === 11) {
return 'h11';
}

if (min === 12 && max === 11) {
return 'h12';
}

throw new Error('Unexpected hour cycle result');
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.

import {AnyCalendarDate} from '../types';
import {CalendarDate} from '../CalendarDate';
import {GregorianCalendar} from './GregorianCalendar';
import {Mutable} from '../utils';
Expand All @@ -25,10 +26,10 @@ export class BuddhistCalendar extends GregorianCalendar {
fromJulianDay(jd: number): CalendarDate {
let date = super.fromJulianDay(jd) as Mutable<CalendarDate>;
date.year -= BUDDHIST_ERA_START;
return date;
return date as CalendarDate;
}

toJulianDay(date: CalendarDate) {
toJulianDay(date: AnyCalendarDate) {
return super.toJulianDay(
new CalendarDate(
date.year + BUDDHIST_ERA_START,
Expand Down
Loading

0 comments on commit ebd4316

Please sign in to comment.