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 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
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
62 changes: 48 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 = formatYear(year)
const monthString = formatNumber(month, 2)
const dayString = formatNumber(day, 2)
return `${yearString}-${monthString}-${dayString}`
Expand All @@ -313,6 +304,25 @@ 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)
}

/**
* Shortcut for creating a new StandardDate
* @param date
* @returns {Date} the standard date
*/
export function newDate (date: string | number | Date): Date {
return new Date(date)
}

/**
* 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 +351,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 @@ -576,14 +589,30 @@ function formatNanosecond (value: NumberOrInteger | string): string {
return value.equals(0) ? '' : '.' + formatNumber(value, 9)
}

/**
*
* @param {Integer|number|string} year The year to be formatted
* @return {string} formatted year
*/
function formatYear (year: NumberOrInteger | string): string {
const yearInteger = int(year)
if (yearInteger.isNegative() || yearInteger.greaterThan(9999)) {
return formatNumber(yearInteger, 6, { usePositiveSign: true })
}
return formatNumber(yearInteger, 4)
}

/**
* @param {Integer|number|string} num the number to format.
* @param {number} [stringLength=undefined] the string length to left-pad to.
* @return {string} formatted and possibly left-padded number as 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 +627,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
63 changes: 42 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,12 @@

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

const MIN_UTC_IN_MS = -8_640_000_000_000_000
const MAX_UTC_IN_MS = 8_640_000_000_000_000
const ONE_DAY_IN_MS = 86_400_000

describe('Date', () => {
describe('.toStandardDate()', () => {
Expand All @@ -33,15 +39,26 @@ 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({
max: temporalUtil.newDate(MAX_UTC_IN_MS - ONE_DAY_IN_MS),
min: temporalUtil.newDate(MIN_UTC_IN_MS + ONE_DAY_IN_MS)
}),
standardDate => {
const date = Date.fromStandardDate(standardDate)
const receivedDate = date.toStandardDate()

const adjustedDateTime = temporalUtil.newDate(standardDate)
adjustedDateTime.setHours(0, offset(receivedDate))

expect(receivedDate.getFullYear()).toEqual(adjustedDateTime.getFullYear())
expect(receivedDate.getMonth()).toEqual(adjustedDateTime.getMonth())
expect(receivedDate.getDate()).toEqual(adjustedDateTime.getDate())
expect(receivedDate.getHours()).toEqual(adjustedDateTime.getHours())
expect(receivedDate.getMinutes()).toEqual(adjustedDateTime.getMinutes())
})
)
})
})
})
Expand All @@ -63,12 +80,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 +154,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
8 changes: 4 additions & 4 deletions packages/neo4j-driver/test/internal/temporal-util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,17 @@ 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(-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(int(2023), int(8), int(16))).toEqual(
'2023-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
10 changes: 5 additions & 5 deletions packages/neo4j-driver/test/temporal-types.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,16 +590,16 @@ describe('#integration temporal-types', () => {
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(-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'
)
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'
Expand All @@ -615,7 +615,7 @@ describe('#integration temporal-types', () => {
).toEqual('0001-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 @@ -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