Skip to content

Add property-based testing to temporal-types conversion #997

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

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
42 changes: 42 additions & 0 deletions packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"esdoc": "^1.1.0",
"esdoc-importpath-plugin": "^1.0.2",
"esdoc-standard-plugin": "^1.0.0",
"fast-check": "^3.1.3",
"jest": "^27.5.1",
"ts-jest": "^27.1.4",
"ts-node": "^10.3.0",
Expand Down
40 changes: 26 additions & 14 deletions packages/core/src/internal/temporal-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,7 @@ export function dateToIsoString (
month: NumberOrInteger | string,
day: NumberOrInteger | string
): string {
year = int(year)
const isNegative = year.isNegative()
if (isNegative) {
year = year.multiply(-1)
}
let yearString = formatNumber(year, 4)
if (isNegative) {
yearString = '-' + yearString
}

const yearString = formatNumber(year, 6, { usePositiveSign: true })
const monthString = formatNumber(month, 2)
const dayString = formatNumber(day, 2)
return `${yearString}-${monthString}-${dayString}`
Expand All @@ -313,6 +304,16 @@ export function isoStringToStandardDate (isoString: string): Date {
return new Date(isoString)
}

/**
* Convert the given utc timestamp to a JavaScript Date object
*
* @param {number} utc Timestamp in UTC
* @returns {Date} the date
*/
export function toStandardDate (utc: number): Date {
return new Date(utc)
}

/**
* Get the total number of nanoseconds from the milliseconds of the given standard JavaScript date and optional nanosecond part.
* @param {global.Date} standardDate the standard JavaScript date.
Expand Down Expand Up @@ -341,11 +342,14 @@ export function totalNanoseconds (
* @return {number} the time zone offset in seconds.
*/
export function timeZoneOffsetInSeconds (standardDate: Date): number {
const secondsPortion = standardDate.getSeconds() >= standardDate.getUTCSeconds()
? standardDate.getSeconds() - standardDate.getUTCSeconds()
: standardDate.getSeconds() - standardDate.getUTCSeconds() + 60
const offsetInMinutes = standardDate.getTimezoneOffset()
if (offsetInMinutes === 0) {
return 0
return 0 + secondsPortion
}
return -1 * offsetInMinutes * SECONDS_PER_MINUTE
return -1 * offsetInMinutes * SECONDS_PER_MINUTE + secondsPortion
}

/**
Expand Down Expand Up @@ -583,7 +587,10 @@ function formatNanosecond (value: NumberOrInteger | string): string {
*/
function formatNumber (
num: NumberOrInteger | string,
stringLength?: number
stringLength?: number,
params?: {
usePositiveSign?: boolean
}
): string {
num = int(num)
const isNegative = num.isNegative()
Expand All @@ -598,7 +605,12 @@ function formatNumber (
numString = '0' + numString
}
}
return isNegative ? '-' + numString : numString
if (isNegative) {
return '-' + numString
} else if (params?.usePositiveSign === true) {
return '+' + numString
}
return numString
}

