UNPKG

@fable-org/fable-library-js

Version:

Core library used by F# projects compiled with fable.io

626 lines (625 loc) 24.3 kB
import { toString as dateToString } from "./Date.js"; import { compare as numericCompare, isNumeric, isIntegral, multiply, toExponential, toFixed, toHex, toPrecision } from "./Numeric.js"; import { escape } from "./RegExp.js"; import { toString } from "./Types.js"; const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\*|\d+)?(?:\.(\d+))?(\w)/g; const interpolateRegExp = /(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)/g; const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; function isLessThan(x, y) { return numericCompare(x, y) < 0; } function cmp(x, y, ic) { function isIgnoreCase(i) { return i === true || i === 1 /* StringComparison.CurrentCultureIgnoreCase */ || i === 3 /* StringComparison.InvariantCultureIgnoreCase */ || i === 5 /* StringComparison.OrdinalIgnoreCase */; } function isOrdinal(i) { return i === 4 /* StringComparison.Ordinal */ || i === 5 /* StringComparison.OrdinalIgnoreCase */; } if (x == null) { return y == null ? 0 : -1; } if (y == null) { return 1; } // everything is bigger than null if (isOrdinal(ic)) { if (isIgnoreCase(ic)) { x = x.toLowerCase(); y = y.toLowerCase(); } return (x === y) ? 0 : (x < y ? -1 : 1); } else { if (isIgnoreCase(ic)) { x = x.toLocaleLowerCase(); y = y.toLocaleLowerCase(); } return x.localeCompare(y); } } export function compare(...args) { switch (args.length) { case 2: return cmp(args[0], args[1], false); case 3: return cmp(args[0], args[1], args[2]); case 4: return cmp(args[0], args[1], args[2] === true); case 5: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), false); case 6: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), args[5]); case 7: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), args[5] === true); default: throw new Error("String.compare: Unsupported number of parameters"); } } export function compareOrdinal(x, y) { return cmp(x, y, 4 /* StringComparison.Ordinal */); } export function compareTo(x, y) { return cmp(x, y, 0 /* StringComparison.CurrentCulture */); } export function startsWith(str, pattern, ic) { if (ic === 4 /* StringComparison.Ordinal */) { // to avoid substring allocation return str.startsWith(pattern); } if (str.length >= pattern.length) { return cmp(str.substr(0, pattern.length), pattern, ic) === 0; } return false; } export function endsWith(str, pattern, ic) { if (ic === 4 /* StringComparison.Ordinal */) { // to avoid substring allocation return str.endsWith(pattern); } if (str.length >= pattern.length) { return cmp(str.substr(str.length - pattern.length, pattern.length), pattern, ic) === 0; } return false; } export function indexOfAny(str, anyOf, ...args) { if (str == null || str === "") { return -1; } const startIndex = (args.length > 0) ? args[0] : 0; if (startIndex < 0) { throw new Error("Start index cannot be negative"); } const length = (args.length > 1) ? args[1] : str.length - startIndex; if (length < 0) { throw new Error("Length cannot be negative"); } if (startIndex + length > str.length) { throw new Error("Invalid startIndex and length"); } const endIndex = startIndex + length; const anyOfAsStr = "".concat.apply("", anyOf); for (let i = startIndex; i < endIndex; i++) { if (anyOfAsStr.indexOf(str[i]) > -1) { return i; } } return -1; } export function printf(input) { return { input, cont: fsFormat(input), }; } export function interpolate(str, values) { let valIdx = 0; let strIdx = 0; let result = ""; interpolateRegExp.lastIndex = 0; let match = interpolateRegExp.exec(str); while (match) { // The first group corresponds to the no-escape char (^|[^%]), the actual pattern starts in the next char // Note: we don't use negative lookbehind because some browsers don't support it yet const matchIndex = match.index + (match[1] || "").length; result += str.substring(strIdx, matchIndex).replace(/%%/g, "%"); const [, , flags, padLength, precision, format] = match; // Save interpolateRegExp.lastIndex before running formatReplacement because the values // may also involve interpolation and make use of interpolateRegExp (see #3078) strIdx = interpolateRegExp.lastIndex; result += formatReplacement(values[valIdx++], flags, padLength, precision, format); // Move interpolateRegExp.lastIndex one char behind to make sure we match the no-escape char next time interpolateRegExp.lastIndex = strIdx - 1; match = interpolateRegExp.exec(str); } result += str.substring(strIdx).replace(/%%/g, "%"); return result; } function continuePrint(cont, arg) { return typeof arg === "string" ? cont(arg) : arg.cont(cont); } export function toConsole(arg) { // Don't remove the lambda here, see #1357 return continuePrint((x) => console.log(x), arg); } export function toConsoleError(arg) { return continuePrint((x) => console.error(x), arg); } export function toText(arg) { return continuePrint((x) => x, arg); } export function toFail(arg) { return continuePrint((x) => { throw new Error(x); }, arg); } function formatReplacement(rep, flags, padLength, precision, format) { let sign = ""; flags = flags || ""; format = format || ""; if (isNumeric(rep)) { if (format.toLowerCase() !== "x") { if (isLessThan(rep, 0)) { rep = multiply(rep, -1); sign = "-"; } else { if (flags.indexOf(" ") >= 0) { sign = " "; } else if (flags.indexOf("+") >= 0) { sign = "+"; } } } precision = precision == null ? null : parseInt(precision, 10); switch (format) { case "f": case "F": precision = precision != null ? precision : 6; rep = toFixed(rep, precision); break; case "g": case "G": rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); break; case "e": case "E": rep = precision != null ? toExponential(rep, precision) : toExponential(rep); break; case "x": rep = toHex(rep); break; case "X": rep = toHex(rep).toUpperCase(); break; default: // AOid rep = String(rep); break; } } else if (rep instanceof Date) { rep = dateToString(rep); } else { rep = toString(rep); } padLength = typeof padLength === "number" ? padLength : parseInt(padLength, 10); if (!isNaN(padLength)) { const zeroFlag = flags.indexOf("0") >= 0; // Use '0' for left padding const minusFlag = flags.indexOf("-") >= 0; // Right padding const ch = minusFlag || !zeroFlag ? " " : "0"; if (ch === "0") { rep = pad(rep, padLength - sign.length, ch, minusFlag); rep = sign + rep; } else { rep = pad(sign + rep, padLength, ch, minusFlag); } } else { rep = sign + rep; } return rep; } function createPrinter(cont, _strParts, _matches, _result = "", padArg = -1) { return (...args) => { // Make copies of the values passed by reference because the function can be used multiple times let result = _result; const strParts = _strParts.slice(); const matches = _matches.slice(); for (const arg of args) { const [, , flags, _padLength, precision, format] = matches[0]; let padLength = _padLength; if (padArg >= 0) { padLength = padArg; padArg = -1; } else if (padLength === "*") { if (arg < 0) { throw new Error("Non-negative number required"); } padArg = arg; continue; } result += strParts[0]; result += formatReplacement(arg, flags, padLength, precision, format); strParts.splice(0, 1); matches.splice(0, 1); } if (matches.length === 0) { result += strParts[0]; return cont(result); } else { return createPrinter(cont, strParts, matches, result, padArg); } }; } export function fsFormat(str) { return (cont) => { fsFormatRegExp.lastIndex = 0; const strParts = []; const matches = []; let strIdx = 0; let match = fsFormatRegExp.exec(str); while (match) { // The first group corresponds to the no-escape char (^|[^%]), the actual pattern starts in the next char // Note: we don't use negative lookbehind because some browsers don't support it yet const matchIndex = match.index + (match[1] || "").length; strParts.push(str.substring(strIdx, matchIndex).replace(/%%/g, "%")); matches.push(match); strIdx = fsFormatRegExp.lastIndex; // Likewise we need to move fsFormatRegExp.lastIndex one char behind to make sure we match the no-escape char next time fsFormatRegExp.lastIndex -= 1; match = fsFormatRegExp.exec(str); } if (strParts.length === 0) { return cont(str.replace(/%%/g, "%")); } else { strParts.push(str.substring(strIdx).replace(/%%/g, "%")); return createPrinter(cont, strParts, matches); } }; } function splitIntAndDecimalPart(value) { let [repInt, repDecimal] = value.split("."); repDecimal === undefined && (repDecimal = ""); return { integral: repInt, decimal: repDecimal }; } function thousandSeparate(value) { return value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } export function format(str, ...args) { let str2; if (typeof str === "object") { // Called with culture info str2 = String(args[0]); args.shift(); } else { str2 = str; } return str2.replace(formatRegExp, (_, idx, padLength, format, precision, pattern) => { if (idx < 0 || idx >= args.length) { throw new Error("Index must be greater or equal to zero and less than the arguments' length."); } let rep = args[idx]; let parts; if (isNumeric(rep)) { precision = precision == "" ? null : parseInt(precision, 10); switch (format) { case "b": case "B": if (!isIntegral(rep)) { throw new Error("Format specifier was invalid."); } rep = (rep >>> 0).toString(2).replace(/^0+/, "").padStart(precision || 1, "0"); break; case "c": case "C": const isNegative = isLessThan(rep, 0); if (isLessThan(rep, 0)) { rep = multiply(rep, -1); } precision = precision == null ? 2 : precision; rep = toFixed(rep, precision); parts = splitIntAndDecimalPart(rep); rep = "¤" + thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); if (isNegative) { rep = "(" + rep + ")"; } break; case "d": case "D": if (!isIntegral(rep)) { throw new Error("Format specifier was invalid."); } rep = String(rep); if (precision != null) { if (rep.startsWith("-")) { rep = "-" + padLeft(rep.substring(1), precision, "0"); } else { rep = padLeft(rep, precision, "0"); } } break; case "e": case "E": rep = precision != null ? toExponential(rep, precision) : toExponential(rep); break; case "f": case "F": precision = precision != null ? precision : 2; rep = toFixed(rep, precision); if (precision > 0) { parts = splitIntAndDecimalPart(rep); rep = parts.integral + "." + padRight(parts.decimal, precision, "0"); } break; case "g": case "G": rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); // TODO: Check why some numbers are formatted with decimal part rep = trimEnd(trimEnd(rep, "0"), "."); break; case "n": case "N": precision = precision != null ? precision : 2; rep = toFixed(rep, precision); parts = splitIntAndDecimalPart(rep); rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); break; case "p": case "P": precision = precision != null ? precision : 2; rep = toFixed(multiply(rep, 100), precision); parts = splitIntAndDecimalPart(rep); rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0") + " %"; break; case "r": case "R": throw new Error("The round-trip format is not supported by Fable"); case "x": case "X": if (!isIntegral(rep)) { throw new Error("Format specifier was invalid."); } precision = precision != null ? precision : 2; rep = padLeft(toHex(rep), precision, "0"); if (format === "X") { rep = rep.toUpperCase(); } break; default: // If we have format and were not able to handle it throw // See: https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#standard-format-specifiers if (format) { throw new Error("Format specifier was invalid."); } if (pattern) { let sign = ""; rep = pattern.replace(/([0#,]+)(\.[0#]+)?/, (_, intPart, decimalPart) => { if (isLessThan(rep, 0)) { rep = multiply(rep, -1); sign = "-"; } decimalPart = decimalPart == null ? "" : decimalPart.substring(1); rep = toFixed(rep, Math.max(decimalPart.length, 0)); let [repInt, repDecimal] = rep.split("."); repDecimal || (repDecimal = ""); const leftZeroes = intPart.replace(/,/g, "").replace(/^#+/, "").length; repInt = padLeft(repInt, leftZeroes, "0"); const rightZeros = decimalPart.replace(/#+$/, "").length; if (rightZeros > repDecimal.length) { repDecimal = padRight(repDecimal, rightZeros, "0"); } else if (rightZeros < repDecimal.length) { repDecimal = repDecimal.substring(0, rightZeros) + repDecimal.substring(rightZeros).replace(/0+$/, ""); } // Thousands separator if (intPart.indexOf(",") > 0) { const i = repInt.length % 3; const thousandGroups = Math.floor(repInt.length / 3); let thousands = i > 0 ? repInt.substr(0, i) + (thousandGroups > 0 ? "," : "") : ""; for (let j = 0; j < thousandGroups; j++) { thousands += repInt.substr(i + j * 3, 3) + (j < thousandGroups - 1 ? "," : ""); } repInt = thousands; } return repDecimal.length > 0 ? repInt + "." + repDecimal : repInt; }); rep = sign + rep; } } } else if (rep instanceof Date) { rep = dateToString(rep, pattern || format); } else { rep = toString(rep); } padLength = parseInt((padLength || " ").substring(1), 10); if (!isNaN(padLength)) { rep = pad(String(rep), Math.abs(padLength), " ", padLength < 0); } return rep; }); } export function initialize(n, f) { if (n < 0) { throw new Error("String length must be non-negative"); } const xs = new Array(n); for (let i = 0; i < n; i++) { xs[i] = f(i); } return xs.join(""); } export function insert(str, startIndex, value) { if (startIndex < 0 || startIndex > str.length) { throw new Error("startIndex is negative or greater than the length of this instance."); } return str.substring(0, startIndex) + value + str.substring(startIndex); } export function isNullOrEmpty(str) { return typeof str !== "string" || str.length === 0; } export function isNullOrWhiteSpace(str) { return typeof str !== "string" || /^\s*$/.test(str); } export function concat(...xs) { return xs.map((x) => String(x)).join(""); } export function join(delimiter, xs) { if (Array.isArray(xs)) { return xs.join(delimiter); } else { return Array.from(xs).join(delimiter); } } export function joinWithIndices(delimiter, xs, startIndex, count) { const endIndexPlusOne = startIndex + count; if (endIndexPlusOne > xs.length) { throw new Error("Index and count must refer to a location within the buffer."); } return xs.slice(startIndex, endIndexPlusOne).join(delimiter); } function notSupported(name) { throw new Error("The environment doesn't support '" + name + "', please use a polyfill."); } export function toBase64String(inArray) { let str = ""; for (let i = 0; i < inArray.length; i++) { str += String.fromCharCode(inArray[i]); } return typeof btoa === "function" ? btoa(str) : notSupported("btoa"); } export function fromBase64String(b64Encoded) { const binary = typeof atob === "function" ? atob(b64Encoded) : notSupported("atob"); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function pad(str, len, ch, isRight) { ch = ch || " "; len = len - str.length; for (let i = 0; i < len; i++) { str = isRight ? str + ch : ch + str; } return str; } export function padLeft(str, len, ch) { return pad(str, len, ch); } export function padRight(str, len, ch) { return pad(str, len, ch, true); } export function remove(str, startIndex, count) { if (startIndex >= str.length) { throw new Error("startIndex must be less than length of string"); } if (typeof count === "number" && (startIndex + count) > str.length) { throw new Error("Index and count must refer to a location within the string."); } return str.slice(0, startIndex) + (typeof count === "number" ? str.substr(startIndex + count) : ""); } export function replace(str, search, replace) { return str.replace(new RegExp(escape(search), "g"), replace); } export function replicate(n, x) { return initialize(n, () => x); } export function getCharAtIndex(input, index) { if (index < 0 || index >= input.length) { throw new Error("Index was outside the bounds of the array."); } return input[index]; } export function split(str, splitters, count, options) { count = typeof count === "number" ? count : undefined; options = typeof options === "number" ? options : 0; if (count && count < 0) { throw new Error("Count cannot be less than zero"); } if (count === 0) { return []; } const removeEmpty = (options & 1) === 1; const trim = (options & 2) === 2; splitters = splitters || []; splitters = splitters.filter(x => x).map(escape); splitters = splitters.length > 0 ? splitters : ["\\s"]; const splits = []; const reg = new RegExp(splitters.join("|"), "g"); let findSplits = true; let i = 0; do { const match = reg.exec(str); if (match === null) { const candidate = trim ? str.substring(i).trim() : str.substring(i); if (!removeEmpty || candidate.length > 0) { splits.push(candidate); } findSplits = false; } else { const candidate = trim ? str.substring(i, match.index).trim() : str.substring(i, match.index); if (!removeEmpty || candidate.length > 0) { if (count != null && splits.length + 1 === count) { splits.push(trim ? str.substring(i).trim() : str.substring(i)); findSplits = false; } else { splits.push(candidate); } } i = reg.lastIndex; } } while (findSplits); return splits; } export function trim(str, ...chars) { if (chars.length === 0) { return str.trim(); } const pattern = "[" + escape(chars.join("")) + "]+"; return str.replace(new RegExp("^" + pattern), "").replace(new RegExp(pattern + "$"), ""); } export function trimStart(str, ...chars) { return chars.length === 0 ? str.trimStart() : str.replace(new RegExp("^[" + escape(chars.join("")) + "]+"), ""); } export function trimEnd(str, ...chars) { return chars.length === 0 ? str.trimEnd() : str.replace(new RegExp("[" + escape(chars.join("")) + "]+$"), ""); } export function filter(pred, x) { return x.split("").filter((c) => pred(c)).join(""); } export function substring(str, startIndex, length) { if ((startIndex + (length || 0) > str.length)) { throw new Error("Invalid startIndex and/or length"); } return length != null ? str.substr(startIndex, length) : str.substr(startIndex); } export function toCharArray2(str, startIndex, length) { return substring(str, startIndex, length).split(""); } export function fmt(strs, ...args) { return ({ strs, args }); } export function fmtWith(fmts) { return (strs, ...args) => ({ strs, args, fmts }); } export function getFormat(s) { const strs = s.strs.map((value) => value.replace(/{/g, '{{').replace(/}/g, '}}')); return s.fmts ? strs .reduce((acc, newPart, index) => acc + `{${String(index - 1) + s.fmts[index - 1]}}` + newPart) : strs .reduce((acc, newPart, index) => acc + `{${index - 1}}` + newPart); }