Skip to content
This repository was archived by the owner on Feb 2, 2019. It is now read-only.

Commit 1dc3e9c

Browse files
Date/time range convenience (#3)
* Add testing dev deps Add dev deps on: mocha, chai and jsverify. Add dev deps on luxon and moment. * Implement the toDateTimeRange utility function. * Apply toDateTimeRange to Patron API endpoints where applicable. * Implement the toDateRange utility function. * Apply toDateRange to Patron API endpoints where applicable. * Set up mocha as the test script.
1 parent 501061d commit 1dc3e9c

File tree

6 files changed

+995
-1
lines changed

6 files changed

+995
-1
lines changed

lib/dt-utils.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
Copyright (C) 2017 The University of Sydney Library
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
'use strict'
19+
20+
21+
/**
22+
* A moment object from the moment.js library.
23+
* @typedef {object} Moment
24+
* @see {@link https://momentjs.com/docs/}
25+
*/
26+
27+
28+
/**
29+
* A DateTime object from the Luxon library.
30+
* @typedef {object} DateTime
31+
* @see {@link http://moment.github.io/luxon/docs/class/src/datetime.js~DateTime.html}
32+
*/
33+
34+
/**
35+
* An Interval object from the Luxon library.
36+
* @typedef {object} Interval
37+
* @see {@link http://moment.github.io/luxon/docs/class/src/interval.js~Interval.html}
38+
*/
39+
40+
41+
/**
42+
* @typedef {(String|Date|Moment|DateTime|undefined|null)} DateLike
43+
*/
44+
45+
46+
/**
47+
* Makes a Sierra API date range from strings, dates, moments.js moments, Luxon DateTimes, or a Luxon Intervals.
48+
*
49+
* Does not validate the first date/time is earlier in time than the second date/time.
50+
*
51+
* If you give a string, it is assumed you've given an already a properly formed date range, and the arg is simply
52+
* returned.
53+
*
54+
* If the arg is undefined or null, undefined is returned.
55+
*
56+
* If you give a Luxon Interval, then the two DateTimes inside the Interval are used as to from the date range. HOWEVER,
57+
* Luxon Intervals do NOT include there endpoint, whereas Sierra API date ranges do. So 1 day will be subtracted from
58+
* the Luxon Interval's exclusive end to make the Sierra API date/time range's inclusive end.
59+
*
60+
* If you give a native Date, a moment.js moments or a Luxon DateTime (not in array), it is converted into a
61+
* ISO 8601 date string in UTC by truncating any time part. It is not put inside Sierra's range syntax; thus specifying
62+
* just the date and not a range of dates.
63+
*
64+
* If you give an array, then the first element of the array is taken to be the start of the range and the second
65+
* argument is taken to be the end of the range.
66+
*
67+
* Strings must be simplified extended ISO format date strings. Native Dates, moment.js moments and Luxon DateTimes are
68+
* converted to ISO 8601 date string in UTC by truncating any time part.
69+
*
70+
* If the first element of the array is undefined or null, then the range will be left-open.
71+
* If the second element of the array is undefined or null, then the range will be right-open.
72+
* If the both elements of the array are undefined or null, then undefined is returned.
73+
*
74+
* @param {(DateLike|Interval|Array.<DateLike>)} range - The date/time range
75+
*
76+
* @see {@link https://techdocs.iii.com/sierraapi/Content/zReference/queryParameters.htm#range_syntax}.
77+
*/
78+
function toDateRange(range) {
79+
if (!range) {
80+
return
81+
}
82+
83+
if (typeof range === 'string') {
84+
return range
85+
}
86+
87+
if (typeof range !== 'object') {
88+
throw new Error(`Invalid date/time range: ${range}`)
89+
}
90+
91+
let from
92+
let to
93+
94+
if (Array.isArray(range)) {
95+
from = range[0]
96+
to = range[1]
97+
} else if (typeof range.start === 'object' && typeof range.end === 'object') {
98+
from = range.start
99+
to = range.end.minus({ day: 1 }) // Luxon Intervals are end-exclusive, but date/time ranges are end-inclusive.
100+
} else if (typeof range.toISO === 'function') {
101+
return range.toISO().slice(0, 10)
102+
} else {
103+
return range.toISOString().slice(0, 10)
104+
}
105+
106+
if (!from && !to) {
107+
return
108+
}
109+
110+
const fromIsoString = _toIsoString(from) || ''
111+
const toIsoString = _toIsoString(to) || ''
112+
return `[${fromIsoString.slice(0, 10)},${toIsoString.slice(0, 10)}]`
113+
}
114+
115+
116+
/**
117+
* Makes a Sierra API date/time range from strings, dates, moments.js moments, Luxon DateTimes, or a Luxon Intervals.
118+
*
119+
* Does not validate the first date/time is earlier in time than the second date/time.
120+
*
121+
* If you give a string, it is assumed you've given an already a properly formed date/time range, and the arg is simply
122+
* returned.
123+
*
124+
* If the arg is undefined or null, undefined is returned.
125+
*
126+
* If you give a Luxon Interval, then the two DateTimes inside the Interval are used as to from the date/time range.
127+
* HOWEVER, Luxon Intervals do NOT include there endpoint, whereas Sierra API date/time ranges do. The Sierra API only
128+
* express date/time down to seconds, and not milliseconds. So 1 second will be subtracted from the Luxon Interval's
129+
* exclusive end to make the Sierra API date/time range's inclusive end.
130+
*
131+
* If you give a native Date, a moment.js moments or a Luxon DateTime (not in array), it is converted into a
132+
* ISO 8601 date/time strings in UTC. It is not put inside Sierra's range syntax; thus specifying just the date/time and
133+
* not a range of date/times.
134+
*
135+
* If you give an array, then the first element of the array is taken to be the start of the range and the second
136+
* argument is taken to be the end of the range.
137+
*
138+
* Strings must be simplified extended ISO format date/time strings. They must be in UTC and have a Z zone designator.
139+
* Native Dates, moment.js moments and Luxon DateTimes are converted to ISO 8601 date/time strings in UTC for you.
140+
*
141+
* If the first element of the array is undefined or null, then the range will be left-open.
142+
* If the second element of the array is undefined or null, then the range will be right-open.
143+
* If the both elements of the array are undefined or null, then undefined is returned.
144+
*
145+
* @param {(DateLike|Interval|Array.<DateLike>)} range - The date/time range
146+
*
147+
* @see {@link https://techdocs.iii.com/sierraapi/Content/zReference/queryParameters.htm#range_syntax}.
148+
*/
149+
150+
function toDateTimeRange(range) {
151+
if (!range) {
152+
return
153+
}
154+
155+
if (typeof range === 'string') {
156+
return range
157+
}
158+
159+
if (typeof range !== 'object') {
160+
throw new Error(`Invalid date/time range: ${range}`)
161+
}
162+
163+
let from
164+
let to
165+
166+
if (Array.isArray(range)) {
167+
from = range[0]
168+
to = range[1]
169+
} else if (typeof range.start === 'object' && typeof range.end === 'object') {
170+
from = range.start
171+
to = range.end.minus({ second: 1 }) // Luxon Intervals are end-exclusive, but date/time ranges are end-inclusive.
172+
} else if (typeof range.toISO === 'function') {
173+
return range.toISO()
174+
} else {
175+
return range.toISOString()
176+
}
177+
178+
if (!from && !to) {
179+
return
180+
}
181+
182+
const fromIsoString = _toIsoString(from) || ''
183+
const toIsoString = _toIsoString(to) || ''
184+
return `[${fromIsoString},${toIsoString}]`
185+
}
186+
187+
188+
function _toIsoString(thing) {
189+
if (thing === undefined || thing === null) {
190+
return thing
191+
}
192+
if (typeof thing === 'object') {
193+
if (typeof thing.toISO === 'function') {
194+
return thing.toISO()
195+
}
196+
if (typeof thing.toISOString === 'function') {
197+
return thing.toISOString()
198+
}
199+
throw new Error(`Don't know how to convert to an ISO date/time string: ${thing}`)
200+
}
201+
return String(thing)
202+
}
203+
204+
205+
module.exports = {
206+
toDateRange,
207+
toDateTimeRange
208+
}

lib/test-support.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (C) 2017 The University of Sydney Library
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
19+
'use strict'
20+
21+
22+
const jsv = require('jsverify')
23+
const { DateTime } = require('luxon')
24+
const moment = require('moment')
25+
26+
27+
const _DEFAULT_JSV_ASSERT_OPTIONS = Object.freeze({ tests: process.env['JSV_TESTS'] || 100 })
28+
29+
function chaiProperty(...jsverifyPropertyArgs) {
30+
const [ name, ...arbs ] = jsverifyPropertyArgs
31+
const propCheckFn = arbs.pop()
32+
const jsvAssertOptions = (
33+
typeof arbs[arbs.length - 1] === 'object'
34+
&& typeof(arbs[arbs.length - 1].generator) !== 'function'
35+
? Object.assign({}, _DEFAULT_JSV_ASSERT_OPTIONS, arbs.pop())
36+
: Object.assign({}, _DEFAULT_JSV_ASSERT_OPTIONS) // Grr. jsv.assert mutates jsvAssertOptions
37+
)
38+
it(name, function () {
39+
jsv.assert(
40+
jsv.forall(...arbs, (...values) => {
41+
propCheckFn(...values)
42+
return true
43+
}),
44+
jsvAssertOptions
45+
)
46+
})
47+
}
48+
49+
50+
51+
const MINIMUM_TEST_TIME = 631152000000 // '1990-01-01T00:00:00.000Z'
52+
const MAXIMUM_TEST_TIME = 2524607999999 // '2049-12-31T23:59:59.999Z'
53+
54+
const ARBITRARY_TIME = jsv.integer(MINIMUM_TEST_TIME, MAXIMUM_TEST_TIME)
55+
const ARBITRARY_PERIOD = jsv.integer(1, Math.floor((MAXIMUM_TEST_TIME - MINIMUM_TEST_TIME) / 2))
56+
57+
const ARBITRARY_DATE = (
58+
ARBITRARY_TIME.smap(
59+
time => new Date(time),
60+
date => date.getTime()
61+
)
62+
)
63+
64+
const ARBITRARY_DATETIME = (
65+
ARBITRARY_TIME.smap(
66+
time => DateTime.fromMillis(time),
67+
datetime => datetime.valueOf()
68+
)
69+
)
70+
71+
const ARBITRARY_MOMENT = (
72+
ARBITRARY_TIME.smap(
73+
time => moment(time),
74+
moment => moment.valueOf()
75+
)
76+
)
77+
78+
const ARBITRARY_ISO_DATE_TIME_STRING = (
79+
ARBITRARY_TIME.smap(
80+
time => new Date(time).toISOString(),
81+
date => new Date(date).getTime()
82+
)
83+
)
84+
85+
const ARBITRARY_ISO_DATE_STRING = (
86+
ARBITRARY_TIME.smap(
87+
time => new Date(time).toISOString().slice(0, 10),
88+
)
89+
)
90+
91+
92+
const ARBITRARY_UNDEFINED_OR_NULL = jsv.oneof([ jsv.constant(undefined), jsv.constant(null) ])
93+
94+
95+
module.exports = {
96+
97+
chaiProperty,
98+
99+
arbitrary: {
100+
101+
date: ARBITRARY_DATE,
102+
dateTime: ARBITRARY_DATETIME,
103+
moment: ARBITRARY_MOMENT,
104+
period: ARBITRARY_PERIOD,
105+
isoDateString: ARBITRARY_ISO_DATE_STRING,
106+
isoDateTimeString: ARBITRARY_ISO_DATE_TIME_STRING,
107+
time: ARBITRARY_TIME,
108+
109+
undefinedOrNull: ARBITRARY_UNDEFINED_OR_NULL,
110+
111+
}
112+
113+
}

0 commit comments

Comments
 (0)