function add (x: NumberOrInteger, y: number): NumberOrInteger {
Expand Down
37 changes: 27 additions & 10 deletions packages/core/src/temporal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,16 +667,7 @@ export class DateTime<T extends NumberOrInteger = Integer> {
* @throws {Error} If the time zone offset is not defined in the object.
*/
toStandardDate (): StandardDate {
if (this.timeZoneOffsetSeconds === undefined) {
throw new Error('Requires DateTime created with time zone offset')
}
return util.isoStringToStandardDate(
// the timezone name should be removed from the
// string, otherwise the javascript parse doesn't
// read the datetime correctly
this.toString().replace(
this.timeZoneId != null ? `[${this.timeZoneId}]` : '', '')
)
return util.toStandardDate(this._toUTC())
}

/**
Expand All @@ -703,6 +694,32 @@ export class DateTime<T extends NumberOrInteger = Integer> {

return localDateTimeStr + timeOffset + timeZoneStr
}

/**
* @private
* @returns {number}
*/
private _toUTC (): number {
if (this.timeZoneOffsetSeconds === undefined) {
throw new Error('Requires DateTime created with time zone offset')
}
const epochSecond = util.localDateTimeToEpochSecond(
this.year,
this.month,
this.day,
this.hour,
this.minute,
this.second,
this.nanosecond
)

const utcSecond = epochSecond.subtract(this.timeZoneOffsetSeconds ?? 0)

return int(utcSecond)
.multiply(1000)
.add(int(this.nanosecond).div(1_000_000))
.toNumber()
}
}

Object.defineProperty(
Expand Down
65 changes: 44 additions & 21 deletions packages/core/test/temporal-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { StandardDate } from '../src/graph-types'
import { LocalDateTime, Date, DateTime } from '../src/temporal-types'
import fc from 'fast-check'

describe('Date', () => {
describe('.toStandardDate()', () => {
Expand All @@ -33,15 +34,33 @@ describe('Date', () => {
})

it('should be the reverse operation of fromStandardDate but losing time information', () => {
const standardDate = new global.Date()

const date = Date.fromStandardDate(standardDate)
const receivedDate = date.toStandardDate()

// Setting 00:00:00:000 UTC
standardDate.setHours(0, -1 * standardDate.getTimezoneOffset(), 0, 0)

expect(receivedDate).toEqual(standardDate)
fc.assert(
fc.property(fc.date(), (standardDate) => {
// @ts-expect-error
if (isNaN(standardDate)) {
// Should not create from a non-valid date.
expect(() => Date.fromStandardDate(standardDate)).toThrow(TypeError)
return
}

const date = Date.fromStandardDate(standardDate)
const receivedDate = date.toStandardDate()

const hour = standardDate.setHours(0, -1 * receivedDate.getTimezoneOffset())

// In some situations, the setHours result in a NaN hour.
// In this case, the test should be discarded
if (isNaN(hour)) {
return
}

expect(receivedDate.getFullYear()).toEqual(standardDate.getFullYear())
expect(receivedDate.getMonth()).toEqual(standardDate.getMonth())
expect(receivedDate.getDate()).toEqual(standardDate.getDate())
expect(receivedDate.getHours()).toEqual(standardDate.getHours())
expect(receivedDate.getMinutes()).toEqual(standardDate.getMinutes())
})
)
})
})
})
Expand All @@ -63,12 +82,14 @@ describe('LocalDateTime', () => {
})

it('should be the reverse operation of fromStandardDate', () => {
const date = new global.Date()

const localDatetime = LocalDateTime.fromStandardDate(date)
const receivedDate = localDatetime.toStandardDate()

expect(receivedDate).toEqual(date)
fc.assert(
fc.property(fc.date(), (date) => {
const localDatetime = LocalDateTime.fromStandardDate(date)
const receivedDate = localDatetime.toStandardDate()

expect(receivedDate).toEqual(date)
})
)
})
})
})
Expand Down Expand Up @@ -135,12 +156,14 @@ describe('DateTime', () => {
})

it('should be the reverse operation of fromStandardDate', () => {
const date = new global.Date()

const datetime = DateTime.fromStandardDate(date)
const receivedDate = datetime.toStandardDate()

expect(receivedDate).toEqual(date)
fc.assert(
fc.property(fc.date(), (date) => {
const datetime = DateTime.fromStandardDate(date)
const receivedDate = datetime.toStandardDate()

expect(receivedDate).toEqual(date)
})
)
})
})
})
Expand Down
16 changes: 8 additions & 8 deletions packages/neo4j-driver/test/internal/temporal-util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,19 @@ describe('#unit temporal-util', () => {
})

