Skip to content

Commit

Permalink
create SimpleDate and CalendarLocale objects (#2839)
Browse files Browse the repository at this point in the history
* create SimpleDate and CalendarLocale objects

* tests

* addressed comments

* make parseDate more robust

* simplify createFormatFunction
  • Loading branch information
mmalerba committed Feb 9, 2017
1 parent a022035 commit e88d0e7
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {PortalModule} from './portal/portal-directives';
import {OverlayModule} from './overlay/overlay-directives';
import {A11yModule} from './a11y/index';
import {MdSelectionModule} from './selection/index';
import {DatetimeModule} from './datetime/index';


// RTL
Expand Down Expand Up @@ -125,6 +126,8 @@ export {coerceNumberProperty} from './coercion/number-property';
// Compatibility
export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibility/compatibility';

// Datetime
export * from './datetime/index';

@NgModule({
imports: [
Expand All @@ -137,6 +140,7 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi
A11yModule,
MdOptionModule,
MdSelectionModule,
DatetimeModule,
],
exports: [
MdLineModule,
Expand All @@ -148,6 +152,7 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi
A11yModule,
MdOptionModule,
MdSelectionModule,
DatetimeModule,
],
})
export class MdCoreModule {
Expand Down
105 changes: 105 additions & 0 deletions src/lib/core/datetime/calendar-locale.spec.ts
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');
})
});
172 changes: 172 additions & 0 deletions src/lib/core/datetime/calendar-locale.ts
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;
}
}
12 changes: 12 additions & 0 deletions src/lib/core/datetime/index.ts
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 {}
12 changes: 12 additions & 0 deletions src/lib/core/datetime/simple-date.spec.ts
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));
});
});
15 changes: 15 additions & 0 deletions src/lib/core/datetime/simple-date.ts
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);
}
}
4 changes: 3 additions & 1 deletion src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
A11yModule,
ProjectionModule,
CompatibilityModule,
DatetimeModule,
} from './core/index';

import {MdButtonToggleModule} from './button-toggle/index';
Expand Down Expand Up @@ -70,7 +71,8 @@ const MATERIAL_MODULES = [
PlatformModule,
ProjectionModule,
CompatibilityModule,
ObserveContentModule
ObserveContentModule,
DatetimeModule,
];

@NgModule({
Expand Down

0 comments on commit e88d0e7

Please sign in to comment.