|
|
|
|
/*
|
|
|
|
|
* Copyright DataStax, Inc.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const utils = require('../../utils');
|
|
|
|
|
const Long = require('long');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Regex to parse dates in the following format YYYY-MM-DDThh:mm:ss.mssZ
|
|
|
|
|
* Looks cumbersome but it's straightforward:
|
|
|
|
|
* - "(\d{1,6})": year mandatory 1 to 6 digits
|
|
|
|
|
* - (?:-(\d{1,2}))?(?:-(\d{1,2}))? two non-capturing groups representing the month and day (1 to 2 digits captured).
|
|
|
|
|
* - (?:T(\d{1,2}?)?(?::(\d{1,2}))?(?::(\d{1,2}))?)?Z? A non-capturing group for the time portion
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
const dateRegex =
|
|
|
|
|
/^[-+]?(\d{1,6})(?:-(\d{1,2}))?(?:-(\d{1,2}))?(?:T(\d{1,2}?)?(?::(\d{1,2}))?(?::(\d{1,2})(?:\.(\d{1,3}))?)?)?Z?$/;
|
|
|
|
|
const multipleBoundariesRegex = /^\[(.+?) TO (.+)]$/;
|
|
|
|
|
const unbounded = Object.freeze(new DateRangeBound(null, -1));
|
|
|
|
|
|
|
|
|
|
const dateRangeType = {
|
|
|
|
|
// single value as in "2001-01-01"
|
|
|
|
|
singleValue: 0,
|
|
|
|
|
// closed range as in "[2001-01-01 TO 2001-01-31]"
|
|
|
|
|
closedRange: 1,
|
|
|
|
|
// open range high as in "[2001-01-01 TO *]"
|
|
|
|
|
openRangeHigh: 2,
|
|
|
|
|
// - 0x03 - open range low as in "[* TO 2001-01-01]"
|
|
|
|
|
openRangeLow: 3,
|
|
|
|
|
// - 0x04 - both ranges open as in "[* TO *]"
|
|
|
|
|
openBoth: 4,
|
|
|
|
|
// - 0x05 - single open range as in "[*]"
|
|
|
|
|
openSingle: 5
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Defines the possible values of date range precision.
|
|
|
|
|
* @type {Object}
|
|
|
|
|
* @property {Number} year
|
|
|
|
|
* @property {Number} month
|
|
|
|
|
* @property {Number} day
|
|
|
|
|
* @property {Number} hour
|
|
|
|
|
* @property {Number} minute
|
|
|
|
|
* @property {Number} second
|
|
|
|
|
* @property {Number} millisecond
|
|
|
|
|
* @memberof module:search
|
|
|
|
|
*/
|
|
|
|
|
const dateRangePrecision = {
|
|
|
|
|
year: 0,
|
|
|
|
|
month: 1,
|
|
|
|
|
day: 2,
|
|
|
|
|
hour: 3,
|
|
|
|
|
minute: 4,
|
|
|
|
|
second: 5,
|
|
|
|
|
millisecond: 6
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new instance of <code>DateRange</code> using a lower bound and an upper bound.
|
|
|
|
|
* <p>Consider using <code>DateRange.fromString()</code> to create instances more easily.</p>
|
|
|
|
|
* @classdesc
|
|
|
|
|
* Represents a range of dates, corresponding to the Apache Solr type
|
|
|
|
|
* <a href="https://cwiki.apache.org/confluence/display/solr/Working+with+Dates"><code>DateRangeField</code></a>.
|
|
|
|
|
* <p>
|
|
|
|
|
* A date range can have one or two bounds, namely lower bound and upper bound, to represent an interval of time.
|
|
|
|
|
* Date range bounds are both inclusive. For example:
|
|
|
|
|
* </p>
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li><code>2015 TO 2016-10</code> represents from the first day of 2015 to the last day of October 2016</li>
|
|
|
|
|
* <li><code>2015</code> represents during the course of the year 2015.</li>
|
|
|
|
|
* <li><code>2017 TO *</code> represents any date greater or equals to the first day of the year 2017.</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
* <p>
|
|
|
|
|
* Note that this JavaScript representation of <code>DateRangeField</code> does not support Dates outside of the range
|
|
|
|
|
* supported by ECMAScript Date: –100,000,000 days to 100,000,000 days measured relative to midnight at the
|
|
|
|
|
* beginning of 01 January, 1970 UTC. Being <code>-271821-04-20T00:00:00.000Z</code> the minimum lower boundary
|
|
|
|
|
* and <code>275760-09-13T00:00:00.000Z</code> the maximum higher boundary.
|
|
|
|
|
* <p>
|
|
|
|
|
* @param {DateRangeBound} lowerBound A value representing the range lower bound, composed by a
|
|
|
|
|
* <code>Date</code> and a precision. Use <code>DateRangeBound.unbounded</code> for an open lower bound.
|
|
|
|
|
* @param {DateRangeBound} [upperBound] A value representing the range upper bound, composed by a
|
|
|
|
|
* <code>Date</code> and a precision. Use <code>DateRangeBound.unbounded</code> for an open upper bound. When it's not
|
|
|
|
|
* defined, the <code>DateRange</code> instance is considered as a single value range.
|
|
|
|
|
* @constructor
|
|
|
|
|
* @memberOf module:datastax/search
|
|
|
|
|
*/
|
|
|
|
|
function DateRange(lowerBound, upperBound) {
|
|
|
|
|
if (!lowerBound) {
|
|
|
|
|
throw new TypeError('The lower boundaries must be defined');
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Gets the lower bound of this range (inclusive).
|
|
|
|
|
* @type {DateRangeBound}
|
|
|
|
|
*/
|
|
|
|
|
this.lowerBound = lowerBound;
|
|
|
|
|
/**
|
|
|
|
|
* Gets the upper bound of this range (inclusive).
|
|
|
|
|
* @type {DateRangeBound|null}
|
|
|
|
|
*/
|
|
|
|
|
this.upperBound = upperBound || null;
|
|
|
|
|
|
|
|
|
|
// Define the type
|
|
|
|
|
if (this.upperBound === null) {
|
|
|
|
|
if (this.lowerBound !== unbounded) {
|
|
|
|
|
this._type = dateRangeType.singleValue;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this._type = dateRangeType.openSingle;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (this.lowerBound !== unbounded) {
|
|
|
|
|
this._type = this.upperBound !== unbounded ? dateRangeType.closedRange : dateRangeType.openRangeHigh;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this._type = this.upperBound !== unbounded ? dateRangeType.openRangeLow : dateRangeType.openBoth;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the value of this DateRange instance and other are the same.
|
|
|
|
|
* @param {DateRange} other
|
|
|
|
|
* @returns {Boolean}
|
|
|
|
|
*/
|
|
|
|
|
DateRange.prototype.equals = function (other) {
|
|
|
|
|
if (!(other instanceof DateRange)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return (other.lowerBound.equals(this.lowerBound) &&
|
|
|
|
|
(other.upperBound ? other.upperBound.equals(this.upperBound) : !this.upperBound));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the string representation of the instance.
|
|
|
|
|
* @return {String}
|
|
|
|
|
*/
|
|
|
|
|
DateRange.prototype.toString = function () {
|
|
|
|
|
if (this.upperBound === null) {
|
|
|
|
|
return this.lowerBound.toString();
|
|
|
|
|
}
|
|
|
|
|
return '[' + this.lowerBound.toString() + ' TO ' + this.upperBound.toString() + ']';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DateRange.prototype.toBuffer = function () {
|
|
|
|
|
// Serializes the value containing:
|
|
|
|
|
// <type>[<time0><precision0><time1><precision1>]
|
|
|
|
|
if (this._type === dateRangeType.openBoth || this._type === dateRangeType.openSingle) {
|
|
|
|
|
return utils.allocBufferFromArray([ this._type ]);
|
|
|
|
|
}
|
|
|
|
|
let buffer;
|
|
|
|
|
let offset = 0;
|
|
|
|
|
if (this._type !== dateRangeType.closedRange) {
|
|
|
|
|
// byte + long + byte
|
|
|
|
|
const boundary = this._type !== dateRangeType.openRangeLow ? this.lowerBound : this.upperBound;
|
|
|
|
|
buffer = utils.allocBufferUnsafe(10);
|
|
|
|
|
buffer.writeUInt8(this._type, offset++);
|
|
|
|
|
offset = writeDate(boundary.date, buffer, offset);
|
|
|
|
|
buffer.writeUInt8(boundary.precision, offset);
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
// byte + long + byte + long + byte
|
|
|
|
|
buffer = utils.allocBufferUnsafe(19);
|
|
|
|
|
buffer.writeUInt8(this._type, offset++);
|
|
|
|
|
offset = writeDate(this.lowerBound.date, buffer, offset);
|
|
|
|
|
buffer.writeUInt8(this.lowerBound.precision, offset++);
|
|
|
|
|
offset = writeDate(this.upperBound.date, buffer, offset);
|
|
|
|
|
buffer.writeUInt8(this.upperBound.precision, offset);
|
|
|
|
|
return buffer;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the <code>DateRange</code> representation of a given string.
|
|
|
|
|
* <p>String representations of dates are always expressed in Coordinated Universal Time (UTC)</p>
|
|
|
|
|
* @param {String} dateRangeString
|
|
|
|
|
*/
|
|
|
|
|
DateRange.fromString = function (dateRangeString) {
|
|
|
|
|
const matches = multipleBoundariesRegex.exec(dateRangeString);
|
|
|
|
|
if (!matches) {
|
|
|
|
|
return new DateRange(DateRangeBound.toLowerBound(DateRangeBound.fromString(dateRangeString)));
|
|
|
|
|
}
|
|
|
|
|
return new DateRange(DateRangeBound.toLowerBound(DateRangeBound.fromString(matches[1])), DateRangeBound.toUpperBound(DateRangeBound.fromString(matches[2])));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deserializes the buffer into a <code>DateRange</code>
|
|
|
|
|
* @param {Buffer} buffer
|
|
|
|
|
* @return {DateRange}
|
|
|
|
|
*/
|
|
|
|
|
DateRange.fromBuffer = function (buffer) {
|
|
|
|
|
if (buffer.length === 0) {
|
|
|
|
|
throw new TypeError('DateRange serialized value must have at least 1 byte');
|
|
|
|
|
}
|
|
|
|
|
const type = buffer.readUInt8(0);
|
|
|
|
|
if (type === dateRangeType.openBoth) {
|
|
|
|
|
return new DateRange(unbounded, unbounded);
|
|
|
|
|
}
|
|
|
|
|
if (type === dateRangeType.openSingle) {
|
|
|
|
|
return new DateRange(unbounded);
|
|
|
|
|
}
|
|
|
|
|
let offset = 1;
|
|
|
|
|
let date1;
|
|
|
|
|
let lowerBound;
|
|
|
|
|
let upperBound = null;
|
|
|
|
|
if (type !== dateRangeType.closedRange) {
|
|
|
|
|
date1 = readDate(buffer, offset);
|
|
|
|
|
offset += 8;
|
|
|
|
|
lowerBound = new DateRangeBound(date1, buffer.readUInt8(offset));
|
|
|
|
|
if (type === dateRangeType.openRangeLow) {
|
|
|
|
|
// lower boundary is open, the first serialized boundary is the upperBound
|
|
|
|
|
upperBound = lowerBound;
|
|
|
|
|
lowerBound = unbounded;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
upperBound = type === dateRangeType.openRangeHigh ? unbounded : null;
|
|
|
|
|
}
|
|
|
|
|
return new DateRange(lowerBound, upperBound);
|
|
|
|
|
}
|
|
|
|
|
date1 = readDate(buffer, offset);
|
|
|
|
|
offset += 8;
|
|
|
|
|
lowerBound = new DateRangeBound(date1, buffer.readUInt8(offset++));
|
|
|
|
|
const date2 = readDate(buffer, offset);
|
|
|
|
|
offset += 8;
|
|
|
|
|
upperBound = new DateRangeBound(date2, buffer.readUInt8(offset));
|
|
|
|
|
return new DateRange(lowerBound, upperBound);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes a Date, long millis since epoch, to a buffer starting from offset.
|
|
|
|
|
* @param {Date} date
|
|
|
|
|
* @param {Buffer} buffer
|
|
|
|
|
* @param {Number} offset
|
|
|
|
|
* @return {Number} The new offset.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function writeDate(date, buffer, offset) {
|
|
|
|
|
const long = Long.fromNumber(date.getTime());
|
|
|
|
|
buffer.writeUInt32BE(long.getHighBitsUnsigned(), offset);
|
|
|
|
|
buffer.writeUInt32BE(long.getLowBitsUnsigned(), offset + 4);
|
|
|
|
|
return offset + 8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reads a Date, long millis since epoch, from a buffer starting from offset.
|
|
|
|
|
* @param {Buffer} buffer
|
|
|
|
|
* @param {Number} offset
|
|
|
|
|
* @return {Date}
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
function readDate(buffer, offset) {
|
|
|
|
|
const long = new Long(buffer.readInt32BE(offset+4), buffer.readInt32BE(offset));
|
|
|
|
|
return new Date(long.toNumber());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @classdesc
|
|
|
|
|
* Represents a date range boundary, composed by a <code>Date</code> and a precision.
|
|
|
|
|
* @param {Date} date The timestamp portion, representing a single moment in time. Consider using
|
|
|
|
|
* <code>Date.UTC()</code> method to build the <code>Date</code> instance.
|
|
|
|
|
* @param {Number} precision The precision portion. Valid values for <code>DateRangeBound</code> precision are
|
|
|
|
|
* defined in the [dateRangePrecision]{@link module:datastax/search~dateRangePrecision} member.
|
|
|
|
|
* @constructor
|
|
|
|
|
* @memberOf module:datastax/search
|
|
|
|
|
*/
|
|
|
|
|
function DateRangeBound(date, precision) {
|
|
|
|
|
/**
|
|
|
|
|
* The timestamp portion of the boundary.
|
|
|
|
|
* @type {Date}
|
|
|
|
|
*/
|
|
|
|
|
this.date = date;
|
|
|
|
|
/**
|
|
|
|
|
* The precision portion of the boundary. Valid values are defined in the
|
|
|
|
|
* [dateRangePrecision]{@link module:datastax/search~dateRangePrecision} member.
|
|
|
|
|
* @type {Number}
|
|
|
|
|
*/
|
|
|
|
|
this.precision = precision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the string representation of the instance.
|
|
|
|
|
* @return {String}
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.prototype.toString = function () {
|
|
|
|
|
if (this.precision === -1) {
|
|
|
|
|
return '*';
|
|
|
|
|
}
|
|
|
|
|
let precision = 0;
|
|
|
|
|
const isoString = this.date.toISOString();
|
|
|
|
|
let i;
|
|
|
|
|
let char;
|
|
|
|
|
// The years take at least the first 4 characters
|
|
|
|
|
for (i = 4; i < isoString.length && precision <= this.precision; i++) {
|
|
|
|
|
char = isoString.charAt(i);
|
|
|
|
|
if (precision === dateRangePrecision.day && char === 'T') {
|
|
|
|
|
precision = dateRangePrecision.hour;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (precision >= dateRangePrecision.hour && char === ':' || char === '.') {
|
|
|
|
|
precision++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (precision < dateRangePrecision.day && char === '-') {
|
|
|
|
|
precision++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let start = 0;
|
|
|
|
|
const firstChar = isoString.charAt(0);
|
|
|
|
|
let sign = '';
|
|
|
|
|
let toRemoveIndex = 4;
|
|
|
|
|
if (firstChar === '+' || firstChar === '-') {
|
|
|
|
|
sign = firstChar;
|
|
|
|
|
if (firstChar === '-') {
|
|
|
|
|
// since we are retaining the -, don't remove as many zeros.
|
|
|
|
|
toRemoveIndex = 3;
|
|
|
|
|
}
|
|
|
|
|
// Remove additional zeros
|
|
|
|
|
for (start = 1; start < toRemoveIndex; start++) {
|
|
|
|
|
if (isoString.charAt(start) !== '0') {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this.precision !== dateRangePrecision.millisecond) {
|
|
|
|
|
// i holds the position of the first char that marks the end of a precision (ie: '-', 'T', ...),
|
|
|
|
|
// we should not include it in the result, except its the 'Z' char for the complete representation
|
|
|
|
|
i--;
|
|
|
|
|
}
|
|
|
|
|
return sign + isoString.substring(start, i);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the value of this DateRange instance and other are the same.
|
|
|
|
|
* @param {DateRangeBound} other
|
|
|
|
|
* @return {boolean}
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.prototype.equals = function (other) {
|
|
|
|
|
if (!(other instanceof DateRangeBound)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (other.precision !== this.precision) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return datesEqual(other.date, this.date);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function datesEqual(d1, d2) {
|
|
|
|
|
const t1 = d1 ? d1.getTime() : null;
|
|
|
|
|
const t2 = d2 ? d2.getTime() : null;
|
|
|
|
|
return t1 === t2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateRangeBound.prototype.isUnbounded = function () {
|
|
|
|
|
return (this.precision === -1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parses a date string and returns a DateRangeBound.
|
|
|
|
|
* @param {String} boundaryString
|
|
|
|
|
* @return {DateRangeBound}
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.fromString = function(boundaryString) {
|
|
|
|
|
if (!boundaryString) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (boundaryString === '*') {
|
|
|
|
|
return unbounded;
|
|
|
|
|
}
|
|
|
|
|
const matches = dateRegex.exec(boundaryString);
|
|
|
|
|
if (!matches) {
|
|
|
|
|
throw TypeError('String provided is not a valid date ' + boundaryString);
|
|
|
|
|
}
|
|
|
|
|
if (matches[7] !== undefined && matches[5] === undefined) {
|
|
|
|
|
// Due to a limitation in the regex, its possible to match dates like 2015T03:02.001, without the seconds
|
|
|
|
|
// portion but with the milliseconds specified.
|
|
|
|
|
throw new TypeError('String representation of the date contains the milliseconds portion but not the seconds: ' +
|
|
|
|
|
boundaryString);
|
|
|
|
|
}
|
|
|
|
|
const builder = new BoundaryBuilder(boundaryString.charAt(0) === '-');
|
|
|
|
|
for (let i = 1; i < matches.length; i++) {
|
|
|
|
|
builder.set(i-1, matches[i], boundaryString);
|
|
|
|
|
}
|
|
|
|
|
return builder.build();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The unbounded {@link DateRangeBound} instance. Unbounded bounds are syntactically represented by a <code>*</code>
|
|
|
|
|
* (star) sign.
|
|
|
|
|
* @type {DateRangeBound}
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.unbounded = unbounded;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a {DateRangeBound} into a lower-bounded bound by rounding down its date
|
|
|
|
|
* based on its precision.
|
|
|
|
|
*
|
|
|
|
|
* @param {DateRangeBound} bound The bound to round down.
|
|
|
|
|
* @returns {DateRangeBound} with the date rounded down to the given precision.
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.toLowerBound = function (bound) {
|
|
|
|
|
if(bound === unbounded) {
|
|
|
|
|
return bound;
|
|
|
|
|
}
|
|
|
|
|
const rounded = new Date(bound.date.getTime());
|
|
|
|
|
// in this case we want to fallthrough
|
|
|
|
|
/* eslint-disable no-fallthrough */
|
|
|
|
|
switch (bound.precision) {
|
|
|
|
|
case dateRangePrecision.year:
|
|
|
|
|
rounded.setUTCMonth(0);
|
|
|
|
|
case dateRangePrecision.month:
|
|
|
|
|
rounded.setUTCDate(1);
|
|
|
|
|
case dateRangePrecision.day:
|
|
|
|
|
rounded.setUTCHours(0);
|
|
|
|
|
case dateRangePrecision.hour:
|
|
|
|
|
rounded.setUTCMinutes(0);
|
|
|
|
|
case dateRangePrecision.minute:
|
|
|
|
|
rounded.setUTCSeconds(0);
|
|
|
|
|
case dateRangePrecision.second:
|
|
|
|
|
rounded.setUTCMilliseconds(0);
|
|
|
|
|
}
|
|
|
|
|
/* eslint-enable no-fallthrough */
|
|
|
|
|
return new DateRangeBound(rounded, bound.precision);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a {DateRangeBound} into a upper-bounded bound by rounding up its date
|
|
|
|
|
* based on its precision.
|
|
|
|
|
*
|
|
|
|
|
* @param {DateRangeBound} bound The bound to round up.
|
|
|
|
|
* @returns {DateRangeBound} with the date rounded up to the given precision.
|
|
|
|
|
*/
|
|
|
|
|
DateRangeBound.toUpperBound = function (bound) {
|
|
|
|
|
if (bound === unbounded) {
|
|
|
|
|
return bound;
|
|
|
|
|
}
|
|
|
|
|
const rounded = new Date(bound.date.getTime());
|
|
|
|
|
// in this case we want to fallthrough
|
|
|
|
|
/* eslint-disable no-fallthrough */
|
|
|
|
|
switch (bound.precision) {
|
|
|
|
|
case dateRangePrecision.year:
|
|
|
|
|
rounded.setUTCMonth(11);
|
|
|
|
|
case dateRangePrecision.month:
|
|
|
|
|
// Advance to the beginning of next month and set day of month to 0
|
|
|
|
|
// which sets the date to the last day of the previous month.
|
|
|
|
|
// This gives us the effect of YYYY-MM-LastDayOfThatMonth
|
|
|
|
|
rounded.setUTCMonth(rounded.getUTCMonth() + 1, 0);
|
|
|
|
|
case dateRangePrecision.day:
|
|
|
|
|
rounded.setUTCHours(23);
|
|
|
|
|
case dateRangePrecision.hour:
|
|
|
|
|
rounded.setUTCMinutes(59);
|
|
|
|
|
case dateRangePrecision.minute:
|
|
|
|
|
rounded.setUTCSeconds(59);
|
|
|
|
|
case dateRangePrecision.second:
|
|
|
|
|
rounded.setUTCMilliseconds(999);
|
|
|
|
|
}
|
|
|
|
|
/* eslint-enable no-fallthrough */
|
|
|
|
|
return new DateRangeBound(rounded, bound.precision);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** @private */
|
|
|
|
|
function BoundaryBuilder(isNegative) {
|
|
|
|
|
this._sign = isNegative ? -1 : 1;
|
|
|
|
|
this._index = 0;
|
|
|
|
|
this._values = new Int32Array(7);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BoundaryBuilder.prototype.set = function (index, value, stringDate) {
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (index > 6) {
|
|
|
|
|
throw new TypeError('Index out of bounds: ' + index);
|
|
|
|
|
}
|
|
|
|
|
if (index > this._index) {
|
|
|
|
|
this._index = index;
|
|
|
|
|
}
|
|
|
|
|
const numValue = +value;
|
|
|
|
|
switch (index) {
|
|
|
|
|
case dateRangePrecision.month:
|
|
|
|
|
if (numValue < 1 || numValue > 12) {
|
|
|
|
|
throw new TypeError('Month portion is not valid for date: ' + stringDate);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case dateRangePrecision.day:
|
|
|
|
|
if (numValue < 1 || numValue > 31) {
|
|
|
|
|
throw new TypeError('Day portion is not valid for date: ' + stringDate);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case dateRangePrecision.hour:
|
|
|
|
|
if (numValue > 23) {
|
|
|
|
|
throw new TypeError('Hour portion is not valid for date: ' + stringDate);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case dateRangePrecision.minute:
|
|
|
|
|
case dateRangePrecision.second:
|
|
|
|
|
if (numValue > 59) {
|
|
|
|
|
throw new TypeError('Minute/second portion is not valid for date: ' + stringDate);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case dateRangePrecision.millisecond:
|
|
|
|
|
if (numValue > 999) {
|
|
|
|
|
throw new TypeError('Millisecond portion is not valid for date: ' + stringDate);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
this._values[index] = numValue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** @return {DateRangeBound} */
|
|
|
|
|
BoundaryBuilder.prototype.build = function () {
|
|
|
|
|
const date = new Date(0);
|
|
|
|
|
let month = this._values[1];
|
|
|
|
|
if (month) {
|
|
|
|
|
// ES Date months are represented from 0 to 11
|
|
|
|
|
month--;
|
|
|
|
|
}
|
|
|
|
|
date.setUTCFullYear(this._sign * this._values[0], month, this._values[2] || 1);
|
|
|
|
|
date.setUTCHours(this._values[3], this._values[4], this._values[5], this._values[6]);
|
|
|
|
|
return new DateRangeBound(date, this._index);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
exports.unbounded = unbounded;
|
|
|
|
|
exports.dateRangePrecision = dateRangePrecision;
|
|
|
|
|
exports.DateRange = DateRange;
|
|
|
|
|
exports.DateRangeBound = DateRangeBound;
|