it('should convert date to ISO string', () => {
expect(util.dateToIsoString(90, 2, 5)).toEqual('0090-02-05')
expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('0001-01-01')
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-0123-12-23')
expect(util.dateToIsoString(90, 2, 5)).toEqual('+000090-02-05')
expect(util.dateToIsoString(int(1), 1, int(1))).toEqual('+000001-01-01')
expect(util.dateToIsoString(-123, int(12), int(23))).toEqual('-000123-12-23')
expect(util.dateToIsoString(int(-999), int(9), int(10))).toEqual(
'-0999-09-10'
'-000999-09-10'
)
expect(util.dateToIsoString(1999, 12, 19)).toEqual('1999-12-19')
expect(util.dateToIsoString(1999, 12, 19)).toEqual('+001999-12-19')
expect(util.dateToIsoString(int(2023), int(8), int(16))).toEqual(
'2023-08-16'
'+002023-08-16'
)
expect(util.dateToIsoString(12345, 12, 31)).toEqual('12345-12-31')
expect(util.dateToIsoString(12345, 12, 31)).toEqual('+012345-12-31')
expect(util.dateToIsoString(int(19191919), int(11), int(30))).toEqual(
'19191919-11-30'
'+19191919-11-30'
)
expect(util.dateToIsoString(-909090, 9, 9)).toEqual('-909090-09-09')
expect(util.dateToIsoString(int(-888999777), int(7), int(26))).toEqual(
Expand Down
24 changes: 12 additions & 12 deletions packages/neo4j-driver/test/temporal-types.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,34 +588,34 @@ describe('#integration temporal-types', () => {
}, 60000)

it('should convert Date to ISO string', () => {
expect(date(2015, 10, 12).toString()).toEqual('2015-10-12')
expect(date(881, 1, 1).toString()).toEqual('0881-01-01')
expect(date(-999, 12, 24).toString()).toEqual('-0999-12-24')
expect(date(-9, 1, 1).toString()).toEqual('-0009-01-01')
expect(date(2015, 10, 12).toString()).toEqual('+002015-10-12')
expect(date(881, 1, 1).toString()).toEqual('+000881-01-01')
expect(date(-999, 12, 24).toString()).toEqual('-000999-12-24')
expect(date(-9, 1, 1).toString()).toEqual('-000009-01-01')
}, 60000)

it('should convert LocalDateTime to ISO string', () => {
expect(localDateTime(1992, 11, 8, 9, 42, 17, 22).toString()).toEqual(
'1992-11-08T09:42:17.000000022'
'+001992-11-08T09:42:17.000000022'
)
expect(localDateTime(-10, 7, 15, 8, 15, 33, 500).toString()).toEqual(
'-0010-07-15T08:15:33.000000500'
'-000010-07-15T08:15:33.000000500'
)
expect(localDateTime(0, 1, 1, 0, 0, 0, 1).toString()).toEqual(
'0000-01-01T00:00:00.000000001'
'+000000-01-01T00:00:00.000000001'
)
}, 60000)

it('should convert DateTime with time zone offset to ISO string', () => {
expect(
dateTimeWithZoneOffset(2025, 9, 17, 23, 22, 21, 999888, 37800).toString()
).toEqual('2025-09-17T23:22:21.000999888+10:30')
).toEqual('+002025-09-17T23:22:21.000999888+10:30')
expect(
dateTimeWithZoneOffset(1, 2, 3, 4, 5, 6, 7, -49376).toString()
).toEqual('0001-02-03T04:05:06.000000007-13:42:56')
).toEqual('+000001-02-03T04:05:06.000000007-13:42:56')
expect(
dateTimeWithZoneOffset(-3, 3, 9, 9, 33, 27, 999000, 15300).toString()
).toEqual('-0003-03-09T09:33:27.000999000+04:15')
).toEqual('-000003-03-09T09:33:27.000999000+04:15')
}, 60000)

it('should convert DateTime with time zone id to ISO-like string', () => {
Expand All @@ -630,7 +630,7 @@ describe('#integration temporal-types', () => {
15000000,
'Europe/Zaporozhye'
).toString()
).toEqual('1949-10-07T06:10:15.015000000[Europe/Zaporozhye]')
).toEqual('+001949-10-07T06:10:15.015000000[Europe/Zaporozhye]')
expect(
dateTimeWithZoneId(
-30455,
Expand All @@ -642,7 +642,7 @@ describe('#integration temporal-types', () => {
123,
'Asia/Yangon'
).toString()
).toEqual('-30455-05-05T12:24:10.000000123[Asia/Yangon]')
).toEqual('-030455-05-05T12:24:10.000000123[Asia/Yangon]')
}, 60000)

it('should expose local time components in time', () => {
Expand Down