Skip to content

Commit 1a90983

Browse files
authored
Fix LocalDateTime.fromDate (#1193)
LocalDateTime.fromDate using Date's month wrongly. This fixes that. It was throwing right away. I think we should backport this and release soon. Also adds fromDate to LocalDate and LocalTime. Date.UTC is deleted since I realized that they are not necessary. Tests pass without it. new Date() returns the local time of the system which is like LocalDateTime.now() in java. The issue happens due to the fact that we don't convert month field of JS Date into our month field. JS Date is 0-11 based and we use 1-12 in LocalDate. Therefore, we should have added 1 to the month field in fromDate constructor. If a user gives a date in january fromDate throws validation error.
1 parent fdee35d commit 1a90983

File tree

2 files changed

+152
-40
lines changed

2 files changed

+152
-40
lines changed

src/core/DateTimeClasses.ts

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ export class LocalTime {
8787
return new LocalTime(hours, minutes, seconds, nano);
8888
}
8989

90+
/**
91+
* Constructs a new instance from Date.
92+
* @param date must be a valid Date. `date.getTime()` should be not NaN
93+
* @throws TypeError if the passed param is not a Date
94+
* @throws RangeError if an invalid Date is passed
95+
*/
96+
static fromDate(date: Date): LocalTime {
97+
if (!(date instanceof Date)) {
98+
throw new TypeError('A Date is not passed');
99+
}
100+
if (isNaN(date.getTime())) {
101+
throw new RangeError('Invalid Date is passed.');
102+
}
103+
return new LocalTime(
104+
date.getHours(),
105+
date.getMinutes(),
106+
date.getSeconds(),
107+
date.getMilliseconds() * 1_000_000
108+
);
109+
}
110+
90111
/**
91112
* Returns the string representation of this local time.
92113
*
@@ -221,6 +242,26 @@ export class LocalDate {
221242
return new LocalDate(yearNumber, monthNumber, dateNumber);
222243
}
223244

245+
/**
246+
* Constructs a new instance from Date.
247+
* @param date must be a valid Date. `date.getTime()` should be not NaN
248+
* @throws TypeError if the passed param is not a Date
249+
* @throws RangeError if an invalid Date is passed
250+
*/
251+
static fromDate(date: Date): LocalDate {
252+
if (!(date instanceof Date)) {
253+
throw new TypeError('A Date is not passed');
254+
}
255+
if (isNaN(date.getTime())) {
256+
throw new RangeError('Invalid Date is passed.');
257+
}
258+
return new LocalDate(
259+
date.getFullYear(),
260+
date.getMonth() + 1, // month start with 0 in Date
261+
date.getDate()
262+
);
263+
}
264+
224265
/**
225266
* Returns the string representation of this local date.
226267
* @returns A string in the form yyyy:mm:dd. Values are zero padded from left
@@ -278,21 +319,19 @@ export class LocalDateTime {
278319
*/
279320
asDate(): Date {
280321
return new Date(
281-
Date.UTC(
282-
this.localDate.year,
283-
this.localDate.month - 1, // month start with 0 in Date
284-
this.localDate.date,
285-
this.localTime.hour,
286-
this.localTime.minute,
287-
this.localTime.second,
288-
Math.floor(this.localTime.nano / 1_000_000)
289-
)
322+
this.localDate.year,
323+
this.localDate.month - 1, // month start with 0 in Date
324+
this.localDate.date,
325+
this.localTime.hour,
326+
this.localTime.minute,
327+
this.localTime.second,
328+
Math.floor(this.localTime.nano / 1_000_000)
290329
);
291330
}
292331

293332
/**
294333
* Constructs a new instance from Date.
295-
* @param date Must be a valid Date. So `date.getTime()` should be not NaN
334+
* @param date must be a valid Date. `date.getTime()` should be not NaN
296335
* @throws TypeError if the passed param is not a Date
297336
* @throws RangeError if an invalid Date is passed
298337
*/
@@ -303,15 +342,7 @@ export class LocalDateTime {
303342
if (isNaN(date.getTime())) {
304343
throw new RangeError('Invalid Date is passed.');
305344
}
306-
return LocalDateTime.from(
307-
date.getUTCFullYear(),
308-
date.getUTCMonth(),
309-
date.getUTCDate(),
310-
date.getUTCHours(),
311-
date.getUTCMinutes(),
312-
date.getUTCSeconds(),
313-
date.getUTCMilliseconds() * 1_000_000
314-
);
345+
return new LocalDateTime(LocalDate.fromDate(date), LocalTime.fromDate(date));
315346
}
316347

317348
/**
@@ -375,7 +406,7 @@ export class OffsetDateTime {
375406

376407
/**
377408
* Constructs a new instance from Date and offset seconds.
378-
* @param date Must be a valid Date. So `date.getTime()` should be not NaN
409+
* @param date must be a valid Date. `date.getTime()` should be not NaN
379410
* @param offsetSeconds Offset in seconds, must be between [-64800, 64800]
380411
* @throws TypeError if a wrong type is passed as argument
381412
* @throws RangeError if an invalid argument value is passed

test/unit/core/DatetimeClasses.js

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
LocalDateTime,
2424
OffsetDateTime,
2525
} = require('../../../lib/core/DateTimeClasses');
26+
const { leftZeroPadInteger } = require('../../../lib/util/DateTimeUtil');
2627

2728
describe('DateTimeClassesTest', function () {
2829
describe('LocalTimeTest', function () {
@@ -110,21 +111,43 @@ describe('DateTimeClassesTest', function () {
110111
(() => LocalTime.fromString(null)).should.throw(TypeError, 'String expected');
111112
(() => LocalTime.fromString()).should.throw(TypeError, 'String expected');
112113
});
114+
115+
it('should construct from fromDate correctly', function () {
116+
const localTime1 = LocalTime.fromDate(new Date(2000, 2, 29, 0, 0, 0, 0));
117+
localTime1.toString().should.be.eq('00:00:00');
118+
const localTime2 = LocalTime.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6));
119+
localTime2.toString().should.be.eq('02:03:04.006000000');
120+
});
121+
122+
it('should throw when constructed from fromDate with a non-date thing', function () {
123+
const nonDateThings = [1, null, '', {}, [], function() {}, class A {}, LocalDateTime.fromDate(new Date())];
124+
nonDateThings.forEach(nonDateThing => {
125+
(() => LocalTime.fromDate(nonDateThing)).should.throw(TypeError, 'A Date is not passed');
126+
});
127+
});
128+
129+
it('should throw when constructed from fromDate with an invalid date', function () {
130+
const invalidDates = [new Date('aa'), new Date({}), new Date(undefined)];
131+
invalidDates.forEach(invalidDate => {
132+
isNaN(invalidDate.getTime).should.be.true;
133+
(() => LocalTime.fromDate(invalidDate)).should.throw(RangeError, 'Invalid Date is passed');
134+
});
135+
});
113136
});
114137
describe('LocalDateTest', function () {
115-
it('should throw RangeError if year is not an integer between -999_999_999-999_999_999(inclusive)',
116-
function () {
117-
(() => new LocalDate(1e9, 1, 1)).should.throw(RangeError, 'Year');
118-
(() => new LocalDate(-1e9, 1, 1)).should.throw(RangeError, 'Year');
119-
(() => new LocalDate(1.1, 1, 1)).should.throw(RangeError, 'All arguments must be integers');
120-
(() => new LocalDate('1', 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
121-
(() => new LocalDate({ 1: 1 }, 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
122-
(() => new LocalDate([], 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
123-
(() => new LocalDate(1e12, 1, 1)).should.throw(RangeError, 'Year');
124-
});
138+
it('should throw RangeError if year is not an integer between -999_999_999-999_999_999(inclusive)', function () {
139+
(() => new LocalDate(1e9, 1, 1)).should.throw(RangeError, 'Year');
140+
(() => new LocalDate(-1e9, 1, 1)).should.throw(RangeError, 'Year');
141+
(() => new LocalDate(1.1, 1, 1)).should.throw(RangeError, 'All arguments must be integers');
142+
(() => new LocalDate('1', 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
143+
(() => new LocalDate({ 1: 1 }, 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
144+
(() => new LocalDate([], 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
145+
(() => new LocalDate(1e12, 1, 1)).should.throw(RangeError, 'Year');
146+
});
125147

126-
it('should throw RangeError if month is not an integer between 0-59(inclusive)', function () {
148+
it('should throw RangeError if month is not an integer between 1-12(inclusive)', function () {
127149
(() => new LocalDate(1, -1, 1)).should.throw(RangeError, 'Month');
150+
(() => new LocalDate(1, 0, 1)).should.throw(RangeError, 'Month');
128151
(() => new LocalDate(1, 1.1, 1)).should.throw(RangeError, 'All arguments must be integers');
129152
(() => new LocalDate(1, 233, 1)).should.throw(RangeError, 'Month');
130153
(() => new LocalDate(1, '1', 1)).should.throw(TypeError, 'All arguments must be numbers');
@@ -147,6 +170,8 @@ describe('DateTimeClassesTest', function () {
147170
});
148171

149172
it('should convert to string correctly', function () {
173+
new LocalDate(999999999, 12, 31).toString().should.be.eq('999999999-12-31');
174+
new LocalDate(0, 1, 1).toString().should.be.eq('0000-01-01');
150175
new LocalDate(2000, 2, 29).toString().should.be.eq('2000-02-29');
151176
new LocalDate(2001, 2, 1).toString().should.be.eq('2001-02-01');
152177
new LocalDate(35, 2, 28).toString().should.be.eq('0035-02-28');
@@ -181,6 +206,16 @@ describe('DateTimeClassesTest', function () {
181206
localtime5.year.should.be.eq(29999);
182207
localtime5.month.should.be.eq(3);
183208
localtime5.date.should.be.eq(29);
209+
210+
const localtime6 = LocalDate.fromString('999999999-12-31');
211+
localtime6.year.should.be.eq(999999999);
212+
localtime6.month.should.be.eq(12);
213+
localtime6.date.should.be.eq(31);
214+
215+
const localtime7 = LocalDate.fromString('0000-01-01');
216+
localtime7.year.should.be.eq(0);
217+
localtime7.month.should.be.eq(1);
218+
localtime7.date.should.be.eq(1);
184219
});
185220

186221
it('should throw RangeError on invalid string', function () {
@@ -205,6 +240,32 @@ describe('DateTimeClassesTest', function () {
205240
(() => LocalDate.fromString(null)).should.throw(TypeError, 'String expected');
206241
(() => LocalDate.fromString()).should.throw(TypeError, 'String expected');
207242
});
243+
244+
it('should construct from fromDate correctly', function () {
245+
const date1 = LocalDate.fromDate(new Date(2000, 2, 29, 2, 3, 4, 6));
246+
date1.toString().should.be.eq('2000-03-29');
247+
const date2 = LocalDate.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6));
248+
date2.toString().should.be.eq('2000-01-29');
249+
const date3 = LocalDate.fromDate(new Date(-2000, 2, 29, 2, 3, 4, 6));
250+
date3.toString().should.be.eq('-2000-03-29');
251+
const date4 = LocalDate.fromDate(new Date(-2000, 0, 29, 2, 3, 4, 6));
252+
date4.toString().should.be.eq('-2000-01-29');
253+
});
254+
255+
it('should throw when constructed from fromDate with a non-date thing', function () {
256+
const nonDateThings = [1, null, '', {}, [], function() {}, class A {}, LocalDateTime.fromDate(new Date())];
257+
nonDateThings.forEach(nonDateThing => {
258+
(() => LocalDate.fromDate(nonDateThing)).should.throw(TypeError, 'A Date is not passed');
259+
});
260+
});
261+
262+
it('should throw when constructed from fromDate with an invalid date', function () {
263+
const invalidDates = [new Date('aa'), new Date({}), new Date(undefined)];
264+
invalidDates.forEach(invalidDate => {
265+
isNaN(invalidDate.getTime).should.be.true;
266+
(() => LocalDate.fromDate(invalidDate)).should.throw(RangeError, 'Invalid Date is passed');
267+
});
268+
});
208269
});
209270
describe('LocalDateTimeTest', function () {
210271
it('should throw RangeError if local time is not valid', function () {
@@ -272,21 +333,30 @@ describe('DateTimeClassesTest', function () {
272333
});
273334

274335
it('fromDate should throw RangeError if date is invalid', function () {
275-
(() => LocalDateTime.fromDate(new Date(-1))).should.throw(RangeError, 'Invalid Date');
276336
(() => LocalDateTime.fromDate(new Date('s'))).should.throw(RangeError, 'Invalid Date');
277337
(() => LocalDateTime.fromDate(1, 1)).should.throw(TypeError, 'A Date is not passed');
278338
(() => LocalDateTime.fromDate('s', 1)).should.throw(TypeError, 'A Date is not passed');
279339
(() => LocalDateTime.fromDate([], 1)).should.throw(TypeError, 'A Date is not passed');
280340
});
281341

282342
it('should construct from fromDate correctly', function () {
283-
const dateTime = LocalDateTime.fromDate(new Date(Date.UTC(2000, 2, 29, 2, 3, 4, 6)));
284-
dateTime.toString().should.be.eq('2000-02-29T02:03:04.006000000');
343+
const dateTime1 = LocalDateTime.fromDate(new Date(2000, 2, 29, 2, 3, 4, 6));
344+
dateTime1.toString().should.be.eq('2000-03-29T02:03:04.006000000');
345+
const dateTime2 = LocalDateTime.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6));
346+
dateTime2.toString().should.be.eq('2000-01-29T02:03:04.006000000');
285347
});
286348

287349
it('should convert to date correctly', function () {
288350
const dateTime = new LocalDateTime(new LocalDate(2000, 2, 29), new LocalTime(2, 19, 4, 6000000));
289-
dateTime.asDate().toISOString().should.be.eq('2000-02-29T02:19:04.006Z');
351+
const asDate = dateTime.asDate();
352+
const date = leftZeroPadInteger(asDate.getDate(), 2);
353+
const month = leftZeroPadInteger(asDate.getMonth() + 1, 2); // Date's month is 0-based
354+
const year = leftZeroPadInteger(asDate.getFullYear(), 4);
355+
const hours = leftZeroPadInteger(asDate.getHours(), 2);
356+
const minutes = leftZeroPadInteger(asDate.getMinutes(), 2);
357+
const seconds = leftZeroPadInteger(asDate.getSeconds(), 2);
358+
359+
`${date}.${month}.${year} ${hours}:${minutes}:${seconds}`.should.be.eq('29.02.2000 02:19:04');
290360
});
291361
});
292362
describe('OffsetDateTimeTest', function () {
@@ -304,7 +374,6 @@ describe('DateTimeClassesTest', function () {
304374
});
305375

306376
it('fromDate should throw RangeError if date is invalid', function () {
307-
(() => OffsetDateTime.fromDate(new Date(-1), 1)).should.throw(RangeError, 'Invalid Date');
308377
(() => OffsetDateTime.fromDate(new Date('s'), 1)).should.throw(RangeError, 'Invalid Date');
309378
(() => OffsetDateTime.fromDate(1, 1)).should.throw(TypeError, 'A Date is not passed');
310379
(() => OffsetDateTime.fromDate('s', 1)).should.throw(TypeError, 'A Date is not passed');
@@ -320,16 +389,29 @@ describe('DateTimeClassesTest', function () {
320389
});
321390

322391
it('should construct from fromDate correctly', function () {
323-
const dateTime3 = OffsetDateTime.fromDate(new Date(Date.UTC(2000, 2, 29, 2, 3, 4, 6)), 1800);
324-
dateTime3.toString().should.be.eq('2000-02-29T02:03:04.006000000+00:30');
392+
const offsetDateTime1 = OffsetDateTime.fromDate(new Date(2000, 2, 29, 2, 3, 4, 6), 1800);
393+
offsetDateTime1.toString().should.be.eq('2000-03-29T02:03:04.006000000+00:30');
394+
const offsetDateTime2 = OffsetDateTime.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6), 1800);
395+
offsetDateTime2.toString().should.be.eq('2000-01-29T02:03:04.006000000+00:30');
325396
});
326397

327398
const dateTime1 = new OffsetDateTime(
328399
new LocalDateTime(new LocalDate(2000, 2, 29), new LocalTime(2, 19, 4, 6000000)), 1000
329400
);
330401

331402
it('should convert to date correctly', function () {
332-
dateTime1.asDate().toISOString().should.be.eq('2000-02-29T02:02:24.006Z');
403+
const asDate = dateTime1.asDate();
404+
405+
const date = leftZeroPadInteger(asDate.getDate(), 2);
406+
const month = leftZeroPadInteger(asDate.getMonth() + 1, 2); // Date's month is 0-based
407+
const year = leftZeroPadInteger(asDate.getFullYear(), 4);
408+
const hours = leftZeroPadInteger(asDate.getHours(), 2);
409+
const minutes = leftZeroPadInteger(asDate.getMinutes(), 2);
410+
const seconds = leftZeroPadInteger(asDate.getSeconds(), 2);
411+
412+
`${date}.${month}.${year} ${hours}:${minutes}:${seconds}`.should.be.eq('29.02.2000 02:02:24');
413+
414+
asDate.getMilliseconds().should.be.equal(6);
333415
});
334416

335417
it('should convert to string correctly', function () {
@@ -390,7 +472,6 @@ describe('DateTimeClassesTest', function () {
390472

391473
offsetSeconds3.should.be.eq(0);
392474

393-
// Timezone info omitted, UTC should be assumed
394475
const offsetDateTime4 = OffsetDateTime.fromString('2021-04-15T07:33:04.914Z');
395476
const offsetSeconds4 = offsetDateTime4.offsetSeconds;
396477
const localDateTime4 = offsetDateTime4.localDateTime;

0 commit comments

Comments
 (0)