fontoxpath
Version:
A minimalistic XPath 3.1 engine in JavaScript
458 lines (409 loc) • 11.9 kB
JavaScript
import DayTimeDuration from './DayTimeDuration';
/**
* @param {string|undefined} match
* @return {number|null}
*/
function parseMatch (match) {
return match ? parseInt(match, 10) : null;
}
/**
* @param {number} year
* @return {string}
*/
function convertYearToString (year) {
let string = year + '';
const isNegative = string.startsWith('-');
if (isNegative) {
string = string.substring(1);
}
return (isNegative ? '-' : '') + string.padStart(4, '0');
}
/**
* @param {number} value
* @return {string}
*/
function convertToTwoCharString (value) {
const string = value + '';
return string.padStart(2, '0');
}
/**
* @param {number} seconds
* @return {string}
*/
function convertSecondsToString (seconds) {
let string = seconds + '';
if (string.split('.')[0].length === 1) {
string = string.padStart(string.length + 1, '0');
}
return string;
}
/**
* @param {DayTimeDuration} timezone
* @return {boolean}
*/
function isUTC (timezone) {
return timezone.getHours() === 0 && timezone.getMinutes() === 0;
}
/**
* @param {DayTimeDuration} timezone
* @return {string}
*/
function timezoneToString (timezone) {
if (isUTC(timezone)) {
return 'Z';
}
return (timezone.isPositive() ? '+' : '-') +
convertToTwoCharString(Math.abs(timezone.getHours())) + ':' +
convertToTwoCharString(Math.abs(timezone.getMinutes()));
}
class DateTime {
constructor (years, months, days, hours, minutes, seconds, secondFraction, timezone, type = 'xs:dateTime') {
this._years = years;
this._months = months;
this._days = days + (hours === 24 ? 1 : 0);
this._hours = hours === 24 ? 0 : hours;
this._minutes = minutes;
this._seconds = seconds;
this._secondFraction = secondFraction;
this._timezone = timezone;
this._type = type;
}
getYear () {
return this._years;
}
getMonth () {
return this._months;
}
getDay () {
return this._days;
}
getHours () {
return this._hours;
}
getMinutes () {
return this._minutes;
}
getSeconds () {
return this._seconds;
}
getFullSeconds () {
return this._seconds + this._secondFraction;
}
getSecondFraction () {
return this._secondFraction;
}
getTimezone () {
return this._timezone;
}
isPositive () {
return this._years >= 0;
}
toJavaScriptDate (implicitTimezone = undefined) {
const timezoneToUse = this._timezone || implicitTimezone || DayTimeDuration.fromTimezoneString('Z');
return new Date(
this._years,
this._months - 1,
this._days,
this._hours - timezoneToUse.getHours(),
this._minutes - timezoneToUse.getMinutes(),
this._seconds + this._secondFraction
);
}
toString () {
switch (this._type) {
case 'xs:dateTime':
return convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) + 'T' +
convertToTwoCharString(this._hours) + ':' +
convertToTwoCharString(this._minutes) + ':' +
convertSecondsToString(this._seconds + this._secondFraction) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:date':
return convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:time':
return convertToTwoCharString(this._hours) + ':' +
convertToTwoCharString(this._minutes) + ':' +
convertSecondsToString(this._seconds + this._secondFraction) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:gDay':
return '---' +
convertToTwoCharString(this._days) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:gMonth':
return '--' +
convertToTwoCharString(this._months) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:gMonthDay':
return '--' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:gYear':
return convertYearToString(this._years) +
(this._timezone ? timezoneToString(this._timezone) : '');
case 'xs:gYearMonth':
return convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) +
(this._timezone ? timezoneToString(this._timezone) : '');
}
throw new Error('Unexpected subType');
}
convertToType (type) {
// xs:date xxxx-xx-xxT00:00:00
// xs:time 1972-12-31Txx:xx:xx
// xs:gYearMonth xxxx-xx-01T00:00:00
// xs:gYear xxxx-01-01T00:00:00
// xs:gMonthDay 1972-xx-xxT00:00:00
// xs:gMonth 1972-xx-01T00:00:00
// xs:gDay 1972-12-xxT00:00:00
switch (type) {
case 'xs:gDay':
return new DateTime(1972, 12, this._days, 0, 0, 0, 0, this._timezone, 'xs:gDay');
case 'xs:gMonth':
return new DateTime(1972, this._months, 1, 0, 0, 0, 0, this._timezone, 'xs:gMonth');
case 'xs:gYear':
return new DateTime(this._years, 1, 1, 0, 0, 0, 0, this._timezone, 'xs:gYear');
case 'xs:gMonthDay':
return new DateTime(1972, this._months, this._days, 0, 0, 0, 0, this._timezone, 'xs:gMonthDay');
case 'xs:gYearMonth':
return new DateTime(this._years, this._months, 1, 0, 0, 0, 0, this._timezone, 'xs:gYearMonth');
case 'xs:time':
return new DateTime(1972, 12, 31, this._hours, this._minutes, this._seconds, this._secondFraction, this._timezone, 'xs:time');
case 'xs:date':
return new DateTime(this._years, this._months, this._days, 0, 0, 0, 0, this._timezone, 'xs:date');
case 'xs:dateTime':
default:
return new DateTime(this._years, this._months, this._days, this._hours, this._minutes, this._seconds, this._secondFraction, this._timezone, 'xs:dateTime');
}
}
}
// dateTime | (-)yyyy-mm-ddThh:mm:ss.ss(Z|[+-]hh:mm)
// time | hh:mm:ss.ss(Z|[+-]hh:mm)
// date | (-)yyyy-mm-dd (Z|[+-]hh:mm)
// gYearMonth | (-)yyyy-mm (Z|[+-]hh:mm)
// gYear | (-)yyyy (Z|[+-]hh:mm)
// gMonthDay | --mm-dd (Z|[+-]hh:mm)
// gDay | ---dd (Z|[+-]hh:mm)
// gMonth | --mm (Z|[+-]hh:mm)
/**
* @static
* @param {string} string
* @return {DateTime}
*/
DateTime.fromString = function (string) {
const regex = /^(?:(-?\d{4,}))?(?:--?(\d\d))?(?:-{1,3}(\d\d))?(T)?(?:(\d\d):(\d\d):(\d\d))?(\.\d+)?(Z|(?:[+-]\d\d:\d\d))?$/;
const match = regex.exec(string);
const years = match[1] ? parseInt(match[1], 10) : null;
const months = parseMatch(match[2]);
const days = parseMatch(match[3]);
const t = match[4];
const hours = parseMatch(match[5]);
const minutes = parseMatch(match[6]);
const seconds = parseMatch(match[7]);
const secondFraction = match[8] ? parseFloat(match[8]) : 0;
const timezone = match[9] ? DayTimeDuration.fromTimezoneString(match[9]) : null;
if (years && (years < -271821 || years > 273860)) {
// These are the JavaScript bounds for date (https://tc39.github.io/ecma262/#sec-time-values-and-time-range)
throw new Error('FODT0001: Datetime year is out of bounds');
}
if (t) {
// There is a T separating the date and time components -> dateTime
return new DateTime(
years,
months,
days,
hours,
minutes,
seconds,
secondFraction,
timezone,
'xs:dateTime');
}
if (hours !== null && minutes !== null && seconds !== null) {
// There is no T separator, but there is a time component -> time
return new DateTime(
1972,
12,
31,
hours,
minutes,
seconds,
secondFraction,
timezone,
'xs:time');
}
if (years !== null && months !== null && days !== null) {
// There is no T separator, but there is a complete date component -> date
return new DateTime(
years,
months,
days,
0,
0,
0,
0,
timezone,
'xs:date');
}
if (years !== null && months !== null) {
// There is no complete date component, but there is a year and a month -> gYearMonth
return new DateTime(
years,
months,
1,
0,
0,
0,
0,
timezone,
'xs:gYearMonth');
}
if (months !== null && days !== null) {
// There is no complete date component, but there is a month and a day -> gMonthDay
return new DateTime(
1972,
months,
days,
0,
0,
0,
0,
timezone,
'xs:gMonthDay');
}
if (years !== null) {
// There is only a year -> gYear
return new DateTime(
years,
1,
1,
0,
0,
0,
0,
timezone,
'xs:gYear');
}
if (months !== null) {
// There is only a month -> gMonth
return new DateTime(
1972,
months,
1,
0,
0,
0,
0,
timezone,
'xs:gMonth');
}
// There is only one option left -> gDay
return new DateTime(
1972,
12,
days,
0,
0,
0,
0,
timezone,
'xs:gDay');
};
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {number}
*/
export function compare (dateTime1, dateTime2, implicitTimezone = undefined) {
const jsTime1 = dateTime1.toJavaScriptDate(implicitTimezone).getTime();
const jsTime2 = dateTime2.toJavaScriptDate(implicitTimezone).getTime();
if (jsTime1 === jsTime2) {
// We should break the tie on the secondFraction property, which has no counterpart in JS dates
if (dateTime1._secondFraction === dateTime2._secondFraction) {
return 0;
}
if (dateTime1._secondFraction > dateTime2._secondFraction) {
return 1;
}
return -1;
}
if (jsTime1 > jsTime2) {
return 1;
}
return -1;
}
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {boolean}
*/
export function equal (dateTime1, dateTime2, implicitTimezone = undefined) {
return compare(dateTime1, dateTime2, implicitTimezone) === 0;
}
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {boolean}
*/
export function lessThan (dateTime1, dateTime2, implicitTimezone = undefined) {
return compare(dateTime1, dateTime2, implicitTimezone) < 0;
}
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {boolean}
*/
export function greaterThan (dateTime1, dateTime2, implicitTimezone = undefined) {
return compare(dateTime1, dateTime2, implicitTimezone) > 0;
}
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {DayTimeDuration}
*/
export function add (dateTime1, dateTime2, implicitTimezone = undefined) {
// Divided by 1000 because date subtraction results in milliseconds
const secondsOfDuration = (dateTime1.toJavaScriptDate(implicitTimezone) + dateTime2.toJavaScriptDate(implicitTimezone)) / 1000;
return new DayTimeDuration(
secondsOfDuration
);
}
/**
* @param {DateTime} dateTime1
* @param {DateTime} dateTime2
* @param {?DayTimeDuration} implicitTimezone
* @return {DayTimeDuration}
*/
export function subtract (dateTime1, dateTime2, implicitTimezone = undefined) {
// Divided by 1000 because date subtraction results in milliseconds
const secondsOfDuration = (dateTime1.toJavaScriptDate(implicitTimezone) - dateTime2.toJavaScriptDate(implicitTimezone)) / 1000;
return new DayTimeDuration(
secondsOfDuration
);
}
/**
* @param {!DateTime} dateTime
* @param {!./AbstractDuration} _duration
* @return {!DateTime}
*/
export function addDuration (dateTime, _duration) {
throw new Error(`Not implemented: adding durations to ${dateTime._type}`);
}
/**
* @param {!DateTime} dateTime
* @param {!./AbstractDuration} _duration
* @return {!DateTime}
*/
export function subtractDuration (dateTime, _duration) {
throw new Error(`Not implemented: subtracting durations from ${dateTime._type}`);
}
export default DateTime;