typesxml
Version:
Open source XML library written in TypeScript
1,119 lines • 43.4 kB
JavaScript
"use strict";
/*******************************************************************************
* Copyright (c) 2023-2026 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/epl-v10.html
*
* Contributors:
* Maxprograms - initial API and implementation
*******************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaTypeValidator = void 0;
const XMLUtils_js_1 = require("../XMLUtils.js");
const XsdRegexTranslator_js_1 = require("./XsdRegexTranslator.js");
class SchemaTypeValidator {
static validateFacets(value, facets, typeName) {
if (facets.whiteSpace === 'replace') {
value = value.replaceAll(/[\t\n\r]/g, ' ');
}
else if (facets.whiteSpace === 'collapse') {
value = value.replaceAll(/[\t\n\r ]+/g, ' ').trim();
}
if (facets.enumeration && facets.enumeration.length > 0) {
if (facets.enumeration.indexOf(value) === -1) {
return false;
}
}
if (facets.patterns && facets.patterns.length > 0) {
for (let g = 0; g < facets.patterns.length; g++) {
const group = facets.patterns[g];
let groupMatched = false;
for (let i = 0; i < group.length; i++) {
if (XsdRegexTranslator_js_1.XsdRegexTranslator.toRegExp(group[i]).test(value)) {
groupMatched = true;
break;
}
}
if (!groupMatched) {
return false;
}
}
}
if (facets.minExclusive !== undefined || facets.maxExclusive !== undefined ||
facets.minInclusive !== undefined || facets.maxInclusive !== undefined) {
const localTypeForRange = typeName !== undefined
? (typeName.indexOf(':') !== -1 ? typeName.substring(typeName.indexOf(':') + 1) : typeName)
: '';
if (localTypeForRange === 'duration') {
if (facets.minExclusive !== undefined &&
SchemaTypeValidator.compareDurations(value, facets.minExclusive) !== null &&
SchemaTypeValidator.compareDurations(value, facets.minExclusive) <= 0) {
return false;
}
if (facets.maxExclusive !== undefined &&
SchemaTypeValidator.compareDurations(value, facets.maxExclusive) !== null &&
SchemaTypeValidator.compareDurations(value, facets.maxExclusive) >= 0) {
return false;
}
if (facets.minInclusive !== undefined &&
SchemaTypeValidator.compareDurations(value, facets.minInclusive) !== null &&
SchemaTypeValidator.compareDurations(value, facets.minInclusive) < 0) {
return false;
}
if (facets.maxInclusive !== undefined &&
SchemaTypeValidator.compareDurations(value, facets.maxInclusive) !== null &&
SchemaTypeValidator.compareDurations(value, facets.maxInclusive) > 0) {
return false;
}
}
else {
const compare = SchemaTypeValidator.getCompareFunction(typeName);
if (facets.minExclusive !== undefined && compare(value, facets.minExclusive) <= 0) {
return false;
}
if (facets.maxExclusive !== undefined && compare(value, facets.maxExclusive) >= 0) {
return false;
}
if (facets.minInclusive !== undefined && compare(value, facets.minInclusive) < 0) {
return false;
}
if (facets.maxInclusive !== undefined && compare(value, facets.maxInclusive) > 0) {
return false;
}
}
}
const localTypeName = typeName !== undefined
? (typeName.indexOf(':') !== -1 ? typeName.substring(typeName.indexOf(':') + 1) : typeName)
: '';
const noLengthFacets = localTypeName === 'QName' || localTypeName === 'NOTATION';
if (!noLengthFacets) {
let effectiveLength;
if (facets.isList) {
const trimmed = value.trim();
effectiveLength = trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
}
else if (localTypeName === 'base64Binary') {
const clean = value.replaceAll(/\s/g, '');
let padding = 0;
for (let i = clean.length - 1; i >= 0 && clean[i] === '='; i--) {
padding++;
}
effectiveLength = Math.floor(clean.length * 3 / 4) - padding;
}
else if (localTypeName === 'hexBinary') {
effectiveLength = Math.floor(value.length / 2);
}
else {
effectiveLength = Array.from(value).length;
}
if (facets.length !== undefined && effectiveLength !== facets.length) {
return false;
}
if (facets.minLength !== undefined && effectiveLength < facets.minLength) {
return false;
}
if (facets.maxLength !== undefined && effectiveLength > facets.maxLength) {
return false;
}
}
if (facets.totalDigits !== undefined || facets.fractionDigits !== undefined) {
const s = value.startsWith('+') || value.startsWith('-') ? value.substring(1) : value;
const dotIdx = s.indexOf('.');
const rawInt = dotIdx === -1 ? s : s.substring(0, dotIdx);
const rawFrac = dotIdx === -1 ? '' : s.substring(dotIdx + 1);
let canonIntStart = 0;
while (canonIntStart < rawInt.length - 1 && rawInt[canonIntStart] === '0') {
canonIntStart++;
}
const canonInt = rawInt.length === 0 ? '0' : rawInt.substring(canonIntStart);
let canonFracEnd = rawFrac.length;
while (canonFracEnd > 0 && rawFrac[canonFracEnd - 1] === '0') {
canonFracEnd--;
}
const canonFrac = rawFrac.substring(0, canonFracEnd);
const total = canonInt.length + canonFrac.length;
if (facets.totalDigits !== undefined && total > facets.totalDigits) {
return false;
}
if (facets.fractionDigits !== undefined && canonFrac.length > facets.fractionDigits) {
return false;
}
}
return true;
}
static getBuiltInWhiteSpace(typeName) {
const colonIndex = typeName.indexOf(':');
const local = colonIndex !== -1 ? typeName.substring(colonIndex + 1) : typeName;
if (local === 'string') {
return 'preserve';
}
if (local === 'normalizedString') {
return 'replace';
}
return 'collapse';
}
static validate(value, typeName, instanceNs) {
const colonIndex = typeName.indexOf(':');
const localType = colonIndex !== -1 ? typeName.substring(colonIndex + 1) : typeName;
switch (localType) {
case 'string':
return true;
case 'error':
return false;
case 'anyURI':
return SchemaTypeValidator.isAnyURI(value);
case 'anySimpleType':
case 'anyAtomicType':
return true;
case 'normalizedString':
return true;
case 'token':
return value === value.replaceAll(/[\t\n\r ]+/g, ' ').trim();
case 'hexBinary': {
const cleanHex = value.replaceAll(/\s/g, '');
return cleanHex.length % 2 === 0 && /^[0-9A-Fa-f]*$/.test(cleanHex);
}
case 'base64Binary': {
const cleanB64 = value.replaceAll(/\s/g, '');
if (cleanB64.length % 4 !== 0) {
return false;
}
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanB64)) {
return false;
}
const eqIdx = cleanB64.indexOf('=');
return eqIdx === -1 || eqIdx >= cleanB64.length - 2;
}
case 'boolean':
return SchemaTypeValidator.isBoolean(value);
// Decimal/float
case 'decimal':
return SchemaTypeValidator.isDecimal(value);
case 'float':
case 'double':
return SchemaTypeValidator.isFloat(value);
// Integer family
case 'integer':
case 'long':
case 'int':
case 'short':
case 'byte':
case 'unsignedLong':
case 'unsignedInt':
case 'unsignedShort':
case 'unsignedByte':
case 'nonNegativeInteger':
case 'positiveInteger':
case 'nonPositiveInteger':
case 'negativeInteger':
return SchemaTypeValidator.isInteger(value, localType);
// Date/time primitives
case 'dateTime':
return SchemaTypeValidator.isDateTime(value);
case 'dateTimeStamp':
return SchemaTypeValidator.isDateTimeStamp(value);
case 'date':
return SchemaTypeValidator.isDate(value);
case 'time':
return SchemaTypeValidator.isTime(value);
case 'duration':
return SchemaTypeValidator.isDuration(value);
case 'dayTimeDuration':
return SchemaTypeValidator.isDayTimeDuration(value);
case 'yearMonthDuration':
return SchemaTypeValidator.isYearMonthDuration(value);
case 'gYear':
return SchemaTypeValidator.isGYear(value);
case 'gYearMonth':
return SchemaTypeValidator.isGYearMonth(value);
case 'gMonth':
return SchemaTypeValidator.isGMonth(value);
case 'gMonthDay':
return SchemaTypeValidator.isGMonthDay(value);
case 'gDay':
return SchemaTypeValidator.isGDay(value);
// Name / token types
case 'Name':
return XMLUtils_js_1.XMLUtils.isValidXMLName(value);
case 'NCName':
case 'ID':
case 'IDREF':
case 'ENTITY':
return XMLUtils_js_1.XMLUtils.isValidNCName(value);
case 'IDREFS':
case 'ENTITIES':
return SchemaTypeValidator.isWhitespaceList(value, XMLUtils_js_1.XMLUtils.isValidNCName);
case 'NMTOKEN':
return XMLUtils_js_1.XMLUtils.isValidNMTOKEN(value);
case 'NMTOKENS':
return SchemaTypeValidator.isWhitespaceList(value, XMLUtils_js_1.XMLUtils.isValidNMTOKEN);
case 'language':
return SchemaTypeValidator.isLanguage(value);
case 'QName':
case 'NOTATION':
return SchemaTypeValidator.isQName(value, instanceNs);
default:
return true;
}
}
static isAnyURI(value) {
// XSD anyURI lexical space: any string that is a valid IRI reference per RFC 3987.
// Reject control characters (U+0000-U+001F, U+007F) which are never allowed in an IRI.
// Reject unbalanced brackets and fragment-invalid sequences.
if (/[\x00-\x1F\x7F]/.test(value)) {
return false;
}
// Check balanced square brackets (used only in IPv6 host).
const opens = (value.match(/\[/g) || []).length;
const closes = (value.match(/\]/g) || []).length;
if (opens !== closes) {
return false;
}
// Percent-encoded octets must be well-formed: %XX where X is hex.
const pct = /%(?![0-9A-Fa-f]{2})/;
if (pct.test(value)) {
return false;
}
return true;
}
static isBoolean(value) {
return value === 'true' || value === 'false' || value === '1' || value === '0';
}
static isDecimal(value) {
let s = value;
if (s.startsWith('+') || s.startsWith('-')) {
s = s.substring(1);
}
if (s.length === 0) {
return false;
}
const dot = s.indexOf('.');
if (dot === -1) {
return /^[0-9]+$/.test(s);
}
const intPart = s.substring(0, dot);
const fracPart = s.substring(dot + 1);
if (intPart.length === 0 && fracPart.length === 0) {
return false;
}
if (intPart.length > 0 && !/^[0-9]+$/.test(intPart)) {
return false;
}
if (fracPart.length > 0 && !/^[0-9]+$/.test(fracPart)) {
return false;
}
return intPart.length > 0 || fracPart.length > 0;
}
static isFloat(value) {
if (value === 'INF' || value === '+INF' || value === '-INF' || value === 'NaN') {
return true;
}
let s = value;
if (s.startsWith('+') || s.startsWith('-')) {
s = s.substring(1);
}
if (s.length === 0) {
return false;
}
const eIdx = s.search(/[eE]/);
let mantissa = s;
if (eIdx !== -1) {
const exp = s.substring(eIdx + 1);
mantissa = s.substring(0, eIdx);
if (exp.length === 0) {
return false;
}
const expDigits = (exp.startsWith('+') || exp.startsWith('-')) ? exp.substring(1) : exp;
if (expDigits.length === 0 || !/^[0-9]+$/.test(expDigits)) {
return false;
}
}
if (mantissa.length === 0) {
return false;
}
const dot = mantissa.indexOf('.');
if (dot === -1) {
return /^[0-9]+$/.test(mantissa);
}
const intPart = mantissa.substring(0, dot);
const fracPart = mantissa.substring(dot + 1);
if (intPart.length === 0 && fracPart.length === 0) {
return false;
}
if (intPart.length > 0 && !/^[0-9]+$/.test(intPart)) {
return false;
}
if (fracPart.length > 0 && !/^[0-9]+$/.test(fracPart)) {
return false;
}
return intPart.length > 0 || fracPart.length > 0;
}
static isInteger(value, typeName) {
if (!/^[+-]?[0-9]+$/.test(value)) {
return false;
}
switch (typeName) {
case 'nonNegativeInteger':
return !value.startsWith('-');
case 'unsignedLong': {
if (value.startsWith('-')) {
return false;
}
const n = BigInt(value.replace(/^\+/, ''));
return n <= BigInt('18446744073709551615');
}
case 'long': {
const n = BigInt(value.replace(/^\+/, ''));
return n >= BigInt('-9223372036854775808') && n <= BigInt('9223372036854775807');
}
case 'positiveInteger':
return !value.startsWith('-') && value.replace(/^\+/, '') !== '0';
case 'nonPositiveInteger': {
if (value.startsWith('-')) {
return true;
}
const stripped = value.replace(/^\+/, '');
return stripped === '0';
}
case 'negativeInteger':
return value.startsWith('-');
case 'byte': {
const n = Number.parseInt(value, 10);
return n >= -128 && n <= 127;
}
case 'short': {
const n = Number.parseInt(value, 10);
return n >= -32768 && n <= 32767;
}
case 'int': {
const n = Number.parseInt(value, 10);
return n >= -2147483648 && n <= 2147483647;
}
case 'unsignedByte': {
const n = Number.parseInt(value, 10);
return n >= 0 && n <= 255;
}
case 'unsignedShort': {
const n = Number.parseInt(value, 10);
return n >= 0 && n <= 65535;
}
case 'unsignedInt': {
const n = Number.parseInt(value, 10);
return n >= 0 && n <= 4294967295;
}
default:
return true;
}
}
static isValidTimezone(tz) {
if (tz === undefined || tz === 'Z') {
return true;
}
const tzM = tz.match(/^([+-])([0-9]{2}):([0-9]{2})$/);
if (!tzM) {
return false;
}
const offsetMinutes = Number.parseInt(tzM[2], 10) * 60 + Number.parseInt(tzM[3], 10);
return offsetMinutes <= 840;
}
static isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
static daysInMonth(year, month) {
const days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (month === 2 && SchemaTypeValidator.isLeapYear(year)) {
return 29;
}
return days[month];
}
static isDateTimeStamp(value) {
const m = value.match(/^(-?)([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$/);
if (!m) {
return false;
}
if (Number.parseInt(m[2], 10) === 0) {
return false;
}
const year = Number.parseInt(m[2], 10) * (m[1] === '-' ? -1 : 1);
const month = Number.parseInt(m[3], 10);
const day = Number.parseInt(m[4], 10);
const hour = Number.parseInt(m[5], 10);
const minute = Number.parseInt(m[6], 10);
const second = Number.parseFloat(m[7]);
if (month < 1 || month > 12) {
return false;
}
if (day < 1 || day > SchemaTypeValidator.daysInMonth(year, month)) {
return false;
}
if (hour === 24) {
if (minute !== 0 || second !== 0) {
return false;
}
}
else if (hour > 23) {
return false;
}
if (minute > 59 || second >= 60) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[9]);
}
static isDayTimeDuration(value) {
if (value === 'P' || value === '-P') {
return false;
}
const m = value.match(/^-?P([0-9]+D)?(T([0-9]+H)?([0-9]+M)?([0-9]+(\.[0-9]+)?S)?)?$/);
if (!m) {
return false;
}
if (m[2] !== undefined && !m[3] && !m[4] && !m[5]) {
return false;
}
if (!m[1] && !m[2]) {
return false;
}
return true;
}
static isYearMonthDuration(value) {
if (value === 'P' || value === '-P') {
return false;
}
const m = value.match(/^-?P([0-9]+Y)?([0-9]+M)?$/);
if (!m) {
return false;
}
if (!m[1] && !m[2]) {
return false;
}
return true;
}
static isDateTime(value) {
const m = value.match(/^(-?)([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
if (Number.parseInt(m[2], 10) === 0) {
return false;
}
const year = Number.parseInt(m[2], 10) * (m[1] === '-' ? -1 : 1);
const month = Number.parseInt(m[3], 10);
const day = Number.parseInt(m[4], 10);
const hour = Number.parseInt(m[5], 10);
const minute = Number.parseInt(m[6], 10);
const second = Number.parseFloat(m[7]);
if (month < 1 || month > 12) {
return false;
}
if (day < 1 || day > SchemaTypeValidator.daysInMonth(year, month)) {
return false;
}
if (hour === 24) {
if (minute !== 0 || second !== 0) {
return false;
}
}
else if (hour > 23) {
return false;
}
if (minute > 59 || second >= 60) {
return false;
}
if (!SchemaTypeValidator.isValidTimezone(m[9])) {
return false;
}
return true;
}
static isDate(value) {
const m = value.match(/^(-?)([0-9]{4,})-([0-9]{2})-([0-9]{2})(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
if (Number.parseInt(m[2], 10) === 0) {
return false;
}
const year = Number.parseInt(m[2], 10) * (m[1] === '-' ? -1 : 1);
const month = Number.parseInt(m[3], 10);
const day = Number.parseInt(m[4], 10);
if (month < 1 || month > 12) {
return false;
}
if (day < 1 || day > SchemaTypeValidator.daysInMonth(year, month)) {
return false;
}
if (!SchemaTypeValidator.isValidTimezone(m[5])) {
return false;
}
return true;
}
static isTime(value) {
const m = value.match(/^([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
const hour = Number.parseInt(m[1], 10);
const minute = Number.parseInt(m[2], 10);
const second = Number.parseFloat(m[3]);
if (hour === 24) {
if (minute !== 0 || second !== 0) {
return false;
}
}
else if (hour > 23) {
return false;
}
if (minute > 59 || second >= 60) {
return false;
}
if (!SchemaTypeValidator.isValidTimezone(m[5])) {
return false;
}
return true;
}
static isDuration(value) {
if (value === 'P' || value === '-P') {
return false;
}
const m = value.match(/^-?P([0-9]+Y)?([0-9]+M)?([0-9]+D)?(T([0-9]+H)?([0-9]+M)?([0-9]+(\.[0-9]+)?S)?)?$/);
if (!m) {
return false;
}
if (m[4] !== undefined && !m[5] && !m[6] && !m[7]) {
return false;
}
return true;
}
static isGYear(value) {
const m = value.match(/^(-?)([0-9]{4,})(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
if (Number.parseInt(m[2], 10) === 0) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[3]);
}
static isGYearMonth(value) {
const m = value.match(/^(-?)([0-9]{4,})-([0-9]{2})(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
if (Number.parseInt(m[2], 10) === 0) {
return false;
}
const month = Number.parseInt(m[3], 10);
if (month < 1 || month > 12) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[4]);
}
static isGMonth(value) {
const m = value.match(/^--([0-9]{2})(--)?(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
const month = Number.parseInt(m[1], 10);
if (month < 1 || month > 12) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[3]);
}
static isGMonthDay(value) {
const m = value.match(/^--([0-9]{2})-([0-9]{2})(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
const month = Number.parseInt(m[1], 10);
const day = Number.parseInt(m[2], 10);
if (month < 1 || month > 12) {
return false;
}
if (day < 1 || day > SchemaTypeValidator.daysInMonth(2000, month)) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[3]);
}
static isGDay(value) {
const m = value.match(/^---([0-9]{2})(Z|[+-][0-9]{2}:[0-9]{2})?$/);
if (!m) {
return false;
}
const day = Number.parseInt(m[1], 10);
if (day < 1 || day > 31) {
return false;
}
return SchemaTypeValidator.isValidTimezone(m[2]);
}
static isLanguage(value) {
return /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.test(value);
}
static isQName(value, instanceNs) {
const parts = value.split(':');
if (parts.length === 1) {
return XMLUtils_js_1.XMLUtils.isValidNCName(parts[0]);
}
if (parts.length === 2) {
if (!XMLUtils_js_1.XMLUtils.isValidNCName(parts[0]) || !XMLUtils_js_1.XMLUtils.isValidNCName(parts[1])) {
return false;
}
if (instanceNs !== undefined && !instanceNs.has(parts[0])) {
return false;
}
return true;
}
return false;
}
static isWhitespaceList(value, checker) {
const trimmed = value.trim();
if (trimmed.length === 0) {
return false;
}
const tokens = trimmed.split(/\s+/);
for (const token of tokens) {
if (!checker(token)) {
return false;
}
}
return true;
}
static getCompareFunction(typeName) {
const colonIndex = typeName ? typeName.indexOf(':') : -1;
const local = typeName ? (colonIndex !== -1 ? typeName.substring(colonIndex + 1) : typeName) : '';
switch (local) {
case 'dateTime': return SchemaTypeValidator.compareDateTimes;
case 'date': return SchemaTypeValidator.compareDates;
case 'time': return SchemaTypeValidator.compareTimes;
case 'gYear': return SchemaTypeValidator.compareGYears;
case 'gYearMonth': return SchemaTypeValidator.compareGYearMonths;
case 'gMonthDay': return SchemaTypeValidator.compareGMonthDays;
case 'gMonth': return SchemaTypeValidator.compareGMonths;
case 'gDay': return SchemaTypeValidator.compareGDays;
default: return SchemaTypeValidator.compareNumericOrLexicographic;
}
}
static dateTimeToMs(s) {
const negative = s.startsWith('-');
const abs = negative ? s.substring(1) : s;
const tIndex = abs.indexOf('T');
if (tIndex === -1) {
return Number.NaN;
}
const datePart = abs.substring(0, tIndex);
let rest = abs.substring(tIndex + 1);
let tzOffsetMs = 0;
if (rest.endsWith('Z')) {
rest = rest.substring(0, rest.length - 1);
}
else {
const tzMatch = rest.match(/([+-])([0-9]{2}):([0-9]{2})$/);
if (tzMatch) {
rest = rest.substring(0, rest.length - tzMatch[0].length);
const sign = tzMatch[1] === '+' ? 1 : -1;
tzOffsetMs = sign * (Number.parseInt(tzMatch[2], 10) * 60 + Number.parseInt(tzMatch[3], 10)) * 60000;
}
}
const dateParts = datePart.split('-');
const year = Number.parseInt(dateParts[0], 10) * (negative ? -1 : 1);
const month = Number.parseInt(dateParts[1], 10) - 1;
const day = Number.parseInt(dateParts[2], 10);
const c1 = rest.indexOf(':');
const c2 = rest.indexOf(':', c1 + 1);
const hours = Number.parseInt(rest.substring(0, c1), 10);
const minutes = Number.parseInt(rest.substring(c1 + 1, c2), 10);
const secFloat = Number.parseFloat(rest.substring(c2 + 1));
const secInt = Math.floor(secFloat);
const ms = Math.round((secFloat - secInt) * 1000);
const d = new Date(Date.UTC(year, month, day, hours, minutes, secInt, ms));
if (Number.isNaN(d.getTime())) {
return Number.NaN;
}
// Subtract tzOffsetMs: a value of +05:00 means local = UTC+5, so UTC = local - 5 h.
return d.getTime() - tzOffsetMs;
}
static dateToMs(s) {
const negative = s.startsWith('-');
let abs = negative ? s.substring(1) : s;
let tzOffsetMs = 0;
if (abs.endsWith('Z')) {
abs = abs.substring(0, abs.length - 1);
}
else {
const tzMatch = abs.match(/([+-])([0-9]{2}):([0-9]{2})$/);
if (tzMatch) {
abs = abs.substring(0, abs.length - tzMatch[0].length);
const sign = tzMatch[1] === '+' ? 1 : -1;
tzOffsetMs = sign * (Number.parseInt(tzMatch[2], 10) * 60 + Number.parseInt(tzMatch[3], 10)) * 60000;
}
}
const parts = abs.split('-');
const year = Number.parseInt(parts[0], 10) * (negative ? -1 : 1);
const month = Number.parseInt(parts[1], 10) - 1;
const day = Number.parseInt(parts[2], 10);
const d = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
if (Number.isNaN(d.getTime())) {
return Number.NaN;
}
return d.getTime() - tzOffsetMs;
}
static timeToMs(s) {
let rest = s;
let tzOffsetMs = 0;
if (rest.endsWith('Z')) {
rest = rest.substring(0, rest.length - 1);
}
else {
const tzMatch = rest.match(/([+-])([0-9]{2}):([0-9]{2})$/);
if (tzMatch) {
rest = rest.substring(0, rest.length - tzMatch[0].length);
const sign = tzMatch[1] === '+' ? 1 : -1;
tzOffsetMs = sign * (Number.parseInt(tzMatch[2], 10) * 60 + Number.parseInt(tzMatch[3], 10)) * 60000;
}
}
const c1 = rest.indexOf(':');
const c2 = rest.indexOf(':', c1 + 1);
const hours = Number.parseInt(rest.substring(0, c1), 10);
const minutes = Number.parseInt(rest.substring(c1 + 1, c2), 10);
const secFloat = Number.parseFloat(rest.substring(c2 + 1));
const secInt = Math.floor(secFloat);
const ms = Math.round((secFloat - secInt) * 1000);
return (hours * 3600 + minutes * 60 + secInt) * 1000 + ms - tzOffsetMs;
}
static compareDateTimes(a, b) {
const msA = SchemaTypeValidator.dateTimeToMs(a);
const msB = SchemaTypeValidator.dateTimeToMs(b);
if (Number.isNaN(msA) || Number.isNaN(msB)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return msA < msB ? -1 : msA > msB ? 1 : 0;
}
static compareDates(a, b) {
const msA = SchemaTypeValidator.dateToMs(a);
const msB = SchemaTypeValidator.dateToMs(b);
if (Number.isNaN(msA) || Number.isNaN(msB)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return msA < msB ? -1 : msA > msB ? 1 : 0;
}
static compareTimes(a, b) {
const msA = SchemaTypeValidator.timeToMs(a);
const msB = SchemaTypeValidator.timeToMs(b);
if (Number.isNaN(msA) || Number.isNaN(msB)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return msA < msB ? -1 : msA > msB ? 1 : 0;
}
static compareGYears(a, b) {
const parseYear = (s) => {
const m = s.match(/^(-?)([0-9]{4,})/);
if (!m) {
return Number.NaN;
}
return Number.parseInt(m[2], 10) * (m[1] === '-' ? -1 : 1);
};
const ya = parseYear(a);
const yb = parseYear(b);
if (Number.isNaN(ya) || Number.isNaN(yb)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return ya < yb ? -1 : ya > yb ? 1 : 0;
}
static compareGYearMonths(a, b) {
const parseYM = (s) => {
const m = s.match(/^(-?)([0-9]{4,})-([0-9]{2})/);
if (!m) {
return Number.NaN;
}
const year = Number.parseInt(m[2], 10) * (m[1] === '-' ? -1 : 1);
return year * 12 + Number.parseInt(m[3], 10);
};
const va = parseYM(a);
const vb = parseYM(b);
if (Number.isNaN(va) || Number.isNaN(vb)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return va < vb ? -1 : va > vb ? 1 : 0;
}
static compareGMonthDays(a, b) {
const parseMD = (s) => {
const m = s.match(/^--([0-9]{2})-([0-9]{2})/);
if (!m) {
return Number.NaN;
}
return Number.parseInt(m[1], 10) * 100 + Number.parseInt(m[2], 10);
};
const va = parseMD(a);
const vb = parseMD(b);
if (Number.isNaN(va) || Number.isNaN(vb)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return va < vb ? -1 : va > vb ? 1 : 0;
}
static compareGMonths(a, b) {
const parseM = (s) => {
const m = s.match(/^--([0-9]{2})/);
if (!m) {
return Number.NaN;
}
return Number.parseInt(m[1], 10);
};
const va = parseM(a);
const vb = parseM(b);
if (Number.isNaN(va) || Number.isNaN(vb)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return va < vb ? -1 : va > vb ? 1 : 0;
}
static compareGDays(a, b) {
const parseD = (s) => {
const m = s.match(/^---([0-9]{2})/);
if (!m) {
return Number.NaN;
}
return Number.parseInt(m[1], 10);
};
const va = parseD(a);
const vb = parseD(b);
if (Number.isNaN(va) || Number.isNaN(vb)) {
return a < b ? -1 : a > b ? 1 : 0;
}
return va < vb ? -1 : va > vb ? 1 : 0;
}
static parseDuration(s) {
const m = s.match(/^(-?)P(?:([0-9]+)Y)?(?:([0-9]+)M)?(?:([0-9]+)D)?(?:T(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+(?:\.[0-9]+)?)S)?)?$/);
if (!m) {
return null;
}
const negative = m[1] === '-';
const years = m[2] ? Number.parseInt(m[2], 10) : 0;
const months = m[3] ? Number.parseInt(m[3], 10) : 0;
const days = m[4] ? Number.parseInt(m[4], 10) : 0;
const hours = m[5] ? Number.parseInt(m[5], 10) : 0;
const minutes = m[6] ? Number.parseInt(m[6], 10) : 0;
const seconds = m[7] ? Number.parseFloat(m[7]) : 0;
const totalMonths = years * 12 + months;
const totalSeconds = days * 86400 + hours * 3600 + minutes * 60 + seconds;
return { negative, months: totalMonths, seconds: totalSeconds };
}
static durationToSeconds(d, refMonthSecs) {
const raw = d.months * refMonthSecs + d.seconds;
return d.negative ? -raw : raw;
}
static compareDurations(a, b) {
const dA = SchemaTypeValidator.parseDuration(a);
const dB = SchemaTypeValidator.parseDuration(b);
if (!dA || !dB) {
return null;
}
const refPoints = [
28 * 86400,
29 * 86400,
30 * 86400,
31 * 86400
];
let result = null;
for (const ref of refPoints) {
const sA = SchemaTypeValidator.durationToSeconds(dA, ref);
const sB = SchemaTypeValidator.durationToSeconds(dB, ref);
const cmp = sA < sB ? -1 : sA > sB ? 1 : 0;
if (result === null) {
result = cmp;
}
else if (result !== cmp) {
return null;
}
}
return result;
}
static compareNumericOrLexicographic(a, b) {
const decimalPattern = /^-?[0-9]+(\.[0-9]+)?$/;
if (decimalPattern.test(a) && decimalPattern.test(b)) {
try {
const parseDecimal = (s) => {
const negative = s.startsWith('-');
const abs = negative ? s.substring(1) : s;
const dotIndex = abs.indexOf('.');
const intPart = dotIndex === -1 ? abs : abs.substring(0, dotIndex);
const fracPart = dotIndex === -1 ? '' : abs.substring(dotIndex + 1);
return { negative, integer: BigInt(intPart), fraction: fracPart };
};
const padFraction = (frac, len) => BigInt(frac.padEnd(len, '0').substring(0, len));
const dA = parseDecimal(a);
const dB = parseDecimal(b);
const fracLen = Math.max(dA.fraction.length, dB.fraction.length);
const scaleA = dA.integer * BigInt(10 ** fracLen) + padFraction(dA.fraction, fracLen);
const scaleB = dB.integer * BigInt(10 ** fracLen) + padFraction(dB.fraction, fracLen);
const signedA = dA.negative ? -scaleA : scaleA;
const signedB = dB.negative ? -scaleB : scaleB;
return signedA < signedB ? -1 : signedA > signedB ? 1 : 0;
}
catch (e) {
// Fall through to float comparison.
}
}
const numA = Number.parseFloat(a);
const numB = Number.parseFloat(b);
if (!Number.isNaN(numA) && !Number.isNaN(numB)) {
return numA < numB ? -1 : numA > numB ? 1 : 0;
}
return a < b ? -1 : a > b ? 1 : 0;
}
static canonicalize(value, typeName, nsMap) {
const colonIndex = typeName.indexOf(':');
const localType = colonIndex !== -1 ? typeName.substring(colonIndex + 1) : typeName;
switch (localType) {
case 'decimal':
return SchemaTypeValidator.canonicalDecimal(value);
case 'integer':
case 'long':
case 'int':
case 'short':
case 'byte':
case 'nonNegativeInteger':
case 'positiveInteger':
case 'unsignedLong':
case 'unsignedInt':
case 'unsignedShort':
case 'unsignedByte':
case 'nonPositiveInteger':
case 'negativeInteger':
return SchemaTypeValidator.canonicalInteger(value);
case 'float':
case 'double':
return SchemaTypeValidator.canonicalFloat(value);
case 'boolean':
if (value === '1') {
return 'true';
}
if (value === '0') {
return 'false';
}
return value;
case 'normalizedString':
return value.replaceAll(/[\t\n\r]/g, ' ');
case 'token':
case 'language':
case 'Name':
case 'NCName':
case 'ID':
case 'IDREF':
case 'ENTITY':
case 'NMTOKEN':
case 'anyURI':
case 'IDREFS':
case 'ENTITIES':
case 'NMTOKENS':
return value.replaceAll(/[\t\n\r ]+/g, ' ').trim();
case 'QName': {
const normalized = value.replaceAll(/[\t\n\r ]+/g, ' ').trim();
if (nsMap !== undefined) {
const qColon = normalized.indexOf(':');
if (qColon !== -1) {
const prefix = normalized.substring(0, qColon);
const localPart = normalized.substring(qColon + 1);
const nsUri = nsMap.get(prefix);
if (nsUri !== undefined) {
return '{' + nsUri + '}' + localPart;
}
}
else {
const defaultNs = nsMap.get('');
if (defaultNs !== undefined) {
return '{' + defaultNs + '}' + normalized;
}
}
}
return normalized;
}
case 'hexBinary':
return value.replaceAll(/\s/g, '').toUpperCase();
case 'base64Binary':
return value.replaceAll(/\s/g, '');
case 'dateTime':
case 'date':
case 'time':
case 'gYear':
case 'gYearMonth':
case 'gMonth':
case 'gMonthDay':
case 'gDay':
case 'dateTimeStamp':
case 'duration':
case 'dayTimeDuration':
case 'yearMonthDuration':
return SchemaTypeValidator.canonicalizeTemporal(value);
default:
return value;
}
}
static canonicalDecimal(value) {
const s = value.trim();
const negative = s.startsWith('-');
const unsigned = (s.startsWith('+') || s.startsWith('-')) ? s.substring(1) : s;
const dotIndex = unsigned.indexOf('.');
let intPart = dotIndex === -1 ? unsigned : unsigned.substring(0, dotIndex);
let fracPart = dotIndex === -1 ? '0' : unsigned.substring(dotIndex + 1);
let intStart = 0;
while (intStart < intPart.length - 1 && intPart[intStart] === '0') {
intStart++;
}
intPart = intPart.length === 0 ? '0' : intPart.substring(intStart);
let fracEnd = fracPart.length;
while (fracEnd > 0 && fracPart[fracEnd - 1] === '0') {
fracEnd--;
}
fracPart = fracEnd === 0 ? '0' : fracPart.substring(0, fracEnd);
const isZero = intPart === '0' && /^0*$/.test(fracPart);
if (isZero) {
return '0.0';
}
return (negative ? '-' : '') + intPart + '.' + fracPart;
}
static canonicalInteger(value) {
const s = value.trim();
const negative = s.startsWith('-');
const unsigned = (s.startsWith('+') || s.startsWith('-')) ? s.substring(1) : s;
const stripped = unsigned.replace(/^0+/, '') || '0';
if (stripped === '0') {
return '0';
}
return (negative ? '-' : '') + stripped;
}
static canonicalFloat(value) {
const trimmed = value.trim();
if (trimmed === 'INF' || trimmed === '+INF') {
return 'INF';
}
if (trimmed === '-INF') {
return '-INF';
}
if (trimmed === 'NaN') {
return 'NaN';
}
return String(Number.parseFloat(trimmed));
}
static canonicalizeTemporal(value) {
// Normalize timezone: +00:00 → Z.
let v = value.trim().replace(/\+00:00$/, 'Z');
// Strip trailing zeros from fractional seconds, e.g. .100 → .1, .000 → remove.
const dotIdx = v.indexOf('.');
if (dotIdx !== -1) {
let fracEnd = dotIdx + 1;
while (fracEnd < v.length && v[fracEnd] >= '0' && v[fracEnd] <= '9') {
fracEnd++;
}
const suffix = v.substring(fracEnd);
let trimEnd = fracEnd;
while (trimEnd > dotIdx + 1 && v[trimEnd - 1] === '0') {
trimEnd--;
}
const frac = trimEnd === dotIdx + 1 ? '' : v.substring(dotIdx, trimEnd);
v = v.substring(0, dotIdx) + frac + suffix;
}
return v;
}
}
exports.SchemaTypeValidator = SchemaTypeValidator;
//# sourceMappingURL=SchemaTypeValidator.js.map