diff --git a/.eslintrc b/.eslintrc index 532a594..a56d2c6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,11 @@ "jest": true }, "rules": { - "valid-jsdoc": 0 + "valid-jsdoc": 0, + "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "@typescript-eslint/consistent-type-imports": [ + "error", + {"prefer": "type-imports", "fixStyle": "separate-type-imports"} + ] } } diff --git a/src/constants/format.ts b/src/constants/format.ts index dd9db12..17c9092 100644 --- a/src/constants/format.ts +++ b/src/constants/format.ts @@ -10,3 +10,15 @@ export const englishFormats = { LLL: 'MMMM D, YYYY h:mm A', LLLL: 'dddd, MMMM D, YYYY h:mm A', } as const satisfies LongDateFormat; + +export const HTML5_INPUT_FORMATS = { + DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // + DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // + DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // + DATE: 'YYYY-MM-DD', // + TIME: 'HH:mm', // + TIME_SECONDS: 'HH:mm:ss', // + TIME_MS: 'HH:mm:ss.SSS', // + WEEK: 'GGGG-[W]WW', // + MONTH: 'YYYY-MM', // +} as const; diff --git a/src/dateTime/__tests__/format.ts b/src/dateTime/__tests__/format.ts new file mode 100644 index 0000000..a7fd91e --- /dev/null +++ b/src/dateTime/__tests__/format.ts @@ -0,0 +1,344 @@ +import {HTML5_INPUT_FORMATS} from '../../constants'; +import {settings} from '../../settings'; +import {dateTime, dateTimeUtc} from '../dateTime'; + +afterEach(() => { + settings.updateLocale({weekStart: 1, yearStart: 1}); +}); + +test('format using constants', () => { + const m = dateTime({input: '2016-01-02T23:40:40.678'}); + expect(m.format(HTML5_INPUT_FORMATS.DATETIME_LOCAL)).toBe('2016-01-02T23:40'); // 'datetime local format constant' + expect(m.format(HTML5_INPUT_FORMATS.DATETIME_LOCAL_SECONDS)).toBe('2016-01-02T23:40:40'); // 'datetime local format constant' + + expect(m.format(HTML5_INPUT_FORMATS.DATETIME_LOCAL_MS)).toBe('2016-01-02T23:40:40.678'); // 'datetime local format constant with seconds and millis' + expect(m.format(HTML5_INPUT_FORMATS.DATE)).toBe('2016-01-02'); // 'date format constant' + expect(m.format(HTML5_INPUT_FORMATS.TIME)).toBe('23:40'); // 'time format constant') + expect(m.format(HTML5_INPUT_FORMATS.TIME_SECONDS)).toBe('23:40:40'); // 'time format constant with seconds' + expect(m.format(HTML5_INPUT_FORMATS.TIME_MS)).toBe('23:40:40.678'); //'time format constant with seconds and millis' + expect(m.format(HTML5_INPUT_FORMATS.WEEK)).toBe('2015-W53'); // 'week format constant' + expect(m.format(HTML5_INPUT_FORMATS.MONTH)).toBe('2016-01'); // 'month format constant' +}); + +test('format YY', () => { + const b = dateTime({input: new Date(2009, 1, 14, 15, 25, 50, 125)}); + expect(b.format('YY')).toBe('09'); // 'YY ---> 09' +}); + +test('format escape brackets', () => { + const b = dateTime({input: new Date(2009, 1, 14, 15, 25, 50, 125)}); + expect(b.format('[day]')).toBe('day'); // 'Single bracket' + expect(b.format('[day] YY [YY]')).toBe('day 09 YY'); // 'Double bracket' + expect(b.format('[YY')).toBe('[09'); // 'Un-ended bracket' + expect(b.format('[[YY]]')).toBe('[YY]'); // 'Double nested brackets' + expect(b.format('[[]')).toBe('['); // 'Escape open bracket' + expect(b.format('[Last]')).toBe('Last'); // 'localized tokens' + expect(b.format('[L] L')).toBe('L 02/14/2009'); // 'localized tokens with escaped localized tokens' + expect(b.format('[L LL LLL LLLL aLa]')).toBe('L LL LLL LLLL aLa'); // 'localized tokens with escaped localized tokens', + expect(b.format('[LLL] LLL')).toBe('LLL February 14, 2009 3:25 PM'); // 'localized tokens with escaped localized tokens (recursion)', + expect(b.format('YYYY[\n]DD[\n]')).toBe('2009\n14\n'); // 'Newlines' +}); + +test('handle negative years', () => { + expect(dateTimeUtc().year(-1).format('YY')).toBe('-01'); // 'YY with negative year' + expect(dateTimeUtc().year(-1).format('YYYY')).toBe('-0001'); // 'YYYY with negative year' + expect(dateTimeUtc().year(-12).format('YY')).toBe('-12'); // 'YY with negative year' + expect(dateTimeUtc().year(-12).format('YYYY')).toBe('-0012'); // 'YYYY with negative year' + expect(dateTimeUtc().year(-123).format('YY')).toBe('-23'); // 'YY with negative year' + expect(dateTimeUtc().year(-123).format('YYYY')).toBe('-0123'); // 'YYYY with negative year' + expect(dateTimeUtc().year(-1234).format('YY')).toBe('-34'); // 'YY with negative year' + expect(dateTimeUtc().year(-1234).format('YYYY')).toBe('-1234'); // 'YYYY with negative year' + expect(dateTimeUtc().year(-12345).format('YY')).toBe('-45'); // 'YY with negative year' + expect(dateTimeUtc().year(-12345).format('YYYY')).toBe('-12345'); // 'YYYY with negative year' +}); + +test('format milliseconds', () => { + let b = dateTime({input: new Date(2009, 1, 14, 15, 25, 50, 123)}); + expect(b.format('S')).toBe('1'); // 'Deciseconds' + expect(b.format('SS')).toBe('12'); // 'Centiseconds' + expect(b.format('SSS')).toBe('123'); // 'Milliseconds' + b = b.millisecond(789); + expect(b.format('S')).toBe('7'); // 'Deciseconds' + expect(b.format('SS')).toBe('78'); // 'Centiseconds' + expect(b.format('SSS')).toBe('789'); // 'Milliseconds' +}); + +test('format timezone', () => { + const b = dateTime({input: new Date(2010, 1, 14, 15, 25, 50, 125)}); + expect(b.format('Z')).toMatch(/^[+-]\d\d:\d\d$/); // 'should be something like '+07:30' + expect(b.format('ZZ')).toMatch(/^[+-]\d{4}$/); // 'should be something like '+0700' +}); + +test('unix timestamp', () => { + const m = dateTime({input: 1234567890123}); + expect(m.format('X')).toBe('1234567890'); // 'unix timestamp without milliseconds' + expect(m.format('X.S')).toBe('1234567890.1'); // 'unix timestamp with deciseconds' + expect(m.format('X.SS')).toBe('1234567890.12'); // 'unix timestamp with centiseconds' + expect(m.format('X.SSS')).toBe('1234567890.123'); // 'unix timestamp with milliseconds' +}); + +test('unix offset milliseconds', () => { + const m = dateTime({input: 1234567890123}); + expect(m.format('x')).toBe('1234567890123'); // 'unix offset in milliseconds' +}); + +test('utcOffset sanity checks', () => { + expect(dateTime({timeZone: 'Europe/Amsterdam'}).utcOffset() % 15).toBe(0); // 'utc offset should be a multiple of 15 (was ' + dateTimeParse().utcOffset() + ')' + + expect(dateTime().utcOffset()).toBe(-new Date().getTimezoneOffset() || 0); // 'utcOffset should return the opposite of getTimezoneOffset' +}); + +test('default format', () => { + const isoRegex = /\d{4}.\d\d.\d\dT\d\d.\d\d.\d\d[+-]\d\d:\d\d/; + expect(isoRegex.exec(dateTime({timeZone: 'Europe/Amsterdam'}).format())).toBeTruthy(); +}); + +test('default UTC format', () => { + const isoRegex = /\d{4}.\d\d.\d\dT\d\d.\d\d.\d\dZ/; + expect(isoRegex.exec(dateTimeUtc().format())).toBeTruthy(); +}); + +test('toJSON', () => { + const date = dateTime({input: '2012-10-09T21:30:40.678+0100'}); + + expect(date.toJSON()).toBe('2012-10-09T20:30:40.678Z'); // 'should output ISO8601 on dateTimeParse.fn.toJSON' + + expect(JSON.stringify({date})).toBe('{"date":"2012-10-09T20:30:40.678Z"}'); // 'should output ISO8601 on JSON.stringify' +}); + +test('toISOString', () => { + let date = dateTimeUtc({input: '2012-10-09T20:30:40.678'}); + + expect(date.toISOString()).toBe('2012-10-09T20:30:40.678Z'); // 'should output ISO8601 on dateTimeParse.fn.toISOString' + + // big years + date = dateTimeUtc({input: [20123, 9, 9, 20, 30, 40, 678]}); + expect(date.toISOString()).toBe('+020123-10-09T20:30:40.678Z'); // 'ISO8601 format on big positive year' + // negative years + date = dateTimeUtc({input: [-1, 9, 9, 20, 30, 40, 678]}); + expect(date.toISOString()).toBe('-000001-10-09T20:30:40.678Z'); // 'ISO8601 format on negative year' + // big negative years + date = dateTimeUtc({input: [-20123, 9, 9, 20, 30, 40, 678]}); + expect(date.toISOString()).toBe('-020123-10-09T20:30:40.678Z'); // 'ISO8601 format on big negative year' + + //invalid dates + date = dateTimeUtc({input: '2017-12-32k'}); + expect(() => date.toISOString()).toThrow(); // 'An invalid date to iso string is null' +}); + +test('toISOString without UTC conversion', () => { + let date = dateTimeUtc({input: '2016-12-31T19:53:45.678'}).utcOffset('+05:30'); + + expect(date.toISOString(true)).toBe('2017-01-01T01:23:45.678+05:30'); // 'should output ISO8601 on dateTimeParse.fn.toISOString' + + // big years + date = dateTime({input: '+020122-12-31T19:53:45.678Z'}).utcOffset('+05:30'); + expect(date.toISOString(true)).toBe('+020123-01-01T01:23:45.678+05:30'); // 'ISO8601 format on big positive year' + // negative years + date = dateTime({input: '-000002-12-31T19:53:45.678Z'}).utcOffset('+05:30'); + expect(date.toISOString(true)).toBe('-000001-01-01T01:23:45.678+05:30'); //'ISO8601 format on negative year' + // big negative years + date = dateTime({input: '-020124-12-31T19:53:45.678Z'}).utcOffset('+05:30'); + expect(date.toISOString(true)).toBe('-020123-01-01T01:23:45.678+05:30'); // 'ISO8601 format on big negative year' + + //invalid dates + date = dateTimeUtc({input: '2017-12-32k'}).utcOffset('+05:30'); + expect(() => date.toISOString(true)).toThrow(); // 'An invalid date to iso string is null' +}); + +test('long years', () => { + expect(dateTimeUtc().year(2).format('YYYYYY')).toBe('+000002'); // 'small year with YYYYYY' + expect(dateTimeUtc().year(2012).format('YYYYYY')).toBe('+002012'); // 'regular year with YYYYYY' + expect(dateTimeUtc().year(20123).format('YYYYYY')).toBe('+020123'); // 'big year with YYYYYY' + + expect(dateTimeUtc().year(-1).format('YYYYYY')).toBe('-000001'); // 'small negative year with YYYYYY' + expect(dateTimeUtc().year(-2012).format('YYYYYY')).toBe('-002012'); // 'negative year with YYYYYY' + expect(dateTimeUtc().year(-20123).format('YYYYYY')).toBe('-020123'); // 'big negative year with YYYYYY' +}); + +test('toISOString() when 0 year', () => { + const date = dateTime({input: '0000-01-01T21:00:00.000Z'}); + expect(date.toISOString()).toBe('0000-01-01T21:00:00.000Z'); + expect(date.toDate().toISOString()).toBe('0000-01-01T21:00:00.000Z'); +}); + +test.each<[string, string]>([ + ['2005-01-02', '2004-53'], + ['2005-12-31', '2005-52'], + ['2007-01-01', '2007-01'], + ['2007-12-30', '2007-52'], + ['2007-12-31', '2008-01'], + ['2008-01-01', '2008-01'], + ['2008-12-28', '2008-52'], + ['2008-12-29', '2009-01'], + ['2008-12-30', '2009-01'], + ['2008-12-31', '2009-01'], + ['2009-01-01', '2009-01'], + ['2009-12-31', '2009-53'], + ['2010-01-01', '2009-53'], + ['2010-01-02', '2009-53'], + ['2010-01-03', '2009-53'], + ['0404-12-31', '0404-53'], + ['0405-12-31', '0405-52'], +])('iso week formats, (%j)', (input, expected) => { + // https://en.wikipedia.org/wiki/ISO_week_date + const isoWeek = expected.split('-')[1]; + const date = dateTime({input, format: 'YYYY-MM-DD'}); + expect(date.format('WW')).toBe(isoWeek); + expect(date.format('W')).toBe(isoWeek.replace(/^0+/, '')); +}); + +test.each<[string, string]>([ + ['2005-01-02', '2004-53'], + ['2005-12-31', '2005-52'], + ['2007-01-01', '2007-01'], + ['2007-12-30', '2007-52'], + ['2007-12-31', '2008-01'], + ['2008-01-01', '2008-01'], + ['2008-12-28', '2008-52'], + ['2008-12-29', '2009-01'], + ['2008-12-30', '2009-01'], + ['2008-12-31', '2009-01'], + ['2009-01-01', '2009-01'], + ['2009-12-31', '2009-53'], + ['2010-01-01', '2009-53'], + ['2010-01-02', '2009-53'], + ['2010-01-03', '2009-53'], + ['0404-12-31', '0404-53'], + ['0405-12-31', '0405-52'], +])('iso week year formats, (%j)', (input, expected) => { + // https://en.wikipedia.org/wiki/ISO_week_date + const isoWeekYear = expected.split('-')[0]; + const date = dateTime({input, format: 'YYYY-MM-DD'}); + expect(date.format('GGGGG')).toBe('0' + isoWeekYear); + expect(date.format('GGGG')).toBe(isoWeekYear); + expect(date.format('GG')).toBe(isoWeekYear.slice(2, 4)); +}); + +test.each<[string, string]>([ + ['2005-01-02', '2004-53'], + ['2005-12-31', '2005-52'], + ['2007-01-01', '2007-01'], + ['2007-12-30', '2007-52'], + ['2007-12-31', '2008-01'], + ['2008-01-01', '2008-01'], + ['2008-12-28', '2008-52'], + ['2008-12-29', '2009-01'], + ['2008-12-30', '2009-01'], + ['2008-12-31', '2009-01'], + ['2009-01-01', '2009-01'], + ['2009-12-31', '2009-53'], + ['2010-01-01', '2009-53'], + ['2010-01-02', '2009-53'], + ['2010-01-03', '2009-53'], + ['0404-12-31', '0404-53'], + ['0405-12-31', '0405-52'], +])('week year formats, (%j)', (input, expected) => { + settings.updateLocale({yearStart: 4}); + const weekYear = expected.split('-')[0]; + const date = dateTime({input, format: 'YYYY-MM-DD'}); + expect(date.format('ggggg')).toBe('0' + weekYear); + expect(date.format('gggg')).toBe(weekYear); + expect(date.format('gg')).toBe(weekYear.slice(2, 4)); +}); + +test('iso weekday formats', () => { + expect(dateTime({input: [1985, 1, 4]}).format('E')).toBe('1'); // 'Feb 4 1985 is Monday -- 1st day' + expect(dateTime({input: [2029, 8, 18]}).format('E')).toBe('2'); // 'Sep 18 2029 is Tuesday -- 2nd day' + expect(dateTime({input: [2013, 3, 24]}).format('E')).toBe('3'); // 'Apr 24 2013 is Wednesday -- 3rd day' + expect(dateTime({input: [2015, 2, 5]}).format('E')).toBe('4'); // 'Mar 5 2015 is Thursday -- 4th day' + expect(dateTime({input: [1970, 0, 2]}).format('E')).toBe('5'); // 'Jan 2 1970 is Friday -- 5th day' + expect(dateTime({input: [2001, 4, 12]}).format('E')).toBe('6'); // 'May 12 2001 is Saturday -- 6th day' + expect(dateTime({input: [2000, 0, 2]}).format('E')).toBe('7'); // 'Jan 2 2000 is Sunday -- 7th day' +}); + +test('weekday formats', () => { + settings.updateLocale({weekStart: 3}); + expect(dateTime({input: [1985, 1, 6]}).format('e')).toBe('0'); // 'Feb 6 1985 is Wednesday -- 0th day' + expect(dateTime({input: [2029, 8, 20]}).format('e')).toBe('1'); // 'Sep 20 2029 is Thursday -- 1st day' + expect(dateTime({input: [2013, 3, 26]}).format('e')).toBe('2'); // 'Apr 26 2013 is Friday -- 2nd day' + expect(dateTime({input: [2015, 2, 7]}).format('e')).toBe('3'); // 'Mar 7 2015 is Saturday -- 3nd day' + expect(dateTime({input: [1970, 0, 4]}).format('e')).toBe('4'); // 'Jan 4 1970 is Sunday -- 4th day' + expect(dateTime({input: [2001, 4, 14]}).format('e')).toBe('5'); // 'May 14 2001 is Monday -- 5th day' + expect(dateTime({input: [2000, 0, 4]}).format('e')).toBe('6'); // 'Jan 4 2000 is Tuesday -- 6th day' +}); + +test('invalid', () => { + const invalid = dateTime({input: NaN}); + expect(invalid.format()).toBe('Invalid Date'); + expect(invalid.format('YYYY-MM-DD')).toBe('Invalid Date'); +}); + +test('quarter formats', () => { + expect(dateTime({input: [1985, 1, 4]}).format('Q')).toBe('1'); //'Feb 4 1985 is Q1' + expect(dateTime({input: [2029, 8, 18]}).format('Q')).toBe('3'); //'Sep 18 2029 is Q3' + expect(dateTime({input: [2013, 3, 24]}).format('Q')).toBe('2'); //'Apr 24 2013 is Q2' + expect(dateTime({input: [2015, 2, 5]}).format('Q')).toBe('1'); //'Mar 5 2015 is Q1' + expect(dateTime({input: [1970, 0, 2]}).format('Q')).toBe('1'); //'Jan 2 1970 is Q1' + expect(dateTime({input: [2001, 11, 12]}).format('Q')).toBe('4'); // 'Dec 12 2001 is Q4' + expect(dateTime({input: [2000, 0, 2]}).format('[Q]Q-YYYY')).toBe('Q1-2000'); // 'Jan 2 2000 is Q1' +}); + +test('quarter ordinal formats', () => { + expect(dateTime({input: [1985, 1, 4]}).format('Qo')).toBe('1st'); // 'Feb 4 1985 is 1st quarter' + expect(dateTime({input: [2029, 8, 18]}).format('Qo')).toBe('3rd'); // 'Sep 18 2029 is 3rd quarter' + expect(dateTime({input: [2013, 3, 24]}).format('Qo')).toBe('2nd'); // 'Apr 24 2013 is 2nd quarter' + expect(dateTime({input: [2015, 2, 5]}).format('Qo')).toBe('1st'); // 'Mar 5 2015 is 1st quarter' + expect(dateTime({input: [1970, 0, 2]}).format('Qo')).toBe('1st'); // 'Jan 2 1970 is 1st quarter' + expect(dateTime({input: [2001, 11, 12]}).format('Qo')).toBe('4th'); // 'Dec 12 2001 is 4th quarter' + expect(dateTime({input: [2000, 0, 2]}).format('Qo [quarter] YYYY')).toBe('1st quarter 2000'); // 'Jan 2 2000 is 1st quarter' +}); + +test('milliseconds', () => { + const m = dateTime({input: '123', format: 'SSS'}); + + expect(m.format('S')).toBe('1'); + expect(m.format('SS')).toBe('12'); + expect(m.format('SSS')).toBe('123'); + expect(m.format('SSSS')).toBe('1230'); + expect(m.format('SSSSS')).toBe('12300'); + expect(m.format('SSSSSS')).toBe('123000'); + expect(m.format('SSSSSSS')).toBe('1230000'); + expect(m.format('SSSSSSSS')).toBe('12300000'); + expect(m.format('SSSSSSSSS')).toBe('123000000'); +}); + +test('hmm and hmmss', () => { + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('hmm')).toBe('1234'); + expect(dateTime({input: '01:34:56', format: 'HH:mm:ss'}).format('hmm')).toBe('134'); + expect(dateTime({input: '13:34:56', format: 'HH:mm:ss'}).format('hmm')).toBe('134'); + + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('hmmss')).toBe('123456'); + expect(dateTime({input: '01:34:56', format: 'HH:mm:ss'}).format('hmmss')).toBe('13456'); + expect(dateTime({input: '13:34:56', format: 'HH:mm:ss'}).format('hmmss')).toBe('13456'); +}); + +test('Hmm and Hmmss', () => { + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('Hmm')).toBe('1234'); + expect(dateTime({input: '01:34:56', format: 'HH:mm:ss'}).format('Hmm')).toBe('134'); + expect(dateTime({input: '13:34:56', format: 'HH:mm:ss'}).format('Hmm')).toBe('1334'); + + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('Hmmss')).toBe('123456'); + expect(dateTime({input: '01:34:56', format: 'HH:mm:ss'}).format('Hmmss')).toBe('13456'); + expect(dateTime({input: '08:34:56', format: 'HH:mm:ss'}).format('Hmmss')).toBe('83456'); + expect(dateTime({input: '18:34:56', format: 'HH:mm:ss'}).format('Hmmss')).toBe('183456'); +}); + +test('k and kk', () => { + expect(dateTime({input: '01:23:45', format: 'HH:mm:ss'}).format('k')).toBe('1'); + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('k')).toBe('12'); + expect(dateTime({input: '01:23:45', format: 'HH:mm:ss'}).format('kk')).toBe('01'); + expect(dateTime({input: '12:34:56', format: 'HH:mm:ss'}).format('kk')).toBe('12'); + expect(dateTime({input: '00:34:56', format: 'HH:mm:ss'}).format('kk')).toBe('24'); + expect(dateTime({input: '00:00:00', format: 'HH:mm:ss'}).format('kk')).toBe('24'); +}); + +test('Y token', () => { + expect(dateTime({input: '2010-01-01', format: 'YYYY-MM-DD'}).format('Y')).toBe('2010'); // 'format 2010 with Y' + expect(dateTime({input: [-123]}).format('Y')).toBe('-0123'); // 'format -123 with Y' + expect(dateTime({input: [12345]}).format('Y')).toBe('+12345'); // 'format 12345 with Y' + expect(dateTime({input: [0]}).format('Y')).toBe('0000'); // 'format 0 with Y' + expect(dateTime({input: [1]}).format('Y')).toBe('0001'); // 'format 1 with Y' + expect(dateTime({input: [9999]}).format('Y')).toBe('9999'); // 'format 9999 with Y' + expect(dateTime({input: [10000]}).format('Y')).toBe('+10000'); // 'format 10000 with Y' +}); diff --git a/src/dateTime/__tests__/weekYear.ts b/src/dateTime/__tests__/weekYear.ts new file mode 100644 index 0000000..09cd37d --- /dev/null +++ b/src/dateTime/__tests__/weekYear.ts @@ -0,0 +1,263 @@ +import {settings} from '../../settings'; +import {dateTime, dateTimeUtc} from '../dateTime'; + +afterEach(() => { + settings.updateLocale({weekStart: 1, yearStart: 1}); +}); + +test('iso week year', () => { + // Some examples taken from https://en.wikipedia.org/wiki/ISO_week + expect(dateTime({input: [2005, 0, 1]}).isoWeekYear()).toBe(2004); + expect(dateTime({input: [2005, 0, 2]}).isoWeekYear()).toBe(2004); + expect(dateTime({input: [2005, 0, 3]}).isoWeekYear()).toBe(2005); + expect(dateTime({input: [2005, 11, 31]}).isoWeekYear()).toBe(2005); + expect(dateTime({input: [2006, 0, 1]}).isoWeekYear()).toBe(2005); + expect(dateTime({input: [2006, 0, 2]}).isoWeekYear()).toBe(2006); + expect(dateTime({input: [2007, 0, 1]}).isoWeekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 30]}).isoWeekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 31]}).isoWeekYear()).toBe(2008); + expect(dateTime({input: [2008, 0, 1]}).isoWeekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 28]}).isoWeekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 29]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2008, 11, 30]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2008, 11, 31]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2009, 0, 1]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 1]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 2]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 3]}).isoWeekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 4]}).isoWeekYear()).toBe(2010); +}); + +test('week year', () => { + // Some examples taken from https://en.wikipedia.org/wiki/ISO_week + settings.updateLocale({weekStart: 1, yearStart: 4}); // like iso + expect(dateTime({input: [2005, 0, 1]}).weekYear()).toBe(2004); + expect(dateTime({input: [2005, 0, 2]}).weekYear()).toBe(2004); + expect(dateTime({input: [2005, 0, 3]}).weekYear()).toBe(2005); + expect(dateTime({input: [2005, 11, 31]}).weekYear()).toBe(2005); + expect(dateTime({input: [2006, 0, 1]}).weekYear()).toBe(2005); + expect(dateTime({input: [2006, 0, 2]}).weekYear()).toBe(2006); + expect(dateTime({input: [2007, 0, 1]}).weekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 30]}).weekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 31]}).weekYear()).toBe(2008); + expect(dateTime({input: [2008, 0, 1]}).weekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 28]}).weekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 29]}).weekYear()).toBe(2009); + expect(dateTime({input: [2008, 11, 30]}).weekYear()).toBe(2009); + expect(dateTime({input: [2008, 11, 31]}).weekYear()).toBe(2009); + expect(dateTime({input: [2009, 0, 1]}).weekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 1]}).weekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 2]}).weekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 3]}).weekYear()).toBe(2009); + expect(dateTime({input: [2010, 0, 4]}).weekYear()).toBe(2010); + + settings.updateLocale({weekStart: 1, yearStart: 1}); + expect(dateTime({input: [2004, 11, 26]}).weekYear()).toBe(2004); + expect(dateTime({input: [2004, 11, 27]}).weekYear()).toBe(2005); + expect(dateTime({input: [2005, 11, 25]}).weekYear()).toBe(2005); + expect(dateTime({input: [2005, 11, 26]}).weekYear()).toBe(2006); + expect(dateTime({input: [2006, 11, 31]}).weekYear()).toBe(2006); + expect(dateTime({input: [2007, 0, 1]}).weekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 30]}).weekYear()).toBe(2007); + expect(dateTime({input: [2007, 11, 31]}).weekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 28]}).weekYear()).toBe(2008); + expect(dateTime({input: [2008, 11, 29]}).weekYear()).toBe(2009); + expect(dateTime({input: [2009, 11, 27]}).weekYear()).toBe(2009); + expect(dateTime({input: [2009, 11, 28]}).weekYear()).toBe(2010); +}); + +test('week numbers 2012/2013', () => { + settings.updateLocale({weekStart: 6, yearStart: 1}); + expect(dateTime({input: '2012-12-28', format: 'YYYY-MM-DD'}).week()).toBe(52); + expect(dateTime({input: '2012-12-29', format: 'YYYY-MM-DD'}).week()).toBe(1); + expect(dateTime({input: '2013-01-01', format: 'YYYY-MM-DD'}).week()).toBe(1); + expect(dateTime({input: '2013-01-08', format: 'YYYY-MM-DD'}).week()).toBe(2); + expect(dateTime({input: '2013-01-11', format: 'YYYY-MM-DD'}).week()).toBe(2); + expect(dateTime({input: '2013-01-12', format: 'YYYY-MM-DD'}).week()).toBe(3); + expect(dateTime({input: '2012-01-01', format: 'YYYY-MM-DD'}).weeksInYear()).toBe(52); +}); + +test('weeks numbers dow:1 doy:4', () => { + settings.updateLocale({weekStart: 1, yearStart: 4}); + expect(dateTime({input: [2012, 0, 1]}).week()).toBe(52); // 'Jan 1 2012 should be week 52' + expect(dateTime({input: [2012, 0, 2]}).week()).toBe(1); // 'Jan 2 2012 should be week 1' + expect(dateTime({input: [2012, 0, 8]}).week()).toBe(1); // 'Jan 8 2012 should be week 1' + expect(dateTime({input: [2012, 0, 9]}).week()).toBe(2); // 'Jan 9 2012 should be week 2' + expect(dateTime({input: [2012, 0, 15]}).week()).toBe(2); // 'Jan 15 2012 should be week 2' + expect(dateTime({input: [2007, 0, 1]}).week()).toBe(1); // 'Jan 1 2007 should be week 1' + expect(dateTime({input: [2007, 0, 7]}).week()).toBe(1); // 'Jan 7 2007 should be week 1' + expect(dateTime({input: [2007, 0, 8]}).week()).toBe(2); // 'Jan 8 2007 should be week 2' + expect(dateTime({input: [2007, 0, 14]}).week()).toBe(2); // 'Jan 14 2007 should be week 2' + expect(dateTime({input: [2007, 0, 15]}).week()).toBe(3); // 'Jan 15 2007 should be week 3' + expect(dateTime({input: [2007, 11, 31]}).week()).toBe(1); // 'Dec 31 2007 should be week 1' + expect(dateTime({input: [2008, 0, 1]}).week()).toBe(1); // 'Jan 1 2008 should be week 1' + expect(dateTime({input: [2008, 0, 6]}).week()).toBe(1); // 'Jan 6 2008 should be week 1' + expect(dateTime({input: [2008, 0, 7]}).week()).toBe(2); // 'Jan 7 2008 should be week 2' + expect(dateTime({input: [2008, 0, 13]}).week()).toBe(2); // 'Jan 13 2008 should be week 2' + expect(dateTime({input: [2008, 0, 14]}).week()).toBe(3); // 'Jan 14 2008 should be week 3' + expect(dateTime({input: [2002, 11, 30]}).week()).toBe(1); // 'Dec 30 2002 should be week 1' + expect(dateTime({input: [2003, 0, 1]}).week()).toBe(1); // 'Jan 1 2003 should be week 1' + expect(dateTime({input: [2003, 0, 5]}).week()).toBe(1); // 'Jan 5 2003 should be week 1' + expect(dateTime({input: [2003, 0, 6]}).week()).toBe(2); // 'Jan 6 2003 should be week 2' + expect(dateTime({input: [2003, 0, 12]}).week()).toBe(2); // 'Jan 12 2003 should be week 2' + expect(dateTime({input: [2003, 0, 13]}).week()).toBe(3); // 'Jan 13 2003 should be week 3' + expect(dateTime({input: [2008, 11, 29]}).week()).toBe(1); // 'Dec 29 2008 should be week 1' + expect(dateTime({input: [2009, 0, 1]}).week()).toBe(1); // 'Jan 1 2009 should be week 1' + expect(dateTime({input: [2009, 0, 4]}).week()).toBe(1); // 'Jan 4 2009 should be week 1' + expect(dateTime({input: [2009, 0, 5]}).week()).toBe(2); // 'Jan 5 2009 should be week 2' + expect(dateTime({input: [2009, 0, 11]}).week()).toBe(2); // 'Jan 11 2009 should be week 2' + expect(dateTime({input: [2009, 0, 13]}).week()).toBe(3); // 'Jan 12 2009 should be week 3' + expect(dateTime({input: [2009, 11, 28]}).week()).toBe(53); // 'Dec 28 2009 should be week 53' + expect(dateTime({input: [2010, 0, 1]}).week()).toBe(53); // 'Jan 1 2010 should be week 53' + expect(dateTime({input: [2010, 0, 3]}).week()).toBe(53); // 'Jan 3 2010 should be week 53' + expect(dateTime({input: [2010, 0, 4]}).week()).toBe(1); // 'Jan 4 2010 should be week 1' + expect(dateTime({input: [2010, 0, 10]}).week()).toBe(1); // 'Jan 10 2010 should be week 1' + expect(dateTime({input: [2010, 0, 11]}).week()).toBe(2); // 'Jan 11 2010 should be week 2' + expect(dateTime({input: [2010, 11, 27]}).week()).toBe(52); // 'Dec 27 2010 should be week 52' + expect(dateTime({input: [2011, 0, 1]}).week()).toBe(52); // 'Jan 1 2011 should be week 52' + expect(dateTime({input: [2011, 0, 2]}).week()).toBe(52); // 'Jan 2 2011 should be week 52' + expect(dateTime({input: [2011, 0, 3]}).week()).toBe(1); // 'Jan 3 2011 should be week 1' + expect(dateTime({input: [2011, 0, 9]}).week()).toBe(1); // 'Jan 9 2011 should be week 1' + expect(dateTime({input: [2011, 0, 10]}).week()).toBe(2); // 'Jan 10 2011 should be week 2' +}); + +test('weeks numbers dow:6 doy:12', () => { + settings.updateLocale({weekStart: 6, yearStart: 1}); + expect(dateTime({input: [2011, 11, 31]}).week()).toBe(1); // 'Dec 31 2011 should be week 1' + expect(dateTime({input: [2012, 0, 6]}).week()).toBe(1); // 'Jan 6 2012 should be week 1' + expect(dateTime({input: [2012, 0, 7]}).week()).toBe(2); // 'Jan 7 2012 should be week 2' + expect(dateTime({input: [2012, 0, 13]}).week()).toBe(2); // 'Jan 13 2012 should be week 2' + expect(dateTime({input: [2012, 0, 14]}).week()).toBe(3); // 'Jan 14 2012 should be week 3' + expect(dateTime({input: [2006, 11, 30]}).week()).toBe(1); // 'Dec 30 2006 should be week 1' + expect(dateTime({input: [2007, 0, 5]}).week()).toBe(1); // 'Jan 5 2007 should be week 1' + expect(dateTime({input: [2007, 0, 6]}).week()).toBe(2); // 'Jan 6 2007 should be week 2' + expect(dateTime({input: [2007, 0, 12]}).week()).toBe(2); // 'Jan 12 2007 should be week 2' + expect(dateTime({input: [2007, 0, 13]}).week()).toBe(3); // 'Jan 13 2007 should be week 3' + expect(dateTime({input: [2007, 11, 29]}).week()).toBe(1); // 'Dec 29 2007 should be week 1' + expect(dateTime({input: [2008, 0, 1]}).week()).toBe(1); // 'Jan 1 2008 should be week 1' + expect(dateTime({input: [2008, 0, 4]}).week()).toBe(1); // 'Jan 4 2008 should be week 1' + expect(dateTime({input: [2008, 0, 5]}).week()).toBe(2); // 'Jan 5 2008 should be week 2' + expect(dateTime({input: [2008, 0, 11]}).week()).toBe(2); // 'Jan 11 2008 should be week 2' + expect(dateTime({input: [2008, 0, 12]}).week()).toBe(3); // 'Jan 12 2008 should be week 3' + expect(dateTime({input: [2002, 11, 28]}).week()).toBe(1); // 'Dec 28 2002 should be week 1' + expect(dateTime({input: [2003, 0, 1]}).week()).toBe(1); // 'Jan 1 2003 should be week 1' + expect(dateTime({input: [2003, 0, 3]}).week()).toBe(1); // 'Jan 3 2003 should be week 1' + expect(dateTime({input: [2003, 0, 4]}).week()).toBe(2); // 'Jan 4 2003 should be week 2' + expect(dateTime({input: [2003, 0, 10]}).week()).toBe(2); // 'Jan 10 2003 should be week 2' + expect(dateTime({input: [2003, 0, 11]}).week()).toBe(3); // 'Jan 11 2003 should be week 3' + expect(dateTime({input: [2008, 11, 27]}).week()).toBe(1); // 'Dec 27 2008 should be week 1' + expect(dateTime({input: [2009, 0, 1]}).week()).toBe(1); // 'Jan 1 2009 should be week 1' + expect(dateTime({input: [2009, 0, 2]}).week()).toBe(1); // 'Jan 2 2009 should be week 1' + expect(dateTime({input: [2009, 0, 3]}).week()).toBe(2); // 'Jan 3 2009 should be week 2' + expect(dateTime({input: [2009, 0, 9]}).week()).toBe(2); // 'Jan 9 2009 should be week 2' + expect(dateTime({input: [2009, 0, 10]}).week()).toBe(3); // 'Jan 10 2009 should be week 3' + expect(dateTime({input: [2009, 11, 26]}).week()).toBe(1); // 'Dec 26 2009 should be week 1' + expect(dateTime({input: [2010, 0, 1]}).week()).toBe(1); // 'Jan 1 2010 should be week 1' + expect(dateTime({input: [2010, 0, 2]}).week()).toBe(2); // 'Jan 2 2010 should be week 2' + expect(dateTime({input: [2010, 0, 8]}).week()).toBe(2); // 'Jan 8 2010 should be week 2' + expect(dateTime({input: [2010, 0, 9]}).week()).toBe(3); // 'Jan 9 2010 should be week 3' + expect(dateTime({input: [2011, 0, 1]}).week()).toBe(1); // 'Jan 1 2011 should be week 1' + expect(dateTime({input: [2011, 0, 7]}).week()).toBe(1); // 'Jan 7 2011 should be week 1' + expect(dateTime({input: [2011, 0, 8]}).week()).toBe(2); // 'Jan 8 2011 should be week 2' + expect(dateTime({input: [2011, 0, 14]}).week()).toBe(2); // 'Jan 14 2011 should be week 2' + expect(dateTime({input: [2011, 0, 15]}).week()).toBe(3); // 'Jan 15 2011 should be week 3' +}); + +test('weeks numbers dow:1 doy:7', () => { + settings.updateLocale({weekStart: 1, yearStart: 1}); + expect(dateTime({input: [2011, 11, 26]}).week()).toBe(1); // 'Dec 26 2011 should be week 1' + expect(dateTime({input: [2012, 0, 1]}).week()).toBe(1); // 'Jan 1 2012 should be week 1' + expect(dateTime({input: [2012, 0, 2]}).week()).toBe(2); // 'Jan 2 2012 should be week 2' + expect(dateTime({input: [2012, 0, 8]}).week()).toBe(2); // 'Jan 8 2012 should be week 2' + expect(dateTime({input: [2012, 0, 9]}).week()).toBe(3); // 'Jan 9 2012 should be week 3' + expect(dateTime({input: [2007, 0, 1]}).week()).toBe(1); // 'Jan 1 2007 should be week 1' + expect(dateTime({input: [2007, 0, 7]}).week()).toBe(1); // 'Jan 7 2007 should be week 1' + expect(dateTime({input: [2007, 0, 8]}).week()).toBe(2); // 'Jan 8 2007 should be week 2' + expect(dateTime({input: [2007, 0, 14]}).week()).toBe(2); // 'Jan 14 2007 should be week 2' + expect(dateTime({input: [2007, 0, 15]}).week()).toBe(3); // 'Jan 15 2007 should be week 3' + expect(dateTime({input: [2007, 11, 31]}).week()).toBe(1); // 'Dec 31 2007 should be week 1' + expect(dateTime({input: [2008, 0, 1]}).week()).toBe(1); // 'Jan 1 2008 should be week 1' + expect(dateTime({input: [2008, 0, 6]}).week()).toBe(1); // 'Jan 6 2008 should be week 1' + expect(dateTime({input: [2008, 0, 7]}).week()).toBe(2); // 'Jan 7 2008 should be week 2' + expect(dateTime({input: [2008, 0, 13]}).week()).toBe(2); // 'Jan 13 2008 should be week 2' + expect(dateTime({input: [2008, 0, 14]}).week()).toBe(3); // 'Jan 14 2008 should be week 3' + expect(dateTime({input: [2002, 11, 30]}).week()).toBe(1); // 'Dec 30 2002 should be week 1' + expect(dateTime({input: [2003, 0, 1]}).week()).toBe(1); // 'Jan 1 2003 should be week 1' + expect(dateTime({input: [2003, 0, 5]}).week()).toBe(1); // 'Jan 5 2003 should be week 1' + expect(dateTime({input: [2003, 0, 6]}).week()).toBe(2); // 'Jan 6 2003 should be week 2' + expect(dateTime({input: [2003, 0, 12]}).week()).toBe(2); // 'Jan 12 2003 should be week 2' + expect(dateTime({input: [2003, 0, 13]}).week()).toBe(3); // 'Jan 13 2003 should be week 3' + expect(dateTime({input: [2008, 11, 29]}).week()).toBe(1); // 'Dec 29 2008 should be week 1' + expect(dateTime({input: [2009, 0, 1]}).week()).toBe(1); // 'Jan 1 2009 should be week 1' + expect(dateTime({input: [2009, 0, 4]}).week()).toBe(1); // 'Jan 4 2009 should be week 1' + expect(dateTime({input: [2009, 0, 5]}).week()).toBe(2); // 'Jan 5 2009 should be week 2' + expect(dateTime({input: [2009, 0, 11]}).week()).toBe(2); // 'Jan 11 2009 should be week 2' + expect(dateTime({input: [2009, 0, 12]}).week()).toBe(3); // 'Jan 12 2009 should be week 3' + expect(dateTime({input: [2009, 11, 28]}).week()).toBe(1); // 'Dec 28 2009 should be week 1' + expect(dateTime({input: [2010, 0, 1]}).week()).toBe(1); // 'Jan 1 2010 should be week 1' + expect(dateTime({input: [2010, 0, 3]}).week()).toBe(1); // 'Jan 3 2010 should be week 1' + expect(dateTime({input: [2010, 0, 4]}).week()).toBe(2); // 'Jan 4 2010 should be week 2' + expect(dateTime({input: [2010, 0, 10]}).week()).toBe(2); // 'Jan 10 2010 should be week 2' + expect(dateTime({input: [2010, 0, 11]}).week()).toBe(3); // 'Jan 11 2010 should be week 3' + expect(dateTime({input: [2010, 11, 27]}).week()).toBe(1); // 'Dec 27 2010 should be week 1' + expect(dateTime({input: [2011, 0, 1]}).week()).toBe(1); // 'Jan 1 2011 should be week 1' + expect(dateTime({input: [2011, 0, 2]}).week()).toBe(1); // 'Jan 2 2011 should be week 1' + expect(dateTime({input: [2011, 0, 3]}).week()).toBe(2); // 'Jan 3 2011 should be week 2' + expect(dateTime({input: [2011, 0, 9]}).week()).toBe(2); // 'Jan 9 2011 should be week 2' + expect(dateTime({input: [2011, 0, 10]}).week()).toBe(3); // 'Jan 10 2011 should be week 3' +}); + +test('weeks numbers dow:0 doy:6', () => { + settings.updateLocale({weekStart: 0, yearStart: 1}); + expect(dateTime({input: [2012, 0, 1]}).week()).toBe(1); // 'Jan 1 2012 should be week 1' + expect(dateTime({input: [2012, 0, 7]}).week()).toBe(1); // 'Jan 7 2012 should be week 1' + expect(dateTime({input: [2012, 0, 8]}).week()).toBe(2); // 'Jan 8 2012 should be week 2' + expect(dateTime({input: [2012, 0, 14]}).week()).toBe(2); // 'Jan 14 2012 should be week 2' + expect(dateTime({input: [2012, 0, 15]}).week()).toBe(3); // 'Jan 15 2012 should be week 3' + expect(dateTime({input: [2006, 11, 31]}).week()).toBe(1); // 'Dec 31 2006 should be week 1' + expect(dateTime({input: [2007, 0, 1]}).week()).toBe(1); // 'Jan 1 2007 should be week 1' + expect(dateTime({input: [2007, 0, 6]}).week()).toBe(1); // 'Jan 6 2007 should be week 1' + expect(dateTime({input: [2007, 0, 7]}).week()).toBe(2); // 'Jan 7 2007 should be week 2' + expect(dateTime({input: [2007, 0, 13]}).week()).toBe(2); // 'Jan 13 2007 should be week 2' + expect(dateTime({input: [2007, 0, 14]}).week()).toBe(3); // 'Jan 14 2007 should be week 3' + expect(dateTime({input: [2007, 11, 29]}).week()).toBe(52); // 'Dec 29 2007 should be week 52' + expect(dateTime({input: [2008, 0, 1]}).week()).toBe(1); // 'Jan 1 2008 should be week 1' + expect(dateTime({input: [2008, 0, 5]}).week()).toBe(1); // 'Jan 5 2008 should be week 1' + expect(dateTime({input: [2008, 0, 6]}).week()).toBe(2); // 'Jan 6 2008 should be week 2' + expect(dateTime({input: [2008, 0, 12]}).week()).toBe(2); // 'Jan 12 2008 should be week 2' + expect(dateTime({input: [2008, 0, 13]}).week()).toBe(3); // 'Jan 13 2008 should be week 3' + expect(dateTime({input: [2002, 11, 29]}).week()).toBe(1); // 'Dec 29 2002 should be week 1' + expect(dateTime({input: [2003, 0, 1]}).week()).toBe(1); // 'Jan 1 2003 should be week 1' + expect(dateTime({input: [2003, 0, 4]}).week()).toBe(1); // 'Jan 4 2003 should be week 1' + expect(dateTime({input: [2003, 0, 5]}).week()).toBe(2); // 'Jan 5 2003 should be week 2' + expect(dateTime({input: [2003, 0, 11]}).week()).toBe(2); // 'Jan 11 2003 should be week 2' + expect(dateTime({input: [2003, 0, 12]}).week()).toBe(3); // 'Jan 12 2003 should be week 3' + expect(dateTime({input: [2008, 11, 28]}).week()).toBe(1); // 'Dec 28 2008 should be week 1' + expect(dateTime({input: [2009, 0, 1]}).week()).toBe(1); // 'Jan 1 2009 should be week 1' + expect(dateTime({input: [2009, 0, 3]}).week()).toBe(1); // 'Jan 3 2009 should be week 1' + expect(dateTime({input: [2009, 0, 4]}).week()).toBe(2); // 'Jan 4 2009 should be week 2' + expect(dateTime({input: [2009, 0, 10]}).week()).toBe(2); // 'Jan 10 2009 should be week 2' + expect(dateTime({input: [2009, 0, 11]}).week()).toBe(3); // 'Jan 11 2009 should be week 3' + expect(dateTime({input: [2009, 11, 27]}).week()).toBe(1); // 'Dec 27 2009 should be week 1' + expect(dateTime({input: [2010, 0, 1]}).week()).toBe(1); // 'Jan 1 2010 should be week 1' + expect(dateTime({input: [2010, 0, 2]}).week()).toBe(1); // 'Jan 2 2010 should be week 1' + expect(dateTime({input: [2010, 0, 3]}).week()).toBe(2); // 'Jan 3 2010 should be week 2' + expect(dateTime({input: [2010, 0, 9]}).week()).toBe(2); // 'Jan 9 2010 should be week 2' + expect(dateTime({input: [2010, 0, 10]}).week()).toBe(3); // 'Jan 10 2010 should be week 3' + expect(dateTime({input: [2010, 11, 26]}).week()).toBe(1); // 'Dec 26 2010 should be week 1' + expect(dateTime({input: [2011, 0, 1]}).week()).toBe(1); // 'Jan 1 2011 should be week 1' + expect(dateTime({input: [2011, 0, 2]}).week()).toBe(2); // 'Jan 2 2011 should be week 2' + expect(dateTime({input: [2011, 0, 8]}).week()).toBe(2); // 'Jan 8 2011 should be week 2' + expect(dateTime({input: [2011, 0, 9]}).week()).toBe(3); // 'Jan 9 2011 should be week 3' +}); + +test('week year setter works', () => { + for (let year = 2000; year <= 2020; year += 1) { + expect( + dateTimeUtc({input: '2012-12-31T00:00:00.000Z'}).isoWeekYear(year).isoWeekYear(), + ).toBe(year); + expect(dateTimeUtc({input: '2012-12-31T00:00:00.000Z'}).weekYear(year).weekYear()).toBe( + year, + ); + } +}); diff --git a/src/dateTime/dateTime.ts b/src/dateTime/dateTime.ts index 9b556f0..a15cc64 100644 --- a/src/dateTime/dateTime.ts +++ b/src/dateTime/dateTime.ts @@ -28,6 +28,7 @@ import { tsToObject, uncomputeOrdinal, weekToGregorian, + weeksInWeekYear, } from '../utils'; import type {DateObject} from '../utils'; @@ -85,8 +86,11 @@ class DateTimeImpl implements DateTime { } toISOString(keepOffset?: boolean): string { + // invalid date throws an error if (keepOffset) { - return this.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) + .toISOString() + .replace('Z', this.format('Z')); } return this.toDate().toISOString(); } @@ -132,6 +136,10 @@ class DateTimeImpl implements DateTime { return this._timeZone === 'system' ? guessUserTimeZone() : this._timeZone; } + if (!this.isValid()) { + return this; + } + const zone = normalizeTimeZone(timeZone, settings.getDefaultTimeZone()); let ts = this.valueOf(); let offset = timeZoneOffset(zone, ts); @@ -269,11 +277,11 @@ class DateTimeImpl implements DateTime { } valueOf(): number { - return this._timestamp; + return this.isValid() ? this._timestamp : NaN; } isSame(input?: DateTimeInput, granularity?: DurationUnit): boolean { - const ts = getTimestamp(input); + const [ts] = getTimestamp(input, 'system'); if (!this.isValid() || isNaN(ts)) { return false; } @@ -281,7 +289,7 @@ class DateTimeImpl implements DateTime { } isBefore(input?: DateTimeInput, granularity?: DurationUnit): boolean { - const ts = getTimestamp(input); + const [ts] = getTimestamp(input, 'system'); if (!this.isValid() || isNaN(ts)) { return false; } @@ -291,7 +299,7 @@ class DateTimeImpl implements DateTime { } isAfter(input?: DateTimeInput, granularity?: DurationUnit): boolean { - const ts = getTimestamp(input); + const [ts] = getTimestamp(input, 'system'); if (!this.isValid() || isNaN(ts)) { return false; } @@ -315,7 +323,7 @@ class DateTimeImpl implements DateTime { const value = DateTimeImpl.isDateTime(amount) ? amount.timeZone(this._timeZone) : createDateTime({ - ts: getTimestamp(amount), + ts: getTimestamp(amount, 'system')[0], timeZone: this._timeZone, locale: this._locale, offset: this._offset, @@ -371,7 +379,7 @@ class DateTimeImpl implements DateTime { } from(formaInput: DateTimeInput, withoutSuffix?: boolean): string { if (!this.isValid()) { - return INVALID_DATE_STRING; + return this._localeData.invalidDate || INVALID_DATE_STRING; } return fromTo(this, formaInput, this._localeData.relativeTime, withoutSuffix, true); } @@ -381,6 +389,9 @@ class DateTimeImpl implements DateTime { if (!locale) { return this._locale; } + if (!this.isValid()) { + return this; + } return createDateTime({ ts: this.valueOf(), timeZone: this._timeZone, @@ -392,13 +403,13 @@ class DateTimeImpl implements DateTime { return new Date(this.valueOf()); } unix(): number { - return Math.floor(this.valueOf() / 1000); + return this.isValid() ? Math.floor(this.valueOf() / 1000) : NaN; } utc(keepLocalTime?: boolean | undefined): DateTime { return this.timeZone(UtcTimeZone, keepLocalTime); } daysInMonth(): number { - return daysInMonth(this._c.year, this._c.month); + return this.isValid() ? daysInMonth(this._c.year, this._c.month) : NaN; } // eslint-disable-next-line complexity @@ -415,9 +426,11 @@ class DateTimeImpl implements DateTime { const settingWeekStuff = newComponents.day !== undefined || newComponents.weekNumber !== undefined || + newComponents.weekYear !== undefined || newComponents.isoWeekNumber !== undefined || newComponents.weekday !== undefined || - newComponents.isoWeekday !== undefined; + newComponents.isoWeekday !== undefined || + newComponents.isoWeekYear !== undefined; const containsDayOfYear = newComponents.dayOfYear !== undefined; const containsYear = newComponents.year !== undefined; @@ -435,10 +448,15 @@ class DateTimeImpl implements DateTime { let mixed; if (settingWeekStuff) { - const {weekday, weekNumber, isoWeekday, isoWeekNumber, day} = newComponents; - const hasLocalWeekData = weekday !== undefined || weekNumber !== undefined; + const {weekday, weekNumber, weekYear, isoWeekday, isoWeekNumber, isoWeekYear, day} = + newComponents; + const hasLocalWeekData = + weekday !== undefined || weekNumber !== undefined || weekYear !== undefined; const hasIsoWeekData = - isoWeekday !== undefined || isoWeekNumber !== undefined || day !== undefined; + isoWeekday !== undefined || + isoWeekNumber !== undefined || + isoWeekYear !== undefined || + day !== undefined; if (hasLocalWeekData && hasIsoWeekData) { throw new Error("Can't mix local week with ISO week"); } @@ -448,7 +466,7 @@ class DateTimeImpl implements DateTime { const weekData = { weekday: (weekday ?? weekInfo.weekday) + 1, weekNumber: weekNumber ?? weekInfo.weekNumber, - weekYear: weekInfo.weekYear, + weekYear: weekYear ?? weekInfo.weekYear, }; mixed = { ...dateComponents, @@ -459,7 +477,7 @@ class DateTimeImpl implements DateTime { const weekData = { weekday: isoWeekday ?? (day === undefined ? weekInfo.isoWeekday : day || 7), weekNumber: isoWeekNumber ?? weekInfo.isoWeekNumber, - weekYear: weekInfo.isoWeekYear, + weekYear: isoWeekYear ?? weekInfo.isoWeekYear, }; mixed = {...dateComponents, ...newComponents, ...weekToGregorian(weekData, 4, 1)}; } @@ -589,6 +607,18 @@ class DateTimeImpl implements DateTime { } return this.isValid() ? this.weekInfo().weekNumber : NaN; } + weekYear(): number; + weekYear(value: number): DateTime; + weekYear(value?: unknown): number | DateTime { + if (typeof value === 'number') { + return this.set('weekYear', value); + } + return this.isValid() ? this.weekInfo().weekYear : NaN; + } + weeksInYear(): number { + const {minDaysInFirstWeek, startOfWeek} = getLocaleWeekValues(this._localeData); + return this.isValid() ? weeksInWeekYear(this.year(), minDaysInFirstWeek, startOfWeek) : NaN; + } isoWeek(): number; isoWeek(value: number): DateTime; isoWeek(value?: number): number | DateTime { @@ -597,6 +627,17 @@ class DateTimeImpl implements DateTime { } return this.isValid() ? this.weekInfo().isoWeekNumber : NaN; } + isoWeekYear(): number; + isoWeekYear(value: number): DateTime; + isoWeekYear(value?: unknown): number | DateTime { + if (typeof value === 'number') { + return this.set('isoWeekYear', value); + } + return this.isValid() ? this.weekInfo().isoWeekYear : NaN; + } + isoWeeksInYear(): number { + return this.isValid() ? weeksInWeekYear(this.year(), 4, 1) : NaN; + } weekday(): number; weekday(value: number): DateTime; weekday(value?: number): number | DateTime { @@ -616,15 +657,22 @@ class DateTimeImpl implements DateTime { } toString(): string { - return this.toDate().toUTCString(); + return this.isValid() + ? this.toDate().toUTCString() + : this._localeData.invalidDate || INVALID_DATE_STRING; } + + toJSON(): string | null { + return this.isValid() ? this.toISOString() : null; + } + /** * Returns a string representation of this DateTime appropriate for the REPL. * @return {string} */ [Symbol.for('nodejs.util.inspect.custom')]() { if (this.isValid()) { - return `DateTime { ts: ${this.toISOString()}, zone: ${this.timeZone()}, locale: ${this.locale()} }`; + return `DateTime { ts: ${this.toISOString()}, zone: ${this.timeZone()}, offset: ${this.utcOffset()}, locale: ${this.locale()} }`; } else { return `DateTime { ${INVALID_DATE_STRING} }`; } @@ -713,16 +761,23 @@ function createDateTime({ return new DateTimeImpl({ts, timeZone, offset, locale: loc, localeData, isValid}); } -function getTimestamp(input: DateTimeInput, format?: string, lang?: string, utc = false) { +function getTimestamp( + input: DateTimeInput, + timezone: string, + format?: string, + lang?: string, + utc = false, +): [ts: number, offset: number] { let ts: number; + let offset: number | undefined; if (isDateTime(input) || typeof input === 'number' || input instanceof Date) { ts = Number(input); } else if (input === null || input === undefined) { ts = Date.now(); } else if (Array.isArray(input)) { - ts = getTimestampFromArray(input, utc); + [ts, offset] = getTimestampFromArray(input, timezone); } else if (typeof input === 'object') { - ts = getTimestampFromObject(input, utc); + [ts, offset] = getTimestampFromObject(input, timezone); } else if (utc) { ts = dayjs.utc(input, format, STRICT).valueOf(); } else { @@ -733,7 +788,9 @@ function getTimestamp(input: DateTimeInput, format?: string, lang?: string, utc ts = localDate.valueOf(); } - return ts; + + offset = offset ?? timeZoneOffset(timezone, ts); + return [ts, offset]; } /** @@ -763,9 +820,7 @@ export function dateTime(opt?: { const timeZoneOrDefault = normalizeTimeZone(timeZone, settings.getDefaultTimeZone()); const locale = dayjs.locale(lang || settings.getLocale(), undefined, true); - const ts = getTimestamp(input, format, lang); - - const offset = timeZoneOffset(timeZoneOrDefault, ts); + const [ts, offset] = getTimestamp(input, timeZoneOrDefault, format, lang); const date = createDateTime({ ts, @@ -782,7 +837,7 @@ export function dateTimeUtc(opt?: {input?: DateTimeInput; format?: FormatInput; const locale = dayjs.locale(lang || settings.getLocale(), undefined, true); - const ts = getTimestamp(input, format, lang, true); + const [ts] = getTimestamp(input, UtcTimeZone, format, lang, true); const date = createDateTime({ ts, diff --git a/src/dateTime/format.ts b/src/dateTime/format.ts index 68dadce..ce51a60 100644 --- a/src/dateTime/format.ts +++ b/src/dateTime/format.ts @@ -40,7 +40,7 @@ export function expandFormat( export const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ'; const formattingTokens = - /(\[[^[]*\])|([Hh]mm(ss)?|Mo|M{1,4}|Do|DDDo|D{1,4}|d{2,4}|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + /(\[[^[]*\])|([Hh]mm(ss)?|Mo|M{1,4}|Do|DDDo|D{1,4}|d{2,4}|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|Y{4,6}|YY?|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; const formatTokenFunctions: Record< string, @@ -57,10 +57,19 @@ export function formatDate( if (formatTokenFunctions[match]) { return formatTokenFunctions[match](date, locale, expandedFormat); } - return match.replace(/^\[|\]$/g, ''); + return removeFormattingTokens(match); }); } +function removeFormattingTokens(input: string) { + return input.replace(/^\[([\s\S)]*)\]$/g, '$1'); +} + +formatTokenFunctions['Y'] = (date) => { + const y = date.year(); + return y <= 9999 ? zeroPad(y, 4) : '+' + y; +}; + formatTokenFunctions['YY'] = (date) => { const y = date.year(); return zeroPad(y % 100, 2); @@ -85,7 +94,8 @@ formatTokenFunctions['MM'] = (date) => { }; formatTokenFunctions['Mo'] = (date, locale) => { - return `${locale.ordinal?.(date.month() + 1, 'M')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.month() + 1, 'M')}`); }; formatTokenFunctions['MMM'] = (date, locale, format) => { @@ -119,7 +129,8 @@ formatTokenFunctions['ww'] = (date) => { }; formatTokenFunctions['wo'] = (date, locale) => { - return `${locale.ordinal?.(date.week(), 'w')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.week(), 'w')}`); }; formatTokenFunctions['W'] = (date) => { @@ -131,7 +142,8 @@ formatTokenFunctions['WW'] = (date) => { }; formatTokenFunctions['Wo'] = (date, locale) => { - return `${locale.ordinal?.(date.isoWeek(), 'W')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.isoWeek(), 'W')}`); }; formatTokenFunctions['d'] = (date) => { @@ -139,7 +151,8 @@ formatTokenFunctions['d'] = (date) => { }; formatTokenFunctions['do'] = (date, locale) => { - return `${locale.ordinal?.(date.day(), 'd')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.day(), 'd')}`); }; formatTokenFunctions['dd'] = (date, locale, format) => { @@ -274,7 +287,8 @@ formatTokenFunctions['Q'] = (date) => { }; formatTokenFunctions['Qo'] = (date, locale) => { - return `${locale.ordinal?.(date.quarter(), 'Q')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.quarter(), 'Q')}`); }; formatTokenFunctions['D'] = (date) => { @@ -286,7 +300,8 @@ formatTokenFunctions['DD'] = (date) => { }; formatTokenFunctions['Do'] = (date, locale) => { - return `${locale.ordinal?.(date.date(), 'D')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.date(), 'D')}`); }; formatTokenFunctions['m'] = (date) => { @@ -378,7 +393,32 @@ formatTokenFunctions['DDDD'] = (date) => { }; formatTokenFunctions['DDDo'] = (date, locale) => { - return `${locale.ordinal?.(date.dayOfYear(), 'DDD')}`; + // dayjs locales ordinal method returns value inside brackets '[' ']' + return removeFormattingTokens(`${locale.ordinal?.(date.dayOfYear(), 'DDD')}`); +}; + +formatTokenFunctions['gg'] = (date) => { + return zeroPad(date.weekYear() % 100, 2); +}; + +formatTokenFunctions['gggg'] = (date) => { + return zeroPad(date.weekYear(), 4); +}; + +formatTokenFunctions['ggggg'] = (date) => { + return zeroPad(date.weekYear(), 5); +}; + +formatTokenFunctions['GG'] = (date) => { + return zeroPad(date.isoWeekYear() % 100, 2); +}; + +formatTokenFunctions['GGGG'] = (date) => { + return zeroPad(date.isoWeekYear(), 4); +}; + +formatTokenFunctions['GGGGG'] = (date) => { + return zeroPad(date.isoWeekYear(), 5); }; function getShort({ diff --git a/src/dateTime/parse.ts b/src/dateTime/parse.ts index 1174dc0..d1251cc 100644 --- a/src/dateTime/parse.ts +++ b/src/dateTime/parse.ts @@ -1,64 +1,53 @@ +import {fixOffset, timeZoneOffset} from '../timeZone'; import type {InputObject} from '../typings'; -import {normalizeComponent, normalizeDateComponents} from '../utils'; +import {normalizeComponent, normalizeDateComponents, objToTS, tsToObject} from '../utils'; +import type {DateObject} from '../utils'; -export function getTimestampFromArray(input: (number | string)[], utc = false) { +export function getTimestampFromArray(input: (number | string)[], timezone: string) { if (input.length === 0) { - return Date.now(); + return getTimestampFromObject({}, timezone); } const dateParts = input.map(Number); - let date: Date; - const [year, month = 0, day = 1, hours = 0, minutes = 0, seconds = 0, milliseconds = 0] = + const [year, month = 0, date = 1, hour = 0, minute = 0, second = 0, millisecond = 0] = dateParts; - if (utc) { - date = new Date(Date.UTC(year, month, day, hours, minutes, seconds, milliseconds)); - } else { - date = new Date(year, month, day, hours, minutes, seconds, milliseconds); - } - - if (year >= 0 && year < 100) { - if (utc) { - date.setUTCFullYear(year, month, day); - } else { - date.setFullYear(year, month, day); - } - } - return date.valueOf(); + return getTimestampFromObject({year, month, date, hour, minute, second, millisecond}, timezone); } -export function getTimestampFromObject(input: InputObject, utc = false) { - if (Object.keys(input).length === 0) { - return Date.now(); - } +const defaultUnitValues = { + year: 1, + month: 1, + date: 1, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, +} as const; +const orderedUnits = ['year', 'month', 'date', 'hour', 'minute', 'second', 'millisecond'] as const; + +export function getTimestampFromObject( + input: InputObject, + timezone: string, +): [ts: number, offset: number] { const normalized = normalizeDateComponents(input, normalizeComponent); - normalized.day = normalized.day ?? normalized.date; - const hasYear = normalized.year !== undefined; - const hasMonth = normalized.month !== undefined; - const hasDate = normalized.date !== undefined; + normalized.date = normalized.day ?? normalized.date; - const now = new Date(Date.now()); - const year = normalized.year ?? utc ? now.getUTCFullYear() : now.getFullYear(); - let month = normalized.month; - if (month === undefined) { - if (!hasYear && !hasDate) { - month = utc ? now.getUTCMonth() : now.getMonth(); + const objNow = tsToObject(Date.now(), timeZoneOffset(timezone, Date.now())); + let foundFirst = false; + for (const unit of orderedUnits) { + if (normalized[unit] !== undefined) { + foundFirst = true; + } else if (foundFirst) { + normalized[unit] = defaultUnitValues[unit]; } else { - month = 0; + normalized[unit] = objNow[unit]; } } - let day = normalized.day; - if (day === undefined) { - if (!hasYear && !hasMonth) { - day = utc ? now.getUTCDate() : now.getDate(); - } else { - day = 1; - } - } - const hours = normalized.hour ?? 0; - const minutes = normalized.minute ?? 0; - const seconds = normalized.second ?? 0; - const milliseconds = normalized.millisecond ?? 0; - - return getTimestampFromArray([year, month, day, hours, minutes, seconds, milliseconds], utc); + const [ts, offset] = fixOffset( + objToTS(normalized as DateObject), + timeZoneOffset(timezone, Date.now()), + timezone, + ); + return [ts, offset]; } diff --git a/src/index.ts b/src/index.ts index bf4d9dd..8e14736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,5 +8,5 @@ export {parse as defaultRelativeParse, isLikeRelative as defaultIsLikeRelative} export {dateTimeParse, isValid, isLikeRelative} from './parser'; export {getTimeZonesList, guessUserTimeZone, isValidTimeZone, timeZoneOffset} from './timeZone'; export type {DateTime, DateTimeInput, Duration, DurationInput} from './typings'; -export {UtcTimeZone} from './constants'; +export {UtcTimeZone, HTML5_INPUT_FORMATS} from './constants'; export {duration, isDuration} from './duration'; diff --git a/src/settings/locales.ts b/src/settings/locales.ts index 6fc07c8..00481e1 100644 --- a/src/settings/locales.ts +++ b/src/settings/locales.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports type LocaleLoader = () => Promise; export const localeLoaders: Record = { diff --git a/src/timeZone/timeZone.ts b/src/timeZone/timeZone.ts index f98f934..7a57499 100644 --- a/src/timeZone/timeZone.ts +++ b/src/timeZone/timeZone.ts @@ -56,7 +56,7 @@ export function timeZoneOffset(zone: TimeZone, ts: number) { } if (zone === 'system') { - return -date.getTimezoneOffset(); + return -date.getTimezoneOffset() || 0; } const dtf = getDateTimeFormat('en-US', { diff --git a/src/typings/dateTime.ts b/src/typings/dateTime.ts index 1a8fcbf..fd9fb7f 100644 --- a/src/typings/dateTime.ts +++ b/src/typings/dateTime.ts @@ -52,7 +52,9 @@ export type AllUnit = | 'E' | 'dayOfYear' | 'dayOfYears' - | 'DDD'; + | 'DDD' + | 'weekYear' + | 'isoWeekYear'; export type InputObject = Partial>; export type SetObject = Partial>; @@ -77,6 +79,7 @@ export interface DateTime { endOf(unitOfTime: StartOfUnit): DateTime; toDate(): Date; toISOString(keepOffset?: boolean): string; + toJSON(): string | null; valueOf(): number; unix(): number; utc(keepLocalTime?: boolean): DateTime; @@ -102,10 +105,22 @@ export interface DateTime { week(): number; /** Sets the week of the year according to the locale. */ week(value: number): DateTime; + /** Gets the week-year according to the locale. */ + weekYear(): number; + /** Sets the week-year according to the locale. */ + weekYear(value: number): DateTime; + /** Gets the number of weeks in the year according to locale */ + weeksInYear(): number; /** Gets the ISO week of the year. First week is the week with the first Thursday of the year (i.e. of January) in it.*/ isoWeek(): number; /** Sets the ISO week of the year. */ isoWeek(value: number): DateTime; + /** Gets the ISO week-year. */ + isoWeekYear(): number; + /** Sets the ISO week-year. */ + isoWeekYear(value: number): DateTime; + /** Gets the number of weeks in the year, according to ISO weeks. */ + isoWeeksInYear(): number; /** Gets the day of the year. */ dayOfYear(): number; /** Sets the day of the year. */ diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 639209c..1906c99 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -163,6 +163,8 @@ const normalizedUnits = { dayOfYear: 'dayOfYear', dayOfYears: 'dayOfYear', DDD: 'dayOfYear', + weekyear: 'weekYear', + isoweekyear: 'isoWeekYear', } as const; export function normalizeComponent(component: string) {