Skip to content

Instantly share code, notes, and snippets.

@Zyzle
Last active November 12, 2020 14:11
Show Gist options
  • Save Zyzle/a9c5daa396882eb155282bcb454b9511 to your computer and use it in GitHub Desktop.
Save Zyzle/a9c5daa396882eb155282bcb454b9511 to your computer and use it in GitHub Desktop.
Luxon date adaptor for angular material
import { LOCALE_ID } from '@angular/core';
import { async, inject, TestBed } from '@angular/core/testing';
import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
import { DateTime, Settings } from 'luxon';
import { LuxonDateAdapter, LUXON_DATE_FORMATS } from './luxon-date-adapter';
// avoid confusion when working with months
const DEC = 12, FEB = 2, JAN = 1, MAR = 3;
describe('LuxonDateAdapter', () => {
let adapter: LuxonDateAdapter;
let assertValidDate: (d: DateTime | null, valid: boolean) => void;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'en' },
{ provide: MAT_DATE_FORMATS, useValue: LUXON_DATE_FORMATS },
{ provide: DateAdapter, useClass: LuxonDateAdapter, deps: [MAT_DATE_LOCALE] },
]
}).compileComponents();
}));
beforeEach(inject([DateAdapter], (dateAdapter: LuxonDateAdapter) => {
Settings.defaultLocale = 'en';
adapter = dateAdapter;
adapter.setLocale('en');
assertValidDate = (d: DateTime | null, valid: boolean) => {
expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`);
expect(adapter.isValid(d)).toBe(valid,
`Expected ${d} to be ${valid ? 'valid' : 'invalid'},` +
` but was ${valid ? 'invalid' : 'valid'}`);
};
}));
it('should get year', () => {
expect(adapter.getYear(DateTime.local(2017, JAN, 1))).toBe(2017);
});
it('should get month', () => {
expect(adapter.getMonth(DateTime.local(2017, JAN, 1))).toBe(0);
});
it('should get date', () => {
expect(adapter.getDate(DateTime.local(2017, JAN, 1))).toBe(1);
});
it('should get day of week', () => {
expect(adapter.getDayOfWeek(DateTime.local(2017, JAN, 1))).toBe(7);
});
it('should get same day of week in a locale with a different first day of the week', () => {
adapter.setLocale('fr');
expect(adapter.getDayOfWeek(DateTime.local(2017, JAN, 1))).toBe(7);
});
it('should get long month names', () => {
expect(adapter.getMonthNames('long')).toEqual([
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
]);
});
it('should get short month names', () => {
expect(adapter.getMonthNames('short')).toEqual([
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]);
});
it('should get narrow month names', () => {
expect(adapter.getMonthNames('narrow')).toEqual([
'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'
]);
});
/** skipping this as it doesn't seem to work */
it('should get month names in a different locale', () => {
adapter.setLocale('zh');
expect(adapter.getMonthNames('long')).toEqual([
'一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'
]);
});
it('should get date names', () => {
expect(adapter.getDateNames()).toEqual([
'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('should get date names in a different locale', () => {
adapter.setLocale('ja-JP');
expect(adapter.getDateNames()).toEqual([
'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('should get long day of week names', () => {
expect(adapter.getDayOfWeekNames('long')).toEqual([
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
]);
});
it('should get short day of week names', () => {
expect(adapter.getDayOfWeekNames('short')).toEqual([
'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'
]);
});
it('should get narrow day of week names', () => {
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
'M', 'T', 'W', 'T', 'F', 'S', 'S'
]);
});
it('should get day of week names in a different locale', () => {
adapter.setLocale('ja-JP');
expect(adapter.getDayOfWeekNames('long')).toEqual([
'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'
]);
});
it('should get year name', () => {
expect(adapter.getYearName(DateTime.local(2017, JAN, 1))).toBe('2017');
});
it('should get year name in a different locale', () => {
adapter.setLocale('ja-JP');
expect(adapter.getYearName(DateTime.local(2017, JAN, 1))).toBe('2017年');
});
it('should get first day of week', () => {
expect(adapter.getFirstDayOfWeek()).toBe(0);
});
/**
* The miss-match here is due to the fact that date-picker is 0 indexed for months
* Luxon is 1 indexed
*/
it('should create Luxon#DateTime', () => {
expect(adapter.createDate(2017, JAN - 1, 1).toISODate()).toEqual(DateTime.local(2017, JAN, 1).toISODate());
});
it('should create Luxon#DateTime with low year number', () => {
expect(adapter.createDate(-1, 1, 1).year).toBe(-1);
expect(adapter.createDate(0, 1, 1).year).toBe(0);
expect(adapter.createDate(50, 1, 1).year).toBe(50);
expect(adapter.createDate(99, 1, 1).year).toBe(99);
expect(adapter.createDate(100, 1, 1).year).toBe(100);
});
it('should get today\'s date', () => {
expect(adapter.sameDate(adapter.today(), DateTime.local()))
.toBe(true, `should be equal to today's date`);
});
it('should parse string according to given format', () => {
expect(adapter.parse('01/02/2017', 'LL/dd/yyyy').toISODate())
.toEqual(DateTime.local(2017, JAN, 2).toISODate());
expect(adapter.parse('01/02/2017', 'dd/LL/yyyy').toISODate())
.toEqual(DateTime.local(2017, FEB, 1).toISODate());
});
it('should parse number', () => {
const timestamp = new Date().getTime();
expect(adapter.parse(timestamp).toISODate()).toEqual(DateTime.fromMillis(timestamp).toISODate());
});
it('should format date according to given format', () => {
expect(adapter.format(DateTime.local(2017, JAN, 2), 'LL/dd/yyyy')).toEqual('01/02/2017');
expect(adapter.format(DateTime.local(2017, JAN, 2), 'dd/LL/yyyy')).toEqual('02/01/2017');
});
it('should format with a different locale', () => {
expect(adapter.format(DateTime.local(2017, JAN, 2), 'DD')).toEqual('Jan 2, 2017');
adapter.setLocale('ja-JP');
expect(adapter.format(DateTime.local(2017, JAN, 2), 'DD')).toEqual('2017年1月2日');
});
it('should add years', () => {
expect(adapter.addCalendarYears(DateTime.local(2017, JAN, 1), 1).toISODate())
.toEqual(DateTime.local(2018, JAN, 1).toISODate());
expect(adapter.addCalendarYears(DateTime.local(2017, JAN, 1), -1).toISODate())
.toEqual(DateTime.local(2016, JAN, 1).toISODate());
});
it('should respect leap years when adding years', () => {
expect(adapter.addCalendarYears(DateTime.local(2016, FEB, 29), 1).toISODate())
.toEqual(DateTime.local(2017, FEB, 28).toISODate());
expect(adapter.addCalendarYears(DateTime.local(2016, FEB, 29), -1).toISODate())
.toEqual(DateTime.local(2015, FEB, 28).toISODate());
});
it('should add months', () => {
expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 1), 1).toISODate())
.toEqual(DateTime.local(2017, FEB, 1).toISODate());
expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 1), -1).toISODate())
.toEqual(DateTime.local(2016, DEC, 1).toISODate());
});
it('should respect month length differences when adding months', () => {
expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 31), 1).toISODate())
.toEqual(DateTime.local(2017, FEB, 28).toISODate());
expect(adapter.addCalendarMonths(DateTime.local(2017, MAR, 31), -1).toISODate())
.toEqual(DateTime.local(2017, FEB, 28).toISODate());
});
it('should add days', () => {
expect(adapter.addCalendarDays(DateTime.local(2017, JAN, 1), 1).toISODate())
.toEqual(DateTime.local(2017, JAN, 2).toISODate());
expect(adapter.addCalendarDays(DateTime.local(2017, JAN, 1), -1).toISODate())
.toEqual(DateTime.local(2016, DEC, 31).toISODate());
});
/** It's ok for the adapter to return the same object as DateTimes are immutable */
it('should clone', () => {
const date = DateTime.local(2017, JAN, 1);
expect(adapter.clone(date).toISODate()).toEqual(date.toISODate());
expect(adapter.clone(date)).toBe(date);
});
it('should compare dates', () => {
expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, JAN, 2))).toBeLessThan(0);
expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, FEB, 1))).toBeLessThan(0);
expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2018, JAN, 1))).toBeLessThan(0);
expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, JAN, 1))).toBe(0);
expect(adapter.compareDate(DateTime.local(2018, JAN, 1), DateTime.local(2017, JAN, 1))).toBeGreaterThan(0);
expect(adapter.compareDate(DateTime.local(2017, FEB, 1), DateTime.local(2017, JAN, 1))).toBeGreaterThan(0);
expect(adapter.compareDate(DateTime.local(2017, JAN, 2), DateTime.local(2017, JAN, 1))).toBeGreaterThan(0);
});
it('should clamp date at lower bound', () => {
expect(adapter.clampDate(
DateTime.local(2017, JAN, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1)))
.toEqual(DateTime.local(2018, JAN, 1));
});
it('should clamp date at upper bound', () => {
expect(adapter.clampDate(
DateTime.local(2020, JAN, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1)))
.toEqual(DateTime.local(2019, JAN, 1));
});
it('should clamp date already within bounds', () => {
expect(adapter.clampDate(
DateTime.local(2018, FEB, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1)))
.toEqual(DateTime.local(2018, FEB, 1));
});
it('should count today as a valid date instance', () => {
const d = DateTime.local();
expect(adapter.isValid(d)).toBe(true);
expect(adapter.isDateInstance(d)).toBe(true);
});
it('should count an invalid date as an invalid date instance', () => {
const d = DateTime.local(NaN);
expect(adapter.isValid(d)).toBe(false);
expect(adapter.isDateInstance(d)).toBe(true);
});
it('should count a string as not a date instance', () => {
const d = '1/1/2017';
expect(adapter.isDateInstance(d)).toBe(false);
});
it('should count a Date as not a date instance', () => {
const d = new Date();
expect(adapter.isDateInstance(d)).toBe(false);
});
it('should create valid dates from valid ISO strings', () => {
assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true);
assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true);
assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true);
assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false);
assertValidDate(adapter.deserialize('1/1/2017'), false);
expect(adapter.deserialize('')).toBeNull();
expect(adapter.deserialize(null)).toBeNull();
assertValidDate(adapter.deserialize(new Date()), true);
assertValidDate(adapter.deserialize(new Date(NaN)), false);
assertValidDate(adapter.deserialize(DateTime.local()), true);
assertValidDate(adapter.deserialize(DateTime.invalid('invalid date')), false);
});
it('returned Moments should have correct locale', () => {
adapter.setLocale('ja-JP');
expect(adapter.createDate(2017, JAN, 1).locale).toBe('ja-JP');
expect(adapter.today().locale).toBe('ja-JP');
expect(adapter.clone(DateTime.local()).locale).toBe('ja-JP');
expect(adapter.parse('01/01/2017', 'LL/dd/yyyy').locale).toBe('ja-JP');
expect(adapter.addCalendarDays(DateTime.local(), 1).locale).toBe('ja-JP');
expect(adapter.addCalendarMonths(DateTime.local(), 1).locale).toBe('ja-JP');
expect(adapter.addCalendarYears(DateTime.local(), 1).locale).toBe('ja-JP');
});
it('should not change locale of Moments passed as params', () => {
const date = DateTime.local();
expect(date.locale).toBe('en');
adapter.setLocale('ja-JP');
adapter.getYear(date);
adapter.getMonth(date);
adapter.getDate(date);
adapter.getDayOfWeek(date);
adapter.getYearName(date);
adapter.getNumDaysInMonth(date);
adapter.clone(date);
adapter.parse(date, 'LL/dd/yyyy');
adapter.format(date, 'LL/dd/yyyy');
adapter.addCalendarDays(date, 1);
adapter.addCalendarMonths(date, 1);
adapter.addCalendarYears(date, 1);
adapter.toIso8601(date);
adapter.isDateInstance(date);
adapter.isValid(date);
expect(date.locale).toBe('en');
});
it('should create invalid date', () => {
assertValidDate(adapter.invalid(), false);
});
});
describe('LuxonDateAdapter with MAT_DATE_LOCALE override', () => {
let adapter: LuxonDateAdapter;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'ja-JP' },
{ provide: MAT_DATE_FORMATS, useValue: LUXON_DATE_FORMATS },
{ provide: DateAdapter, useClass: LuxonDateAdapter, deps: [MAT_DATE_LOCALE] },
]
}).compileComponents();
}));
beforeEach(inject([DateAdapter], (d: LuxonDateAdapter) => {
adapter = d;
}));
it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => {
expect(adapter.format(DateTime.local(2017, JAN, 2), 'DD')).toEqual('2017年1月2日');
});
});
/**
* Copyright 2018, Colin McCulloch
* MIT License, https://opensource.org/licenses/MIT
*/
import { Inject, Injectable, Optional } from '@angular/core';
import { DateAdapter, MatDateFormats, MAT_DATE_LOCALE } from '@angular/material';
import { DateTime, Info, Settings } from 'luxon';
export const LUXON_DATE_FORMATS: MatDateFormats = {
parse: {
dateInput: 'dd/LL/yyyy',
},
display: {
dateInput: 'dd/LL/yyyy',
monthYearLabel: 'LLL yyyy',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'LLL yyyy'
}
};
/** Creates an array and fills it with values.
//copied from NativeDateAdapter
*/
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
const valuesArray = Array(length);
for (let i = 0; i < length; i++) {
valuesArray[i] = valueFunction(i);
}
return valuesArray;
}
// TODO(mmalerba): Remove when we no longer support safari 9.
/** Whether the browser supports the Intl API. */
const SUPPORTS_INTL_API = typeof Intl !== 'undefined';
/** The default date names to use if Intl API is not available. */
const DEFAULT_DATE_NAMES = range(31, i => String(i + 1));
export class LuxonDateAdapter extends DateAdapter<DateTime> {
constructor(@Optional() @Inject(MAT_DATE_LOCALE) matDateLocale: string) {
super();
this.setLocale(matDateLocale);
}
setLocale(locale) {
super.setLocale(locale);
Settings.defaultLocale = locale;
}
getYear(date: DateTime): number {
return date.year;
}
getMonth(date: DateTime): number {
// The Datepicker uses this to index into the 0 indexed
// getMonthNames array so far as I can tell. Because Luxon uses
// 1-12 for months we need to subtract one.
return date.month - 1;
}
getDate(date: DateTime): number {
return date.day;
}
getDayOfWeek(date: DateTime): number {
return date.weekday;
}
getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
return Info.months(style);
}
getDateNames(): string[] {
if (SUPPORTS_INTL_API) {
const dtf = new Intl.DateTimeFormat(this.locale, { day: 'numeric' });
return range(31, i => this._stripDirectionalityCharacters(
dtf.format(new Date(2017, 0, i + 1))));
}
return DEFAULT_DATE_NAMES;
}
getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
return Info.weekdays(style);
}
getYearName(date: DateTime): string {
if (SUPPORTS_INTL_API) {
const dtf = new Intl.DateTimeFormat(this.locale, { year: 'numeric' });
const valueOfDate = date.valueOf();
return this._stripDirectionalityCharacters(dtf.format(valueOfDate));
}
return String(this.getYear(date));
}
getFirstDayOfWeek(): number {
return 0;
}
getNumDaysInMonth(date: DateTime): number {
return date.daysInMonth;
}
clone(date: DateTime): DateTime {
return date;
}
createDate(year: number, month: number, date: number): DateTime {
// luxon utc uses 1-12 for dates, but datepicker passes in 0-11 .
month += 1;
return DateTime.local(year, month, date);
}
today(): DateTime {
return DateTime.local();
}
format(date: DateTime, displayFormat: any): string {
return date.toFormat(displayFormat);
}
addCalendarYears(date: DateTime, years: number): DateTime {
return date.plus({ years: years });
}
addCalendarMonths(date: DateTime, months: number): DateTime {
return date.plus({ months: months });
}
addCalendarDays(date: DateTime, days: number): DateTime {
return date.plus({ days: days });
}
toIso8601(date: DateTime): string {
return date.toISO();
}
isDateInstance(obj: any): boolean {
return (obj instanceof DateTime);
}
isValid(date: DateTime): boolean {
return date.isValid;
}
invalid(): DateTime {
return DateTime.invalid('Invalid set via luxon-date-adapter.');
}
parse(value: any, parseFormat?: any): DateTime | null {
if (value && typeof value === 'number') {
const fromTimestamp = DateTime.fromMillis(value);
if (fromTimestamp.isValid) {
return fromTimestamp;
}
}
if (value && typeof value === 'string') {
// first try to parse an ISO date
const aDateTime = DateTime.fromISO(value);
if (aDateTime.isValid) {
return aDateTime;
}
// otherwise try to parse according to specified format (useful for user entered values?).
return DateTime.fromFormat(value, parseFormat);
}
return value;
}
/**
* Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while
* other browsers do not. We remove them to make output consistent and because they interfere with
* date parsing.
* @param str The string to strip direction characters from.
* @returns The stripped string.
*/
private _stripDirectionalityCharacters(str: string) {
return str.replace(/[\u200e\u200f]/g, '');
}
deserialize(value: any): DateTime | null {
let date;
if (value instanceof Date) {
date = DateTime.fromJSDate(value);
}
if (typeof value === 'string') {
if (!value) {
return null;
}
date = DateTime.fromISO(value);
}
if (date && this.isValid(date)) {
return date;
}
return super.deserialize(value);
}
}
@evenicoulddoit
Copy link

Hey - I'm hopeful to use this in a project of ours, would be great if you could wrap it up into an npm package!

@arkD
Copy link

arkD commented Aug 9, 2018

This was extremely helpful, Thank You!

@evenicoulddoit
Copy link

evenicoulddoit commented Oct 8, 2018

Just noticed, I think the getDayOfWeek should be 0-indexed, so I think a - 1 is necessary
https://github.com/angular/material2/blob/877de5691b8dd755af4a2f77b4cf57f2d374e107/src/lib/core/datetime/date-adapter.ts#L64

@matheo
Copy link

matheo commented Jan 22, 2019

@Zyzle did the japanese tests work for you?

@Zyzle
Copy link
Author

Zyzle commented Jun 14, 2019

@matheo from what i remember yes they did (sorry it's been a long time since i looked at this) what are you seeing when they run?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment