You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

537 lines
18 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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;