From 7420ad0acd33d51c440d1bf9702a9dabac703a22 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Wed, 2 May 2018 16:28:53 +0200 Subject: [PATCH 1/4] add LocalDateTime --- src/LocalDate.js | 4 +- src/LocalDateTime.js | 45 ++++++++++++ src/index.js | 3 + src/util.js | 9 +++ .../{getters.test.js => date.getters.test.js} | 0 .../{parser.test.js => date.parser.test.js} | 0 ....test.js => date.staticProperties.test.js} | 0 test/tests/dateTime.getters.test.js | 19 +++++ test/tests/dateTime.parser.test.js | 69 +++++++++++++++++++ test/tests/dateTime.staticProperties.test.js | 24 +++++++ 10 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/LocalDateTime.js create mode 100644 src/util.js rename test/tests/{getters.test.js => date.getters.test.js} (100%) rename test/tests/{parser.test.js => date.parser.test.js} (100%) rename test/tests/{staticProperties.test.js => date.staticProperties.test.js} (100%) create mode 100644 test/tests/dateTime.getters.test.js create mode 100644 test/tests/dateTime.parser.test.js create mode 100644 test/tests/dateTime.staticProperties.test.js diff --git a/src/LocalDate.js b/src/LocalDate.js index 60d461a..2b72cda 100644 --- a/src/LocalDate.js +++ b/src/LocalDate.js @@ -1,8 +1,6 @@ // @flow -function pad2(number: number): number | string { - return number < 10 ? `0${number}` : number; -} +import { pad2 } from './util'; function warn(message: string) { if (process.env.NODE_ENV !== 'production') { diff --git a/src/LocalDateTime.js b/src/LocalDateTime.js new file mode 100644 index 0000000..5f47cf3 --- /dev/null +++ b/src/LocalDateTime.js @@ -0,0 +1,45 @@ +// @flow + +import { pad2, pad3 } from './util'; + +export default class LocalDateTime extends Date { + + static ISO_DATE_TIME_FORMAT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(.(\d{3}))?$/ + + static test(isoDateTime: string) { + return LocalDateTime.ISO_DATE_TIME_FORMAT.test(isoDateTime); + } + + constructor(value: LocalDateTime | string | void) { + if (typeof value === 'undefined') { + super(new Date()); + } else if (value instanceof LocalDateTime) { + super( + value.getFullYear(), value.getMonth(), value.getDate(), + value.getHours(), value.getMinutes(), value.getSeconds(), value.getMilliseconds() + ); + } else if (typeof value === 'string' && LocalDateTime.ISO_DATE_TIME_FORMAT.test(value)) { + const [ + year, + month, + date, + hours, + minutes, + seconds, + , + milliseconds + ] = LocalDateTime.ISO_DATE_TIME_FORMAT.exec(value).slice(1).map(s => parseInt(s, 10)); + super(year, month - 1, date, hours, minutes, seconds, milliseconds || 0); + } else { + throw new Error('Invalid date supplied. Please specify an ISO date time string (YYYY-MM-DDTHH:mm:SS) or a LocalDateTime object.\nhttps://github.com/buildo/local-date#parser'); // eslint-disable-line max-len + } + } + + toISOString(): string { + const date = [this.getFullYear(), pad2(this.getMonth() + 1), pad2(this.getDate())].join('-'); + const time = [pad2(this.getHours()), pad2(this.getMinutes()), pad2(this.getSeconds())].join(':'); + const milliseconds = pad3(this.getMilliseconds()); + return `${date}T${time}.${milliseconds}`; + } + +} diff --git a/src/index.js b/src/index.js index 87ee328..2f9f195 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,7 @@ // @flow import LocalDate from './LocalDate'; +import LocalDateTime from './LocalDateTime'; export default LocalDate; +export { LocalDate, LocalDateTime }; + diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..55981b9 --- /dev/null +++ b/src/util.js @@ -0,0 +1,9 @@ +// @flow + +export function pad2(number: number): number | string { + return number < 10 ? `0${number}` : number; +} + +export function pad3(number: number): number | string { + return number < 10 ? `00${number}` : number < 100 ? `0${number}` : number; +} diff --git a/test/tests/getters.test.js b/test/tests/date.getters.test.js similarity index 100% rename from test/tests/getters.test.js rename to test/tests/date.getters.test.js diff --git a/test/tests/parser.test.js b/test/tests/date.parser.test.js similarity index 100% rename from test/tests/parser.test.js rename to test/tests/date.parser.test.js diff --git a/test/tests/staticProperties.test.js b/test/tests/date.staticProperties.test.js similarity index 100% rename from test/tests/staticProperties.test.js rename to test/tests/date.staticProperties.test.js diff --git a/test/tests/dateTime.getters.test.js b/test/tests/dateTime.getters.test.js new file mode 100644 index 0000000..3a51df3 --- /dev/null +++ b/test/tests/dateTime.getters.test.js @@ -0,0 +1,19 @@ +// @flow + +import LocalDateTime from '../../src/LocalDateTime'; + +describe('Getters', () => { + + const localDateTime = new LocalDateTime(); + + it('toISOString should return an ISO date time', () => { + expect(localDateTime.toISOString()).toMatch(LocalDateTime.ISO_DATE_TIME_FORMAT); + expect(() => new LocalDateTime(localDateTime.toISOString())).not.toThrow(); + }); + + it('toJSON should return an ISO date time', () => { + expect(localDateTime.toJSON()).toMatch(LocalDateTime.ISO_DATE_TIME_FORMAT); + expect(() => new LocalDateTime(localDateTime.toJSON())).not.toThrow(); + }); + +}); diff --git a/test/tests/dateTime.parser.test.js b/test/tests/dateTime.parser.test.js new file mode 100644 index 0000000..241d7c5 --- /dev/null +++ b/test/tests/dateTime.parser.test.js @@ -0,0 +1,69 @@ +import LocalDateTime from '../../src/LocalDateTime'; + +console.warn = () => {}; // eslint-disable-line no-console + +describe('Parser', () => { + + it('Should be an instance of Date', () => { + expect(new LocalDateTime() instanceof Date).toBe(true); + }); + + it('Should be an instance of LocalDateTime', () => { + expect(new LocalDateTime() instanceof LocalDateTime).toBe(true); + }); + + it('Should return a LocalDateTime with current date time if no argument is passed and should not consider the timezone', () => { + const today = new Date(); + const todayLocalDateTime = new LocalDateTime(); + + expect(today.getFullYear()).toEqual(todayLocalDateTime.getFullYear()); + expect(today.getMonth()).toEqual(todayLocalDateTime.getMonth()); + expect(today.getDate()).toEqual(todayLocalDateTime.getDate()); + expect(today.getHours()).toEqual(todayLocalDateTime.getHours()); + expect(today.getMinutes()).toEqual(todayLocalDateTime.getMinutes()); + expect(today.getSeconds()).toEqual(todayLocalDateTime.getSeconds()); + expect(today.getMilliseconds()).toEqual(todayLocalDateTime.getMilliseconds()); + }); + + it('Should parse ISO date times without considering the timezone', () => { + const year = 1991; + const month = 6; + const day = 4; + const hours = 10; + const minutes = 10; + const seconds = 42; + const milliseconds = 210; + const isoDate = `${year}-0${month}-0${day}T${hours}:${minutes}:${seconds}.${milliseconds}`; + const localDateTime = new LocalDateTime(isoDate); + + expect(localDateTime.getFullYear()).toEqual(year); + expect(localDateTime.getMonth()).toEqual(month - 1); + expect(localDateTime.getDate()).toEqual(day); + expect(localDateTime.getHours()).toEqual(hours); + expect(localDateTime.getMinutes()).toEqual(minutes); + expect(localDateTime.getSeconds()).toEqual(seconds); + expect(localDateTime.getMilliseconds()).toEqual(milliseconds); + }); + + it('Should clone instances of LocalDateTimes', () => { + const localDateTime = new LocalDateTime('2016-05-20T10:10:42'); + const clonedLocalDateTime = new LocalDateTime(localDateTime); + + expect(localDateTime).toEqual(clonedLocalDateTime); + }); + + it('Should throw an error if argument is invalid', () => { + const newLocalDateTime = argument => () => { + return new LocalDateTime(argument); + }; + + expect(newLocalDateTime(new Date())).toThrow(); + expect(newLocalDateTime(null)).toThrow(); + expect(newLocalDateTime(1483549074687)).toThrow(); // timestamp + expect(newLocalDateTime(2016, 5, 21)).toThrow(); + expect(newLocalDateTime('2017-01-04T18:04:52Z')).toThrow(); + expect(newLocalDateTime('2017-01-04T18:04:52.438Z')).toThrow(); + expect(newLocalDateTime('2017-01-04T18:04:52+00:00')).toThrow(); + }); + +}); diff --git a/test/tests/dateTime.staticProperties.test.js b/test/tests/dateTime.staticProperties.test.js new file mode 100644 index 0000000..7d9443d --- /dev/null +++ b/test/tests/dateTime.staticProperties.test.js @@ -0,0 +1,24 @@ +// @flow + +import LocalDateTime from '../../src/LocalDateTime'; + +describe('Static properties', () => { + + it('ISO_DATE_TIME_FORMAT should be a valid RegExp', () => { + expect(LocalDateTime.ISO_DATE_TIME_FORMAT instanceof RegExp).toBe(true); + }); + + it('test should return "true" if argument is a valid (local) ISO date time', () => { + expect(LocalDateTime.test('2016-05-20T10:10:42')).toBe(true); + expect(LocalDateTime.test('2016-05-20T10:10:42.234')).toBe(true); + }); + + it('test should return "false" if argument is not a valid (local) ISO date time', () => { + expect(LocalDateTime.test('2017/01/04')).toBe(false); + expect(LocalDateTime.test('05-20-2017')).toBe(false); + expect(LocalDateTime.test('2017-01-04T18:04:52Z')).toBe(false); + expect(LocalDateTime.test('2017-01-04T18:04:52.438Z')).toBe(false); + expect(LocalDateTime.test('2017-01-04T18:04:52+00:00')).toBe(false); + }); + +}); From 432ae5f5066f20d4ed06b0bb810843c22161b7ea Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Wed, 2 May 2018 16:39:08 +0200 Subject: [PATCH 2/4] update README --- README.md | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 83738b7..dc7d0cd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # LocalDate -Replacement of `Date` for dealing with dates independent of time or timezone. +Replacement of `Date` for dealing with dates independent of timezone. ## Why To avoid some: @@ -23,7 +23,7 @@ const date = new Date('1991-06-04'); date.getDate(); // -> 3 (Date always applies user's timezone!) ``` -`LocalDate` replaces the parser of `Date` with a simpler and stricter one which will consider only the date part (and conceptually **adapt** the timezone to it instead of the opposite). +`LocalDate` and `LocalDateTime` replace the parser of `Date` with a simpler and stricter one which will consider only the date part (or the date + time parts), and conceptually **adapt** the timezone to it instead of the opposite. ```js // GMT -05:00 (New York) @@ -36,7 +36,7 @@ new LocalDate('1991-06-04') == new Date(1991, 5, 4); ## Install ``` -npm i --save local-date +yarn add local-date ``` ## Browser Support @@ -49,17 +49,17 @@ import 'local-date/lib/polyfills/array-from'; ``` ## Usage -`LocalDate` extends `Date` so it reflects its API for most things. +`LocalDate` and `LocalDateTime` extend `Date` so it reflects its API for most things. The only parts that change are the parser and the formatter `toISOString`. -To help users check wether a string is a valid ISO date or not, `LocalDate` has also a static method `test`. +To help users check wether a string is a valid ISO date or not, `LocalDate` and `LocalDateTime` have also a static method `test`. ### Parser -There are three possible ways to instantiate a `LocalDate`: +There are three possible ways to instantiate a `LocalDate` (`LocalDateTime`): -1. ISO date +1. ISO date (datetime) string 2. no argument -3. another `LocalDate` instance +3. another `LocalDate` (`LocalDateTime`) instance #### 1) ISO date This is the standard way to instantiate a `LocalDate`: by passing it an ISO date string. @@ -73,11 +73,17 @@ localDate.getMonth(); // -> 4 (timezone independent!) localDate.getDate(); // -> 20 (timezone independent!) ``` +Similarly, with a `LocalDateTime`: + +```js +const localDateTime = new LocalDateTime('2016-05-20T10:10:42'); +``` + #### 2) no argument -`new LocalDate()` will return a `LocalDate` containing the current date for the user's timezone (internally it uses `new Date()`) +`new LocalDate()` (`new LocalDateTime()`) will return a `LocalDate` (`LocalDateTime`) containing the current date for the user's timezone (internally it uses `new Date()`) -#### 3) another `LocalDate` instance -This method is useful if you need to clone an instance of `LocalDate`: +#### 3) another `LocalDate` (`LocalDateTime`) instance +This method is useful if you need to clone an instance of `LocalDate` (`LocalDateTime`): ```js const localDate = new LocalDate('2016-05-20'); @@ -100,9 +106,12 @@ date.toISOString(); // -> ""2016-05-20T00:00:00.000Z"" (it contains the time as ``` ### `test` -To check if a string is a valid ISO date or not, you can use the static method `LocalDate.test`: +To check if a string is a valid ISO date or not, you can use the static method `LocalDate.test` (`LocalDateTime.test`): ```js LocalDate.test('2016-05-20'); // -> true LocalDate.test('2016-05-20T00:00:00.000Z'); // -> false +LocalDateTime.test('2016-05-20T00:00:00'); // -> true +LocalDateTime.test('2016-05-20T00:00:00.000'); // -> true +LocalDateTime.test('2016-05-20T00:00:00.000Z'); // -> false ``` From 1d781f5ffb9706edc667b414ecc33edaf3beb8a8 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Wed, 2 May 2018 16:55:37 +0200 Subject: [PATCH 3/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc7d0cd..b5e25d4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ import 'local-date/lib/polyfills/array-from'; ``` ## Usage -`LocalDate` and `LocalDateTime` extend `Date` so it reflects its API for most things. +`LocalDate` and `LocalDateTime` extend `Date` so they reflect its API for most things. The only parts that change are the parser and the formatter `toISOString`. To help users check wether a string is a valid ISO date or not, `LocalDate` and `LocalDateTime` have also a static method `test`. From c7b4f08a476e334320447489ea5b17f068b07ee7 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Wed, 2 May 2018 17:10:53 +0200 Subject: [PATCH 4/4] lint --- src/LocalDateTime.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LocalDateTime.js b/src/LocalDateTime.js index 5f47cf3..433e286 100644 --- a/src/LocalDateTime.js +++ b/src/LocalDateTime.js @@ -37,7 +37,9 @@ export default class LocalDateTime extends Date { toISOString(): string { const date = [this.getFullYear(), pad2(this.getMonth() + 1), pad2(this.getDate())].join('-'); - const time = [pad2(this.getHours()), pad2(this.getMinutes()), pad2(this.getSeconds())].join(':'); + const time = [ + pad2(this.getHours()), pad2(this.getMinutes()), pad2(this.getSeconds()) + ].join(':'); const milliseconds = pad3(this.getMilliseconds()); return `${date}T${time}.${milliseconds}`; }