Skip to content

Commit

Permalink
feat(datepicker): date format finding (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
roll314 authored and pimenovoleg committed Jun 7, 2019
1 parent fa5f15f commit 075c91d
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 7 deletions.
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 @@ -457,4 +472,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':
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);
}
}

0 comments on commit 075c91d

Please sign in to comment.