X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/31c8b17d21df315e62587419c88bee178b2b44f0..e1cb83c479df2a3e4a5e918867a135ff9dde8121:/string.js diff --git a/string.js b/string.js index 6d4d229..ddc05d7 100644 --- a/string.js +++ b/string.js @@ -1,15 +1,19 @@ -// ♓🌟 Piscēs ∷ string.js -// ==================================================================== -// -// Copyright © 2022–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 . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ string.js + * + * Copyright © 2022–2023, 2025 Lady [@ Ladys 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 { bind, call, + completesNormally, createArrowFunction, createCallableFunction, identity, @@ -19,10 +23,16 @@ import { stringIteratorFunction, } from "./iterable.js"; import { + defineOwnDataProperty, defineOwnProperties, getOwnPropertyDescriptors, + objectCreate, + setPropertyValues, setPrototype, } from "./object.js"; +import { sameValue, toLength, UNDEFINED } from "./value.js"; + +const PISCĒS = "♓🧩 Piscēs"; const RE = RegExp; const { prototype: rePrototype } = RE; @@ -33,14 +43,14 @@ const { exec: reExec } = rePrototype; export const { /** - * A `RegExp`like object which only matches entire strings, and may + * A `RegExp´‐like object which only matches entire strings, and may * have additional constraints specified. * * Matchers are callable objects and will return true if they are * called with a string that they match, and false otherwise. - * Matchers will always return false if called with nonstrings, - * although other methods like `::exec` coerce their arguments and - * may still return true. + * Matchers will always return false if called with nonstrings, altho + * other methods like `::exec´ coerce their arguments and may still + * return true. */ Matcher, } = (() => { @@ -63,17 +73,28 @@ export const { Object.getOwnPropertyDescriptor(rePrototype, "sticky").get; const getUnicode = Object.getOwnPropertyDescriptor(rePrototype, "unicode").get; + const getUnicodeSets = + Object.getOwnPropertyDescriptor(rePrototype, "unicodeSets").get; + /** + * The internal implementation of `Matcher´. + * + * ※ This class extends the identity function to enable the addition + * of private fields to the callable matcher function it constructs. + * + * ※ This class is not exposed. + */ const Matcher = class extends identity { #constraint; #regExp; /** - * Constructs a new `Matcher` from the provided source. + * Constructs a new `Matcher´ from the provided source. * * If the provided source is a regular expression, then it must - * have the unicode flag set. Otherwise, it is interpreted as the - * string source of a regular expression with the unicode flag set. + * have either the unicode flag set or the unicode sets flag set. + * Otherwise, it is interpreted as the string source of a regular + * expression with the unicode flag set. * * Other flags are taken from the provided regular expression * object, if any are present. @@ -84,83 +105,93 @@ export const { * third argument. If provided, it will be called with three * arguments whenever a match appears successful: first, the string * being matched, second, the match result, and third, the - * `Matcher` object itself. If the return value of this call is + * `Matcher´ object itself. If the return value of this call is * falsey, then the match will be considered a failure. * * ☡ If the provided source regular expression uses nongreedy * quantifiers, it may not match the whole string even if a match * with the whole string is possible. Surround the regular - * expression with `^(?:` and `)$` if you don’t want nongreedy + * expression with `^(?:´ and `)$´ if you don¦t want nongreedy * regular expressions to fail when shorter matches are possible. */ - constructor(source, name = undefined, constraint = null) { + constructor(source, name = UNDEFINED, constraint = null) { super( ($) => { if (typeof $ !== "string") { // The provided value is not a string. return false; } else { - // The provided value is a string. Set the `.lastIndex` of - // the regular expression to 0 and see if the first attempt - // at a match matches the whole string and passes the - // provided constraint (if present). + // The provided value is a string. + // + // Set the `.lastIndex´ of the regular expression to 0, and + // see if the first attempt at a match successfully matches + // the whole string and passes the provided constraint (if + // present). regExp.lastIndex = 0; const result = call(reExec, regExp, [$]); - return result?.[0] === $ && - (constraint === null || constraint($, result, this)); + return result?.[0] === $ + && (constraint === null || constraint($, result, this)); } }, ); const regExp = this.#regExp = (() => { - try { - call(reExec, source, [""]); // throws if source not a RegExp - } catch { - return new RE(`${source}`, "u"); - } - const unicode = call(getUnicode, source, []); - if (!unicode) { - // The provided regular expression does not have a unicode - // flag. - throw new TypeError( - `Piscēs: Cannot create Matcher from non‐Unicode RegExp: ${source}`, - ); + if (completesNormally(() => call(reExec, source, [""]))) { + // The provided source is a `RegExp´. + if ( + !call(getUnicode, source, []) + && !call(getUnicodeSets, source, []) + ) { + // The provided regular expression does not have a unicode + // flag or unicode sets flag. + throw new TypeError( + `${PISCĒS}: Cannot create Matcher from non‐Unicode RegExp: ${source}`, + ); + } else { + // The provided regular expression has a unicode flag or + // unicode sets flag. + return new RE(source); + } } else { - // The provided regular expression has a unicode flag. - return new RE(source); + // The provided source is not a `RegExp´. + // + // Create one using it as the source string. + return new RE(`${source}`, "u"); } })(); if (constraint !== null && typeof constraint !== "function") { throw new TypeError( - "Piscēs: Cannot construct Matcher: Constraint is not callable.", + `${PISCĒS}: Cannot construct Matcher: Constraint is not callable.`, ); } else { this.#constraint = constraint; return defineOwnProperties( setPrototype(this, matcherPrototype), { - lastIndex: { + lastIndex: setPropertyValues(objectCreate(null), { configurable: false, enumerable: false, value: 0, writable: false, - }, - name: { - value: name != null + }), + name: defineOwnDataProperty( + objectCreate(null), + "value", + name != null ? `${name}` : `Matcher(${call(reToString, regExp, [])})`, - }, + ), }, ); } } - /** Gets whether the dot‐all flag is present on this `Matcher`. */ + /** Gets whether the dot‐all flag is present on this `Matcher´. */ get dotAll() { return call(getDotAll, this.#regExp, []); } /** - * Executes this `Matcher` on the provided value and returns the + * Executes this `Matcher´ on the provided value and returns the * result if there is a match, or null otherwise. * * Matchers only match if they can match the entire value on the @@ -177,8 +208,8 @@ export const { regExp.lastIndex = 0; const result = call(reExec, regExp, [string]); if ( - result?.[0] === string && - (constraint === null || constraint(string, result, this)) + result?.[0] === string + && (constraint === null || constraint(string, result, this)) ) { // The entire string was matched and the constraint, if // present, returned a truthy value. @@ -191,76 +222,91 @@ export const { } /** - * Gets the flags present on this `Matcher`. + * Gets the flags present on this `Matcher´. * - * ※ This needs to be defined because the internal `RegExp` object - * may have flags which are not yet recognized by ♓🌟 Piscēs. + * ※ This needs to be defined because the internal `RegExp´ object + * may have flags which are not yet recognized by ♓🧩 Piscēs. */ get flags() { return call(getFlags, this.#regExp, []); } - /** Gets whether the global flag is present on this `Matcher`. */ + /** Gets whether the global flag is present on this `Matcher´. */ get global() { return call(getGlobal, this.#regExp, []); } /** - * Gets whether the has‐indices flag is present on this `Matcher`. + * Gets whether the has‐indices flag is present on this `Matcher´. */ get hasIndices() { return call(getHasIndices, this.#regExp, []); } /** - * Gets whether the ignore‐case flag is present on this `Matcher`. + * Gets whether the ignore‐case flag is present on this `Matcher´. */ get ignoreCase() { return call(getIgnoreCase, this.#regExp, []); } /** - * Gets whether the multiline flag is present on this `Matcher`. + * Gets whether the multiline flag is present on this `Matcher´. */ get multiline() { return call(getMultiline, this.#regExp, []); } - /** Gets the regular expression source for this `Matcher`. */ + /** Gets the regular expression source for this `Matcher´. */ get source() { return call(getSource, this.#regExp, []); } - /** Gets whether the sticky flag is present on this `Matcher`. */ + /** Gets whether the sticky flag is present on this `Matcher´. */ get sticky() { return call(getSticky, this.#regExp, []); } /** - * Gets whether the unicode flag is present on this `Matcher`. - * - * ※ This will always be true. + * Gets whether the unicode flag is present on this `Matcher´. */ get unicode() { return call(getUnicode, this.#regExp, []); } + + /** + * Gets whether the unicode sets flag is present on this `Matcher´. + */ + get unicodeSets() { + return call(getUnicodeSets, this.#regExp, []); + } }; - const matcherConstructor = defineOwnProperties( + const matcherConstructor = Object.defineProperties( class extends RegExp { constructor(...args) { return new Matcher(...args); } }, { - name: { value: "Matcher" }, - length: { value: 1 }, + name: defineOwnDataProperty( + Object.create(null), + "value", + "Matcher", + ), + length: defineOwnDataProperty(Object.create(null), "value", 1), }, ); const matcherPrototype = defineOwnProperties( matcherConstructor.prototype, getOwnPropertyDescriptors(Matcher.prototype), - { constructor: { value: matcherConstructor } }, + { + constructor: defineOwnDataProperty( + Object.create(null), + "value", + matcherConstructor, + ), + }, ); return { Matcher: matcherConstructor }; @@ -299,11 +345,30 @@ export const { }; })(); +/** + * Returns −0 if the provided argument is `"-0"´; returns a number + * representing the index if the provided argument is a canonical + * numeric index string; otherwise, returns undefined. + * + * There is no clamping of the numeric index, but note that numbers + * above 2^53 − 1 are not safe nor valid integer indices. + */ +export const canonicalNumericIndexString = ($) => { + if (typeof $ !== "string") { + return UNDEFINED; + } else if ($ === "-0") { + return -0; + } else { + const n = +$; + return $ === `${n}` ? n : UNDEFINED; + } +}; + export const { /** * Returns an iterator over the codepoints in the string representation * of the provided value according to the algorithm of - * `String::[Symbol.iterator]`. + * `String::[Symbol.iterator]´. */ characters, @@ -370,55 +435,93 @@ export const { /** * Returns the character at the provided position in the string * representation of the provided value according to the algorithm of - * `String::codePointAt`. + * `String::codePointAt´. */ export const getCharacter = ($, pos) => { const codepoint = getCodepoint($, pos); return codepoint == null - ? undefined + ? UNDEFINED : stringFromCodepoints(codepoint); }; -/** - * Returns the code unit at the provided position in the string - * representation of the provided value according to the algorithm of - * `String::charAt`, except that out‐of‐bounds values return undefined - * in place of nan. - */ export const { + /** + * Returns the code unit at the provided position in the string + * representation of the provided value according to the algorithm of + * `String::charAt´, except that out‐of‐bounds values return + * undefined in place of nan. + */ getCodeUnit, + /** + * Returns a string created from the provided code units. + * + * ※ This is effectively an alias for `String.fromCharCode´, but + * with the same error behaviour as `String.fromCodePoint´. + * + * ☡ This function throws an error if provided with an argument which + * is not an integral number from 0 to FFFF₁₆ inclusive. + */ + stringFromCodeUnits, + /** * Returns the result of catenating the string representations of the * provided values, returning a new string according to the algorithm - * of `String::concat`. + * of `String::concat´. * * ※ If no arguments are given, this function returns the empty * string. This is different behaviour than if an explicit undefined * first argument is given, in which case the resulting string will - * begin with `"undefined"`. + * begin with `"undefined"´. */ stringCatenate, } = (() => { + const { fromCharCode } = String; const { charCodeAt, concat } = String.prototype; - const { isNaN: isNan } = Number; + const { + isInteger: isIntegralNumber, + isNaN: isNan, + } = Number; return { getCodeUnit: ($, n) => { const codeUnit = call(charCodeAt, $, [n]); - return isNan(codeUnit) ? undefined : codeUnit; + return isNan(codeUnit) ? UNDEFINED : codeUnit; }, - stringCatenate: defineOwnProperties( + stringCatenate: Object.defineProperties( (...args) => call(concat, "", args), { name: { value: "stringCatenate" }, length: { value: 2 } }, ), + stringFromCodeUnits: Object.defineProperties( + (...codeUnits) => { + for (let index = 0; index < codeUnits.length; ++index) { + // Iterate over each provided code unit and throw if it is + // out of range. + const nextCU = +codeUnits[index]; + if ( + !isIntegralNumber(nextCU) || nextCU < 0 || nextCU > 0xFFFF + ) { + // The code unit is not an integral number between 0 and + // 0xFFFF; this is an error. + throw new RangeError( + `${PISCĒS}: Code unit out of range: ${nextCU}.`, + ); + } else { + // The code unit is acceptable. + /* do nothing */ + } + } + return call(fromCharCode, UNDEFINED, codeUnits); + }, + { name: { value: "stringFromCodeUnits" }, length: { value: 1 } }, + ), }; })(); /** * Returns the codepoint at the provided position in the string * representation of the provided value according to the algorithm of - * `String::codePointAt`. + * `String::codePointAt´. */ export const getCodepoint = createCallableFunction( stringPrototype.codePointAt, @@ -428,7 +531,7 @@ export const getCodepoint = createCallableFunction( /** * Returns the index of the first occurrence of the search string in * the string representation of the provided value according to the - * algorithm of `String::indexOf`. + * algorithm of `String::indexOf´. */ export const getFirstSubstringIndex = createCallableFunction( stringPrototype.indexOf, @@ -438,17 +541,47 @@ export const getFirstSubstringIndex = createCallableFunction( /** * Returns the index of the last occurrence of the search string in the * string representation of the provided value according to the - * algorithm of `String::lastIndexOf`. + * algorithm of `String::lastIndexOf´. */ export const getLastSubstringIndex = createCallableFunction( stringPrototype.lastIndexOf, { name: "getLastSubstringIndex" }, ); +/** Returns whether the provided value is an array index. */ +export const isArrayIndexString = ($) => { + const value = canonicalNumericIndexString($); + if (value !== UNDEFINED) { + // The provided value is a canonical numeric index string. + // + // Return whether it is in range for array indices. + return sameValue(value, 0) + || value === toLength(value) && value > 0 && value < -1 >>> 0; + } else { + // The provided value is not a canonical numeric index string. + return false; + } +}; + +/** Returns whether the provided value is an integer index string. */ +export const isIntegerIndexString = ($) => { + const value = canonicalNumericIndexString($); + if (value !== UNDEFINED) { + // The provided value is a canonical numeric index string. + // + // Return whether it is in range for integer indices. + return sameValue(value, 0) + || value === toLength(value) && value > 0; + } else { + // The provided value is not a canonical numeric index string. + return false; + } +}; + /** * Returns the result of joining the provided iterable. * - * If no separator is provided, it defaults to ",". + * If no separator is provided, it defaults to `","´. * * If a value is nullish, it will be stringified as the empty string. */ @@ -458,7 +591,7 @@ export const join = (() => { call( arrayJoin, [...$], - [separator === undefined ? "," : `${separator}`], + [separator === UNDEFINED ? "," : `${separator}`], ); return join; })(); @@ -467,58 +600,16 @@ export const join = (() => { * Returns a string created from the raw value of the tagged template * literal. * - * ※ This is effectively an alias for `String.raw`. + * ※ This is effectively an alias for `String.raw´. */ export const rawString = createArrowFunction(String.raw, { name: "rawString", }); -export const { - /** - * Returns a string created from the provided code units. - * - * ※ This is effectively an alias for `String.fromCharCode`, but - * with the same error behaviour as `String.fromCodePoint`. - * - * ☡ This function throws an error if provided with an argument which - * is not an integral number from 0 to FFFF₁₆ inclusive. - */ - stringFromCodeUnits, -} = (() => { - const { fromCharCode } = String; - const { isInteger: isIntegralNumber } = Number; - - return { - stringFromCodeUnits: defineOwnProperties( - (...codeUnits) => { - for (let index = 0; index < codeUnits.length; ++index) { - // Iterate over each provided code unit and throw if it is - // out of range. - const nextCU = +codeUnits[index]; - if ( - !isIntegralNumber(nextCU) || nextCU < 0 || nextCU > 0xFFFF - ) { - // The code unit is not an integral number between 0 and - // 0xFFFF. - throw new RangeError( - `Piscēs: Code unit out of range: ${nextCU}.`, - ); - } else { - // The code unit is acceptable. - /* do nothing */ - } - } - return call(fromCharCode, undefined, codeUnits); - }, - { name: { value: "stringFromCodeUnits" }, length: { value: 1 } }, - ), - }; -})(); - /** * Returns a string created from the provided codepoints. * - * ※ This is effectively an alias for `String.fromCodePoint`. + * ※ This is effectively an alias for `String.fromCodePoint´. * * ☡ This function throws an error if provided with an argument which * is not an integral number from 0 to 10FFFF₁₆ inclusive. @@ -554,7 +645,7 @@ export const splitOnCommas = ($) => /** * Returns whether the string representation of the provided value ends * with the provided search string according to the algorithm of - * `String::endsWith`. + * `String::endsWith´. */ export const stringEndsWith = createCallableFunction( stringPrototype.endsWith, @@ -564,7 +655,7 @@ export const stringEndsWith = createCallableFunction( /** * Returns whether the string representation of the provided value * contains the provided search string according to the algorithm of - * `String::includes`. + * `String::includes´. */ export const stringIncludes = createCallableFunction( stringPrototype.includes, @@ -574,7 +665,7 @@ export const stringIncludes = createCallableFunction( /** * Returns the result of matching the string representation of the * provided value with the provided matcher according to the algorithm - * of `String::match`. + * of `String::match´. */ export const stringMatch = createCallableFunction( stringPrototype.match, @@ -584,7 +675,7 @@ export const stringMatch = createCallableFunction( /** * Returns the result of matching the string representation of the * provided value with the provided matcher according to the algorithm - * of `String::matchAll`. + * of `String::matchAll´. */ export const stringMatchAll = createCallableFunction( stringPrototype.matchAll, @@ -593,7 +684,7 @@ export const stringMatchAll = createCallableFunction( /** * Returns the normalized form of the string representation of the - * provided value according to the algorithm of `String::normalize`. + * provided value according to the algorithm of `String::normalize´. */ export const stringNormalize = createCallableFunction( stringPrototype.normalize, @@ -603,7 +694,7 @@ export const stringNormalize = createCallableFunction( /** * Returns the result of padding the end of the string representation * of the provided value padded until it is the desired length - * according to the algorithm of `String::padEnd`. + * according to the algorithm of `String::padEnd´. */ export const stringPadEnd = createCallableFunction( stringPrototype.padEnd, @@ -613,7 +704,7 @@ export const stringPadEnd = createCallableFunction( /** * Returns the result of padding the start of the string representation * of the provided value padded until it is the desired length - * according to the algorithm of `String::padStart`. + * according to the algorithm of `String::padStart´. */ export const stringPadStart = createCallableFunction( stringPrototype.padStart, @@ -623,7 +714,7 @@ export const stringPadStart = createCallableFunction( /** * Returns the result of repeating the string representation of the * provided value the provided number of times according to the - * algorithm of `String::repeat`. + * algorithm of `String::repeat´. */ export const stringRepeat = createCallableFunction( stringPrototype.repeat, @@ -633,7 +724,7 @@ export const stringRepeat = createCallableFunction( /** * Returns the result of replacing the string representation of the * provided value with the provided replacement, using the provided - * matcher and according to the algorithm of `String::replace`. + * matcher and according to the algorithm of `String::replace´. */ export const stringReplace = createCallableFunction( stringPrototype.replace, @@ -643,7 +734,7 @@ export const stringReplace = createCallableFunction( /** * Returns the result of replacing the string representation of the * provided value with the provided replacement, using the provided - * matcher and according to the algorithm of `String::replaceAll`. + * matcher and according to the algorithm of `String::replaceAll´. */ export const stringReplaceAll = createCallableFunction( stringPrototype.replaceAll, @@ -653,7 +744,7 @@ export const stringReplaceAll = createCallableFunction( /** * Returns the result of searching the string representation of the * provided value using the provided matcher and according to the - * algorithm of `String::search`. + * algorithm of `String::search´. */ export const stringSearch = createCallableFunction( stringPrototype.search, @@ -662,7 +753,7 @@ export const stringSearch = createCallableFunction( /** * Returns a slice of the string representation of the provided value - * according to the algorithm of `String::slice`. + * according to the algorithm of `String::slice´. */ export const stringSlice = createCallableFunction( stringPrototype.slice, @@ -672,7 +763,7 @@ export const stringSlice = createCallableFunction( /** * Returns the result of splitting of the string representation of the * provided value on the provided separator according to the algorithm - * of `String::split`. + * of `String::split´. */ export const stringSplit = createCallableFunction( stringPrototype.split, @@ -682,7 +773,7 @@ export const stringSplit = createCallableFunction( /** * Returns whether the string representation of the provided value * starts with the provided search string according to the algorithm of - * `String::startsWith`. + * `String::startsWith´. */ export const stringStartsWith = createCallableFunction( stringPrototype.startsWith, @@ -692,10 +783,10 @@ export const stringStartsWith = createCallableFunction( /** * Returns the value of the provided string. * - * ※ This is effectively an alias for the `String::valueOf`. + * ※ This is effectively an alias for the `String::valueOf´. * * ☡ This function throws if the provided argument is not a string and - * does not have a `[[StringData]]` slot. + * does not have a `[[StringData]]´ slot. */ export const stringValue = createCallableFunction( stringPrototype.valueOf, @@ -725,7 +816,7 @@ export const stripLeadingAndTrailingAsciiWhitespace = ($) => /** * Returns a substring of the string representation of the provided - * value according to the algorithm of `String::substring`. + * value according to the algorithm of `String::substring´. */ export const substring = createCallableFunction( stringPrototype.substring,