fontoxpath
Version:
A minimalistic XPath 3.1 engine in JavaScript
421 lines (382 loc) • 10.8 kB
JavaScript
import Timezone from './Timezone';
/**
* @param {string|undefined} match
* @return {number|null}
*/
function parseMatch (match) {
return match ? parseInt(match, 10) : null;
}
// Helper function to compare two normalized dateTimes
function compareNormalizedDateTime (dateTime1, dateTime2) {
const fields = [
[dateTime1._years, dateTime2._years],
[dateTime1._months, dateTime2._months],
[dateTime1._days, dateTime2._days],
[dateTime1._hours, dateTime2._hours],
[dateTime1._minutes, dateTime2._minutes],
[dateTime1._seconds, dateTime2._seconds],
[dateTime1._secondFraction, dateTime2._secondFraction]
];
for (let i = 0; i < 7; i++) {
if (fields[i][0] > fields[i][1]) {
return 1;
}
if (fields[i][0] < fields[i][1]) {
return -1;
}
}
return 0;
}
function convertYearToString (year) {
let string = year + '';
switch (string.length) {
case 1:
string = '000' + string;
break;
case 2:
string = '00' + string;
break;
case 3:
string = '0' + string;
break;
}
return string;
}
function convertToTwoCharString (value) {
const string = value + '';
return string.padStart(2, '0');
}
function convertSecondsToString (seconds) {
let string = seconds + '';
if (string.split('.')[0].length === 1) {
string = string.padStart(string.length + 1, '0');
}
return string;
}
class DateTime {
constructor (years, months, days, hours, minutes, seconds, secondFraction, timezone, isPositive, type = 'xs:dateTime') {
this._years = years;
this._months = months;
this._days = days;
this._hours = hours;
this._minutes = minutes;
this._seconds = seconds;
this._secondFraction = secondFraction;
this._timezone = timezone; // TODO: convert to dayTimeDuration
this._isPositive = isPositive;
this._type = type;
}
normalize (timezone = undefined) {
if (timezone === undefined && (this._timezone === null || this._timezone.isUTC())) {
// Noting to normalize
return this;
}
const timezoneToUse = timezone ? timezone : this._timezone;
const timezoneHours = timezoneToUse.getHours();
const timezoneMinutes = timezoneToUse.getMinutes();
let newDateTime;
if (timezoneToUse.isPositive()) {
newDateTime = new Date(this._years, this._months - 1, this._days, this._hours - timezoneHours, this._minutes - timezoneMinutes);
}
else {
newDateTime = new Date(this._years, this._months - 1, this._days, this._hours + timezoneHours, this._minutes + timezoneMinutes);
}
const years = newDateTime.getFullYear();
const months = newDateTime.getMonth() + 1;
const days = newDateTime.getDate();
const hours = newDateTime.getHours();
const minutes = newDateTime.getMinutes();
return new DateTime(years, months, days, hours, minutes, this._seconds, this._secondFraction, Timezone.fromString('Z'), this._isPositive);
}
// returns -1 if this < other, 0 if this === other, 1 if this > other, undefined if indeterminate
compare (other) {
const normalizedThis = this.normalize();
const normalizedOther = other.normalize();
// Both have a timezone (Z at this point) or both do not have any timezone
if (normalizedThis._timezone && normalizedOther._timezone ||
!normalizedThis._timezone && !normalizedOther._timezone) {
return compareNormalizedDateTime(normalizedThis, normalizedOther);
}
// If only this has a timezone
if (normalizedThis._timezone && !normalizedOther._timezone) {
const normalizedOtherMin = other.normalize(Timezone.fromString('+14:00'));
const normalizedOtherMax = other.normalize(Timezone.fromString('-14:00'));
if (compareNormalizedDateTime(normalizedThis, normalizedOtherMin) < 0) {
return -1;
}
if (compareNormalizedDateTime(normalizedThis, normalizedOtherMax) > 0) {
return 1;
}
return undefined;
}
// If only other has a timezone
const normalizedThisMin = this.normalize(Timezone.fromString('+14:00'));
const normalizedThisMax = this.normalize(Timezone.fromString('-14:00'));
if (compareNormalizedDateTime(normalizedThisMax, normalizedOther) < 0) {
return -1;
}
if (compareNormalizedDateTime(normalizedThisMin, normalizedOther) > 0) {
return 1;
}
return undefined;
}
toString () {
switch (this._type) {
case 'xs:dateTime':
return (this._isPositive ? '' : '-') +
convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) + 'T' +
convertToTwoCharString(this._hours) + ':' +
convertToTwoCharString(this._minutes) + ':' +
convertSecondsToString(this._seconds + this._secondFraction) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:date':
return (this._isPositive ? '' : '-') +
convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:time':
return convertToTwoCharString(this._hours) + ':' +
convertToTwoCharString(this._minutes) + ':' +
convertSecondsToString(this._seconds + this._secondFraction) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:gDay':
return '---' +
convertToTwoCharString(this._days) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:gMonth':
return '--' +
convertToTwoCharString(this._months) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:gMonthDay':
return '--' +
convertToTwoCharString(this._months) + '-' +
convertToTwoCharString(this._days) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:gYear':
return (this._isPositive ? '' : '-') +
convertYearToString(this._years) +
(this._timezone ? this._timezone.toString() : '');
case 'xs:gYearMonth':
return (this._isPositive ? '' : '-') +
convertYearToString(this._years) + '-' +
convertToTwoCharString(this._months) +
(this._timezone ? this._timezone.toString() : '');
}
throw new Error('Unexpected subType');
}
convertToType (type) {
switch (type) {
case 'xs:gDay':
this._years = 0;
this._months = 0;
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._isPositive = true;
this._type = 'xs:gDay';
break;
case 'xs:gMonth':
this._years = 0;
this._days = 0;
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._isPositive = true;
this._type = 'xs:gMonth';
break;
case 'xs:gYear':
this._months = 0;
this._days = 0;
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._isPositive = true;
this._type = 'xs:gYear';
break;
case 'xs:gMonthDay':
this._years = 0;
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._isPositive = true;
this._type = 'xs:gMonthDay';
break;
case 'xs:gYearMonth':
this._days = 0;
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._isPositive = true;
this._type = 'xs:gYearMonth';
break;
case 'xs:time':
this._years = 0;
this._months = 0;
this._days = 0;
this._isPositive = true;
this._type = 'xs:time';
break;
case 'xs:date':
this._hours = 0;
this._minutes = 0;
this._seconds = 0;
this._secondFraction = 0;
this._type = 'xs:date';
break;
case 'xs:dateTime':
default:
this._type = 'xs:dateTime';
}
return this;
}
}
// 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,2}(\d\d))?(T)?(?:(\d\d):(\d\d):(\d\d))?(\.\d+)?(Z|(?:[+-]\d\d:\d\d))?/;
const match = regex.exec(string);
const isPositive = !match[1];
const years = parseMatch(match[2]);
const months = parseMatch(match[3]);
const days = parseMatch(match[4]);
const t = match[5];
const hours = parseMatch(match[6]);
const minutes = parseMatch(match[7]);
const seconds = parseMatch(match[8]);
const secondFraction = match[9] ? parseFloat(match[9]) : 0;
const timezone = match[10] ? Timezone.fromString(match[10]) : 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,
isPositive,
'xs:dateTime');
}
if (hours !== null && minutes !== null && seconds !== null) {
// There is no T separator, but there is a time component -> time
return new DateTime(
0,
0,
0,
hours,
minutes,
seconds,
secondFraction,
timezone,
true,
'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,
isPositive,
'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,
0,
0,
0,
0,
0,
timezone,
isPositive,
'xs:gYearMonth');
}
if (!isPositive && months !== null && days !== null) {
// There is no complete date component, but there is a month and a day -> gMonthDay
return new DateTime(
0,
months,
days,
0,
0,
0,
0,
timezone,
true,
'xs:gMonthDay');
}
if (years !== null) {
// There is only a year -> gYear
return new DateTime(
years,
0,
0,
0,
0,
0,
0,
timezone,
isPositive,
'xs:gYear');
}
if (!isPositive && months !== null) {
// There is only a month -> gMonth
return new DateTime(
0,
months,
0,
0,
0,
0,
0,
timezone,
true,
'xs:gMonth');
}
// There is only one option left -> gDay
return new DateTime(
0,
0,
days,
0,
0,
0,
0,
timezone,
true,
'xs:gDay');
};
export default DateTime;