Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API-1178 Fix LocalDateTime.fromDate (#1180) [5.0.x] #1193

Merged
merged 1 commit into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 51 additions & 20 deletions src/core/DateTimeClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ export class LocalTime {
return new LocalTime(hours, minutes, seconds, nano);
}

/**
* Constructs a new instance from Date.
* @param date must be a valid Date. `date.getTime()` should be not NaN
* @throws TypeError if the passed param is not a Date
* @throws RangeError if an invalid Date is passed
*/
static fromDate(date: Date): LocalTime {
if (!(date instanceof Date)) {
throw new TypeError('A Date is not passed');
}
if (isNaN(date.getTime())) {
throw new RangeError('Invalid Date is passed.');
}
return new LocalTime(
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds() * 1_000_000
);
}

/**
* Returns the string representation of this local time.
*
Expand Down Expand Up @@ -221,6 +242,26 @@ export class LocalDate {
return new LocalDate(yearNumber, monthNumber, dateNumber);
}

/**
* Constructs a new instance from Date.
* @param date must be a valid Date. `date.getTime()` should be not NaN
* @throws TypeError if the passed param is not a Date
* @throws RangeError if an invalid Date is passed
*/
static fromDate(date: Date): LocalDate {
if (!(date instanceof Date)) {
throw new TypeError('A Date is not passed');
}
if (isNaN(date.getTime())) {
throw new RangeError('Invalid Date is passed.');
}
return new LocalDate(
date.getFullYear(),
date.getMonth() + 1, // month start with 0 in Date
date.getDate()
);
}

/**
* Returns the string representation of this local date.
* @returns A string in the form yyyy:mm:dd. Values are zero padded from left
Expand Down Expand Up @@ -278,21 +319,19 @@ export class LocalDateTime {
*/
asDate(): Date {
return new Date(
Date.UTC(
this.localDate.year,
this.localDate.month - 1, // month start with 0 in Date
this.localDate.date,
this.localTime.hour,
this.localTime.minute,
this.localTime.second,
Math.floor(this.localTime.nano / 1_000_000)
)
this.localDate.year,
this.localDate.month - 1, // month start with 0 in Date
this.localDate.date,
this.localTime.hour,
this.localTime.minute,
this.localTime.second,
Math.floor(this.localTime.nano / 1_000_000)
);
}

/**
* Constructs a new instance from Date.
* @param date Must be a valid Date. So `date.getTime()` should be not NaN
* @param date must be a valid Date. `date.getTime()` should be not NaN
* @throws TypeError if the passed param is not a Date
* @throws RangeError if an invalid Date is passed
*/
Expand All @@ -303,15 +342,7 @@ export class LocalDateTime {
if (isNaN(date.getTime())) {
throw new RangeError('Invalid Date is passed.');
}
return LocalDateTime.from(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds() * 1_000_000
);
return new LocalDateTime(LocalDate.fromDate(date), LocalTime.fromDate(date));
}

/**
Expand Down Expand Up @@ -375,7 +406,7 @@ export class OffsetDateTime {

/**
* Constructs a new instance from Date and offset seconds.
* @param date Must be a valid Date. So `date.getTime()` should be not NaN
* @param date must be a valid Date. `date.getTime()` should be not NaN
* @param offsetSeconds Offset in seconds, must be between [-64800, 64800]
* @throws TypeError if a wrong type is passed as argument
* @throws RangeError if an invalid argument value is passed
Expand Down
121 changes: 101 additions & 20 deletions test/unit/core/DatetimeClasses.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
LocalDateTime,
OffsetDateTime,
} = require('../../../lib/core/DateTimeClasses');
const { leftZeroPadInteger } = require('../../../lib/util/DateTimeUtil');

describe('DateTimeClassesTest', function () {
describe('LocalTimeTest', function () {
Expand Down Expand Up @@ -110,21 +111,43 @@ describe('DateTimeClassesTest', function () {
(() => LocalTime.fromString(null)).should.throw(TypeError, 'String expected');
(() => LocalTime.fromString()).should.throw(TypeError, 'String expected');
});

it('should construct from fromDate correctly', function () {
const localTime1 = LocalTime.fromDate(new Date(2000, 2, 29, 0, 0, 0, 0));
localTime1.toString().should.be.eq('00:00:00');
const localTime2 = LocalTime.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6));
localTime2.toString().should.be.eq('02:03:04.006000000');
});

it('should throw when constructed from fromDate with a non-date thing', function () {
const nonDateThings = [1, null, '', {}, [], function() {}, class A {}, LocalDateTime.fromDate(new Date())];
nonDateThings.forEach(nonDateThing => {
(() => LocalTime.fromDate(nonDateThing)).should.throw(TypeError, 'A Date is not passed');
});
});

it('should throw when constructed from fromDate with an invalid date', function () {
const invalidDates = [new Date('aa'), new Date({}), new Date(undefined)];
invalidDates.forEach(invalidDate => {
isNaN(invalidDate.getTime).should.be.true;
(() => LocalTime.fromDate(invalidDate)).should.throw(RangeError, 'Invalid Date is passed');
});
});
});
describe('LocalDateTest', function () {
it('should throw RangeError if year is not an integer between -999_999_999-999_999_999(inclusive)',
function () {
(() => new LocalDate(1e9, 1, 1)).should.throw(RangeError, 'Year');
(() => new LocalDate(-1e9, 1, 1)).should.throw(RangeError, 'Year');
(() => new LocalDate(1.1, 1, 1)).should.throw(RangeError, 'All arguments must be integers');
(() => new LocalDate('1', 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate({ 1: 1 }, 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate([], 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate(1e12, 1, 1)).should.throw(RangeError, 'Year');
});
it('should throw RangeError if year is not an integer between -999_999_999-999_999_999(inclusive)', function () {
(() => new LocalDate(1e9, 1, 1)).should.throw(RangeError, 'Year');
(() => new LocalDate(-1e9, 1, 1)).should.throw(RangeError, 'Year');
(() => new LocalDate(1.1, 1, 1)).should.throw(RangeError, 'All arguments must be integers');
(() => new LocalDate('1', 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate({ 1: 1 }, 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate([], 1, 1)).should.throw(TypeError, 'All arguments must be numbers');
(() => new LocalDate(1e12, 1, 1)).should.throw(RangeError, 'Year');
});

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

it('should convert to string correctly', function () {
new LocalDate(999999999, 12, 31).toString().should.be.eq('999999999-12-31');
new LocalDate(0, 1, 1).toString().should.be.eq('0000-01-01');
new LocalDate(2000, 2, 29).toString().should.be.eq('2000-02-29');
new LocalDate(2001, 2, 1).toString().should.be.eq('2001-02-01');
new LocalDate(35, 2, 28).toString().should.be.eq('0035-02-28');
Expand Down Expand Up @@ -181,6 +206,16 @@ describe('DateTimeClassesTest', function () {
localtime5.year.should.be.eq(29999);
localtime5.month.should.be.eq(3);
localtime5.date.should.be.eq(29);

const localtime6 = LocalDate.fromString('999999999-12-31');
localtime6.year.should.be.eq(999999999);
localtime6.month.should.be.eq(12);
localtime6.date.should.be.eq(31);

const localtime7 = LocalDate.fromString('0000-01-01');
localtime7.year.should.be.eq(0);
localtime7.month.should.be.eq(1);
localtime7.date.should.be.eq(1);
});

it('should throw RangeError on invalid string', function () {
Expand All @@ -205,6 +240,32 @@ describe('DateTimeClassesTest', function () {
(() => LocalDate.fromString(null)).should.throw(TypeError, 'String expected');
(() => LocalDate.fromString()).should.throw(TypeError, 'String expected');
});

it('should construct from fromDate correctly', function () {
const date1 = LocalDate.fromDate(new Date(2000, 2, 29, 2, 3, 4, 6));
date1.toString().should.be.eq('2000-03-29');
const date2 = LocalDate.fromDate(new Date(2000, 0, 29, 2, 3, 4, 6));
date2.toString().should.be.eq('2000-01-29');
const date3 = LocalDate.fromDate(new Date(-2000, 2, 29, 2, 3, 4, 6));
date3.toString().should.be.eq('-2000-03-29');
const date4 = LocalDate.fromDate(new Date(-2000, 0, 29, 2, 3, 4, 6));
date4.toString().should.be.eq('-2000-01-29');
});

it('should throw when constructed from fromDate with a non-date thing', function () {
const nonDateThings = [1, null, '', {}, [], function() {}, class A {}, LocalDateTime.fromDate(new Date())];
nonDateThings.forEach(nonDateThing => {
(() => LocalDate.fromDate(nonDateThing)).should.throw(TypeError, 'A Date is not passed');
});
});

it('should throw when constructed from fromDate with an invalid date', function () {
const invalidDates = [new Date('aa'), new Date({}), new Date(undefined)];
invalidDates.forEach(invalidDate => {
isNaN(invalidDate.getTime).should.be.true;
(() => LocalDate.fromDate(invalidDate)).should.throw(RangeError, 'Invalid Date is passed');
});
});
});
describe('LocalDateTimeTest', function () {
it('should throw RangeError if local time is not valid', function () {
Expand Down Expand Up @@ -272,21 +333,30 @@ describe('DateTimeClassesTest', function () {
});

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

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

it('should convert to date correctly', function () {
const dateTime = new LocalDateTime(new LocalDate(2000, 2, 29), new LocalTime(2, 19, 4, 6000000));
dateTime.asDate().toISOString().should.be.eq('2000-02-29T02:19:04.006Z');
const asDate = dateTime.asDate();
const date = leftZeroPadInteger(asDate.getDate(), 2);
const month = leftZeroPadInteger(asDate.getMonth() + 1, 2); // Date's month is 0-based
const year = leftZeroPadInteger(asDate.getFullYear(), 4);
const hours = leftZeroPadInteger(asDate.getHours(), 2);
const minutes = leftZeroPadInteger(asDate.getMinutes(), 2);
const seconds = leftZeroPadInteger(asDate.getSeconds(), 2);

`${date}.${month}.${year} ${hours}:${minutes}:${seconds}`.should.be.eq('29.02.2000 02:19:04');
});
});
describe('OffsetDateTimeTest', function () {
Expand All @@ -304,7 +374,6 @@ describe('DateTimeClassesTest', function () {
});

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

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

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

it('should convert to date correctly', function () {
dateTime1.asDate().toISOString().should.be.eq('2000-02-29T02:02:24.006Z');
const asDate = dateTime1.asDate();

const date = leftZeroPadInteger(asDate.getDate(), 2);
const month = leftZeroPadInteger(asDate.getMonth() + 1, 2); // Date's month is 0-based
const year = leftZeroPadInteger(asDate.getFullYear(), 4);
const hours = leftZeroPadInteger(asDate.getHours(), 2);
const minutes = leftZeroPadInteger(asDate.getMinutes(), 2);
const seconds = leftZeroPadInteger(asDate.getSeconds(), 2);

`${date}.${month}.${year} ${hours}:${minutes}:${seconds}`.should.be.eq('29.02.2000 02:02:24');

asDate.getMilliseconds().should.be.equal(6);
});

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

offsetSeconds3.should.be.eq(0);

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