-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create SimpleDate and CalendarLocale objects (#2839)
* create SimpleDate and CalendarLocale objects * tests * addressed comments * make parseDate more robust * simplify createFormatFunction
- Loading branch information
Showing
7 changed files
with
324 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import {inject, TestBed, async} from '@angular/core/testing'; | ||
import {CalendarLocale} from './calendar-locale'; | ||
import {DatetimeModule} from './index'; | ||
import {SimpleDate} from './simple-date'; | ||
|
||
|
||
describe('DefaultCalendarLocale', () => { | ||
let calendarLocale: CalendarLocale; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [DatetimeModule], | ||
}); | ||
|
||
TestBed.compileComponents(); | ||
})); | ||
|
||
beforeEach(inject([CalendarLocale], (cl: CalendarLocale) => { | ||
calendarLocale = cl; | ||
})); | ||
|
||
it('lists months', () => { | ||
expect(calendarLocale.months).toEqual([ | ||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', | ||
'October', 'November', 'December' | ||
]); | ||
}); | ||
|
||
it('lists short months', () => { | ||
expect(calendarLocale.shortMonths).toEqual([ | ||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' | ||
]); | ||
}); | ||
|
||
it('lists narrow months', () => { | ||
expect(calendarLocale.narrowMonths).toEqual([ | ||
'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D' | ||
]); | ||
}); | ||
|
||
it('lists days', () => { | ||
expect(calendarLocale.days).toEqual([ | ||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' | ||
]); | ||
}); | ||
|
||
it('lists short days', () => { | ||
expect(calendarLocale.shortDays).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); | ||
}); | ||
|
||
it('lists narrow days', () => { | ||
expect(calendarLocale.narrowDays).toEqual(['S', 'M', 'T', 'W', 'T', 'F', 'S']); | ||
}); | ||
|
||
it('lists dates', () => { | ||
expect(calendarLocale.dates).toEqual([ | ||
null, '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', | ||
'17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' | ||
]); | ||
}); | ||
|
||
it('has first day of the week', () => { | ||
expect(calendarLocale.firstDayOfWeek).toBe(0); | ||
}); | ||
|
||
it('has calendar label', () => { | ||
expect(calendarLocale.calendarLabel).toBe('Calendar'); | ||
}); | ||
|
||
it('has open calendar label', () => { | ||
expect(calendarLocale.openCalendarLabel).toBe('Open calendar'); | ||
}); | ||
|
||
it('parses SimpleDate from string', () => { | ||
expect(calendarLocale.parseDate('1/1/2017')).toEqual(new SimpleDate(2017, 0, 1)); | ||
}); | ||
|
||
it('parses SimpleDate from number', () => { | ||
let timestamp = new Date().getTime(); | ||
expect(calendarLocale.parseDate(timestamp)) | ||
.toEqual(SimpleDate.fromNativeDate(new Date(timestamp))); | ||
}); | ||
|
||
it ('parses SimpleDate from SimpleDate by copying', () => { | ||
let originalSimpleDate = new SimpleDate(2017, 0, 1); | ||
expect(calendarLocale.parseDate(originalSimpleDate)).toEqual(originalSimpleDate); | ||
}); | ||
|
||
it('parses null for invalid dates', () => { | ||
expect(calendarLocale.parseDate('hello')).toBeNull(); | ||
}); | ||
|
||
it('formats SimpleDates', () => { | ||
expect(calendarLocale.formatDate(new SimpleDate(2017, 0, 1))).toEqual('1/1/2017'); | ||
}); | ||
|
||
it('gets header label for calendar month', () => { | ||
expect(calendarLocale.getCalendarMonthHeaderLabel(new SimpleDate(2017, 0, 1))) | ||
.toEqual('Jan 2017'); | ||
}); | ||
|
||
it('gets header label for calendar year', () => { | ||
expect(calendarLocale.getCalendarYearHeaderLabel(new SimpleDate(2017, 0, 1))).toBe('2017'); | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import {SimpleDate} from './simple-date'; | ||
import {Injectable} from '@angular/core'; | ||
|
||
|
||
/** Whether the browser supports the Intl API. */ | ||
const SUPPORTS_INTL_API = !!Intl; | ||
|
||
|
||
/** Creates an array and fills it with values. */ | ||
function range<T>(length: number, valueFunction: (index: number) => T): T[] { | ||
return Array.apply(null, Array(length)).map((v: undefined, i: number) => valueFunction(i)); | ||
} | ||
|
||
|
||
/** | ||
* This class encapsulates the details of how to localize all information needed for displaying a | ||
* calendar. It is used by md-datepicker to render a properly localized calendar. Unless otherwise | ||
* specified by the user DefaultCalendarLocale will be provided as the CalendarLocale. | ||
*/ | ||
@Injectable() | ||
export abstract class CalendarLocale { | ||
/** Labels to use for the long form of the month. (e.g. 'January') */ | ||
months: string[]; | ||
|
||
/** Labels to use for the short form of the month. (e.g. 'Jan') */ | ||
shortMonths: string[]; | ||
|
||
/** Labels to use for the narrow form of the month. (e.g. 'J') */ | ||
narrowMonths: string[]; | ||
|
||
/** Labels to use for the long form of the week days. (e.g. 'Sunday') */ | ||
days: string[]; | ||
|
||
/** Labels to use for the short form of the week days. (e.g. 'Sun') */ | ||
shortDays: string[]; | ||
|
||
/** Labels to use for the narrow form of the week days. (e.g. 'S') */ | ||
narrowDays: string[]; | ||
|
||
/** | ||
* Labels to use for the dates of the month. (e.g. null, '1', '2', ..., '31'). | ||
* Note that the 0th index is null, since there is no January 0th. | ||
*/ | ||
dates: string[]; | ||
|
||
/** The first day of the week. (e.g. 0 = Sunday, 6 = Saturday). */ | ||
firstDayOfWeek: number; | ||
|
||
/** A label for the calendar popup (used by screen readers). */ | ||
calendarLabel: string; | ||
|
||
/** A label for the button used to open the calendar popup (used by screen readers). */ | ||
openCalendarLabel: string; | ||
|
||
/** | ||
* Parses a SimpleDate from a value. | ||
* @param value The value to parse. | ||
*/ | ||
parseDate: (value: any) => SimpleDate; | ||
|
||
/** | ||
* Formats a SimpleDate to a string. | ||
* @param date The date to format. | ||
*/ | ||
formatDate: (date: SimpleDate) => string; | ||
|
||
/** | ||
* Gets a label to display as the heading for the specified calendar month. | ||
* @param date A date that falls within the month to be labeled. | ||
*/ | ||
getCalendarMonthHeaderLabel: (date: SimpleDate) => string; | ||
|
||
/** | ||
* Gets a label to display as the heading for the specified calendar year. | ||
* @param date A date that falls within the year to be labeled. | ||
*/ | ||
getCalendarYearHeaderLabel: (date: SimpleDate) => string; | ||
} | ||
|
||
|
||
/** | ||
* The default implementation of CalendarLocale. This implementation is a best attempt at | ||
* localization using only the functionality natively available in JS. If more robust localization | ||
* is needed, an alternate class can be provided as the CalendarLocale for the app. | ||
*/ | ||
export class DefaultCalendarLocale implements CalendarLocale { | ||
months = SUPPORTS_INTL_API ? this._createMonthsArray('long') : | ||
[ | ||
'January', | ||
'February', | ||
'March', | ||
'April', | ||
'May', | ||
'June', | ||
'July', | ||
'August', | ||
'September', | ||
'October', | ||
'November', | ||
'December' | ||
]; | ||
|
||
shortMonths = SUPPORTS_INTL_API ? this._createMonthsArray('short') : | ||
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; | ||
|
||
narrowMonths = SUPPORTS_INTL_API ? this._createMonthsArray('narrow') : | ||
['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']; | ||
|
||
days = SUPPORTS_INTL_API ? this._createDaysArray('long') : | ||
['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; | ||
|
||
shortDays = SUPPORTS_INTL_API ? this._createDaysArray('short') : | ||
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; | ||
|
||
narrowDays = SUPPORTS_INTL_API ? this._createDaysArray('narrow') : | ||
['S', 'M', 'T', 'W', 'T', 'F', 'S']; | ||
|
||
dates = [null].concat( | ||
SUPPORTS_INTL_API ? this._createDatesArray('numeric') : range(31, i => String(i + 1))); | ||
|
||
firstDayOfWeek = 0; | ||
|
||
calendarLabel = 'Calendar'; | ||
|
||
openCalendarLabel = 'Open calendar'; | ||
|
||
parseDate(value: any) { | ||
if (value instanceof SimpleDate) { | ||
return value; | ||
} | ||
let timestamp = typeof value == 'number' ? value : Date.parse(value); | ||
return isNaN(timestamp) ? null : SimpleDate.fromNativeDate(new Date(timestamp)); | ||
} | ||
|
||
formatDate = this._createFormatFunction(undefined) || | ||
((date: SimpleDate) => date.toNativeDate().toDateString()); | ||
|
||
getCalendarMonthHeaderLabel = this._createFormatFunction({month: 'short', year: 'numeric'}) || | ||
((date: SimpleDate) => this.shortMonths[date.month] + ' ' + date.year); | ||
|
||
getCalendarYearHeaderLabel = this._createFormatFunction({year: 'numeric'}) || | ||
((date: SimpleDate) => String(date.year)); | ||
|
||
private _createMonthsArray(format: string) { | ||
let dtf = new Intl.DateTimeFormat(undefined, {month: format}); | ||
return range(12, i => dtf.format(new Date(2017, i, 1))); | ||
} | ||
|
||
private _createDaysArray(format: string) { | ||
let dtf = new Intl.DateTimeFormat(undefined, {weekday: format}); | ||
return range(7, i => dtf.format(new Date(2017, 0, i + 1))); | ||
} | ||
|
||
private _createDatesArray(format: string) { | ||
let dtf = new Intl.DateTimeFormat(undefined, {day: format}); | ||
return range(31, i => dtf.format(new Date(2017, 0, i + 1))); | ||
} | ||
|
||
/** | ||
* Creates a function to format SimpleDates as strings using Intl.DateTimeFormat. | ||
* @param options The options to use for Intl.DateTimeFormat. | ||
* @returns The newly created format function, or null if the Intl API is not available. | ||
* @private | ||
*/ | ||
private _createFormatFunction(options: Object): (date: SimpleDate) => string { | ||
if (SUPPORTS_INTL_API) { | ||
let dtf = new Intl.DateTimeFormat(undefined, options); | ||
return (date: SimpleDate) => dtf.format(date.toNativeDate()); | ||
} | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import {NgModule} from '@angular/core'; | ||
import {DefaultCalendarLocale, CalendarLocale} from './calendar-locale'; | ||
|
||
|
||
export * from './calendar-locale'; | ||
export * from './simple-date'; | ||
|
||
|
||
@NgModule({ | ||
providers: [{provide: CalendarLocale, useClass: DefaultCalendarLocale}], | ||
}) | ||
export class DatetimeModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import {SimpleDate} from './simple-date'; | ||
|
||
|
||
describe('SimpleDate', () => { | ||
it('can be created from native Date', () => { | ||
expect(SimpleDate.fromNativeDate(new Date(2017, 0, 1))).toEqual(new SimpleDate(2017, 0, 1)); | ||
}); | ||
|
||
it('can be converted to native Date', () => { | ||
expect(new SimpleDate(2017, 0, 1).toNativeDate()).toEqual(new Date(2017, 0, 1)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* A replacement for the native JS Date class that allows us to avoid dealing with time zone | ||
* details and the time component of the native Date. | ||
*/ | ||
export class SimpleDate { | ||
static fromNativeDate(nativeDate: Date) { | ||
return new SimpleDate(nativeDate.getFullYear(), nativeDate.getMonth(), nativeDate.getDate()); | ||
} | ||
|
||
constructor(public year: number, public month: number, public date: number) {} | ||
|
||
toNativeDate() { | ||
return new Date(this.year, this.month, this.date); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters