Skip to content

feat(datepicker): date format finding #133

Merged
merged 8 commits into from
Jun 7, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions packages/mosaic-dev/datepicker/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DateAdapter, MC_DATE_LOCALE } from '@ptsecurity/cdk/datetime';
import {
MC_MOMENT_DATE_ADAPTER_OPTIONS,
McMomentDateModule,
MomentDateAdapter
} from '@ptsecurity/mosaic-moment-adapter/adapter';
Expand All @@ -33,8 +34,9 @@ const moment = _rollupMoment || _moment;
styleUrls: ['./styles.scss'],
encapsulation: ViewEncapsulation.None,
providers: [
{provide: MC_DATE_LOCALE, useValue: 'ru'},
{provide: DateAdapter, useClass: MomentDateAdapter, deps: [MC_DATE_LOCALE]}
{ provide: MC_DATE_LOCALE, useValue: 'ru' },
{ provide: MC_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { findDateFormat: true } },
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [ MC_DATE_LOCALE, MC_MOMENT_DATE_ADAPTER_OPTIONS ] }
]
})
export class DemoComponent {
Expand Down
119 changes: 119 additions & 0 deletions packages/mosaic-moment-adapter/adapter/moment-date-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MC_DATE_LOCALE
} from '@ptsecurity/cdk/datetime';
import * as moment from 'moment';
// tslint:disable-next-line:no-duplicate-imports
import { Moment } from 'moment';

import { MC_MOMENT_DATE_ADAPTER_OPTIONS, MomentDateModule } from './index';
Expand Down Expand Up @@ -364,7 +365,125 @@ describe('MomentDateAdapter', () => {
expect(adapter.addCalendarMonths(moment(), 1).locale()).toBe('ja');
expect(adapter.addCalendarYears(moment(), 1).locale()).toBe('ja');
});
});

describe('MomentDateAdapter findDateFormat = true', () => {
let adapter: MomentDateAdapter;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MomentDateModule],
providers: [{
provide: MC_MOMENT_DATE_ADAPTER_OPTIONS,
useValue: {findDateFormat: true}
}]
}).compileComponents();
}));

beforeEach(inject([DateAdapter], (dateAdapter: MomentDateAdapter) => {
moment.locale('en');
adapter = dateAdapter;
adapter.setLocale('en');
}));

it('should parse ISO', () => {
adapter.setLocale('ru');
const utcDate = new Date(2019, 5, 3, 14, 50, 30);
utcDate.setMinutes(utcDate.getMinutes() - utcDate.getTimezoneOffset());
expect(adapter.parse('2019-06-03T14:50:30.000Z', '')!.toDate())
.toEqual(utcDate);
});

it('should parse dashed date', () => {
adapter.setLocale('ru');
// finishing year
expect(adapter.parse('03-06-2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
expect(adapter.parse('03-06-19', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
// leading year
expect(adapter.parse('2019-06-03', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

adapter.setLocale('en');
// finishing year
expect(adapter.parse('03-06-2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
// short year
expect(adapter.parse('03-06-19', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

// leading year
expect(adapter.parse('2019-06-03', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

});

it('should parse slashed date', () => {
adapter.setLocale('ru');
expect(adapter.parse('03/06/2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
// short year
expect(adapter.parse('03/06/19', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

adapter.setLocale('en');
// US by default
expect(adapter.parse('03/06/2019', '')!.toDate())
.toEqual(new Date(2019, 2, 6));

// short year
expect(adapter.parse('03/06/19', '')!.toDate())
.toEqual(new Date(2019, 2, 6));

// month order guessing
expect(adapter.parse('23/06/2019', '')!.toDate())
.toEqual(new Date(2019, 5, 23));
});

it('should parse doted date', () => {
adapter.setLocale('ru');
expect(adapter.parse('03.06.2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
expect(adapter.parse('03.06.19', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

adapter.setLocale('en');
expect(adapter.parse('03.06.2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
expect(adapter.parse('03.06.19', '')!.toDate())
.toEqual(new Date(2019, 5, 3));
});

it('should parse long formatted date', () => {
adapter.setLocale('ru');
expect(adapter.parse('3 июня 2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

expect(adapter.parse('6 фев 2019', '')!.toDate())
.toEqual(new Date(2019, 1, 6));

adapter.setLocale('en');
expect(adapter.parse('June 3rd 2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

expect(adapter.parse('Feb 6th 2019', '')!.toDate())
.toEqual(new Date(2019, 1, 6));

expect(adapter.parse('3 June 2019', '')!.toDate())
.toEqual(new Date(2019, 5, 3));

expect(adapter.parse('6 Feb 2019', '')!.toDate())
.toEqual(new Date(2019, 1, 6));
});

it('should parse unix timestamp', () => {
adapter.setLocale('ru');
const utcDate = new Date(2019, 5, 3, 14, 50, 30);
utcDate.setMinutes(utcDate.getMinutes() - utcDate.getTimezoneOffset());
expect(adapter.parse('1559573430', '')!.toDate())
.toEqual(utcDate);
});
});

describe('MomentDateAdapter with MC_DATE_LOCALE override', () => {
Expand Down
152 changes: 147 additions & 5 deletions packages/mosaic-moment-adapter/adapter/moment-date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface IMcMomentDateAdapterOptions {
* {@default false}
*/
useUtc: boolean;
/**
* whether should parse method try guess date format
* {@default false}
*/
findDateFormat: boolean;
}

/** InjectionToken for moment date adapter to configure options. */
Expand All @@ -45,7 +50,8 @@ export const MC_MOMENT_DATE_ADAPTER_OPTIONS = new InjectionToken<IMcMomentDateAd
// tslint:disable:naming-convention
export function MC_MOMENT_DATE_ADAPTER_OPTIONS_FACTORY(): IMcMomentDateAdapterOptions {
return {
useUtc: false
useUtc: false,
findDateFormat: false
};
}

Expand Down Expand Up @@ -206,12 +212,21 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
}

parse(value: any, parseFormat: string | string[]): Moment | null {
// tslint:disable:triple-equals
if (value && typeof value == 'string') {
return this.createMoment(value, parseFormat, this.locale);
if (value) {
if (value && typeof value === 'string') {
if (this.options && this.options.findDateFormat) {
return this.findFormat(value);
}

return parseFormat
? this.createMoment(value, parseFormat, this.locale)
: this.createMoment(value).locale(this.locale);
}

return this.createMoment(value).locale(this.locale);
}

return value ? this.createMoment(value).locale(this.locale) : null;
return null;
}

format(date: Moment, displayFormat: string): string {
Expand Down Expand Up @@ -445,4 +460,131 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
private configureTranslator(locale: string): void {
this.messageformat = new MessageFormat(locale);
}

private isNumeric(value: any): boolean {
return !isNaN(parseFloat(value)) && isFinite(value);
}

private findFormat(value: string): Moment | null {
if (!value) {
return null;
}

// default test - iso
const isoDate = this.createMoment(value, moment.ISO_8601, this.locale);

if (isoDate.isValid()) {
return isoDate;
}

if (this.isNumeric(value)) {
// unix time sec
return this.createMoment(value, 'X', this.locale);
}

// long months naming: D MMM YYYY, MMM Do YYYY with short case support
if (
/^\d{1,2}\s\S+\s(\d{2}|\d{4})$/.test(value.trim()) ||
/^\S+\s\d{1,2}[a-z]{2}\s(\d{2}|\d{4})$/.test(value.trim())
) {
return this.parseWithSpace(value);
}

// slash notation: DD/MM/YYYY, MM/DD/YYYY with short case support
if (/^\d{1,2}\/\d{1,2}\/(\d{2}|\d{4})$/.test(value)) {
return this.parseWithSlash(value);
}

// dash notation: DD-MM-YYYY, YYYY-DD-MM with short case support
if (/(^(\d{1,2}|\d{4})-\d{1,2}-\d{1,2}$)|(^\d{1,2}-\d{1,2}-(\d{2}|\d{4})$)/.test(value)) {
return this.parseWithDash(value);
}

// dot notation: DD.MM.YYYY with short case support
if (/^\d{1,2}\.\d{1,2}\.(\d{2}|\d{4})$/.test(value)) {
return this.parseWithDot(value);
}

return null;
}

private parseWithSpace(value: string): Moment | null {
switch (this.locale) {
case 'ru':
lskramarov marked this conversation as resolved.
Show resolved Hide resolved
return this.createMoment(value, 'DD MMMM YYYY', this.locale);
case 'en':
// 16 Feb 2019 vs Feb 16th 2019, covers Feb and February cases
if (this.isNumeric(value[0])) {
return this.createMoment(value, 'D MMMM YYYY', this.locale);
}

return this.createMoment(value, 'MMMM Do YYYY', this.locale);
default:
throw new Error(`Locale ${this.locale} is not supported`);
}
}

private parseWithSlash(value: string): Moment | null {
switch (this.locale) {
case 'ru':
return this.createMoment(value, 'DD/MM/YYYY', this.locale);
// todo do we use generalized locales? en vs en-US; until not we try to guess
case 'en':
// US vs UK
const parts = value.split('/');
const datePartsCount = 3;
if (parts.length !== datePartsCount) {
return null;
}

const firstPart = parts[0].trim();
const secondPart = parts[1].trim();

if (!this.isNumeric(firstPart) || !this.isNumeric(secondPart)) {
return null;
}

const monthsInYears = 12;

const canFirstBeMonth = +firstPart <= monthsInYears;
const canSecondByMonth = +secondPart <= monthsInYears;

// first two parts cannot be month
if (!canFirstBeMonth && !canSecondByMonth) {
return null;
}

const canDetermineWhereMonth = canFirstBeMonth && canSecondByMonth;

if (canDetermineWhereMonth) {
// use US format by default
return this.createMoment(value, 'MM/DD/YYYY', this.locale);
}

return canFirstBeMonth && !canSecondByMonth
? this.createMoment(value, 'MM/DD/YYYY', this.locale)
: this.createMoment(value, 'DD/MM/YYYY', this.locale);
default:
throw new Error(`Locale ${this.locale} is not supported`);
}
}

private parseWithDash(value: string): Moment | null {
// leading year vs finishing year
const parts = value.split('-');
if (parts[0].length === 0) {
return null;
}

const maxDayOrMonthCharsCount = 2;

return parts[0].length <= maxDayOrMonthCharsCount
? this.createMoment(value, 'DD-MM-YYYY', this.locale)
: this.createMoment(value, 'YYYY-MM-DD', this.locale);
}

private parseWithDot(value: string): Moment | null {
// covers two cases YYYY and YY (for current year)
return this.createMoment(value, 'DD.MM.YYYY', this.locale);
}
}