Skip to content
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

Merged
merged 5 commits into from
Feb 2, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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', () => {
Copy link
Member

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?

Copy link
Contributor Author

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

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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this start with null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 calendarLocale.dates[date + 1] all the time (especially since months and days are 0-indexed), so I added null as the representation for dates[0]

'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');
})
});
168 changes: 168 additions & 0 deletions src/lib/core/datetime/calendar-locale.ts
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The 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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
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 @@ -68,7 +69,8 @@ const MATERIAL_MODULES = [
PlatformModule,
ProjectionModule,
CompatibilityModule,
ObserveContentModule
ObserveContentModule,
DatetimeModule,
];

@NgModule({
Expand Down