-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
create SimpleDate and CalendarLocale objects #2839
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this start with null? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a comment in the base class, basically I thought it would be nice to not have to do |
||
'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'); | ||
}) | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add class description |
||
/** 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add class description |
||
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))); | ||
} | ||
|
||
private _createFormatFunction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you leave a description of this function? It's especially difficult to visually parse the signature of the function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, also simplified it since its just as easy to return null and specify the fallback elsewhere |
||
options: Object, fallback: (date: SimpleDate) => string): (date: SimpleDate) => string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix identations |
||
if (SUPPORTS_INTL_API) { | ||
let dtf = new Intl.DateTimeFormat(undefined, options); | ||
return (date: SimpleDate) => dtf.format(date.toNativeDate()); | ||
} | ||
return fallback; | ||
} | ||
} |
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 {} |
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)); | ||
}); | ||
}); |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add class description |
||
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests don't seem to well-capture using the current locale with fallback behavior. Any ideas on how to improve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately I don't think there's any way to set the browser's locale from JS