328 lines
6.1 KiB
JavaScript
328 lines
6.1 KiB
JavaScript
'use strict'
|
|
|
|
const RANGE_EMPTY = 1 << 1
|
|
const RANGE_LB_INC = 1 << 2
|
|
const RANGE_UB_INC = 1 << 3
|
|
const RANGE_LB_INF = (1 << 4)
|
|
const RANGE_UB_INF = (1 << 5)
|
|
|
|
const EMPTY = 'empty'
|
|
const INFINITY = 'infinity'
|
|
|
|
class RangeError extends Error {}
|
|
|
|
class Range {
|
|
constructor (lower, upper, mask = 0) {
|
|
this.lower = lower
|
|
this.upper = upper
|
|
this.mask = mask
|
|
}
|
|
|
|
/**
|
|
* @param {number} flag
|
|
*/
|
|
hasMask (flag) {
|
|
return (this.mask & flag) === flag
|
|
}
|
|
|
|
isEmpty () {
|
|
return this.hasMask(RANGE_EMPTY)
|
|
}
|
|
|
|
isBounded () {
|
|
return !this.hasMask(RANGE_LB_INF) && !this.hasMask(RANGE_UB_INF)
|
|
}
|
|
|
|
isLowerBoundClosed () {
|
|
return this.hasLowerBound() && this.hasMask(RANGE_LB_INC)
|
|
}
|
|
|
|
isUpperBoundClosed () {
|
|
return this.hasUpperBound() && this.hasMask(RANGE_UB_INC)
|
|
}
|
|
|
|
hasLowerBound () {
|
|
return !this.hasMask(RANGE_LB_INF)
|
|
}
|
|
|
|
hasUpperBound () {
|
|
return !this.hasMask(RANGE_UB_INF)
|
|
}
|
|
|
|
containsPoint (point) {
|
|
const l = this.hasLowerBound()
|
|
const u = this.hasUpperBound()
|
|
|
|
if (l && u) {
|
|
const inLower = this.hasMask(RANGE_LB_INC)
|
|
? this.lower <= point
|
|
: this.lower < point
|
|
const inUpper = this.hasMask(RANGE_UB_INC)
|
|
? this.upper >= point
|
|
: this.upper > point
|
|
|
|
return inLower && inUpper
|
|
} else if (l) {
|
|
return this.hasMask(RANGE_LB_INC)
|
|
? this.lower <= point
|
|
: this.lower < point
|
|
} else if (u) {
|
|
return this.hasMask(RANGE_UB_INC)
|
|
? this.upper >= point
|
|
: this.upper > point
|
|
}
|
|
|
|
// INFINITY
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* @param {Range} range
|
|
*/
|
|
containsRange (range) {
|
|
return (
|
|
(!range.hasLowerBound() || this.containsPoint(range.lower)) &&
|
|
(!range.hasUpperBound() || this.containsPoint(range.upper))
|
|
)
|
|
}
|
|
|
|
toPostgres (prepareValue) {
|
|
return serialize(this, prepareValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} input
|
|
* @returns {Range}
|
|
*/
|
|
function parse (input, transform = x => x) {
|
|
input = input.trim()
|
|
|
|
if (input === EMPTY) {
|
|
return new Range(null, null, RANGE_EMPTY)
|
|
}
|
|
|
|
let ptr = 0
|
|
let mask = 0
|
|
|
|
if (input[ptr] === '[') {
|
|
mask |= RANGE_LB_INC
|
|
ptr += 1
|
|
} else if (input[ptr] === '(') {
|
|
ptr += 1
|
|
} else {
|
|
throw new RangeError(
|
|
`Unexpected character '${input[ptr]}'. Position: ${ptr}`
|
|
)
|
|
}
|
|
|
|
const lb = parseBound(input, ptr)
|
|
if (lb.infinite) {
|
|
mask |= RANGE_LB_INF
|
|
}
|
|
ptr = lb.ptr
|
|
|
|
if (input[ptr] === ',') {
|
|
ptr += 1
|
|
} else {
|
|
throw new RangeError(
|
|
`Expected comma as the delimiter, got '${input[ptr]}'. Position: ${ptr}`
|
|
)
|
|
}
|
|
|
|
const ub = parseBound(input, ptr)
|
|
if (ub.infinite) {
|
|
mask |= RANGE_UB_INF
|
|
}
|
|
ptr = ub.ptr
|
|
|
|
if (input[ptr] === ']') {
|
|
mask |= RANGE_UB_INC
|
|
ptr += 1
|
|
} else if (input[ptr] === ')') {
|
|
ptr += 1
|
|
} else {
|
|
throw new RangeError(
|
|
`Unexpected character '${input[ptr]}'. Position: ${ptr}`
|
|
)
|
|
}
|
|
|
|
let lower = null
|
|
let upper = null
|
|
|
|
if ((mask & RANGE_LB_INF) !== RANGE_LB_INF) {
|
|
lower = transform(lb.value)
|
|
}
|
|
|
|
if ((mask & RANGE_UB_INF) !== RANGE_UB_INF) {
|
|
upper = transform(ub.value)
|
|
}
|
|
|
|
return new Range(lower, upper, mask)
|
|
}
|
|
|
|
/**
|
|
* @param {string} input
|
|
* @param {number} ptr
|
|
* @returns {{ value: string | null; infinite: boolean; ptr: number }}
|
|
*/
|
|
function parseBound (input, ptr) {
|
|
if (input[ptr] === ',' || input[ptr] === ')' || input[ptr] === ']') {
|
|
return {
|
|
infinite: true,
|
|
value: null,
|
|
ptr
|
|
}
|
|
} else {
|
|
let inQuote = false
|
|
let value = ''
|
|
let pos = ptr
|
|
|
|
while (
|
|
inQuote ||
|
|
!(input[ptr] === ',' || input[ptr] === ')' || input[ptr] === ']')
|
|
) {
|
|
const ch = input[ptr++]
|
|
|
|
if (ch === undefined) {
|
|
throw new RangeError(`Unexpected end of input. Position: ${ptr}`)
|
|
}
|
|
if (ch === '\\') {
|
|
if (input[ptr] === undefined) {
|
|
throw new RangeError(`Unexpected end of input. Position: ${ptr}`)
|
|
}
|
|
|
|
value += input.slice(pos, ptr - 1) + input[ptr]
|
|
ptr += 1
|
|
pos = ptr
|
|
} else if (ch === '"') {
|
|
if (!inQuote) {
|
|
inQuote = true
|
|
pos += 1
|
|
} else if (input[ptr] === '"') {
|
|
value += input.slice(pos, ptr - 1) + input[ptr]
|
|
ptr += 1
|
|
pos = ptr
|
|
} else {
|
|
inQuote = false
|
|
value += input.slice(pos, ptr - 1)
|
|
pos = ptr + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ptr > pos) {
|
|
value += input.slice(pos, ptr)
|
|
}
|
|
|
|
if (value.endsWith(INFINITY)) {
|
|
return {
|
|
infinite: true,
|
|
value: null,
|
|
ptr
|
|
}
|
|
}
|
|
|
|
return {
|
|
infinite: false,
|
|
value,
|
|
ptr
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Range} range
|
|
*/
|
|
function serialize (range, format = x => x) {
|
|
if (range.hasMask(RANGE_EMPTY)) {
|
|
return EMPTY
|
|
}
|
|
|
|
let s = ''
|
|
|
|
s += range.isLowerBoundClosed() ? '[' : '('
|
|
s += range.hasLowerBound() ? serializeBound(format(range.lower)) : ''
|
|
s += ','
|
|
s += range.hasUpperBound() ? serializeBound(format(range.upper)) : ''
|
|
s += range.isUpperBoundClosed() ? ']' : ')'
|
|
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* @param {any} bnd
|
|
*/
|
|
function serializeBound (bnd) {
|
|
let needsQuotes = false
|
|
let pos = 0
|
|
let value = ''
|
|
|
|
if (typeof bnd !== 'string') {
|
|
if (typeof bnd === 'number' || typeof bnd === 'bigint') return bnd.toString()
|
|
|
|
bnd = String(bnd)
|
|
}
|
|
|
|
if (bnd === null || bnd.length === 0) {
|
|
return '""'
|
|
}
|
|
|
|
bnd = bnd.trim()
|
|
|
|
for (let i = 0; i < bnd.length; i++) {
|
|
const ch = bnd[i]
|
|
|
|
if (
|
|
ch === '"' ||
|
|
ch === '\\' ||
|
|
ch === '(' ||
|
|
ch === ')' ||
|
|
ch === '[' ||
|
|
ch === ']' ||
|
|
ch === ',' ||
|
|
ch === ' '
|
|
) {
|
|
needsQuotes = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (needsQuotes) {
|
|
value += '"'
|
|
}
|
|
|
|
let ptr = 0
|
|
for (; ptr < bnd.length; ptr++) {
|
|
const ch = bnd[ptr]
|
|
|
|
if (ch === '"' || ch === '\\') {
|
|
value += bnd.slice(pos, ptr + 1) + ch
|
|
pos = ptr + 1
|
|
}
|
|
}
|
|
|
|
if (ptr > pos) {
|
|
value += bnd.slice(pos, ptr)
|
|
}
|
|
|
|
if (needsQuotes) {
|
|
value += '"'
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
module.exports = {
|
|
Range,
|
|
RangeError,
|
|
RANGE_EMPTY,
|
|
RANGE_LB_INC,
|
|
RANGE_UB_INC,
|
|
RANGE_LB_INF,
|
|
RANGE_UB_INF,
|
|
|
|
parse,
|
|
serialize
|
|
}
|