From: Lady Date: Sat, 15 Jul 2023 22:04:04 +0000 (-0700) Subject: Add all X·S·D non‐auxillary functions X-Git-Tag: 0.1.0 X-Git-Url: https://git.ladys.computer/Sutra/commitdiff_plain/refs/tags/0.1.0 Add all X·S·D non‐auxillary functions Some function implementations differ from that literally specified in the specification because exact adherence would lead to floating‐point rounding errors. The literal implementation is still provided, al·be·it commented out. Auxillary functions (defined as those functions in the X·S·D specification with a grey background ∣ `aux` class) are implemented, but not exported. Some of these are not even used due to the above and will likely be removed in a future version of this module. It is expected that further optimization of the actually exported functions will probably take place at some point, but this initial release provides extreme spec adherence for posterity (and validation of the test suite). --- diff --git a/deno.lock b/deno.lock index 7688268..ff92c57 100644 --- a/deno.lock +++ b/deno.lock @@ -6,6 +6,7 @@ "https://deno.land/std@0.192.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", "https://deno.land/std@0.192.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", "https://deno.land/std@0.192.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/std@0.192.0/testing/bdd.ts": "59f7f7503066d66a12e50ace81bfffae5b735b6be1208f5684b630ae6b4de1d0", "https://git.ladys.computer/Pisces/blob_plain/0.4.0:/binary.js": "4dd2ba7bc3c0d7b31b24fdd8350f3d976ae8c916b283cedc06dd3b768bbcb812", "https://git.ladys.computer/Pisces/blob_plain/0.4.0:/collection.js": "3d9f2f9817273aeffd6735ecf5cd2a00ed7a9327859fb026e729b434ab98c59b", "https://git.ladys.computer/Pisces/blob_plain/0.4.0:/function.js": "0ca111c31ff3694822082e785c52d71c079c0269f3fb3b97c0d145a4a4ca31d1", diff --git a/deps.js b/deps.js index 471ec38..dc9c66a 100644 --- a/deps.js +++ b/deps.js @@ -8,15 +8,20 @@ // file, You can obtain one at . export { + abs, bind, call, floor, + get8BitUnsignedIntegralItem, + getByteLength, + getFirstSubstringIndex, getPrototype, isArrayBuffer, isFiniteNumber, isIntegralNumber, ITERATOR, Matcher, + min, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, @@ -25,9 +30,17 @@ export { POSITIVE_ZERO, rawString, sameValue, + set8BitIntegralItem, + stringCatenate, + stringIncludes, + stringPadEnd, + stringPadStart, + stringSlice, stringSplit, substring, + toExponentialNotation, toFloat32, + toNumber, type, UNDEFINED, } from "https://git.ladys.computer/Pisces/blob_plain/0.4.0:/mod.js"; diff --git a/dev-deps.js b/dev-deps.js index 3b374d6..6ceedab 100644 --- a/dev-deps.js +++ b/dev-deps.js @@ -9,5 +9,13 @@ export { assert, + assertAlmostEquals, + assertEquals, + assertObjectMatch, assertStrictEquals, + assertThrows, } from "https://deno.land/std@0.192.0/testing/asserts.ts"; +export { + describe, + it, +} from "https://deno.land/std@0.192.0/testing/bdd.ts"; diff --git a/xsd/functions.js b/xsd/functions.js index 4ad5b5c..08acc8e 100644 --- a/xsd/functions.js +++ b/xsd/functions.js @@ -8,16 +8,33 @@ // file, You can obtain one at . import { + abs, bind, call, + get8BitUnsignedIntegralItem, + getByteLength, + getFirstSubstringIndex, getPrototype, isArrayBuffer, isFiniteNumber, isIntegralNumber, ITERATOR, + Matcher, + min, objectCreate, + rawString, sameValue, + set8BitIntegralItem, + stringCatenate, + stringIncludes, + stringPadEnd, + stringPadStart, + stringSlice, + stringSplit, + substring, + toExponentialNotation, toFloat32, + toNumber, type, } from "../deps.js"; import { div, mod } from "./operators.js"; @@ -438,3 +455,2736 @@ const { ), }; })(); + +/** + * Maps each digit to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a digit string. + * + * ※ This function is not exposed. + */ +const digitValue = (d) => +ensureMatches(lexicalDigit, d); + +/** + * Maps a sequence of digits to the position‐weighted sum of the terms + * numerical values. + * + * See . + * + * ☡ This function throws if the provided value is not an iterable of + * digit strings. + * + * ※ Although elsewhere the X·S·D specification claims that sequences + * count from zero, this sequence clearly must count from one. + * + * ※ This function is not exposed. + */ +const digitSequenceValue = (S) => { + let sum = 0; + for (const S_i of S) { + sum = sum * 10 + digitValue(S_i); + } + return sum; +}; + +/** + * Maps a sequence of digits to the position‐weighted sum of the terms + * numerical values, weighted appropriately for fractional digits. + * + * See . + * + * ☡ This function throws if the provided value is not an iterable of + * digit strings. + * + * ※ The X·S·D specification erroneously specifies ·digitValue·(Si) − + * 10^(−i), but ·digitValue·(Si) × 10^(−i) is correct. + * + * ※ Although elsewhere the X·S·D specification claims that sequences + * count from zero, this sequence clearly must count from one. + * + * ※ This function is not exposed. + */ +const fractionDigitSequenceValue = (S) => { + let sum = 0; + let i = 0; + for (const S_i of S) { + sum += digitValue(S_i) * 10 ** --i; + } + return sum; +}; + +/** + * Maps a fracFrag to the appropriate fractional decimal number. + * + * See . + * + * ☡ This function throws if the provided value is not a fracFrag + * string. + * + * ※ This function is not exposed. + */ +//deno-lint-ignore no-unused-vars +const fractionFragValue = (N) => + fractionDigitSequenceValue( + literalSequence(ensureMatches(fracFrag, N)), + ); + +/** + * Maps an unsignedNoDecimalPtNumeral to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * unsignedNoDecimalPtNumeral string. + */ +export const unsignedNoDecimalMap = (N) => + digitSequenceValue( + literalSequence(ensureMatches(unsignedNoDecimalPtNumeral, N)), + ); + +/** + * Maps an noDecimalPtNumeral to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * noDecimalPtNumeral string. + */ +export const noDecimalMap = (N) => { + switch (ensureMatches(noDecimalPtNumeral, N)[0]) { + case "-": + return -unsignedNoDecimalMap(substring(N, 1)) || 0; + case "+": + return unsignedNoDecimalMap(substring(N, 1)); + default: + return unsignedNoDecimalMap(N); + } +}; + +/** + * Maps an unsignedDecimalPtNumeral to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * unsignedDecimalPtNumeral string. + * + * ※ This function makes use of the built·in Ecmascript float parsing + * to avoid rounding errors. + */ +export const unsignedDecimalPtMap = (D) => { + // const N = substring( + // ensureMatches(unsignedDecimalPtNumeral, D), + // 0, + // getFirstSubstringIndex(D, "."), + // ); + // const F = substring(D, N.length + 1); + // if (F === "") { + // return unsignedNoDecimalMap(N); + // } else if (N === "") { + // return fractionFragValue(F); + // } else { + // return unsignedNoDecimalMap(N) + fractionFragValue(F); + // } + return stringToFloat(ensureMatches(unsignedDecimalPtNumeral, D)); +}; + +/** + * Maps an decimalPtNumeral to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * decimalPtNumeral string. + */ +export const decimalPtMap = (N) => { + switch (ensureMatches(decimalPtNumeral, N)[0]) { + case "-": + return -unsignedDecimalPtMap(substring(N, 1)) || 0; + case "+": + return unsignedDecimalPtMap(substring(N, 1)); + default: + return unsignedDecimalPtMap(N); + } +}; + +/** + * Maps a scientificNotationNumeral to its numerical value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * scientificNotationNumeral string. + * + * ※ The X·S·D specification erroneously specifies + * ·unsignedDecimalPtMap·(E), but ·noDecimalMap·(E) is correct. + * + * ※ This function makes use of the built·in Ecmascript float parsing + * to avoid rounding errors. + */ +export const scientificMap = (N) => { + // const C = substring( + // ensureMatches(scientificNotationNumeral, N), + // 0, + // (() => { + // let index = 0; + // for (const char of literalSequence(N)) { + // if (char === "e" || char === "E") { + // return index; + // } else { + // ++index; + // continue; + // } + // } + // })(), + // ); + // const E = substring(N, C.length + 1); + // return getFirstSubstringIndex(N, ".") !== -1 + // ? decimalPtMap(C) * 10 ** noDecimalMap(E) + // : noDecimalMap(C) * 10 ** noDecimalMap(E); + return stringToFloat(ensureMatches(scientificNotationNumeral, N)) || + 0; +}; + +/** + * Maps each integer between 0 and 9 to the corresponding digit. + * + * See . + * + * ☡ This function throws if the provided value is not an integral + * number between 0 and 9 inclusive. + * + * ※ This function is not exposed. + */ +const digit = (i) => { + if (!(isIntegralNumber(i) && i >= 0 && i <= 9)) { + throw new TypeError( + `सूत्र: Expected an integral number between 0 and 9 inclusive, but got: ${i}.`, + ); + } else { + return `${i}`; + } +}; + +/** + * Maps each nonnegative integer to a sequence of integers used by + * ·digitSeq· to ultimately create an unsignedNoDecimalPtNumeral. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ The sequence produced by this function is infinite. + * + * ※ This function is not exposed. + */ +const digitRemainderSeq = (i) => { + nonnegativeInteger(i); + return generatorSequence(function* () { + let s_j = i; + while (true) { + yield s_j; + s_j = div(s_j, 10); + } + }); +}; + +/** + * Maps each nonnegative integer to a sequence of integers used by + * ·unsignedNoDecimalPtCanonicalMap· to create an + * unsignedNoDecimalPtNumeral. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ The sequence produced by this function is infinite. + * + * ※ This function is not exposed. + */ +const digitSeq = (i) => { + nonnegativeInteger(i); + return generatorSequence(function* () { + for (const i_j of digitRemainderSeq(i)) { + yield mod(i_j, 10); + } + }); +}; + +/** + * Maps a sequence of nonnegative integers to the index before the + * first zero term. + * + * See . + * + * ☡ This function throws if the values of the provided sequence are + * not nonnegative integers. + * + * ※ The X·S·D specification erroneously describes this function as + * giving the index *of* the first zero term, but it is actually the + * index of the last nonzero term. + * + * ※ The X·S·D specification erroneously describes this function as + * taking a sequence of nonnegative integers, but it is called with + * ·FractionDigitRemainderSeq·, which is a list of nonnegative decimal + * numbers. + * + * ※ This function is not exposed. + */ +const lastSignificantDigit = (s) => { + let j = 0; + for (const s_j of s) { + if (nonnegativeDecimalNumber(s_j) === 0) { + return j === 0 ? 0 : j - 1; + } else { + ++j; + continue; + } + } +}; + +/** + * Maps each nonnegative decimal number less than 1 to a sequence of + * decimal numbers used by ·fractionDigitSeq· to ultimately create an + * unsignedNoDecimalPtNumeral. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number less than 1. + * + * ※ The X·S·D specification erroneously specifies s_0 = f − 10 and + * s_(j+1) = (s_j ·mod· 1) − 10, but s_0 = f × 10 and s_(j+1) = (s_j + * ·mod· 1) × 10 is correct. + * + * ※ The implementation of this function uses string operations + * because Ecmascript does not (currently) have a lossless decimal + * type. + * + * ※ The sequence produced by this function is infinite. + * + * ※ This function is not exposed. + */ +const FractionDigitRemainderSeq = (f) => { + if (!(isFiniteNumber(f) && f >= 0 && f < 1)) { + throw new TypeError( + `सूत्र: Expected a nonnegative finite number less than 1, but got: ${f}.`, + ); + } else { + return generatorSequence(function* () { + let s_j = f * 10; + while (true) { + yield s_j; + s_j = mod(s_j, 1) * 10; + } + }); + } +}; + +/** + * Maps each nonnegative decimal number less than 1 to a sequence of + * integers used by ·fractionDigitsCanonicalFragmentMap· to ultimately + * create an unsignedNoDecimalPtNumeral. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number less than 1. + * + * ※ The sequence produced by this function is infinite. + * + * ※ This function is not exposed. + */ +const fractionDigitSeq = (f) => { + if (!(isFiniteNumber(f) && f >= 0 && f < 1)) { + throw new TypeError( + `सूत्र: Expected a nonnegative finite number less than 1, but got: ${f}.`, + ); + } else { + return generatorSequence(function* () { + for (const s_j of FractionDigitRemainderSeq(f)) { + yield div(s_j, 1); + } + }); + } +}; + +/** + * Maps each nonnegative decimal number less than 1 to a ·literal· used + * by ·unsignedDecimalPtCanonicalMap· to create an + * unsignedDecimalPtNumeral. + * + * See . + * + * ☡ This function throws if the provided value is not nonnegative and + * less than 1. + * + * ※ This function is not exposed. + */ +//deno-lint-ignore no-unused-vars +const fractionDigitsCanonicalFragmentMap = (f) => + stringCatenate(...generatorSequence(function* () { + const l = lastSignificantDigit(FractionDigitRemainderSeq(f)); + let j = 0; + for (const f_j of fractionDigitSeq(f)) { + yield digit(f_j); + if (j === l) { + break; + } else { + ++j; + continue; + } + } + })); + +/** + * Maps a nonnegative integer to a unsignedNoDecimalPtNumeral, its + * ·canonical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + */ +export const unsignedNoDecimalPtCanonicalMap = (i) => { + nonnegativeInteger(i); + return stringCatenate(...generatorSequence(function* () { + const l = lastSignificantDigit(digitRemainderSeq(i)); + const digits = new Array(l + 1); + let j = 0; + for (const i_j of digitSeq(i)) { + digits[l - j] = digit(i_j); + if (j === l) { + break; + } else { + ++j; + continue; + } + } + for (j = 0; j <= l; ++j) { + yield digits[j]; + } + })); +}; + +/** + * Maps an integer to a noDecimalPtNumeral, its ·canonical + * representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer. + */ +export const noDecimalPtCanonicalMap = (i) => + integer(i) < 0 + ? stringCatenate("-", unsignedNoDecimalPtCanonicalMap(-i)) + : unsignedNoDecimalPtCanonicalMap(i); + +/** + * Maps a nonnegative decimal number to a unsignedDecimalPtNumeral, its + * ·canonical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number. + * + * ※ This function makes use of the built·in Ecmascript float + * serialization to avoid rounding errors. + */ +export const unsignedDecimalPtCanonicalMap = (n) => { + // nonnegativeDecimalNumber(n); + // return stringCatenate( + // unsignedNoDecimalPtCanonicalMap(div(n, 1)), + // ".", + // fractionDigitsCanonicalFragmentMap(mod(n, 1)), + // ); + const exp = toExponentialNotation(nonnegativeDecimalNumber(n)); + const eIndex = getFirstSubstringIndex(exp, "e"); + const exponent = +substring(exp, eIndex + 1); + const zeroPaddedMantissa = stringPadEnd( + stringPadStart( + exp[0], + -exponent + 1, + "0", + ) + (eIndex > 1 ? substring(exp, 2, eIndex) : ""), + exponent + 2, + "0", + ); + const decimalPoint = exponent < 0 ? 1 : exponent + 1; + return stringCatenate( + substring(zeroPaddedMantissa, 0, decimalPoint), + ".", + substring(zeroPaddedMantissa, decimalPoint), + ); +}; + +/** + * Maps a decimal number to a decimalPtNumeral, its ·canonical + * representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a finite number. + */ +export const decimalPtCanonicalMap = (n) => + decimalNumber(n) < 0 + ? stringCatenate("-", unsignedDecimalPtCanonicalMap(-n)) + : unsignedDecimalPtCanonicalMap(n); + +/** + * Maps a nonnegative decimal number to a + * unsignedScientificNotationNumeral, its ·canonical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number. + * + * ※ This function makes use of the built·in Ecmascript float + * serialization to avoid rounding errors. + */ +export const unsignedScientificCanonicalMap = (n) => { + // nonnegativeDecimalNumber(n); + // return stringCatenate( + // unsignedDecimalPtCanonicalMap(n / 10 ** div(log10(n), 1)), + // "E", + // noDecimalPtCanonicalMap(div(log10(n), 1)), + // ); + const exp = toExponentialNotation(nonnegativeDecimalNumber(n)); + const [mantissa, exponent] = stringSplit(exp, "e"); + return stringCatenate( + mantissa, + stringIncludes(mantissa, ".") ? "E" : ".0E", + exponent[0] === "+" ? substring(exponent, 1) : exponent, + ); +}; + +/** + * Maps a decimal number to a scientificNotationNumeral, its ·canonical + * representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a finite number. + */ +export const scientificCanonicalMap = (n) => + decimalNumber(n) < 0 + ? stringCatenate("-", unsignedScientificCanonicalMap(-n)) + : unsignedScientificCanonicalMap(n); + +/** + * Maps the ·lexical representations· of ·special values· used with + * some numerical datatypes to those ·special values·. + * + * See . + * + * ☡ This function throws if the provided value is not a + * numericalSpecialRep string. + */ +export const specialRepValue = (c) => { + switch (ensureMatches(numericalSpecialRep, c)) { + case "INF": + case "+INF": + return positiveInfinity; + case "-INF": + return negativeInfinity; + case "NaN": + return notANumber; + } +}; + +/** + * Maps the ·special values· used with some numerical datatypes to + * their ·canonical representations·. + * + * See . + * + * ☡ This function throws if the provided value is not nan or positive + * or negative infinity. + */ +export const specialRepCanonicalMap = (c) => { + if (sameValue(c, positiveInfinity)) { + return "INF"; + } else if (sameValue(c, negativeInfinity)) { + return "-INF"; + } else if (sameValue(c, notANumber)) { + return "NaN"; + } else { + throw new TypeError( + `सूत्र: Expected a special value, but got: ${c}.`, + ); + } +}; + +/** + * Maps a decimalLexicalRep onto a decimal value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * decimalLexicalRep string. + */ +export const decimalLexicalMap = (LEX) => { + ensureMatches(decimalLexicalRep, LEX); + if (noDecimalPtNumeral(LEX)) { + return noDecimalMap(LEX); + } else if (decimalPtNumeral(LEX)) { + return decimalPtMap(LEX); + } +}; + +/** + * Maps a decimal to its ·canonical representation·, a + * decimalLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a finite number. + */ +export const decimalCanonicalMap = (d) => + isIntegralNumber(decimalNumber(d)) + ? noDecimalPtCanonicalMap(d) + : decimalPtCanonicalMap(d); + +/** + * Rounds a non-zero decimal number to the nearest floating-point + * value. + * + * See . + * + * ☡ This function throws if the provided value is zero or is not a + * finite number. + * + * ※ This function uses native Ecmascript float rounding methods and + * only supports a cWidth of 24 or 53. + * + * ※ This function is not exposed. + */ +const floatingPointRound = (nV, cWidth, eMin, eMax) => { + if (!isFiniteNumber(nV) || nV == 0) { + throw new TypeError( + `सूत्र: Expected a finite nonzero number, but got: ${nV}.`, + ); + } else if (cWidth !== 24 && cWidth !== 53) { + throw new TypeError( + `सूत्र: Unsupported cWidth: ${cWidth}.`, + ); + } else if ( + cWidth === 24 && (eMin !== -149 || eMax !== 104) || + cWidth === 53 && (eMin !== -1074 || eMax !== 971) + ) { + throw new TypeError( + `सूत्र: Unsupported eMin and eMax for cWidth: ${cWidth}.`, + ); + } else { + return cWidth <= 24 ? toFloat32(nV) : +nV; + } +}; + +/** + * Maps a decimal number to that value rounded by some power of 10. + * + * See . + * + * ☡ This function throws if the provided value is not a finite number. + * + * ☡ This function throws if the provided power of 10 is not a + * nonnegative integer. + * + * ※ This function is not exposed. + */ +const round = (n, k) => { + decimalNumber(n); + nonnegativeInteger(k); + return div(n / 10 ** k + 0.5, 1) * 10 ** k; +}; + +/** + * Maps a decimal number (c × 10e) to successive approximations. + * + * See . + * + * ☡ This function throws if the provided values are not a nonnegative + * integer, an integer, an a nonnegative integer. + * + * ※ This function is not exposed. + */ +//deno-lint-ignore no-unused-vars +const floatApprox = (c, e, j) => { + nonnegativeInteger(c); + integer(e); + nonnegativeInteger(j); + return round(c, j) * 10 ** e; +}; + +/** + * Maps a floatRep onto a float value. + * + * See . + * + * ☡ This function throws if the provided value is not a floatRep + * string. + */ +export const floatLexicalMap = (LEX) => { + ensureMatches(floatRep, LEX); + if (numericalSpecialRep(LEX)) { + return specialRepValue(LEX); + } else { + const nV = noDecimalPtNumeral(LEX) + ? noDecimalMap(LEX) + : decimalPtNumeral(LEX) + ? decimalPtMap(LEX) + : scientificMap(LEX); + const v = nV == 0 ? 0 : floatingPointRound(nV, 24, -149, 104); + return v == 0 ? LEX[0] === "-" ? negativeZero : positiveZero : v; + } +}; + +/** + * Maps a doubleRep onto a double value. + * + * See . + * + * ☡ This function throws if the provided value is not a doubleRep + * string. + */ +export const doubleLexicalMap = (LEX) => { + ensureMatches(doubleRep, LEX); + if (numericalSpecialRep(LEX)) { + return specialRepValue(LEX); + } else { + const nV = noDecimalPtNumeral(LEX) + ? noDecimalMap(LEX) + : decimalPtNumeral(LEX) + ? decimalPtMap(LEX) + : scientificMap(LEX); + const v = nV == 0 ? 0 : floatingPointRound(nV, 53, -1074, 971); + return v == 0 ? LEX[0] === "-" ? negativeZero : positiveZero : v; + } +}; + +/** + * Maps a float to its ·canonical representation·, a floatRep. + * + * See . + * + * ☡ This function throws if the provided value is not a 32‐bit float. + */ +export const floatCanonicalMap = (f) => { + float(f); + return f === positiveInfinity || f === negativeInfinity || + sameValue(f, notANumber) + ? specialRepCanonicalMap(f) + : sameValue(f, 0) + ? "0.0E0" + : sameValue(f, -0) + ? "-0.0E0" + : scientificCanonicalMap(f); +}; + +/** + * Maps a double to its ·canonical representation·, a doubleRep. + * + * See . + * + * ☡ This function throws if the provided value is not a number. + */ +export const doubleCanonicalMap = (f) => { + double(f); + return f === positiveInfinity || f === negativeInfinity || + sameValue(f, notANumber) + ? specialRepCanonicalMap(f) + : sameValue(f, 0) + ? "0.0E0" + : sameValue(f, -0) + ? "-0.0E0" + : scientificCanonicalMap(f); +}; + +/** + * Maps a duYearFrag to an integer, intended as part of the value of + * the ·months· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duYearFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ This function is not exposed. + */ +const duYearFragmentMap = (Y) => + noDecimalMap(stringSlice(ensureMatches(duYearFrag, Y), 0, -1)); + +/** + * Maps a duMonthFrag to an integer, intended as part of the value of + * the ·months· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duMonthFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ This function is not exposed. + */ +const duMonthFragmentMap = (M) => + noDecimalMap(stringSlice(ensureMatches(duMonthFrag, M), 0, -1)); + +/** + * Maps a duDayFrag to an integer, intended as part of the value of the + * ·seconds· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duDayFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ This function is not exposed. + */ +const duDayFragmentMap = (D) => + noDecimalMap(stringSlice(ensureMatches(duDayFrag, D), 0, -1)); + +/** + * Maps a duHourFrag to an integer, intended as part of the value of + * the ·seconds· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duHourFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ The X·S·D specification has a copypasta typo in the definition of + * this function; it takes an argument H which necessarily is followed + * by an "H". There is no D which necessarily begins with "D". + * + * ※ This function is not exposed. + */ +const duHourFragmentMap = (H) => + noDecimalMap(stringSlice(ensureMatches(duHourFrag, H), 0, -1)); + +/** + * Maps a duMinuteFrag to an integer, intended as part of the value of + * the ·seconds· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duMinuteFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ This function is not exposed. + */ +const duMinuteFragmentMap = (M) => + noDecimalMap(stringSlice(ensureMatches(duMinuteFrag, M), 0, -1)); + +/** + * Maps a duSecondFrag to an integer, intended as part of the value of + * the ·seconds· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duMinuteFrag + * string. + * + * ※ The X·S·D specification erroneously specifies the numeral as + * following the letter, but it precedes it. + * + * ※ This function is not exposed. + */ +const duSecondFragmentMap = (S) => + getFirstSubstringIndex(ensureMatches(duSecondFrag, S), ".") !== -1 + ? decimalPtMap(stringSlice(S, 0, -1)) + : noDecimalMap(stringSlice(S, 0, -1)); + +/** + * Maps a duYearMonthFrag into an integer, intended as part of the + * ·months· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * duYearMonthFrag string. + * + * ※ This function is not exposed. + */ +const duYearMonthFragmentMap = (YM) => { + let Y, M; + new Matcher( + rawString`(?\d+Y)?(?\d+M)?`, + undefined, + (_, { groups }) => { + Y = groups.Y; + M = groups.M; + return true; + }, + )(ensureMatches(duYearMonthFrag, YM)); + const y = Y ? duYearFragmentMap(Y) : 0; + const m = M ? duMonthFragmentMap(M) : 0; + return 12 * y + m; +}; + +/** + * Maps a duTimeFrag into an integer, intended as part of the ·seconds· + * property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duTimeFrag + * string. + * + * ※ The X·S·D specification erroneously applies ·duDayFragmentMap· to + * H; ·duHourFragmentMap· is correct. + * + * ※ This function is not exposed. + */ +const duTimeFragmentMap = (T) => { + let H, M, S; + new Matcher( + rawString`T(?\d+H)?(?\d+M)?(?[\d.]+S)?`, + undefined, + (_, { groups }) => { + H = groups.H; + M = groups.M; + S = groups.S; + return true; + }, + )(ensureMatches(duTimeFrag, T)); + const h = H ? duHourFragmentMap(H) : 0; + const m = M ? duMinuteFragmentMap(M) : 0; + const s = S ? duSecondFragmentMap(S) : 0; + return 3600 * h + 60 * m + s; +}; + +/** + * Maps a duDayTimeFrag into a decimal number, which is the potential + * value of the ·seconds· property of a duration value. + * + * See . + * + * ☡ This function throws if the provided value is not a duDayTimeFrag + * string. + * + * ※ This function is not exposed. + */ +const duDayTimeFragmentMap = (DT) => { + let D, T; + new Matcher( + rawString`(?\d+D)?(?T.+)?`, + undefined, + (_, { groups }) => { + D = groups.D; + T = groups.T; + return true; + }, + )(ensureMatches(duDayTimeFrag, DT)); + const d = D ? duDayFragmentMap(D) : 0; + const t = T ? duTimeFragmentMap(T) : 0; + return 86400 * d + t; +}; + +/** + * Separates the durationLexicalRep into the month part and the seconds + * part, then maps them into the ·months· and ·seconds· of the duration + * value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * durationLexicalRep string. + */ +export const durationMap = (DUR) => { + let Y, D; + new Matcher( + rawString`-?P(?(?:\d+Y)?(?:\d+M)?)(?(?:\d+D)?(?:T.+)?)`, + undefined, + (_, { groups }) => { + Y = groups.Y; + D = groups.D; + return true; + }, + )(ensureMatches(durationLexicalRep, DUR)); + const s = DUR[0] === "-" ? -1 : 1; + return { + months: Y ? s * duYearMonthFragmentMap(Y) + 0 : 0, // cast −0 to 0 + seconds: D ? s * duDayTimeFragmentMap(D) + 0 : 0, // cast −0 to 0 + }; +}; + +/** + * Maps the lexical representation into the ·months· of a + * yearMonthDuration value. (A yearMonthDuration's ·seconds· is always + * zero.) ·yearMonthDurationMap· is a restriction of ·durationMap·. + * + * See . + * + * ☡ This function throws if the provided value is not a + * yearMonthDurationLexicalRep string. + */ +export const yearMonthDurationMap = (YM) => { + const s = YM[0] === "-" ? -1 : 1; + const Y = substring( + ensureMatches(yearMonthDurationLexicalRep, YM), + s === -1 ? 2 : 1, + ); + return { + months: s * duYearMonthFragmentMap(Y) + 0, // cast −0 to 0 + seconds: 0, + }; +}; + +/** + * Maps the lexical representation into the ·seconds· of a + * dayTimeDuration value. (A dayTimeDuration's ·months· is always + * zero.) ·dayTimeDurationMap· is a restriction of ·durationMap·. + * + * See . + * + * ☡ This function throws if the provided value is not a + * dayTimeDurationLexicalRep string. + * + * ※ The X·S·D specification erroneously describes the argument as a + * dayTimeDuration value rather than a dayTimeDurationLexicalRep. + */ +export const dayTimeDurationMap = (DT) => { + const s = DT[0] === "-" ? -1 : 1; + const D = substring( + ensureMatches(dayTimeDurationLexicalRep, DT), + s === -1 ? 2 : 1, + ); + return { + months: 0, + seconds: s * duDayTimeFragmentMap(D) + 0, // cast −0 to 0 + }; +}; + +/** + * Maps a nonnegative integer, presumably the absolute value of the + * ·months· of a duration value, to a duYearMonthFrag, a fragment of a + * duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ This function is not exposed. + */ +const duYearMonthCanonicalFragmentMap = (ym) => { + nonnegativeInteger(ym); + const y = div(ym, 12); + const m = mod(ym, 12); + return y !== 0 && m !== 0 + ? stringCatenate( + unsignedNoDecimalPtCanonicalMap(y), + "Y", + unsignedNoDecimalPtCanonicalMap(m), + "M", + ) + : y !== 0 + ? stringCatenate( + unsignedNoDecimalPtCanonicalMap(y), + "Y", + ) + : stringCatenate( + unsignedNoDecimalPtCanonicalMap(m), + "M", + ); +}; + +/** + * Maps a nonnegative integer, presumably the day normalized value from + * the ·seconds· of a duration value, to a duDayFrag, a fragment of a + * duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ Despite the description, this function does not necessarily + * return a duDayFrag (it can return an empty string). + * + * ※ This function is not exposed. + */ +const duDayCanonicalFragmentMap = (d) => + d !== 0 + ? stringCatenate( + unsignedNoDecimalPtCanonicalMap(d), + "D", + ) + : ""; + +/** + * Maps a nonnegative integer, presumably the hour normalized value + * from the ·seconds· of a duration value, to a duHourFrag, a fragment + * of a duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ Despite the description, this function does not necessarily + * return a duHourFrag (it can return an empty string). + * + * ※ This function is not exposed. + */ +const duHourCanonicalFragmentMap = (h) => + h !== 0 + ? stringCatenate( + unsignedNoDecimalPtCanonicalMap(h), + "H", + ) + : ""; + +/** + * Maps a nonnegative integer, presumably the minute normalized value + * from the ·seconds· of a duration value, to a duMinuteFrag, a + * fragment of a duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer. + * + * ※ Despite the description, this function does not necessarily + * return a duMinuteFrag (it can return an empty string). + * + * ※ This function is not exposed. + */ +const duMinuteCanonicalFragmentMap = (m) => + m !== 0 + ? stringCatenate( + unsignedNoDecimalPtCanonicalMap(m), + "M", + ) + : ""; + +/** + * Maps a nonnegative decimal number, presumably the second normalized + * value from the ·seconds· of a duration value, to a duSecondFrag, a + * fragment of a duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number. + * + * ※ Despite the description, this function does not necessarily + * return a duSecondFrag (it can return an empty string). + * + * ※ This function is not exposed. + */ +const duSecondCanonicalFragmentMap = (s) => + s !== 0 + ? stringCatenate( + isIntegralNumber(s) + ? unsignedNoDecimalPtCanonicalMap(s) + : unsignedDecimalPtCanonicalMap(s), + "S", + ) + : ""; + +/** + * Maps three nonnegative numbers, presumably the hour, minute, and + * second normalized values from a duration's ·seconds·, to a + * duTimeFrag, a fragment of a duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if either of the first two provided values + * are not nonnegative integers, or if the final provided value is not + * a nonnegative finite number. + * + * ※ Despite the description, this function does not necessarily + * return a duTimeFrag (it can return an empty string). + * + * ※ This function is not exposed. + */ +const duTimeCanonicalFragmentMap = (h, m, s) => + h !== 0 || m !== 0 || s !== 0 + ? stringCatenate( + "T", + duHourCanonicalFragmentMap(h), + duMinuteCanonicalFragmentMap(m), + duSecondCanonicalFragmentMap(s), + ) + : ""; + +/** + * Maps a nonnegative decimal number, presumably the absolute value of + * the ·seconds· of a duration value, to a duDayTimeFrag, a fragment of + * a duration ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number. + * + * ※ This function is not exposed. + */ +const duDayTimeCanonicalFragmentMap = (ss) => { + nonnegativeDecimalNumber(ss); + const d = div(ss, 86400); + const h = div(mod(ss, 86400), 3600); + const m = div(mod(ss, 3600), 60); + const s = mod(ss, 60); + return ss !== 0 + ? stringCatenate( + duDayCanonicalFragmentMap(d), + duTimeCanonicalFragmentMap(h, m, s), + ) + : "T0S"; +}; + +/** + * Maps a duration's property values to durationLexicalRep fragments + * and combines the fragments into a complete durationLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * duration value. + */ +export const durationCanonicalMap = (v) => { + const { months: m, seconds: s } = duration(v); + const sgn = m < 0 || s < 0 ? "-" : ""; + return m !== 0 && s !== 0 + ? stringCatenate( + sgn, + "P", + duYearMonthCanonicalFragmentMap(abs(m)), + duDayTimeCanonicalFragmentMap(abs(s)), + ) + : m !== 0 + ? stringCatenate( + sgn, + "P", + duYearMonthCanonicalFragmentMap(abs(m)), + ) + : stringCatenate( + sgn, + "P", + duDayTimeCanonicalFragmentMap(abs(s)), + ); +}; + +/** + * Maps a yearMonthDuration's ·months· value to a + * yearMonthDurationLexicalRep. (The ·seconds· value is necessarily + * zero and is ignored.) ·yearMonthDurationCanonicalMap· is a + * restriction of ·durationCanonicalMap·. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * yearMonthDuration value. + */ +export const yearMonthDurationCanonicalMap = (ym) => { + const { months: m, seconds: s } = duration(ym); + if (s !== 0) { + throw new TypeError( + `सूत्र: Expected the provided yearMonthDuration to have a value of zero for seconds, but got: ${s}.`, + ); + } else { + const sgn = m < 0 ? "-" : ""; + return stringCatenate( + sgn, + "P", + duYearMonthCanonicalFragmentMap(abs(m)), + ); + } +}; + +/** + * Maps a dayTimeDuration's ·seconds· value to a + * dayTimeDurationLexicalRep. (The ·months· value is necessarily zero + * and is ignored.) ·dayTimeDurationCanonicalMap· is a restriction of + * ·durationCanonicalMap·. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * dayTimeDuration value. + */ +export const dayTimeDurationCanonicalMap = (dt) => { + const { months: m, seconds: s } = duration(dt); + if (m !== 0) { + throw new TypeError( + `सूत्र: Expected the provided dayTimeDuration to have a value of zero for months, but got: ${m}.`, + ); + } else { + const sgn = s < 0 ? "-" : ""; + return stringCatenate( + sgn, + "P", + duDayTimeCanonicalFragmentMap(abs(s)), + ); + } +}; + +/** + * If month (mo) is out of range, adjust month and year (yr) + * accordingly; otherwise, make no change. + * + * See . + * + * ☡ This function throws if the provided values are not integers. + * + * ※ This function is not exposed. + */ +const normalizeMonth = (yr, mo) => { + integer(yr); + integer(mo); + return [yr + div(mo - 1, 12), mod(mo - 1, 12) + 1]; +}; + +/** + * If month (mo) is out of range, or day (da) is out of range for the + * appropriate month, then adjust values accordingly, otherwise make no + * change. + * + * See . + * + * ☡ This function throws if the provided values are not integers. + * + * ※ This function is not exposed. + */ +const normalizeDay = (yr, mo, da) => { + integer(yr); + integer(mo); + integer(da); + [yr, mo] = normalizeMonth(yr, mo); + let limit = daysInMonth(yr, mo); + while (da > limit || da <= 0) { + if (da > limit) { + da -= limit; + mo += 1; + [yr, mo] = normalizeMonth(yr, mo); + limit = daysInMonth(yr, mo); + } + if (da <= 0) { + mo -= 1; + [yr, mo] = normalizeMonth(yr, mo); + limit = daysInMonth(yr, mo); + da += limit; + } + } + return [yr, mo, da]; +}; + +/** + * Normalizes minute, hour, month, and year values to values that obey + * the appropriate constraints. + * + * See . + * + * ☡ This function throws if the provided values are not integers. + * + * ※ This function is not exposed. + */ +const normalizeMinute = (yr, mo, da, hr, mi) => { + integer(yr); + integer(mo); + integer(da); + integer(hr); + integer(mi); + hr += div(mi, 60); + mi = mod(mi, 60); + da += div(hr, 24); + hr = mod(hr, 24); + [yr, mo, da] = normalizeDay(yr, mo, da); + return [yr, mo, da, hr, mi]; +}; + +/** + * Normalizes minute, hour, month, and year values to values that obey + * the appropriate constraints. (This algorithm ignores leap seconds.) + * + * See . + * + * ☡ This function throws if the first five provided values are not + * integers or the sixth is not a finite number. + * + * ※ This function is not exposed. + */ +const normalizeSecond = (yr, mo, da, hr, mi, se) => { + integer(yr); + integer(mo); + integer(da); + integer(hr); + integer(mi); + decimalNumber(se); + mi += div(se, 60); + se = mod(se, 60); + [yr, mo, da, hr, mi] = normalizeMinute(yr, mo, da, hr, mi); + return [yr, mo, da, hr, mi, se]; +}; + +/** + * Normalizes minute, hour, month, and year values to values that obey + * the appropriate constraints. (This algorithm ignores leap seconds.) + * + * See . + * + * ☡ This function throws if the first value is not absent or an + * integer or if the second value is not an integer between 1 and 12 + * inclusive. + */ +export const daysInMonth = (y, m) => { + if (y !== absent && !isIntegralNumber(y)) { + throw new TypeError( + `सूत्र: Expected an optional integer, but got: ${y}.`, + ); + } else if (!(isIntegralNumber(m) && m >= 1 && m <= 12)) { + throw new TypeError( + `सूत्र: Expected an integer between 1 and 12 inclusive, but got: ${m}.`, + ); + } else { + return m === 2 + ? y === absent || y % 4 || !(y % 100) && y % 400 ? 28 : 29 + : m === 4 || m === 6 || m === 9 || m === 11 + ? 30 + : 31; + } +}; + +/** + * Returns an instance of the date/timeSevenPropertyModel with property + * values as specified in the arguments. If an argument is omitted, the + * corresponding property is set to absent. + * + * See . + * + * ※ The X·S·D specification erroneously fails to account for the + * value `"--02-29"`, which `·newDateTime·` treats as 01 March because + * it assumes February has 28 days when the year is absent. This is a + * correct assumption generally, but February should be treated as + * having 29 days when a value of `"--02-29"` is explicity given. + * + * ☡ This function throws if the provided values do not match the + * ranges expected by the Date∕time Seven‐Property Model. + */ +export const newDateTime = (Yr, Mo, Da, Hr, Mi, Se, Tz) => { + if (Yr !== absent && !isIntegralNumber(Yr)) { + throw new TypeError( + `सूत्र: Expected an optional integer, but got: ${Yr}.`, + ); + } else if ( + Mo !== absent && !(isIntegralNumber(Mo) && Mo >= 1 && Mo <= 12) + ) { + throw new TypeError( + `सूत्र: Expected an optional integer between 1 and 12 inclusive, but got: ${Mo}.`, + ); + } else if ( + Da !== absent && !(isIntegralNumber(Da) && Da >= 1 && Da <= 31) + ) { + throw new TypeError( + `सूत्र: Expected an optional integer between 1 and 31 inclusive, but got: ${Da}.`, + ); + } else if ( + Hr !== absent && !(isIntegralNumber(Hr) && Hr >= 0 && Hr <= 24) + ) { + throw new TypeError( + `सूत्र: Expected an optional integer between 0 and 24 inclusive, but got: ${Hr}.`, + ); + } else if ( + Mi !== absent && !(isIntegralNumber(Mi) && Mi >= 0 && Mi <= 59) + ) { + throw new TypeError( + `सूत्र: Expected an optional integer between 0 and 59 inclusive, but got: ${Mi}.`, + ); + } else if ( + Se !== absent && !(isFiniteNumber(Se) && Se >= 0 && Se < 60) + ) { + throw new TypeError( + `सूत्र: Expected an optional finite number greater than or equal to 0 and less than 60, but got: ${Se}.`, + ); + } else if ( + Tz !== absent && !(isIntegralNumber(Tz) && Tz >= -840 && Tz <= 840) + ) { + throw new TypeError( + `सूत्र: Expected an optional integer between -840 and 840 inclusive, but got: ${Tz}.`, + ); + } else { + let yr = Yr !== absent ? Yr : Mo === 2 && Da === 29 ? 0 : 1; + let mo = Mo !== absent ? Mo : 1; + let da = Da !== absent ? Da : 1; + let hr = Hr !== absent ? Hr : 0; + let mi = Mi !== absent ? Mi : 0; + let se = Se !== absent ? Se : 0; + [yr, mo, da, hr, mi, se] = normalizeSecond(yr, mo, da, hr, mi, se); + return { + year: Yr === absent ? absent : yr, + month: Mo === absent ? absent : mo, + day: Da === absent ? absent : da, + hour: Hr === absent ? absent : hr, + minute: Mi === absent ? absent : mi, + second: Se === absent ? absent : se, + timezoneOffset: Tz, + }; + } +}; + +/** + * Adds a duration to a dateTime value, producing another dateTime + * value. + * + * See . + * + * ※ The X·S·D specification erroneously fails to account for the + * value `"--02-29"`, which `·dateTimePlusDuration·` treats as 01 March + * because it assumes February has 28 days when the year is absent. + * This is a correct assumption generally, but February should be + * treated as having 29 days when a value of `"--02-29"` is explicity + * given. + * + * ☡ This function throws if the provided duration or + * date/timeSevenPropertyModel values are not valid. + * + * ※ Despite the name and description, this function can add a + * duration to any date/timeSevenPropertyModel value. + */ +export const dateTimePlusDuration = (du, dt) => { + const { months, seconds } = duration(du); + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(dt); + let yr = year !== absent ? year : month === 2 && day === 29 ? 0 : 1; + let mo = month !== absent ? month : 1; + let da = day !== absent ? day : 1; + let hr = hour !== absent ? hour : 0; + let mi = minute !== absent ? minute : 0; + let se = second !== absent ? second : 0; + mo += months; + [yr, mo] = normalizeMonth(yr, mo); + da = min(da, daysInMonth(yr, mo)); + se += seconds; + [yr, mo, da, hr, mi, se] = normalizeSecond(yr, mo, da, hr, mi, se); + return newDateTime( + year === absent ? absent : yr, + month === absent ? absent : mo, + day === absent ? absent : da, + hour === absent ? absent : hr, + minute === absent ? absent : mi, + second === absent ? absent : se, + timezoneOffset, + ); +}; + +/** + * Maps a date/timeSevenPropertyModel value to the decimal number + * representing its position on the "time line". + * + * See . + * + * ☡ This function throws if the provided date/timeSevenPropertyModel + * value is not valid. + */ +export const timeOnTimeline = (dt) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(dt); + const yr = year === absent ? 1971 : year - 1; + const mo = month === absent ? 12 : month; + const da = day === absent ? daysInMonth(yr + 1, mo) - 1 : day - 1; + const hr = hour === absent ? 0 : hour; + const mi = (minute === absent ? 0 : minute) - + (timezoneOffset === absent ? 0 : timezoneOffset); + const se = second === absent ? 0 : second; + let ToTI = 31536000 * yr; + ToTI += 86400 * (div(yr, 400) - div(yr, 100) + div(yr, 4)); + for (let m = 1; m < mo; ++m) { + ToTI += 86400 * daysInMonth(yr + 1, m); + } + ToTI += 86400 * da; + ToTI += 3600 * hr + 60 * mi + se; + return ToTI; +}; + +/** + * Maps a yearFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto an integer, presumably the ·year· property of + * a date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a yearFrag + * string. + */ +export const yearFragValue = (YR) => + noDecimalMap(ensureMatches(yearFrag, YR)); + +/** + * Maps a monthFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto an integer, presumably the ·month· property of + * a date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a monthFrag + * string. + */ +export const monthFragValue = (MO) => + unsignedNoDecimalMap(ensureMatches(monthFrag, MO)); + +/** + * Maps a dayFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto an integer, presumably the ·day· property of a + * date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a dayFrag + * string. + */ +export const dayFragValue = (DA) => + unsignedNoDecimalMap(ensureMatches(dayFrag, DA)); + +/** + * Maps a hourFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto an integer, presumably the ·hour· property of + * a date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a hourFrag + * string. + */ +export const hourFragValue = (HR) => + unsignedNoDecimalMap(ensureMatches(hourFrag, HR)); + +/** + * Maps a minuteFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto an integer, presumably the ·minute· property + * of a date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a minuteFrag + * string. + */ +export const minuteFragValue = (MI) => + unsignedNoDecimalMap(ensureMatches(minuteFrag, MI)); + +/** + * Maps a secondFrag, part of a date/timeSevenPropertyModel's ·lexical + * representation·, onto a decimal number, presumably the ·second· + * property of a date/timeSevenPropertyModel value. + * + * See . + * + * ☡ This function throws if the provided value is not a secondFrag + * string. + */ +export const secondFragValue = (SE) => + getFirstSubstringIndex(ensureMatches(secondFrag, SE), ".") === -1 + ? unsignedNoDecimalMap(SE) + : unsignedDecimalPtMap(SE); + +/** + * Maps a timezoneFrag, part of a date/timeSevenPropertyModel's + * ·lexical representation·, onto an integer, presumably the + * ·timezoneOffset· property of a date/timeSevenPropertyModel value. + * + * See . + * + * ※ The X·S·D specification erroneously specifies + * ·unsignedDecimalPtMap·(H) and ·unsignedDecimalPtMap·(M), but + * ·unsignedNoDecimalMap·(H) and ·unsignedDecimalPtMap·(M) is correct. + * + * ☡ This function throws if the provided value is not a timezoneFrag + * string. + */ +export const timezoneFragValue = (TZ) => { + const s = ensureMatches(timezoneFrag, TZ)[0]; + if (s === "Z") { + return 0; + } else { + let H, M; + new Matcher( + rawString`[+-](?\d{2}):(?\d{2})`, + undefined, + (_, { groups }) => { + H = groups.H; + M = groups.M; + return true; + }, + )(TZ); + return s === "-" + ? -(unsignedNoDecimalMap(H) * 60 + unsignedNoDecimalMap(M)) + 0 + : unsignedNoDecimalMap(H) * 60 + unsignedNoDecimalMap(M); + } +}; + +/** + * Maps a dateTimeLexicalRep to a dateTime value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * dateTimeLexicalRep string. + */ +export const dateTimeLexicalMap = (LEX) => { + let Y, MO, D, H, MI, S, T; + new Matcher( + rawString`(?-?\d+)-(?\d+)-(?\d+)T(?:24:00:00(?:\.0+)?|(?\d+):(?\d+):(?[\d.]+))(?.+)?`, + undefined, + (_, { groups }) => { + Y = groups.Y; + MO = groups.MO; + D = groups.D; + H = groups.H ?? absent; + MI = groups.MI ?? absent; + S = groups.S ?? absent; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(dateTimeLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return H === absent + ? newDateTime( + yearFragValue(Y), + monthFragValue(MO), + dayFragValue(D), + 24, + 0, + 0, + tz, + ) + : newDateTime( + yearFragValue(Y), + monthFragValue(MO), + dayFragValue(D), + hourFragValue(H), + minuteFragValue(MI), + secondFragValue(S), + tz, + ); +}; + +/** + * Maps a timeLexicalMap to a time value. + * + * See . + * + * ☡ This function throws if the provided value is not a timeLexicalMap + * string. + */ +export const timeLexicalMap = (LEX) => { + let H, MI, S, T; + new Matcher( + rawString`(?:24:00:00(?:\.0+)?|(?\d+):(?\d+):(?[\d.]+))(?.+)?`, + undefined, + (_, { groups }) => { + H = groups.H ?? absent; + MI = groups.MI ?? absent; + S = groups.S ?? absent; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(timeLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return H === absent + ? newDateTime( + absent, + absent, + absent, + 0, + 0, + 0, + tz, + ) + : newDateTime( + absent, + absent, + absent, + hourFragValue(H), + minuteFragValue(MI), + secondFragValue(S), + tz, + ); +}; + +/** + * Maps a dateLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a dateLexicalRep + * string. + */ +export const dateLexicalMap = (LEX) => { + let Y, M, D, T; + new Matcher( + rawString`(?-?\d+)-(?\d+)-(?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + Y = groups.Y; + M = groups.M; + D = groups.D; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(dateLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + yearFragValue(Y), + monthFragValue(M), + dayFragValue(D), + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a gYearMonthLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * gYearMonthLexicalRep string. + */ +export const gYearMonthLexicalMap = (LEX) => { + let Y, M, T; + new Matcher( + rawString`(?-?\d+)-(?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + Y = groups.Y; + M = groups.M; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(gYearMonthLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + yearFragValue(Y), + monthFragValue(M), + absent, + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a gYearLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * gYearLexicalRep string. + */ +export const gYearLexicalMap = (LEX) => { + let Y, T; + new Matcher( + rawString`(?-?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + Y = groups.Y; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(gYearLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + yearFragValue(Y), + absent, + absent, + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a gMonthDayLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * gMonthDayLexicalRep string. + */ +export const gMonthDayLexicalMap = (LEX) => { + let M, D, T; + new Matcher( + rawString`--(?\d+)-(?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + M = groups.M; + D = groups.D; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(gMonthDayLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + absent, + monthFragValue(M), + dayFragValue(D), + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a gDayLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a gDayLexicalRep + * string. + */ +export const gDayLexicalMap = (LEX) => { + let D, T; + new Matcher( + rawString`---(?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + D = groups.D; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(gDayLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + absent, + absent, + dayFragValue(D), + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a gMonthLexicalRep to a date value. + * + * See . + * + * ☡ This function throws if the provided value is not a + * gMonthLexicalRep string. + */ +export const gMonthLexicalMap = (LEX) => { + let M, T; + new Matcher( + rawString`--(?\d+)(?.+)?`, + undefined, + (_, { groups }) => { + M = groups.M; + T = groups.T ?? absent; + return true; + }, + )(ensureMatches(gMonthLexicalRep, LEX)); + const tz = T === absent ? absent : timezoneFragValue(T); + return newDateTime( + absent, + monthFragValue(M), + absent, + absent, + absent, + absent, + tz, + ); +}; + +/** + * Maps a nonnegative integer less than 100 onto an unsigned + * always-two-digit numeral. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * integer less than 100. + * + * ※ This function is not exposed. + */ +const unsTwoDigitCanonicalFragmentMap = (i) => { + if (!(isIntegralNumber(i) && i >= 0 && i < 100)) { + throw new TypeError( + `सूत्र: Expected a nonnegative integer less than 100, but got: ${i}.`, + ); + } else { + return stringCatenate(digit(div(i, 10)), digit(mod(i, 10))); + } +}; + +/** + * Maps an integer between -10000 and 10000 onto an always-four-digit numeral. + * + * See . + * + * ☡ This function throws if the provided value is not an integer whose + * absolute value is less than 10000. + * + * ※ This function is not exposed. + */ +const fourDigitCanonicalFragmentMap = (i) => { + if (!(isIntegralNumber(i) && abs(i) < 10000)) { + throw new TypeError( + `सूत्र: Expected an integer whose absolute value is less than 10000, but got: ${i}.`, + ); + } else { + return i < 0 + ? stringCatenate( + "-", + unsTwoDigitCanonicalFragmentMap(div(-i, 100)), + unsTwoDigitCanonicalFragmentMap(mod(-i, 100)), + ) + : stringCatenate( + unsTwoDigitCanonicalFragmentMap(div(i, 100)), + unsTwoDigitCanonicalFragmentMap(mod(i, 100)), + ); + } +}; + +/** + * Maps an integer, presumably the ·year· property of a + * date/timeSevenPropertyModel value, onto a yearFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer. + */ +export const yearCanonicalFragmentMap = (y) => + abs(integer(y)) > 9999 + ? noDecimalPtCanonicalMap(y) + : fourDigitCanonicalFragmentMap(y); + +/** + * Maps an integer, presumably the ·month· property of a + * date/timeSevenPropertyModel value, onto a monthFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer + * between 1 and 12 inclusive. + */ +export const monthCanonicalFragmentMap = (m) => { + if (!(isIntegralNumber(m) && m >= 1 && m <= 12)) { + throw new TypeError( + `सूत्र: Expected an integer between 1 and 12 inclusive, but got: ${m}.`, + ); + } else { + return unsTwoDigitCanonicalFragmentMap(m); + } +}; + +/** + * Maps an integer, presumably the ·day· property of a + * date/timeSevenPropertyModel value, onto a dayFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer + * between 1 and 31 inclusive. + */ +export const dayCanonicalFragmentMap = (d) => { + if (!(isIntegralNumber(d) && d >= 1 && d <= 31)) { + throw new TypeError( + `सूत्र: Expected an integer between 1 and 31 inclusive, but got: ${d}.`, + ); + } else { + return unsTwoDigitCanonicalFragmentMap(d); + } +}; + +/** + * Maps an integer, presumably the ·hour· property of a + * date/timeSevenPropertyModel value, onto an hourFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer + * between 0 and 23 inclusive. + */ +export const hourCanonicalFragmentMap = (h) => { + if (!(isIntegralNumber(h) && h >= 0 && h <= 23)) { + throw new TypeError( + `सूत्र: Expected an integer between 0 and 23 inclusive, but got: ${h}.`, + ); + } else { + return unsTwoDigitCanonicalFragmentMap(h); + } +}; + +/** + * Maps an integer, presumably the ·minute· property of a + * date/timeSevenPropertyModel value, onto a minuteFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer + * between 0 and 59 inclusive. + */ +export const minuteCanonicalFragmentMap = (m) => { + if (!(isIntegralNumber(m) && m >= 0 && m <= 59)) { + throw new TypeError( + `सूत्र: Expected an integer between 0 and 59 inclusive, but got: ${m}.`, + ); + } else { + return unsTwoDigitCanonicalFragmentMap(m); + } +}; + +/** + * Maps a decimal number, presumably the ·second· property of a + * date/timeSevenPropertyModel value, onto a secondFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not a nonnegative + * finite number less than 60. + * + * ※ The X·S·D specification identifies the argument to this function + * as “a nonnegative decimal number less than 70”, but second values 60 + * or larger are not supported by X·S·D. + * + * ※ This function makes use of the built·in Ecmascript float + * serialization to avoid rounding errors. + */ +export const secondCanonicalFragmentMap = (s) => { + if (!(isFiniteNumber(s) && s < 60)) { + throw new TypeError( + `सूत्र: Expected a nonnegative finite number less than 60, but got: ${s}.`, + ); + } else { + // return isIntegralNumber(s) + // ? unsTwoDigitCanonicalFragmentMap(s) + // : stringCatenate( + // unsTwoDigitCanonicalFragmentMap(div(s, 1)), + // ".", + // fractionDigitsCanonicalFragmentMap(mod(s, 1)), + // ); + return isIntegralNumber(s) + ? unsTwoDigitCanonicalFragmentMap(s) + : s < 10 + ? stringCatenate("0", decimalPtCanonicalMap(s)) + : decimalPtCanonicalMap(s); + } +}; + +/** + * Maps an integer, presumably the ·timezoneOffset· property of a + * date/timeSevenPropertyModel value, onto a timezoneFrag, part of a + * date/timeSevenPropertyModel's ·lexical representation·. + * + * See . + * + * ☡ This function throws if the provided value is not an integer + * between −840 and 840 inclusive. + */ +export const timezoneCanonicalFragmentMap = (t) => { + if (!(isIntegralNumber(t) && t >= -840 && t <= 840)) { + throw new TypeError( + `सूत्र: Expected an integer between −840 and 840 inclusive, but got: ${t}.`, + ); + } else { + return t === 0 ? "Z" : t < 0 + ? stringCatenate( + "-", + unsTwoDigitCanonicalFragmentMap(div(-t, 60)), + ":", + unsTwoDigitCanonicalFragmentMap(mod(-t, 60)), + ) + : stringCatenate( + "+", + unsTwoDigitCanonicalFragmentMap(div(t, 60)), + ":", + unsTwoDigitCanonicalFragmentMap(mod(t, 60)), + ); + } +}; + +/** + * Maps a dateTime value to a dateTimeLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * dateTime. + */ +export const dateTimeCanonicalMap = (dt) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(dt); + ensurePresence("year", year, true); + ensurePresence("month", month, true); + ensurePresence("day", day, true); + ensurePresence("hour", hour, true); + ensurePresence("minute", minute, true); + ensurePresence("second", second, true); + const DT = stringCatenate( + yearCanonicalFragmentMap(year), + "-", + monthCanonicalFragmentMap(month), + "-", + dayCanonicalFragmentMap(day), + "T", + hourCanonicalFragmentMap(hour), + ":", + minuteCanonicalFragmentMap(minute), + ":", + secondCanonicalFragmentMap(second), + ); + return timezoneOffset === absent + ? DT + : stringCatenate(DT, timezoneCanonicalFragmentMap(timezoneOffset)); +}; + +/** + * Maps a time value to a timeLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete time. + */ +export const timeCanonicalMap = (ti) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(ti); + ensurePresence("year", year, false); + ensurePresence("month", month, false); + ensurePresence("day", day, false); + ensurePresence("hour", hour, true); + ensurePresence("minute", minute, true); + ensurePresence("second", second, true); + const T = stringCatenate( + hourCanonicalFragmentMap(hour), + ":", + minuteCanonicalFragmentMap(minute), + ":", + secondCanonicalFragmentMap(second), + ); + return timezoneOffset === absent + ? T + : stringCatenate(T, timezoneCanonicalFragmentMap(timezoneOffset)); +}; + +/** + * Maps a date value to a dateLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete date. + */ +export const dateCanonicalMap = (da) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(da); + ensurePresence("year", year, true); + ensurePresence("month", month, true); + ensurePresence("day", day, true); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + const D = stringCatenate( + yearCanonicalFragmentMap(year), + "-", + monthCanonicalFragmentMap(month), + "-", + dayCanonicalFragmentMap(day), + ); + return timezoneOffset === absent + ? D + : stringCatenate(D, timezoneCanonicalFragmentMap(timezoneOffset)); +}; + +/** + * Maps a gYearMonth value to a gYearMonthLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * gYearMonth. + */ +export const gYearMonthCanonicalMap = (ym) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(ym); + ensurePresence("year", year, true); + ensurePresence("month", month, true); + ensurePresence("day", day, false); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + const YM = stringCatenate( + yearCanonicalFragmentMap(year), + "-", + monthCanonicalFragmentMap(month), + ); + return timezoneOffset === absent + ? YM + : stringCatenate(YM, timezoneCanonicalFragmentMap(timezoneOffset)); +}; + +/** + * Maps a gYear value to a gYearLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * gYear. + */ +export const gYearCanonicalMap = (gY) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(gY); + ensurePresence("year", year, true); + ensurePresence("month", month, false); + ensurePresence("day", day, false); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + return timezoneOffset === absent + ? yearCanonicalFragmentMap(year) + : stringCatenate( + yearCanonicalFragmentMap(year), + timezoneCanonicalFragmentMap(timezoneOffset), + ); +}; + +/** + * Maps a gMonthDay value to a gMonthDayLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * gMonthDay. + */ +export const gMonthDayCanonicalMap = (md) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(md); + ensurePresence("year", year, false); + ensurePresence("month", month, true); + ensurePresence("day", day, true); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + const MD = stringCatenate( + "--", + monthCanonicalFragmentMap(month), + "-", + dayCanonicalFragmentMap(day), + ); + return timezoneOffset === absent + ? MD + : stringCatenate(MD, timezoneCanonicalFragmentMap(timezoneOffset)); +}; + +/** + * Maps a gDay value to a gDayLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete gDay. + */ +export const gDayCanonicalMap = (gD) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(gD); + ensurePresence("year", year, false); + ensurePresence("month", month, false); + ensurePresence("day", day, true); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + return timezoneOffset === absent + ? stringCatenate("---", dayCanonicalFragmentMap(day)) + : stringCatenate( + "---", + dayCanonicalFragmentMap(day), + timezoneCanonicalFragmentMap(timezoneOffset), + ); +}; + +/** + * Maps a gMonth value to a gMonthLexicalRep. + * + * See . + * + * ☡ This function throws if the provided value is not a complete + * gMonth. + */ +export const gMonthCanonicalMap = (gM) => { + const { + year, + month, + day, + hour, + minute, + second, + timezoneOffset, + } = date·timeSevenPropertyModel(gM); + ensurePresence("year", year, false); + ensurePresence("month", month, true); + ensurePresence("day", day, false); + ensurePresence("hour", hour, false); + ensurePresence("minute", minute, false); + ensurePresence("second", second, false); + return timezoneOffset === absent + ? stringCatenate("--", dayCanonicalFragmentMap(month)) + : stringCatenate( + "--", + dayCanonicalFragmentMap(month), + timezoneCanonicalFragmentMap(timezoneOffset), + ); +}; + +/** + * Maps a ·literal· matching the stringRep production to a string + * value. + * + * See . + * + * ☡ This function throws if the provided value is not a stringRep + * string. + */ +export const stringLexicalMap = (LEX) => ensureMatches(stringRep, LEX); + +/** + * Maps a ·literal· matching the booleanRep production to a boolean + * value. + * + * See . + * + * ☡ This function throws if the provided value is not a booleanRep + * string. + */ +export const booleanLexicalMap = (LEX) => { + ensureMatches(booleanRep, LEX); + return LEX === "true" || LEX === "1"; +}; + +/** + * Maps a string value to a stringRep. + * + * See . + * + * ☡ This function throws if the provided value is not a valid string. + */ +export const stringCanonicalMap = (s) => ensureMatches(stringRep, s); + +/** + * Maps a boolean value to a booleanRep. + * + * See . + * + * ☡ This function throws if the provided value is not a boolean. + */ +export const booleanCanonicalMap = (b) => { + if (typeof b !== "boolean") { + throw new TypeError(`सूत्र: Expected a boolean, but got: ${b}.`); + } else { + return b ? "true" : "false"; + } +}; + +/** + * Maps a ·literal· matching the hexBinary production to a sequence of + * octets in the form of a hexBinary value. + * + * See . + * + * ☡ This function throws if the provided value is not a hexBinary + * string. + * + * ※ This function returns an `ArrayBuffer`. + */ +export const hexBinaryMap = (LEX) => { + ensureMatches(hexBinary, LEX); + const bufferLength = LEX.length / 2; + const o = new ArrayBuffer(bufferLength); + for (let bufferIndex = 0, index = 0; bufferIndex < bufferLength;) { + set8BitIntegralItem( + o, + bufferIndex, + hexOctetMap(substring(LEX, index, index = ++bufferIndex * 2)), + ); + } + return o; +}; + +/** + * Maps a ·literal· matching the hexOctet production to a single octet. + * + * See . + * + * ☡ This function throws if the provided value is not a hexOctet + * string. + * + * ※ This function is not exposed. + */ +const hexOctetMap = (LEX) => { + const [d0, d1] = ensureMatches(hexOctet, LEX); + let octet = 0; + let bit = 8; + for (const digit of hexDigitMap(d0)) { + octet |= digit << --bit; + } + for (const digit of hexDigitMap(d1)) { + octet |= digit << --bit; + } + return octet; +}; + +/** + * Maps a hexadecimal digit (a character matching the hexDigit + * production) to a sequence of four binary digits. + * + * See . + * + * ☡ This function throws if the provided value is not a hexDigit + * string. + * + * ※ It would be way simpler and more efficient to just return a + * single value here rather than a sequence of digits. + * + * ※ This function is not exposed. + */ +const hexDigitMap = (d) => { + switch (ensureMatches(hexDigit, d)) { + case "0": + return binaryDigitSequence("0000"); + case "1": + return binaryDigitSequence("0001"); + case "2": + return binaryDigitSequence("0010"); + case "3": + return binaryDigitSequence("0011"); + case "4": + return binaryDigitSequence("0100"); + case "5": + return binaryDigitSequence("0101"); + case "6": + return binaryDigitSequence("0110"); + case "7": + return binaryDigitSequence("0111"); + case "8": + return binaryDigitSequence("1000"); + case "9": + return binaryDigitSequence("1001"); + case "A": + case "a": + return binaryDigitSequence("1010"); + case "B": + case "b": + return binaryDigitSequence("1011"); + case "C": + case "c": + return binaryDigitSequence("1100"); + case "D": + case "d": + return binaryDigitSequence("1101"); + case "E": + case "e": + return binaryDigitSequence("1110"); + case "F": + case "f": + return binaryDigitSequence("1111"); + } +}; + +/** + * Maps a hexBinary value to a literal matching the hexBinary + * production. + * + * See . + * + * ☡ This function throws if the provided value is not an + * `ArrayBuffer`. + */ +export const hexBinaryCanonical = (o) => { + hexBinaryValue(o); + const length = getByteLength(o); + return stringCatenate( + "", + ...generatorSequence(function* () { + for (let index = 0; index < length; ++index) { + yield hexOctetCanonical( + toNumber(get8BitUnsignedIntegralItem(o, index)), + ); + } + }), + ); +}; + +/** + * Maps a binary octet to a literal matching the hexOctet production. + * + * See . + * + * ☡ This function throws if the provided value is not a hexOctet + * string. + * + * ※ This function is not exposed. + */ +const hexOctetCanonical = (o) => { + if (!(isIntegralNumber(o) && o >= 0 && o <= 0xFF)) { + throw new TypeError( + `सूत्र: Expected a binary octet but got: ${o}.`, + ); + } else { + const lo = generatorSequence(function* () { + for (let i = 1; i <= 4; ++i) { + const offset = 4 - i; + yield (o & 1 << offset) >> offset; + } + }); + const hi = generatorSequence(function* () { + for (let i = 1; i <= 4; ++i) { + const offset = 8 - i; + yield (o & 1 << offset) >> offset; + } + }); + return hexDigitCanonical(hi) + hexDigitCanonical(lo); + } +}; + +/** + * Maps a four-bit sequence to a hexadecimal digit (a literal matching + * the hexDigit production). + * + * See . + * + * ☡ This function throws if the provided value is not an iterable of + * binary digits. + * + * ※ It would be way simpler and more efficient to just call this with + * a single value rather than a sequence of digits. + * + * ※ This function is not exposed. + */ +const hexDigitCanonical = (d) => { + let sum = 0; + for (const digit of d) { + if (digit != 0 && digit != 1) { + throw new TypeError( + `सूत्र: Expected a binary digit but got: ${digit}.`, + ); + } else { + sum = (sum << 1) | digit; + } + } + switch (sum) { + case 0b0000: + return "0"; + case 0b0001: + return "1"; + case 0b0010: + return "2"; + case 0b0011: + return "3"; + case 0b0100: + return "4"; + case 0b0101: + return "5"; + case 0b0110: + return "6"; + case 0b0111: + return "7"; + case 0b1000: + return "8"; + case 0b1001: + return "9"; + case 0b1010: + return "A"; + case 0b1011: + return "B"; + case 0b1100: + return "C"; + case 0b1101: + return "D"; + case 0b1110: + return "E"; + case 0b1111: + return "F"; + } +}; diff --git a/xsd/functions.test.js b/xsd/functions.test.js new file mode 100644 index 0000000..8692c33 --- /dev/null +++ b/xsd/functions.test.js @@ -0,0 +1,3463 @@ +// ♓️🪡 सूत्र ∷ xsd/functions.test.js +// ==================================================================== +// +// Copyright © 2023 Lady [@ Lady’s Computer]. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at . + +import { + assertAlmostEquals, + assertEquals, + assertObjectMatch, + assertStrictEquals, + assertThrows, + describe, + it, +} from "../dev-deps.js"; +import { + booleanCanonicalMap, + booleanLexicalMap, + dateCanonicalMap, + dateLexicalMap, + dateTimeCanonicalMap, + dateTimeLexicalMap, + dateTimePlusDuration, + dayCanonicalFragmentMap, + dayFragValue, + daysInMonth, + dayTimeDurationCanonicalMap, + dayTimeDurationMap, + decimalCanonicalMap, + decimalLexicalMap, + decimalPtCanonicalMap, + decimalPtMap, + doubleCanonicalMap, + doubleLexicalMap, + durationCanonicalMap, + durationMap, + floatCanonicalMap, + floatLexicalMap, + gDayCanonicalMap, + gDayLexicalMap, + gMonthCanonicalMap, + gMonthDayCanonicalMap, + gMonthDayLexicalMap, + gMonthLexicalMap, + gYearCanonicalMap, + gYearLexicalMap, + gYearMonthCanonicalMap, + gYearMonthLexicalMap, + hexBinaryCanonical, + hexBinaryMap, + hourCanonicalFragmentMap, + hourFragValue, + minuteCanonicalFragmentMap, + minuteFragValue, + monthCanonicalFragmentMap, + monthFragValue, + newDateTime, + noDecimalMap, + noDecimalPtCanonicalMap, + scientificCanonicalMap, + scientificMap, + secondCanonicalFragmentMap, + secondFragValue, + specialRepCanonicalMap, + specialRepValue, + stringCanonicalMap, + stringLexicalMap, + timeCanonicalMap, + timeLexicalMap, + timeOnTimeline, + timezoneCanonicalFragmentMap, + timezoneFragValue, + unsignedDecimalPtCanonicalMap, + unsignedDecimalPtMap, + unsignedNoDecimalMap, + unsignedNoDecimalPtCanonicalMap, + unsignedScientificCanonicalMap, + yearCanonicalFragmentMap, + yearFragValue, + yearMonthDurationCanonicalMap, + yearMonthDurationMap, +} from "./functions.js"; + +describe("unsignedNoDecimalMap", () => { + it("maps the value", () => { + assertStrictEquals(unsignedNoDecimalMap("231"), 231); + }); + + it("throws an error if the value has a decimal point", () => { + assertThrows(() => { + unsignedNoDecimalMap("231."); + }); + assertThrows(() => { + unsignedNoDecimalMap(".231"); + }); + assertThrows(() => { + unsignedNoDecimalMap("23.1"); + }); + }); + + it("throws an error if the value has a sign", () => { + assertThrows(() => { + unsignedNoDecimalMap("+231"); + }); + assertThrows(() => { + unsignedNoDecimalMap("-231"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + unsignedNoDecimalMap(231); + }); + }); +}); + +describe("noDecimalMap", () => { + it("maps the value", () => { + assertStrictEquals(noDecimalMap("231"), 231); + assertStrictEquals(noDecimalMap("-69"), -69); + assertStrictEquals(noDecimalMap("+72"), +72); + assertStrictEquals(noDecimalMap("0"), 0); + assertStrictEquals(noDecimalMap("-0"), 0); + }); + + it("throws an error if the value has a decimal point", () => { + assertThrows(() => { + noDecimalMap("231."); + }); + assertThrows(() => { + noDecimalMap(".231"); + }); + assertThrows(() => { + noDecimalMap("23.1"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + noDecimalMap(231); + }); + }); +}); + +describe("unsignedDecimalPtMap", () => { + it("maps the value", () => { + assertStrictEquals(unsignedDecimalPtMap("231."), 231); + assertStrictEquals(unsignedDecimalPtMap(".231"), .231); + assertStrictEquals(unsignedDecimalPtMap("23.1"), 23.1); + }); + + it("throws an error if the value has no decimal point", () => { + assertThrows(() => { + unsignedDecimalPtMap("231"); + }); + }); + + it("throws an error if the value has a sign", () => { + assertThrows(() => { + unsignedDecimalPtMap("+231."); + }); + assertThrows(() => { + unsignedDecimalPtMap("-231."); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + unsignedDecimalPtMap(231); + }); + }); +}); + +describe("decimalPtMap", () => { + it("maps the value", () => { + assertStrictEquals(decimalPtMap("231."), 231); + assertStrictEquals(decimalPtMap("+.231"), .231); + assertStrictEquals(decimalPtMap("-23.1"), -23.1); + assertStrictEquals(decimalPtMap("0.0"), 0); + assertStrictEquals(decimalPtMap("-0.0"), 0); + }); + + it("throws an error if the value has no decimal point", () => { + assertThrows(() => { + decimalPtMap("231"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + decimalPtMap(231); + }); + }); +}); + +describe("scientificMap", () => { + it("maps the value", () => { + assertStrictEquals(scientificMap("23E1"), 230); + assertStrictEquals(scientificMap("+2.3E-1"), .23); + assertStrictEquals(scientificMap("-.23E1"), -2.3); + assertStrictEquals(scientificMap("0.0E0"), 0); + assertStrictEquals(scientificMap("-0.0E-0"), 0); + }); + + it("throws an error if the value has no E", () => { + assertThrows(() => { + scientificMap("231"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + decimalPtMap(231); + }); + }); +}); + +describe("unsignedNoDecimalPtCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(unsignedNoDecimalPtCanonicalMap(231), "231"); + assertStrictEquals( + unsignedNoDecimalPtCanonicalMap(1e23), + "100000000000000000000000", + ); + assertStrictEquals(unsignedNoDecimalPtCanonicalMap(0), "0"); + assertStrictEquals(unsignedNoDecimalPtCanonicalMap(-0), "0"); + }); + + it("throws an error if the value has a fractional part", () => { + assertThrows(() => { + unsignedNoDecimalPtCanonicalMap(23.1); + }); + }); + + it("throws an error if the value is negative", () => { + assertThrows(() => { + unsignedNoDecimalPtCanonicalMap(-231); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + unsignedNoDecimalPtCanonicalMap("231"); + }); + }); +}); + +describe("noDecimalPtCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(noDecimalPtCanonicalMap(231), "231"); + assertStrictEquals(noDecimalPtCanonicalMap(-231), "-231"); + assertStrictEquals(noDecimalPtCanonicalMap(0), "0"); + assertStrictEquals(noDecimalPtCanonicalMap(-0), "0"); + }); + + it("throws an error if the value has a fractional part", () => { + assertThrows(() => { + noDecimalPtCanonicalMap(23.1); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + noDecimalPtCanonicalMap("231"); + }); + }); +}); + +describe("unsignedDecimalPtCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(unsignedDecimalPtCanonicalMap(4), "4.0"); + assertStrictEquals(unsignedDecimalPtCanonicalMap(231), "231.0"); + assertStrictEquals(unsignedDecimalPtCanonicalMap(23.1), "23.1"); + assertStrictEquals(unsignedDecimalPtCanonicalMap(.24), "0.24"); + assertStrictEquals( + unsignedDecimalPtCanonicalMap(10 ** 21.5), + "3162277660168379400000.0", + ); + assertStrictEquals(unsignedDecimalPtCanonicalMap(.0085), "0.0085"); + assertStrictEquals( + unsignedDecimalPtCanonicalMap(10 ** -6.5), + "0.0000003162277660168379", + ); + assertStrictEquals(unsignedDecimalPtCanonicalMap(0), "0.0"); + assertStrictEquals(unsignedDecimalPtCanonicalMap(-0), "0.0"); + }); + + it("throws an error if the value is negative", () => { + assertThrows(() => { + unsignedDecimalPtCanonicalMap(-231); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + unsignedDecimalPtCanonicalMap("231.0"); + }); + }); +}); + +describe("decimalPtCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(decimalPtCanonicalMap(231), "231.0"); + assertStrictEquals(decimalPtCanonicalMap(-23.1), "-23.1"); + assertStrictEquals(decimalPtCanonicalMap(0), "0.0"); + assertStrictEquals(decimalPtCanonicalMap(-0), "0.0"); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + decimalPtCanonicalMap("231.0"); + }); + }); +}); + +describe("unsignedScientificCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(unsignedScientificCanonicalMap(4), "4.0E0"); + assertStrictEquals(unsignedScientificCanonicalMap(231), "2.31E2"); + assertStrictEquals(unsignedScientificCanonicalMap(23), "2.3E1"); + assertStrictEquals(unsignedScientificCanonicalMap(.24), "2.4E-1"); + assertStrictEquals( + unsignedScientificCanonicalMap(10 ** 21.5), + "3.1622776601683794E21", + ); + assertStrictEquals( + unsignedScientificCanonicalMap(.0085), + "8.5E-3", + ); + assertStrictEquals( + unsignedScientificCanonicalMap(10 ** -6.5), + "3.162277660168379E-7", + ); + assertStrictEquals(unsignedScientificCanonicalMap(0), "0.0E0"); + assertStrictEquals(unsignedScientificCanonicalMap(-0), "0.0E0"); + }); + + it("throws an error if the value is negative", () => { + assertThrows(() => { + unsignedScientificCanonicalMap(-231); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + unsignedScientificCanonicalMap("2.31E2"); + }); + }); +}); + +describe("scientificCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(scientificCanonicalMap(231), "2.31E2"); + assertStrictEquals(scientificCanonicalMap(-.231), "-2.31E-1"); + assertStrictEquals(scientificCanonicalMap(0), "0.0E0"); + assertStrictEquals(scientificCanonicalMap(-0), "0.0E0"); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + scientificCanonicalMap("2.31E2"); + }); + }); +}); + +describe("specialRepValue", () => { + it("maps the value", () => { + assertStrictEquals(specialRepValue("INF"), Infinity); + assertStrictEquals(specialRepValue("+INF"), Infinity); + assertStrictEquals(specialRepValue("-INF"), -Infinity); + assertStrictEquals(specialRepValue("NaN"), NaN); + }); + + it("throws an error if the value is not a special represention", () => { + assertThrows(() => { + specialRepValue("231"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + specialRepValue(Infinity); + }); + }); +}); + +describe("specialRepCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(specialRepCanonicalMap(Infinity), "INF"); + assertStrictEquals(specialRepCanonicalMap(-Infinity), "-INF"); + assertStrictEquals(specialRepCanonicalMap(NaN), "NaN"); + }); + + it("throws an error if the value is not a special value", () => { + assertThrows(() => { + specialRepCanonicalMap(231); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + specialRepCanonicalMap("NaN"); + }); + }); +}); + +describe("decimalLexicalMap", () => { + it("maps the value", () => { + assertStrictEquals(decimalLexicalMap("4.0"), 4); + assertStrictEquals(decimalLexicalMap("-231"), -231); + assertStrictEquals(decimalLexicalMap("+23.1"), 23.1); + assertStrictEquals(decimalLexicalMap("-0.24"), -0.24); + assertStrictEquals(decimalLexicalMap(".008500"), 0.0085); + assertStrictEquals(decimalLexicalMap("0"), 0); + assertStrictEquals(decimalLexicalMap("-0"), 0); + }); + + it("throws an error if the value is in exponential notation", () => { + assertThrows(() => { + decimalLexicalMap("23E1"); + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + decimalLexicalMap(231); + }); + }); +}); + +describe("decimalCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(decimalCanonicalMap(4), "4"); + assertStrictEquals(decimalCanonicalMap(-231), "-231"); + assertStrictEquals(decimalCanonicalMap(23.1), "23.1"); + assertStrictEquals(decimalCanonicalMap(-.24), "-0.24"); + assertStrictEquals( + decimalCanonicalMap(10 ** 21.5), + "3162277660168379400000", + ); + assertStrictEquals(decimalCanonicalMap(.0085), "0.0085"); + assertStrictEquals( + decimalCanonicalMap(10 ** -6.5), + "0.0000003162277660168379", + ); + assertStrictEquals(decimalCanonicalMap(0), "0"); + assertStrictEquals(decimalCanonicalMap(-0), "0"); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + decimalCanonicalMap("231"); + }); + }); +}); + +describe("floatLexicalMap", () => { + it("maps the value", () => { + assertStrictEquals(floatLexicalMap("4.0"), Math.fround(4)); + assertStrictEquals(floatLexicalMap("-231"), Math.fround(-231)); + assertStrictEquals(floatLexicalMap("+23.1"), Math.fround(23.1)); + assertStrictEquals(floatLexicalMap("-0.24"), Math.fround(-0.24)); + assertStrictEquals( + floatLexicalMap(".008500"), + Math.fround(0.0085), + ); + assertStrictEquals(floatLexicalMap("23E1"), Math.fround(230)); + assertStrictEquals(floatLexicalMap("4E-20"), Math.fround(4e-20)); + assertStrictEquals(doubleLexicalMap("+0"), 0); + assertStrictEquals(doubleLexicalMap("-0"), -0); + assertStrictEquals(doubleLexicalMap("INF"), Infinity); + assertStrictEquals(doubleLexicalMap("+INF"), Infinity); + assertStrictEquals(doubleLexicalMap("-INF"), -Infinity); + assertStrictEquals(doubleLexicalMap("NaN"), NaN); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + floatLexicalMap(231); + }); + }); +}); + +describe("doubleLexicalMap", () => { + it("maps the value", () => { + assertStrictEquals(doubleLexicalMap("4.0"), 4); + assertStrictEquals(doubleLexicalMap("-231"), -231); + assertStrictEquals(doubleLexicalMap("+23.1"), 23.1); + assertStrictEquals(doubleLexicalMap("-0.24"), -0.24); + assertStrictEquals(doubleLexicalMap(".008500"), 0.0085); + assertStrictEquals(doubleLexicalMap("23E1"), 230); + assertStrictEquals(doubleLexicalMap("4E-20"), 4e-20); + assertStrictEquals(doubleLexicalMap("+0"), 0); + assertStrictEquals(doubleLexicalMap("-0"), -0); + assertStrictEquals(doubleLexicalMap("INF"), Infinity); + assertStrictEquals(doubleLexicalMap("+INF"), Infinity); + assertStrictEquals(doubleLexicalMap("-INF"), -Infinity); + assertStrictEquals(doubleLexicalMap("NaN"), NaN); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + doubleLexicalMap(231); + }); + }); +}); + +describe("floatCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(floatCanonicalMap(4), "4.0E0"); + assertStrictEquals(floatCanonicalMap(-231), "-2.31E2"); + assertStrictEquals( + floatCanonicalMap(23.100000381469727), + "2.3100000381469727E1", + ); + assertStrictEquals( + floatCanonicalMap(-0.23999999463558197), + "-2.3999999463558197E-1", + ); + assertStrictEquals( + floatCanonicalMap(0.008500000461935997), + "8.500000461935997E-3", + ); + assertStrictEquals( + floatCanonicalMap(3.1622776321769755e21), + "3.1622776321769755E21", + ); + assertStrictEquals( + floatCanonicalMap(3.1622775509276835e-7), + "3.1622775509276835E-7", + ); + assertStrictEquals(floatCanonicalMap(0), "0.0E0"); + assertStrictEquals(floatCanonicalMap(-0), "-0.0E0"); + assertStrictEquals(floatCanonicalMap(Infinity), "INF"); + assertStrictEquals(floatCanonicalMap(-Infinity), "-INF"); + assertStrictEquals(floatCanonicalMap(NaN), "NaN"); + }); + + it("throws an error if the value is not a float", () => { + assertThrows(() => { + floatCanonicalMap(23.1); + }); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + floatCanonicalMap("231"); + }); + }); +}); + +describe("doubleCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(doubleCanonicalMap(4), "4.0E0"); + assertStrictEquals(doubleCanonicalMap(-231), "-2.31E2"); + assertStrictEquals(doubleCanonicalMap(23.1), "2.31E1"); + assertStrictEquals(doubleCanonicalMap(-0.24), "-2.4E-1"); + assertStrictEquals(doubleCanonicalMap(.0085), "8.5E-3"); + assertStrictEquals( + doubleCanonicalMap(10 ** 21.5), + "3.1622776601683794E21", + ); + assertStrictEquals( + doubleCanonicalMap(10 ** -6.5), + "3.162277660168379E-7", + ); + assertStrictEquals(doubleCanonicalMap(0), "0.0E0"); + assertStrictEquals(doubleCanonicalMap(-0), "-0.0E0"); + assertStrictEquals(doubleCanonicalMap(Infinity), "INF"); + assertStrictEquals(doubleCanonicalMap(-Infinity), "-INF"); + assertStrictEquals(doubleCanonicalMap(NaN), "NaN"); + }); + + it("throws an error if the value is not a number", () => { + assertThrows(() => { + doubleCanonicalMap("231"); + }); + }); +}); + +describe("durationMap", () => { + it("maps the value", () => { + assertEquals(durationMap("P0Y0M0DT0H0M0.0S"), { + months: 0, + seconds: 0, + }); + assertStrictEquals(durationMap("-P0Y0M0DT0H0M0.0S").months, 0); + assertStrictEquals(durationMap("-P0Y0M0DT0H0M0.0S").seconds, 0); + assertEquals(durationMap("-P1Y2M"), { months: -14, seconds: 0 }); + assertStrictEquals(durationMap("-P1Y2M").seconds, 0); + assertEquals(durationMap("-P3DT5H7M11S"), { + months: 0, + seconds: -277631, + }); + assertStrictEquals(durationMap("-P3DT5H7M11S").months, 0); + assertEquals(durationMap("-P1Y2M3DT5H7M11.13S"), { + months: -14, + seconds: -277631.13, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + durationMap(0); + }); + assertThrows(() => { + durationMap({ months: 0, seconds: 0 }); + }); + }); + + it("throws an error if the value has no fields", () => { + assertThrows(() => { + durationMap("P"); + }); + }); + + it("throws an error if the value has a T but no time", () => { + assertThrows(() => { + durationMap("P0Y0M0DT"); + }); + }); +}); + +describe("yearMonthDurationMap", () => { + it("maps the value", () => { + assertEquals(yearMonthDurationMap("P0Y0M"), { + months: 0, + seconds: 0, + }); + assertStrictEquals(yearMonthDurationMap("-P0Y0M").months, 0); + assertStrictEquals(yearMonthDurationMap("-P0Y0M").seconds, 0); + assertEquals(yearMonthDurationMap("-P1Y2M"), { + months: -14, + seconds: 0, + }); + assertStrictEquals(yearMonthDurationMap("-P1Y2M").seconds, 0); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + yearMonthDurationMap(0); + }); + assertThrows(() => { + yearMonthDurationMap({ months: 0, seconds: 0 }); + }); + }); + + it("throws an error if the value has no fields", () => { + assertThrows(() => { + yearMonthDurationMap("P"); + }); + }); +}); + +describe("dayTimeDurationMap", () => { + it("maps the value", () => { + assertEquals(dayTimeDurationMap("P0DT0H0M0.0S"), { + months: 0, + seconds: 0, + }); + assertStrictEquals(dayTimeDurationMap("-P0DT0H0M0.0S").months, 0); + assertStrictEquals(dayTimeDurationMap("-P0DT0H0M0.0S").seconds, 0); + assertEquals(dayTimeDurationMap("-P3DT5H7M11.13S"), { + months: 0, + seconds: -277631.13, + }); + assertStrictEquals( + dayTimeDurationMap("-P3DT5H7M11.13S").months, + 0, + ); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + dayTimeDurationMap(0); + }); + assertThrows(() => { + dayTimeDurationMap({ months: 0, seconds: 0 }); + }); + }); + + it("throws an error if the value has no fields", () => { + assertThrows(() => { + dayTimeDurationMap("P"); + }); + }); + + it("throws an error if the value has a T but no time", () => { + assertThrows(() => { + dayTimeDurationMap("P0DT"); + }); + }); +}); + +describe("durationCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals( + durationCanonicalMap({ months: 0, seconds: 0 }), + "PT0S", + ); + assertStrictEquals( + durationCanonicalMap({ months: -0, seconds: -0 }), + "PT0S", + ); + assertStrictEquals( + durationCanonicalMap({ months: 23, seconds: 1234.5 }), + "P1Y11MT20M34.5S", + ); + assertStrictEquals( + durationCanonicalMap({ months: -14, seconds: -277631 }), + "-P1Y2M3DT5H7M11S", + ); + }); + + it("throws an error if the value is not a duration", () => { + assertThrows(() => { + durationCanonicalMap("PT0S"); + }); + assertThrows(() => { + durationCanonicalMap({}); + }); + assertThrows(() => { + durationCanonicalMap(0); + }); + }); + + it("throws an error if the signs of the terms do not match", () => { + assertThrows(() => { + durationCanonicalMap({ months: 1, seconds: -1 }); + }); + }); +}); + +describe("yearMonthDurationCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals( + yearMonthDurationCanonicalMap({ months: 0, seconds: 0 }), + "P0M", + ); + assertStrictEquals( + yearMonthDurationCanonicalMap({ months: -0, seconds: -0 }), + "P0M", + ); + assertStrictEquals( + yearMonthDurationCanonicalMap({ months: 23, seconds: 0 }), + "P1Y11M", + ); + assertStrictEquals( + yearMonthDurationCanonicalMap({ months: -14, seconds: 0 }), + "-P1Y2M", + ); + }); + + it("throws an error if the value is not a duration", () => { + assertThrows(() => { + yearMonthDurationCanonicalMap("P0M"); + }); + assertThrows(() => { + yearMonthDurationCanonicalMap({}); + }); + assertThrows(() => { + yearMonthDurationCanonicalMap(0); + }); + }); + + it("throws an error if the value of seconds is not zero", () => { + assertThrows(() => { + yearMonthDurationCanonicalMap({ months: 1, seconds: 1 }); + }); + }); +}); + +describe("dayTimeDurationCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals( + dayTimeDurationCanonicalMap({ months: 0, seconds: 0 }), + "PT0S", + ); + assertStrictEquals( + dayTimeDurationCanonicalMap({ months: -0, seconds: -0 }), + "PT0S", + ); + assertStrictEquals( + dayTimeDurationCanonicalMap({ months: 0, seconds: 1234.5 }), + "PT20M34.5S", + ); + assertStrictEquals( + dayTimeDurationCanonicalMap({ months: 0, seconds: -277631 }), + "-P3DT5H7M11S", + ); + }); + + it("throws an error if the value is not a duration", () => { + assertThrows(() => { + dayTimeDurationCanonicalMap("PT0S"); + }); + assertThrows(() => { + dayTimeDurationCanonicalMap({}); + }); + assertThrows(() => { + dayTimeDurationCanonicalMap(0); + }); + }); + + it("throws an error if the value of months is not zero", () => { + assertThrows(() => { + dayTimeDurationCanonicalMap({ months: 1, seconds: 1 }); + }); + }); +}); + +describe("daysInMonth", () => { + it("returns the correct value when no year is supplied", () => { + assertStrictEquals(daysInMonth(undefined, 1), 31); + assertStrictEquals(daysInMonth(undefined, 2), 28); + assertStrictEquals(daysInMonth(undefined, 3), 31); + assertStrictEquals(daysInMonth(undefined, 4), 30); + assertStrictEquals(daysInMonth(undefined, 5), 31); + assertStrictEquals(daysInMonth(undefined, 6), 30); + assertStrictEquals(daysInMonth(undefined, 7), 31); + assertStrictEquals(daysInMonth(undefined, 8), 31); + assertStrictEquals(daysInMonth(undefined, 9), 30); + assertStrictEquals(daysInMonth(undefined, 10), 31); + assertStrictEquals(daysInMonth(undefined, 11), 30); + assertStrictEquals(daysInMonth(undefined, 12), 31); + }); + + it("returns the correct value when a year is supplied", () => { + assertStrictEquals(daysInMonth(1972, 12), 31); + assertStrictEquals(daysInMonth(1972, 2), 29); + assertStrictEquals(daysInMonth(1970, 2), 28); + assertStrictEquals(daysInMonth(2000, 2), 29); + assertStrictEquals(daysInMonth(1000, 2), 28); + assertStrictEquals(daysInMonth(800, 2), 29); + assertStrictEquals(daysInMonth(100, 2), 28); + assertStrictEquals(daysInMonth(0, 2), 29); + }); + + it("throws an error if the month is not an integer between 1 and 12", () => { + assertThrows(() => { + daysInMonth(undefined, 0); + }); + assertThrows(() => { + daysInMonth(undefined, 13); + }); + assertThrows(() => { + daysInMonth(undefined, "1"); + }); + }); + + it("throws an error if the year is not undefined or an integer", () => { + assertThrows(() => { + daysInMonth(null, 1); + }); + assertThrows(() => { + daysInMonth(2.31, 1); + }); + assertThrows(() => { + daysInMonth("undefined", 1); + }); + assertThrows(() => { + daysInMonth("1", 1); + }); + }); +}); + +describe("newDateTime", () => { + it("returns a new datetime object", () => { + assertEquals(newDateTime(1, 1, 1, 0, 0, 0, 0), { + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(newDateTime(-1, 12, 31, 23, 59, 59.99, -840), { + year: -1, + month: 12, + day: 31, + hour: 23, + minute: 59, + second: 59.99, + timezoneOffset: -840, + }); + }); + + it("fills absent values with undefined", () => { + assertEquals(newDateTime(), { + year: undefined, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("normalizes the date", () => { + assertEquals(newDateTime(1, 2, 31, 24), { + year: 1, + month: 3, + day: 4, + hour: 0, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(newDateTime(1, undefined, 31, 24), { + year: 1, + month: undefined, + day: 1, + hour: 0, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("forbids out‐of‐range values", () => { + assertThrows(() => { + newDateTime(undefined, 0); + }); + assertThrows(() => { + newDateTime(undefined, 13); + }); + assertThrows(() => { + newDateTime(undefined, undefined, 0); + }); + assertThrows(() => { + newDateTime(undefined, undefined, 32); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, -1); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, 25); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, undefined, -1); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, undefined, 61); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + -1, + ); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + 61, + ); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + -841, + ); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 841, + ); + }); + }); + + it("forbids values of the incorrect type", () => { + assertThrows(() => { + newDateTime(1.23); + }); + assertThrows(() => { + newDateTime("1"); + }); + assertThrows(() => { + newDateTime(undefined, 1.23); + }); + assertThrows(() => { + newDateTime(undefined, "1"); + }); + assertThrows(() => { + newDateTime(undefined, undefined, 1.23); + }); + assertThrows(() => { + newDateTime(undefined, undefined, "1"); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, 1.23); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, "1"); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, undefined, 1.23); + }); + assertThrows(() => { + newDateTime(undefined, undefined, undefined, undefined, "1"); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + "1", + ); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 1.23, + ); + }); + assertThrows(() => { + newDateTime( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "1", + ); + }); + }); +}); + +describe("dateTimePlusDuration", () => { + it("fills absent values with undefined", () => { + assertEquals(dateTimePlusDuration({ months: 0, seconds: 0 }, {}), { + year: undefined, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("correctly adds the duration to the datetime", () => { + // These examples are taken from the X·S·D spec. + const fuzzyResult = dateTimePlusDuration({ + months: 15, + seconds: 457803.3, + }, { + year: 2000, + month: 1, + day: 12, + hour: 12, + minute: 13, + second: 14, + timezoneOffset: 0, + }); + assertObjectMatch(fuzzyResult, { + year: 2001, + month: 4, + day: 17, + hour: 19, + minute: 23, + // seconds must be matched fuzzily + timezoneOffset: 0, + }); + assertAlmostEquals(fuzzyResult.second, 17.3); + assertEquals( + dateTimePlusDuration({ months: -3, seconds: 0 }, { + year: 2000, + month: 1, + }), + { + year: 1999, + month: 10, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + assertEquals( + dateTimePlusDuration({ months: 0, seconds: 118800 }, { + year: 2000, + month: 1, + day: 12, + }), + { + year: 2000, + month: 1, + day: 13, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + assertEquals( + dateTimePlusDuration( + { months: 1, seconds: 0 }, + dateTimePlusDuration({ months: 0, seconds: 86400 }, { + year: 2000, + month: 3, + day: 30, + }), + ), + { + year: 2000, + month: 4, + day: 30, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + assertEquals( + dateTimePlusDuration( + { months: 0, seconds: 86400 }, + dateTimePlusDuration({ months: 1, seconds: 0 }, { + year: 2000, + month: 3, + day: 30, + }), + ), + { + year: 2000, + month: 5, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + }); + + it("follows 28 February with 01 March", () => { + assertEquals( + dateTimePlusDuration({ months: 0, seconds: 86400 }, { + month: 2, + day: 28, + }), + { + year: undefined, + month: 3, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + }); + + it("allows 29 February without a year", () => { + assertEquals( + dateTimePlusDuration({ months: 0, seconds: 1 }, { + month: 2, + day: 29, + }), + { + year: undefined, + month: 2, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + }); + + it("follows 29 February with 01 March", () => { + assertEquals( + dateTimePlusDuration({ months: 0, seconds: 86400 }, { + month: 2, + day: 29, + }), + { + year: undefined, + month: 3, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }, + ); + }); + + it("throws if not provided with a valid duration", () => { + assertThrows(() => { + dateTimePlusDuration({ months: 0 }, {}); + }); + assertThrows(() => { + dateTimePlusDuration({ seconds: 0 }, {}); + }); + assertThrows(() => { + dateTimePlusDuration({}, {}); + }); + assertThrows(() => { + dateTimePlusDuration(0, {}); + }); + assertThrows(() => { + dateTimePlusDuration("PT0S", {}); + }); + }); + + it("throws if not provided with a valid date·time", () => { + assertThrows(() => { + dateTimePlusDuration({ months: 0, seconds: 0 }, 0); + }); + assertThrows(() => { + dateTimePlusDuration({ seconds: 0 }, "2000-01-12T12:13:14Z"); + }); + }); +}); + +describe("timeOnTimeline", () => { + it("gets the time", () => { + assertStrictEquals( + timeOnTimeline({ + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }), + 0, + ); + assertStrictEquals( + timeOnTimeline({ + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 840, + }), + -50400, + ); + assertStrictEquals(timeOnTimeline({}), 62230204800); + assertStrictEquals(timeOnTimeline({ year: 0 }), -86400); + }); + + it("throws if not provided with a valid date·time", () => { + assertThrows(() => { + timeOnTimeline(0); + }); + assertThrows(() => { + timeOnTimeline("2000-01-12T12:13:14Z"); + }); + }); +}); + +describe("yearFragValue", () => { + it("maps the value", () => { + assertStrictEquals(yearFragValue("0001"), 1); + assertStrictEquals(yearFragValue("-0000"), 0); + assertStrictEquals(yearFragValue("-10000"), -10000); + assertStrictEquals(yearFragValue("99999"), 99999); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + yearFragValue(1); + }); + }); + + it("throws an error if the value is not a valid yearFrag", () => { + assertThrows(() => { + yearFragValue(""); + }); + assertThrows(() => { + yearFragValue("1"); + }); + assertThrows(() => { + yearFragValue("01000"); + }); + }); +}); + +describe("monthFragValue", () => { + it("maps the value", () => { + assertStrictEquals(monthFragValue("01"), 1); + assertStrictEquals(monthFragValue("10"), 10); + assertStrictEquals(monthFragValue("12"), 12); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + monthFragValue(1); + }); + }); + + it("throws an error if the value is not a valid monthFrag", () => { + assertThrows(() => { + monthFragValue(""); + }); + assertThrows(() => { + monthFragValue("1"); + }); + assertThrows(() => { + monthFragValue("-1"); + }); + assertThrows(() => { + monthFragValue("-01"); + }); + assertThrows(() => { + monthFragValue("00"); + }); + assertThrows(() => { + monthFragValue("13"); + }); + assertThrows(() => { + monthFragValue("20"); + }); + }); +}); + +describe("dayFragValue", () => { + it("maps the value", () => { + assertStrictEquals(dayFragValue("01"), 1); + assertStrictEquals(dayFragValue("10"), 10); + assertStrictEquals(dayFragValue("19"), 19); + assertStrictEquals(dayFragValue("20"), 20); + assertStrictEquals(dayFragValue("29"), 29); + assertStrictEquals(dayFragValue("30"), 30); + assertStrictEquals(dayFragValue("31"), 31); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + dayFragValue(1); + }); + }); + + it("throws an error if the value is not a valid dayFrag", () => { + assertThrows(() => { + dayFragValue(""); + }); + assertThrows(() => { + dayFragValue("1"); + }); + assertThrows(() => { + dayFragValue("-1"); + }); + assertThrows(() => { + dayFragValue("-01"); + }); + assertThrows(() => { + dayFragValue("00"); + }); + assertThrows(() => { + dayFragValue("32"); + }); + assertThrows(() => { + dayFragValue("40"); + }); + }); +}); + +describe("hourFragValue", () => { + it("maps the value", () => { + assertStrictEquals(hourFragValue("00"), 0); + assertStrictEquals(hourFragValue("01"), 1); + assertStrictEquals(hourFragValue("10"), 10); + assertStrictEquals(hourFragValue("19"), 19); + assertStrictEquals(hourFragValue("20"), 20); + assertStrictEquals(hourFragValue("23"), 23); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + hourFragValue(0); + }); + }); + + it("throws an error if the value is not a valid hourFrag", () => { + assertThrows(() => { + hourFragValue(""); + }); + assertThrows(() => { + hourFragValue("1"); + }); + assertThrows(() => { + hourFragValue("-1"); + }); + assertThrows(() => { + hourFragValue("-01"); + }); + assertThrows(() => { + hourFragValue("24"); + }); + assertThrows(() => { + hourFragValue("30"); + }); + assertThrows(() => { + hourFragValue("01.23"); + }); + }); +}); + +describe("minuteFragValue", () => { + it("maps the value", () => { + assertStrictEquals(minuteFragValue("00"), 0); + assertStrictEquals(minuteFragValue("01"), 1); + assertStrictEquals(minuteFragValue("10"), 10); + assertStrictEquals(minuteFragValue("19"), 19); + assertStrictEquals(minuteFragValue("50"), 50); + assertStrictEquals(minuteFragValue("59"), 59); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + minuteFragValue(0); + }); + }); + + it("throws an error if the value is not a valid minuteFrag", () => { + assertThrows(() => { + minuteFragValue(""); + }); + assertThrows(() => { + minuteFragValue("1"); + }); + assertThrows(() => { + minuteFragValue("-1"); + }); + assertThrows(() => { + minuteFragValue("-01"); + }); + assertThrows(() => { + minuteFragValue("60"); + }); + assertThrows(() => { + minuteFragValue("01.23"); + }); + }); +}); + +describe("secondFragValue", () => { + it("maps the value", () => { + assertStrictEquals(secondFragValue("00"), 0); + assertStrictEquals(secondFragValue("00.00000"), 0); + assertStrictEquals(secondFragValue("01"), 1); + assertStrictEquals(secondFragValue("10"), 10); + assertStrictEquals(secondFragValue("19"), 19); + assertStrictEquals(secondFragValue("50"), 50); + assertStrictEquals(secondFragValue("59"), 59); + assertStrictEquals(secondFragValue("59.99"), 59.99); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + secondFragValue(0); + }); + }); + + it("throws an error if the value is not a valid secondFrag", () => { + assertThrows(() => { + secondFragValue(""); + }); + assertThrows(() => { + secondFragValue("1"); + }); + assertThrows(() => { + secondFragValue("-1"); + }); + assertThrows(() => { + secondFragValue("-01"); + }); + assertThrows(() => { + secondFragValue("60"); + }); + }); +}); + +describe("timezoneFragValue", () => { + it("maps the value", () => { + assertStrictEquals(timezoneFragValue("Z"), 0); + assertStrictEquals(timezoneFragValue("+00:00"), 0); + assertStrictEquals(timezoneFragValue("-00:00"), 0); + assertStrictEquals(timezoneFragValue("+00:01"), 1); + assertStrictEquals(timezoneFragValue("+00:59"), 59); + assertStrictEquals(timezoneFragValue("+01:23"), 83); + assertStrictEquals(timezoneFragValue("-09:23"), -563); + assertStrictEquals(timezoneFragValue("-10:23"), -623); + assertStrictEquals(timezoneFragValue("+13:59"), 839); + assertStrictEquals(timezoneFragValue("+14:00"), 840); + assertStrictEquals(timezoneFragValue("-14:00"), -840); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + timezoneFragValue(0); + }); + }); + + it("throws an error if the value is not a valid timezoneFrag", () => { + assertThrows(() => { + timezoneFragValue(""); + }); + assertThrows(() => { + timezoneFragValue("0"); + }); + assertThrows(() => { + timezoneFragValue("+Z"); + }); + assertThrows(() => { + timezoneFragValue("-Z"); + }); + assertThrows(() => { + timezoneFragValue("Z+00:00"); + }); + assertThrows(() => { + timezoneFragValue("00:00"); + }); + assertThrows(() => { + timezoneFragValue("0000"); + }); + assertThrows(() => { + timezoneFragValue("+0000"); + }); + assertThrows(() => { + timezoneFragValue("840"); + }); + assertThrows(() => { + timezoneFragValue("+840"); + }); + assertThrows(() => { + timezoneFragValue("+-00:00"); + }); + assertThrows(() => { + timezoneFragValue("-+00:00"); + }); + assertThrows(() => { + timezoneFragValue("+14:01"); + }); + assertThrows(() => { + timezoneFragValue("+15:00"); + }); + }); +}); + +describe("dateTimeLexicalMap", () => { + it("maps the value", () => { + assertEquals(dateTimeLexicalMap("0001-01-01T00:00:00.00Z"), { + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(dateTimeLexicalMap("-0001-12-31T24:00:00Z"), { + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(dateTimeLexicalMap("-0000-01-01T00:00:00-00:00"), { + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(dateTimeLexicalMap("0001-01-01T00:00:00.00"), { + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: undefined, + }); + assertEquals(dateTimeLexicalMap("99999-12-31T23:59:59.99+14:00"), { + year: 99999, + month: 12, + day: 31, + hour: 23, + minute: 59, + second: 59.99, + timezoneOffset: 840, + }); + assertEquals(dateTimeLexicalMap("1972-02-29T00:00:00"), { + year: 1972, + month: 2, + day: 29, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: undefined, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + dateTimeLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid dateTime", () => { + assertThrows(() => { + dateTimeLexicalMap(""); + }); + assertThrows(() => { + dateTimeLexicalMap("1"); + }); + assertThrows(() => { + dateTimeLexicalMap("10-10-10T00:00:00"); + }); + assertThrows(() => { + dateTimeLexicalMap("2345-67-89T01:23:45"); + }); + assertThrows(() => { + dateTimeLexicalMap("01000-10-10T10:10:10"); + }); + assertThrows(() => { + dateTimeLexicalMap("0001-01-01T00:00:00.00+0000"); + }); + assertThrows(() => { + dateTimeLexicalMap("0001-01-01T00:00:00.00+14:01"); + }); + assertThrows(() => { + dateTimeLexicalMap("1970-02-29T00:00:00.00"); + }); + }); +}); + +describe("timeLexicalMap", () => { + it("maps the value", () => { + assertEquals(timeLexicalMap("00:00:00.00Z"), { + year: undefined, + month: undefined, + day: undefined, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(timeLexicalMap("24:00:00Z"), { + year: undefined, + month: undefined, + day: undefined, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + assertEquals(timeLexicalMap("23:59:59.99+14:00"), { + year: undefined, + month: undefined, + day: undefined, + hour: 23, + minute: 59, + second: 59.99, + timezoneOffset: 840, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + timeLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid time", () => { + assertThrows(() => { + timeLexicalMap(""); + }); + assertThrows(() => { + timeLexicalMap("1"); + }); + assertThrows(() => { + timeLexicalMap("00:00"); + }); + assertThrows(() => { + timeLexicalMap("23:45:67"); + }); + assertThrows(() => { + timeLexicalMap("-10:10:10"); + }); + assertThrows(() => { + timeLexicalMap("00:00:00.00+0000"); + }); + assertThrows(() => { + timeLexicalMap("00:00:00.00+14:01"); + }); + assertThrows(() => { + timeLexicalMap("T00:00:00"); + }); + }); +}); + +describe("dateLexicalMap", () => { + it("maps the value", () => { + assertEquals(dateLexicalMap("0001-01-01Z"), { + year: 1, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(dateLexicalMap("-0001-12-31Z"), { + year: -1, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(dateLexicalMap("-0000-01-01-00:00"), { + year: 0, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(dateLexicalMap("0001-01-01"), { + year: 1, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(dateLexicalMap("99999-12-31+14:00"), { + year: 99999, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + assertEquals(dateLexicalMap("1972-02-29"), { + year: 1972, + month: 2, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + dateLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid date", () => { + assertThrows(() => { + dateLexicalMap(""); + }); + assertThrows(() => { + dateLexicalMap("1"); + }); + assertThrows(() => { + dateLexicalMap("10-10-10"); + }); + assertThrows(() => { + dateLexicalMap("2345-67-89"); + }); + assertThrows(() => { + dateLexicalMap("01000-10-10"); + }); + assertThrows(() => { + dateLexicalMap("0001-01-01+0000"); + }); + assertThrows(() => { + dateLexicalMap("0001-01-01+14:01"); + }); + assertThrows(() => { + dateLexicalMap("1970-02-29"); + }); + assertThrows(() => { + dateLexicalMap("1972-02-29T00:00:00"); + }); + }); +}); + +describe("gYearMonthLexicalMap", () => { + it("maps the value", () => { + assertEquals(gYearMonthLexicalMap("0001-01Z"), { + year: 1, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearMonthLexicalMap("-0001-12Z"), { + year: -1, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearMonthLexicalMap("-0000-01-00:00"), { + year: 0, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearMonthLexicalMap("0001-01"), { + year: 1, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(gYearMonthLexicalMap("99999-12+14:00"), { + year: 99999, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + assertEquals(gYearMonthLexicalMap("1972-02"), { + year: 1972, + month: 2, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + gYearMonthLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid gYearMonth", () => { + assertThrows(() => { + gYearMonthLexicalMap(""); + }); + assertThrows(() => { + gYearMonthLexicalMap("1"); + }); + assertThrows(() => { + gYearMonthLexicalMap("10-10"); + }); + assertThrows(() => { + gYearMonthLexicalMap("2345-67"); + }); + assertThrows(() => { + gYearMonthLexicalMap("01000-10"); + }); + assertThrows(() => { + gYearMonthLexicalMap("0001-01+0000"); + }); + assertThrows(() => { + gYearMonthLexicalMap("0001-01+14:01"); + }); + assertThrows(() => { + gYearMonthLexicalMap("1970-01-01"); + }); + assertThrows(() => { + gYearMonthLexicalMap("1972-02T00:00:00"); + }); + }); +}); + +describe("gYearLexicalMap", () => { + it("maps the value", () => { + assertEquals(gYearLexicalMap("0001Z"), { + year: 1, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearLexicalMap("-0001Z"), { + year: -1, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearLexicalMap("-0000-00:00"), { + year: 0, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gYearLexicalMap("0001"), { + year: 1, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(gYearLexicalMap("99999+14:00"), { + year: 99999, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + assertEquals(gYearLexicalMap("1972"), { + year: 1972, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + gYearLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid gYear", () => { + assertThrows(() => { + gYearLexicalMap(""); + }); + assertThrows(() => { + gYearLexicalMap("1"); + }); + assertThrows(() => { + gYearLexicalMap("10"); + }); + assertThrows(() => { + gYearLexicalMap("01000"); + }); + assertThrows(() => { + gYearLexicalMap("0001+0000"); + }); + assertThrows(() => { + gYearLexicalMap("0001+14:01"); + }); + assertThrows(() => { + gYearLexicalMap("1970-01"); + }); + assertThrows(() => { + gYearLexicalMap("1972T00:00:00"); + }); + }); +}); + +describe("gMonthDayLexicalMap", () => { + it("maps the value", () => { + assertEquals(gMonthDayLexicalMap("--01-01Z"), { + year: undefined, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthDayLexicalMap("--12-31Z"), { + year: undefined, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthDayLexicalMap("--01-01-00:00"), { + year: undefined, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthDayLexicalMap("--01-01"), { + year: undefined, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(gMonthDayLexicalMap("--12-31+14:00"), { + year: undefined, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + assertEquals(gMonthDayLexicalMap("--02-29"), { + year: undefined, + month: 2, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + gMonthDayLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid gMonthDay", () => { + assertThrows(() => { + gMonthDayLexicalMap(""); + }); + assertThrows(() => { + gMonthDayLexicalMap("1"); + }); + assertThrows(() => { + gMonthDayLexicalMap("--67-89"); + }); + assertThrows(() => { + gMonthDayLexicalMap("---10-10"); + }); + assertThrows(() => { + gMonthDayLexicalMap("--01-01+0000"); + }); + assertThrows(() => { + gMonthDayLexicalMap("--01-01+14:01"); + }); + assertThrows(() => { + gMonthDayLexicalMap("--02-30"); + }); + assertThrows(() => { + gMonthDayLexicalMap("1970-01-01"); + }); + assertThrows(() => { + gMonthDayLexicalMap("--02-29T00:00:00"); + }); + }); +}); + +describe("gDayLexicalMap", () => { + it("maps the value", () => { + assertEquals(gDayLexicalMap("---01Z"), { + year: undefined, + month: undefined, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gDayLexicalMap("---31Z"), { + year: undefined, + month: undefined, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gDayLexicalMap("---01-00:00"), { + year: undefined, + month: undefined, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gDayLexicalMap("---01"), { + year: undefined, + month: undefined, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(gDayLexicalMap("---31+14:00"), { + year: undefined, + month: undefined, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + gDayLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid gDay", () => { + assertThrows(() => { + gDayLexicalMap(""); + }); + assertThrows(() => { + gDayLexicalMap("1"); + }); + assertThrows(() => { + gDayLexicalMap("10"); + }); + assertThrows(() => { + gDayLexicalMap("---89"); + }); + assertThrows(() => { + gDayLexicalMap("----10"); + }); + assertThrows(() => { + gDayLexicalMap("---01+0000"); + }); + assertThrows(() => { + gDayLexicalMap("---01+14:01"); + }); + assertThrows(() => { + gDayLexicalMap("--01-01"); + }); + assertThrows(() => { + gDayLexicalMap("1970-01-01"); + }); + assertThrows(() => { + gDayLexicalMap("---29T00:00:00"); + }); + }); +}); + +describe("gMonthLexicalMap", () => { + it("maps the value", () => { + assertEquals(gMonthLexicalMap("--01Z"), { + year: undefined, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthLexicalMap("--12Z"), { + year: undefined, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthLexicalMap("--01-00:00"), { + year: undefined, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }); + assertEquals(gMonthLexicalMap("--01"), { + year: undefined, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }); + assertEquals(gMonthLexicalMap("--12+14:00"), { + year: undefined, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + gMonthLexicalMap(0); + }); + }); + + it("throws an error if the value is not a valid gMonth", () => { + assertThrows(() => { + gMonthLexicalMap(""); + }); + assertThrows(() => { + gMonthLexicalMap("1"); + }); + assertThrows(() => { + gMonthLexicalMap("01"); + }); + assertThrows(() => { + gMonthLexicalMap("--67"); + }); + assertThrows(() => { + gMonthLexicalMap("---10"); + }); + assertThrows(() => { + gMonthLexicalMap("--01+0000"); + }); + assertThrows(() => { + gMonthLexicalMap("--01+14:01"); + }); + assertThrows(() => { + gMonthLexicalMap("--13"); + }); + assertThrows(() => { + gMonthLexicalMap("1970-01-01"); + }); + assertThrows(() => { + gMonthLexicalMap("--02T00:00:00"); + }); + }); +}); + +describe("yearCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(yearCanonicalFragmentMap(1), "0001"); + assertStrictEquals(yearCanonicalFragmentMap(0), "0000"); + assertStrictEquals(yearCanonicalFragmentMap(-0), "0000"); + assertStrictEquals(yearCanonicalFragmentMap(-1), "-0001"); + assertStrictEquals(yearCanonicalFragmentMap(10000), "10000"); + assertStrictEquals(yearCanonicalFragmentMap(-10000), "-10000"); + }); + + it("throws an error if the value is not an integer", () => { + assertThrows(() => { + yearCanonicalFragmentMap("0001"); + }); + assertThrows(() => { + yearCanonicalFragmentMap(1.2); + }); + }); +}); + +describe("monthCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(monthCanonicalFragmentMap(1), "01"); + assertStrictEquals(monthCanonicalFragmentMap(12), "12"); + }); + + it("throws an error if the value is not a positive integer less than 13", () => { + assertThrows(() => { + monthCanonicalFragmentMap("01"); + }); + assertThrows(() => { + monthCanonicalFragmentMap(0); + }); + assertThrows(() => { + monthCanonicalFragmentMap(-1); + }); + assertThrows(() => { + monthCanonicalFragmentMap(1.2); + }); + assertThrows(() => { + monthCanonicalFragmentMap(13); + }); + }); +}); + +describe("dayCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(dayCanonicalFragmentMap(1), "01"); + assertStrictEquals(dayCanonicalFragmentMap(12), "12"); + assertStrictEquals(dayCanonicalFragmentMap(21), "21"); + assertStrictEquals(dayCanonicalFragmentMap(30), "30"); + assertStrictEquals(dayCanonicalFragmentMap(31), "31"); + }); + + it("throws an error if the value is not a positive integer less than 32", () => { + assertThrows(() => { + dayCanonicalFragmentMap("01"); + }); + assertThrows(() => { + dayCanonicalFragmentMap(0); + }); + assertThrows(() => { + dayCanonicalFragmentMap(-1); + }); + assertThrows(() => { + dayCanonicalFragmentMap(1.2); + }); + assertThrows(() => { + dayCanonicalFragmentMap(32); + }); + }); +}); + +describe("hourCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(hourCanonicalFragmentMap(0), "00"); + assertStrictEquals(hourCanonicalFragmentMap(-0), "00"); + assertStrictEquals(hourCanonicalFragmentMap(1), "01"); + assertStrictEquals(hourCanonicalFragmentMap(12), "12"); + assertStrictEquals(hourCanonicalFragmentMap(21), "21"); + assertStrictEquals(hourCanonicalFragmentMap(23), "23"); + }); + + it("throws an error if the value is not a nonnegative integer less than 24", () => { + assertThrows(() => { + hourCanonicalFragmentMap("00"); + }); + assertThrows(() => { + hourCanonicalFragmentMap(-1); + }); + assertThrows(() => { + hourCanonicalFragmentMap(1.2); + }); + assertThrows(() => { + hourCanonicalFragmentMap(24); + }); + }); +}); + +describe("minuteCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(minuteCanonicalFragmentMap(0), "00"); + assertStrictEquals(minuteCanonicalFragmentMap(-0), "00"); + assertStrictEquals(minuteCanonicalFragmentMap(1), "01"); + assertStrictEquals(minuteCanonicalFragmentMap(12), "12"); + assertStrictEquals(minuteCanonicalFragmentMap(21), "21"); + assertStrictEquals(minuteCanonicalFragmentMap(32), "32"); + assertStrictEquals(minuteCanonicalFragmentMap(45), "45"); + assertStrictEquals(minuteCanonicalFragmentMap(59), "59"); + }); + + it("throws an error if the value is not a nonnegative integer less than 60", () => { + assertThrows(() => { + minuteCanonicalFragmentMap("00"); + }); + assertThrows(() => { + minuteCanonicalFragmentMap(-1); + }); + assertThrows(() => { + minuteCanonicalFragmentMap(1.2); + }); + assertThrows(() => { + minuteCanonicalFragmentMap(60); + }); + }); +}); + +describe("secondCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(secondCanonicalFragmentMap(0), "00"); + assertStrictEquals(secondCanonicalFragmentMap(-0), "00"); + assertStrictEquals(secondCanonicalFragmentMap(1), "01"); + assertStrictEquals(secondCanonicalFragmentMap(1.2), "01.2"); + assertStrictEquals(secondCanonicalFragmentMap(12), "12"); + assertStrictEquals(secondCanonicalFragmentMap(21), "21"); + assertStrictEquals(secondCanonicalFragmentMap(23.1), "23.1"); + assertStrictEquals(secondCanonicalFragmentMap(32), "32"); + assertStrictEquals(secondCanonicalFragmentMap(45), "45"); + assertStrictEquals(secondCanonicalFragmentMap(59), "59"); + }); + + it("throws an error if the value is not a nonnegative decimal number less than 60", () => { + assertThrows(() => { + secondCanonicalFragmentMap("00"); + }); + assertThrows(() => { + secondCanonicalFragmentMap(-1); + }); + assertThrows(() => { + secondCanonicalFragmentMap(60); + }); + }); +}); + +describe("timezoneCanonicalFragmentMap", () => { + it("maps the value", () => { + assertStrictEquals(timezoneCanonicalFragmentMap(0), "Z"); + assertStrictEquals(timezoneCanonicalFragmentMap(-0), "Z"); + assertStrictEquals(timezoneCanonicalFragmentMap(1), "+00:01"); + assertStrictEquals(timezoneCanonicalFragmentMap(-1), "-00:01"); + assertStrictEquals(timezoneCanonicalFragmentMap(839), "+13:59"); + assertStrictEquals(timezoneCanonicalFragmentMap(-839), "-13:59"); + assertStrictEquals(timezoneCanonicalFragmentMap(840), "+14:00"); + assertStrictEquals(timezoneCanonicalFragmentMap(-840), "-14:00"); + }); + + it("throws an error if the value is not an integer between -840 and 840 inclusive", () => { + assertThrows(() => { + timezoneCanonicalFragmentMap("00"); + }); + assertThrows(() => { + timezoneCanonicalFragmentMap(-841); + }); + assertThrows(() => { + timezoneCanonicalFragmentMap(841); + }); + assertThrows(() => { + timezoneCanonicalFragmentMap(1.2); + }); + }); +}); + +describe("dateTimeCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + dateTimeCanonicalMap({ + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }), + "0001-01-01T00:00:00Z", + ); + assertEquals( + dateTimeCanonicalMap({ + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }), + "0000-01-01T00:00:00Z", + ); + assertEquals( + dateTimeCanonicalMap({ + year: -0, + month: 1, + day: 1, + hour: -0, + minute: -0, + second: -0, + timezoneOffset: -0, + }), + "0000-01-01T00:00:00Z", + ); + assertEquals( + dateTimeCanonicalMap({ + year: 1, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: undefined, + }), + "0001-01-01T00:00:00", + ); + assertEquals( + dateTimeCanonicalMap({ + year: 99999, + month: 12, + day: 31, + hour: 23, + minute: 59, + second: 59.99, + timezoneOffset: 840, + }), + "99999-12-31T23:59:59.99+14:00", + ); + assertEquals( + dateTimeCanonicalMap({ + year: 1972, + month: 2, + day: 29, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: undefined, + }), + "1972-02-29T00:00:00", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + dateTimeCanonicalMap(0); + }); + assertThrows(() => { + dateTimeCanonicalMap("0001-01-01T00:00:00Z"); + }); + assertThrows(() => { + dateTimeCanonicalMap({ + year: null, + month: null, + day: null, + hour: null, + minute: null, + second: null, + }); + }); + }); + + it("throws an error if the value is not a complete dateTime value", () => { + assertThrows(() => { + dateTimeCanonicalMap({}); + }); + assertThrows(() => { + dateTimeCanonicalMap({ + year: 1972, + month: 12, + day: 31, + timezoneOffset: 0, + }); + }); + }); +}); + +describe("timeCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + timeCanonicalMap({ + year: undefined, + month: undefined, + day: undefined, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }), + "00:00:00Z", + ); + assertEquals( + timeCanonicalMap({ + year: undefined, + month: undefined, + day: undefined, + hour: -0, + minute: -0, + second: -0, + timezoneOffset: -0, + }), + "00:00:00Z", + ); + assertEquals( + timeCanonicalMap({ + year: undefined, + month: undefined, + day: undefined, + hour: 23, + minute: 59, + second: 59.99, + timezoneOffset: 840, + }), + "23:59:59.99+14:00", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + timeCanonicalMap(0); + }); + assertThrows(() => { + timeCanonicalMap("00:00:00Z"); + }); + assertThrows(() => { + timeCanonicalMap({ + year: undefined, + month: undefined, + day: undefined, + hour: null, + minute: null, + second: null, + }); + }); + }); + + it("throws an error if the value is not a complete time value", () => { + assertThrows(() => { + timeCanonicalMap({}); + }); + assertThrows(() => { + timeCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + timeCanonicalMap({ + year: undefined, + month: undefined, + day: undefined, + hour: 0, + minute: 0, + second: undefined, + timezoneOffset: 0, + }); + }); + }); +}); + +describe("dateCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + dateCanonicalMap({ + year: 1, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0001-01-01Z", + ); + assertEquals( + dateCanonicalMap({ + year: 0, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0000-01-01Z", + ); + assertEquals( + dateCanonicalMap({ + year: -0, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: -0, + }), + "0000-01-01Z", + ); + assertEquals( + dateCanonicalMap({ + year: 1, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "0001-01-01", + ); + assertEquals( + dateCanonicalMap({ + year: 99999, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "99999-12-31+14:00", + ); + assertEquals( + dateCanonicalMap({ + year: 1972, + month: 2, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "1972-02-29", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + dateCanonicalMap(0); + }); + assertThrows(() => { + dateCanonicalMap("0001-01-01Z"); + }); + assertThrows(() => { + dateCanonicalMap({ + year: null, + month: null, + day: null, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete date value", () => { + assertThrows(() => { + dateCanonicalMap({}); + }); + assertThrows(() => { + dateCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + dateCanonicalMap({ year: 1972 }); + }); + }); +}); + +describe("gYearMonthCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + gYearMonthCanonicalMap({ + year: 1, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0001-01Z", + ); + assertEquals( + gYearMonthCanonicalMap({ + year: 0, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0000-01Z", + ); + assertEquals( + gYearMonthCanonicalMap({ + year: -0, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: -0, + }), + "0000-01Z", + ); + assertEquals( + gYearMonthCanonicalMap({ + year: 1, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "0001-01", + ); + assertEquals( + gYearMonthCanonicalMap({ + year: 99999, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "99999-12+14:00", + ); + assertEquals( + gYearMonthCanonicalMap({ + year: 1972, + month: 2, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "1972-02", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + gYearMonthCanonicalMap(0); + }); + assertThrows(() => { + gYearMonthCanonicalMap("0001-01Z"); + }); + assertThrows(() => { + gYearMonthCanonicalMap({ + year: null, + month: null, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete yearMonth value", () => { + assertThrows(() => { + gYearMonthCanonicalMap({}); + }); + assertThrows(() => { + gYearMonthCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + gYearMonthCanonicalMap({ year: 1972, month: 12, day: 31 }); + }); + assertThrows(() => { + gYearMonthCanonicalMap({ year: 1972 }); + }); + }); +}); + +describe("gYearCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + gYearCanonicalMap({ + year: 1, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0001Z", + ); + assertEquals( + gYearCanonicalMap({ + year: 0, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "0000Z", + ); + assertEquals( + gYearCanonicalMap({ + year: -0, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: -0, + }), + "0000Z", + ); + assertEquals( + gYearCanonicalMap({ + year: 1, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "0001", + ); + assertEquals( + gYearCanonicalMap({ + year: 99999, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "99999+14:00", + ); + assertEquals( + gYearCanonicalMap({ + year: 1972, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "1972", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + gYearCanonicalMap(0); + }); + assertThrows(() => { + gYearCanonicalMap("0001Z"); + }); + assertThrows(() => { + gYearCanonicalMap({ + year: null, + month: undefined, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete gYear value", () => { + assertThrows(() => { + gYearCanonicalMap({}); + }); + assertThrows(() => { + gYearCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + gYearCanonicalMap({ year: 1972, month: 12, day: 31 }); + }); + }); +}); + +describe("gMonthDayCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + gMonthDayCanonicalMap({ + year: undefined, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "--01-01Z", + ); + assertEquals( + gMonthDayCanonicalMap({ + year: undefined, + month: 1, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "--01-01", + ); + assertEquals( + gMonthDayCanonicalMap({ + year: undefined, + month: 12, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "--12-31+14:00", + ); + assertEquals( + gMonthDayCanonicalMap({ + year: undefined, + month: 2, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "--02-29", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + gMonthDayCanonicalMap(0); + }); + assertThrows(() => { + gMonthDayCanonicalMap("--01-01Z"); + }); + assertThrows(() => { + gMonthDayCanonicalMap({ + year: undefined, + month: null, + day: null, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete gMonthDay value", () => { + assertThrows(() => { + gMonthDayCanonicalMap({}); + }); + assertThrows(() => { + gMonthDayCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + gMonthDayCanonicalMap({ year: 1972, month: 12, day: 31 }); + }); + assertThrows(() => { + gMonthDayCanonicalMap({ day: 31 }); + }); + }); +}); + +describe("gDayCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + gDayCanonicalMap({ + year: undefined, + month: undefined, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "---01Z", + ); + assertEquals( + gDayCanonicalMap({ + year: undefined, + month: undefined, + day: 1, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "---01", + ); + assertEquals( + gDayCanonicalMap({ + year: undefined, + month: undefined, + day: 31, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "---31+14:00", + ); + assertEquals( + gDayCanonicalMap({ + year: undefined, + month: undefined, + day: 29, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "---29", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + gDayCanonicalMap(0); + }); + assertThrows(() => { + gDayCanonicalMap("---01Z"); + }); + assertThrows(() => { + gDayCanonicalMap({ + year: undefined, + month: undefined, + day: null, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete gDay value", () => { + assertThrows(() => { + gDayCanonicalMap({}); + }); + assertThrows(() => { + gDayCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + gDayCanonicalMap({ year: 1972, month: 12, day: 31 }); + }); + }); +}); + +describe("gMonthCanonicalMap", () => { + it("maps the value", () => { + assertEquals( + gMonthCanonicalMap({ + year: undefined, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 0, + }), + "--01Z", + ); + assertEquals( + gMonthCanonicalMap({ + year: undefined, + month: 1, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "--01", + ); + assertEquals( + gMonthCanonicalMap({ + year: undefined, + month: 12, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: 840, + }), + "--12+14:00", + ); + assertEquals( + gMonthCanonicalMap({ + year: undefined, + month: 2, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + timezoneOffset: undefined, + }), + "--02", + ); + }); + + it("throws an error if the value is not a date/timeSevenPropertyModel value", () => { + assertThrows(() => { + gMonthCanonicalMap(0); + }); + assertThrows(() => { + gMonthCanonicalMap("--01Z"); + }); + assertThrows(() => { + gMonthCanonicalMap({ + year: undefined, + month: null, + day: undefined, + hour: undefined, + minute: undefined, + second: undefined, + }); + }); + }); + + it("throws an error if the value is not a complete gMonth value", () => { + assertThrows(() => { + gMonthCanonicalMap({}); + }); + assertThrows(() => { + gMonthCanonicalMap({ + year: 1972, + month: 12, + day: 31, + hour: 0, + minute: 0, + second: 0, + timezoneOffset: 0, + }); + }); + assertThrows(() => { + gMonthCanonicalMap({ year: 1972, month: 12, day: 31 }); + }); + }); +}); + +describe("stringLexicalMap", () => { + it("maps the value", () => { + assertStrictEquals(stringLexicalMap(""), ""); + assertStrictEquals(stringLexicalMap("etaoin"), "etaoin"); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + stringLexicalMap({}); + }); + assertThrows(() => { + stringLexicalMap(0); + }); + assertThrows(() => { + stringLexicalMap(new String()); + }); + }); +}); + +describe("booleanLexicalMap", () => { + it("maps the value", () => { + assertStrictEquals(booleanLexicalMap("0"), false); + assertStrictEquals(booleanLexicalMap("false"), false); + assertStrictEquals(booleanLexicalMap("1"), true); + assertStrictEquals(booleanLexicalMap("true"), true); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + booleanLexicalMap({}); + }); + assertThrows(() => { + booleanLexicalMap(0); + }); + assertThrows(() => { + booleanLexicalMap(false); + }); + }); +}); + +describe("stringCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(stringCanonicalMap(""), ""); + assertStrictEquals(stringCanonicalMap("etaoin"), "etaoin"); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + stringCanonicalMap({}); + }); + assertThrows(() => { + stringCanonicalMap(0); + }); + assertThrows(() => { + stringCanonicalMap(new String()); + }); + }); +}); + +describe("booleanCanonicalMap", () => { + it("maps the value", () => { + assertStrictEquals(booleanCanonicalMap(false), "false"); + assertStrictEquals(booleanCanonicalMap(true), "true"); + }); + + it("throws an error if the value is not a boolean", () => { + assertThrows(() => { + booleanCanonicalMap({}); + }); + assertThrows(() => { + booleanCanonicalMap(0); + }); + assertThrows(() => { + booleanCanonicalMap("false"); + }); + assertThrows(() => { + booleanCanonicalMap(new Boolean()); + }); + }); +}); + +describe("hexBinaryMap", () => { + it("maps the value", () => { + assertEquals( + new Uint8Array(hexBinaryMap("")), + new Uint8Array([]), + ); + assertEquals( + new Uint8Array(hexBinaryMap("4b494249")), + new Uint8Array([75, 73, 66, 73]), + ); + assertEquals( + new Uint8Array(hexBinaryMap("626173653634")), + new Uint8Array([98, 97, 115, 101, 54, 52]), + ); + assertEquals( + new Uint8Array(hexBinaryMap("14FB9C03D97E")), + new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9, 0x7E]), + ); + }); + + it("throws an error if the value is not a string", () => { + assertThrows(() => { + hexBinaryMap({}); + }); + assertThrows(() => { + hexBinaryMap(0); + }); + assertThrows(() => { + hexBinaryMap(new ArrayBuffer()); + }); + }); + + it("throws an error if the value is not a valid hexBinary", () => { + assertThrows(() => { + hexBinaryMap("A"); + }); + assertThrows(() => { + hexBinaryMap("EG"); + }); + }); +}); + +describe("hexBinaryCanonical", () => { + it("maps the value", () => { + assertStrictEquals( + hexBinaryCanonical(new ArrayBuffer()), + "", + ); + assertEquals( + hexBinaryCanonical(new Uint8Array([75, 73, 66, 73]).buffer), + "4B494249", + ); + assertEquals( + hexBinaryCanonical( + new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, + ), + "626173653634", + ); + assertEquals( + hexBinaryCanonical( + new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9, 0x7E]).buffer, + ), + "14FB9C03D97E", + ); + }); + + it("throws an error if the value is not an array buffer", () => { + assertThrows(() => { + hexBinaryCanonical({}); + }); + assertThrows(() => { + hexBinaryCanonical(0); + }); + assertThrows(() => { + hexBinaryCanonical(""); + }); + assertThrows(() => { + hexBinaryCanonical(new Uint8Array([])); + }); + }); +}); diff --git a/xsd/productions.js b/xsd/productions.js index 86a8e2f..84de96b 100644 --- a/xsd/productions.js +++ b/xsd/productions.js @@ -1,3 +1,4 @@ +//deno-lint-ignore-file no-unused-vars // ♓️🪡 सूत्र ∷ xsd/productions.js // ==================================================================== //