UNPKG

jexl-extended

Version:

Extended grammar for Javascript Expression Language (JEXL)

1,492 lines (1,491 loc) 49.5 kB
import { formatInTimeZone, fromZonedTime } from "date-fns-tz"; import { findIana } from "windows-iana"; import { parse as dateParse, add as dateAdd, format as dateFormat, } from "date-fns"; import { v4 as uuidv4 } from "uuid"; import jexl from "."; /** * Casts the input to a string. * * @example * string(123) // "123" * 123|string // "123" * @group Conversion * * @param input The input can be any type. * @param prettify If true, the output will be pretty-printed. * @returns The input converted to a JSON string representation. */ export const toString = (input, prettify = false) => { return JSON.stringify(input, null, prettify ? 2 : 0); }; /** * Parses the string and returns a JSON object. * * @example * toJson('{"key": "value"}') // { key: "value" } * '{"name": "John", "age": 30}'|toJson // { name: "John", age: 30 } * @group Conversion * * @param input The JSON string to parse. * @returns The parsed JSON object or value. * @throws {SyntaxError} If the string is not valid JSON. */ export const toJson = (input) => { return JSON.parse(input); }; /** * Returns the number of characters in a string, or the length of an array. * * @example * length("hello") // 5 * length([1, 2, 3]) // 3 * @group Utility * * @param input The input can be a string, an array, or an object. * @returns The number of characters in a string, or the length of an array. */ export const length = (input) => { if (typeof input === "string") { return input.length; } if (Array.isArray(input)) { return input.length; } if (typeof input === "object" && input !== null) { return Object.keys(input).length; } return 0; }; /** * Gets a substring of a string. * * @example * substring("hello world", 0, 5) // "hello" * @group String * * @param input The input string. * @param start The starting index of the substring. * @param length The length of the substring. * @returns The substring of the input string. */ export const substring = (input, start, length) => { let str = input; if (typeof str !== "string") { str = JSON.stringify(str); } if (typeof str === "string") { let startNum = start; let len = length ?? str.length; if (startNum < 0) { startNum = str.length + start; if (startNum < 0) { startNum = 0; } } if (startNum + len > str.length) { len = str.length - startNum; } if (len < 0) { len = 0; } return str.substring(startNum, startNum + len); } return ""; }; /** * Returns the substring before the first occurrence of the character sequence chars in str. * * @example substringBefore("hello world", " ") // "hello" * @group String * @param input The input string. * @param chars The character sequence to search for. * @returns The substring before the first occurrence of the character sequence chars in str. */ export const substringBefore = (input, chars) => { const str = typeof input === "string" ? input : JSON.stringify(input); const charsStr = typeof chars === "string" ? chars : JSON.stringify(chars); const index = str.indexOf(charsStr); if (index === -1) { return str; } return str.substring(0, index); }; /** * Returns the substring after the first occurrence of the character sequence chars in str. * * @example * substringAfter("hello world", " ") // "world" * @group String * * @param input The input string. * @param chars The character sequence to search for. * @returns The substring after the first occurrence of the character sequence chars in str. */ export const substringAfter = (input, chars) => { const str = typeof input === "string" ? input : JSON.stringify(input); const charsStr = typeof chars === "string" ? chars : JSON.stringify(chars); const index = str.indexOf(charsStr); if (index === -1) { return ""; } return str.substring(index + charsStr.length); }; /** * Converts the input string to uppercase. * * @example * uppercase("hello") // "HELLO" * "hello world"|uppercase // "HELLO WORLD" * @group String * * @param input The input to convert to uppercase. Non-string inputs are converted to JSON string first. * @returns The uppercase string. */ export const uppercase = (input) => { const str = typeof input === "string" ? input : JSON.stringify(input); return str.toUpperCase(); }; /** * Converts the input string to lowercase. * * @example * lowercase("HELLO") // "hello" * "HELLO WORLD"|lowercase // "hello world" * @group String * * @param input The input to convert to lowercase. Non-string inputs are converted to JSON string first. * @returns The lowercase string. */ export const lowercase = (input) => { const str = typeof input === "string" ? input : JSON.stringify(input); return str.toLowerCase(); }; const splitRegex = /(?<!^)(?=[A-Z])|[`~!@#%^&*()|+\\\-=?;:'.,\s_']+/; /** * Converts the input string to camel case. * * @example * camelCase("foo bar") // "fooBar" * "hello-world"|camelCase // "helloWorld" * camelCase("HELLO_WORLD") // "helloWorld" * @group String * * @param input The input string to convert to camel case. * @returns The camel case string, or empty string if input is not a string. */ export const camelCase = (input) => { if (typeof input !== "string") return ""; return input .split(splitRegex) .map((word, index) => { if (index === 0) return word.toLowerCase(); return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }) .join(""); }; /** * Converts the input string to pascal case. * * @example * pascalCase("foo bar") // "FooBar" * "hello-world"|pascalCase // "HelloWorld" * pascalCase("HELLO_WORLD") // "HelloWorld" * @group String * * @param input The input string to convert to pascal case. * @returns The pascal case string, or empty string if input is not a string. */ export const pascalCase = (input) => { if (typeof input !== "string") return ""; return input .split(splitRegex) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(""); }; /** * Trims whitespace from both ends of a string. * * @example * trim(" hello ") // "hello" * " world "|trim // "world" * trim("__hello__", "_") // "hello" * @group String * * @param input The input string to trim. * @param trimChar Optional character to trim instead of whitespace. * @returns The trimmed string, or empty string if input is not a string. */ export const trim = (input, trimChar) => { if (typeof input === "string") { if (trimChar) { return input.replace(new RegExp(`^${trimChar}+|${trimChar}+$`, "g"), ""); } return input.trim(); } return ""; }; /** * Pads the input string to the specified width. * * @example * pad("hello", 10) // "hello " * pad("world", -8, "0") // "000world" * "foo"|pad(5, ".") // "foo.." * @group String * * @param input The input to pad. Non-string inputs are converted to JSON string first. * @param width The target width. Positive values pad to the right, negative values pad to the left. * @param char The character to use for padding. Defaults to space. * @returns The padded string. */ export const pad = (input, width, char = " ") => { const str = typeof input !== "string" ? JSON.stringify(input) : input; if (width > 0) { return str.padEnd(width, char); } else { return str.padStart(-width, char); } }; /** * Checks if the input string or array contains the specified value. * * @example * contains("hello world", "world") // true * "foo-bar"|contains("bar") // true * contains([1, 2, 3], 2) // true * @group String * * @param input The input string or array to search in. * @param search The value to search for. * @returns True if the input contains the search value, false otherwise. */ export const contains = (input, search) => { if (typeof input === "string" || Array.isArray(input)) { return input.includes(search); } return false; }; /** * Checks if the input string starts with the specified substring. * * @example * startsWith("hello world", "hello") // true * "foo-bar"|startsWith("foo") // true * startsWith("test", "xyz") // false * @group String * * @param input The input string to check. * @param search The substring to search for at the beginning. * @returns True if the input starts with the search string, false otherwise. */ export const startsWith = (input, search) => { if (typeof input === "string") { return input.startsWith(search); } return false; }; /** * Checks if the input string ends with the specified substring. * * @example * endsWith("hello world", "world") // true * "foo-bar"|endsWith("bar") // true * endsWith("test", "xyz") // false * @group String * * @param input The input string to check. * @param search The substring to search for at the end. * @returns True if the input ends with the search string, false otherwise. */ export const endsWith = (input, search) => { if (typeof input === "string") { return input.endsWith(search); } return false; }; /** * Splits the input string into an array of substrings. * * @example * split("foo,bar,baz", ",") // ["foo", "bar", "baz"] * "one-two-three"|split("-") // ["one", "two", "three"] * split("hello world", " ") // ["hello", "world"] * @group String * * @param input The input string to split. * @param separator The separator string to split on. * @returns An array of substrings, or empty array if input is not a string. */ export const split = (input, separator) => { if (typeof input === "string") { return input.split(separator); } return []; }; /** * Joins elements of an array into a string. * * @example * arrayJoin(["foo", "bar", "baz"], ",") // "foo,bar,baz" * ["one", "two", "three"]|arrayJoin("-") // "one-two-three" * arrayJoin([1, 2, 3]) // "1,2,3" * @group Array * * @param input The input array to join. * @param separator The separator string to use between elements. Defaults to comma. * @returns The joined string, or undefined if input is not an array. */ export const arrayJoin = (input, separator) => { if (Array.isArray(input)) { return input.join(separator); } return undefined; }; /** * Replaces occurrences of a specified string with a replacement string. * * @example * replace("foo-bar-baz", "-", "_") // "foo_bar_baz" * "hello world"|replace("world", "there") // "hello there" * replace("test test test", "test", "demo") // "demo demo demo" * @group String * * @param input The input string to perform replacements on. * @param search The string to search for and replace. * @param replacement The string to replace matches with. Defaults to empty string. * @returns The string with replacements made, or undefined if input is not a string. */ export const replace = (input, search, replacement) => { if (typeof input === "string" && typeof search === "string") { const _replacement = replacement === undefined ? "" : replacement; return input.replace(new RegExp(search, "g"), _replacement); } return undefined; }; /** * Encodes a string to Base64. * * @example * base64Encode("hello") // "aGVsbG8=" * "hello world"|base64Encode // "aGVsbG8gd29ybGQ=" * base64Encode("test") // "dGVzdA==" * @group Encoding * * @param input The input string to encode. * @returns The Base64 encoded string, or empty string if input is not a string or encoding fails. */ export const base64Encode = (input) => { if (typeof input === "string") { try { encodeURIComponent(input); const bytes = new TextEncoder().encode(input); const binString = String.fromCodePoint(...bytes); return btoa(binString); } catch (error) { return ""; } } return ""; }; /** * Decodes a Base64 encoded string. * * @example * base64Decode("aGVsbG8=") // "hello" * "aGVsbG8gd29ybGQ="|base64Decode // "hello world" * base64Decode("dGVzdA==") // "test" * @group Encoding * * @param input The Base64 encoded string to decode. * @returns The decoded string, or empty string if input is not a string. */ export const base64Decode = (input) => { if (typeof input === "string") { const binString = atob(input); const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)); return new TextDecoder().decode(bytes); } return ""; }; /** * Encodes a string or object to URI component format. * * @example * formUrlEncoded("hello world") // "hello%20world" * formUrlEncoded({name: "John", age: 30}) // "name=John&age=30" * "hello & world"|formUrlEncoded // "hello%20%26%20world" * @group Encoding * * @param input The input string or object to encode. * @returns The URL encoded string, or empty string if input is not a string or object. */ export const formUrlEncoded = (input) => { if (typeof input === "string") { return encodeURIComponent(input); } else if (typeof input === "object") { return Object.keys(input) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`) .join("&"); } return ""; }; /** * Converts the input to a number. * * @example * toNumber("123") // 123 * "45.67"|toNumber // 45.67 * toNumber("abc") // NaN * @group Conversion * * @param input The input to convert to a number. * @returns The numeric value, or NaN if conversion fails. */ export const toNumber = (input) => { if (typeof input === "number") return input; if (typeof input === "string") return parseFloat(input); return NaN; }; /** * Parses a string and returns an integer. * * @example * parseInteger("123") // 123 * "45.67"|parseInteger // 45 * parseInteger(123.89) // 123 * @group Conversion * * @param input The input to parse as an integer. * @returns The integer value, or NaN if parsing fails. */ export const parseInteger = (input) => { if (typeof input === "string") { return parseInt(input, 10); } else if (typeof input === "number") { return Math.floor(input); } return NaN; }; /** * Returns the absolute value of a number. * * @example * absoluteValue(-5) // 5 * (-10)|absoluteValue // 10 * absoluteValue(3.14) // 3.14 * @group Math * * @param input The input number to get the absolute value of. * @returns The absolute value, or NaN if input cannot be converted to a number. */ export const absoluteValue = (input) => { const num = toNumber(input); return isNaN(num) ? NaN : Math.abs(num); }; /** * Rounds a number down to the nearest integer. * * @example * floor(3.7) // 3 * (3.14)|floor // 3 * floor(-2.8) // -3 * @group Math * * @param input The input number to round down. * @returns The rounded down integer, or NaN if input cannot be converted to a number. */ export const floor = (input) => { const num = toNumber(input); return isNaN(num) ? NaN : Math.floor(num); }; /** * Rounds a number up to the nearest integer. * * @example * ceil(3.2) // 4 * (3.14)|ceil // 4 * ceil(-2.8) // -2 * @group Math * * @param input The input number to round up. * @returns The rounded up integer, or NaN if input cannot be converted to a number. */ export const ceil = (input) => { const num = toNumber(input); return isNaN(num) ? NaN : Math.ceil(num); }; /** * Rounds a number to the nearest integer or to specified decimal places. * * @example * round(3.7) // 4 * round(3.14159, 2) // 3.14 * (2.567)|round // 3 * @group Math * * @param input The input number to round. * @param decimals Optional number of decimal places to round to. * @returns The rounded number, or NaN if input cannot be converted to a number. */ export const round = (input, decimals) => { const num = toNumber(input); return isNaN(num) ? NaN : decimals ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : Math.round(num); }; /** * Returns the value of a number raised to a power. * * @example * power(2, 3) // 8 * (2)|power(4) // 16 * power(9) // 81 (defaults to power of 2) * @group Math * * @param input The base number. * @param exponent The exponent to raise the base to. Defaults to 2. * @returns The result of base raised to the exponent, or NaN if input cannot be converted to a number. */ export const power = (input, exponent) => { const num = toNumber(input); const exp = exponent === undefined ? 2 : exponent; return isNaN(num) ? NaN : Math.pow(num, exp); }; /** * Returns the square root of a number. * * @example * sqrt(16) // 4 * (25)|sqrt // 5 * sqrt(2) // 1.4142135623730951 * @group Math * * @param input The input number to get the square root of. * @returns The square root of the input, or NaN if input cannot be converted to a number. */ export const sqrt = (input) => { const num = toNumber(input); return isNaN(num) ? NaN : Math.sqrt(num); }; /** * Generates a random number between 0 (inclusive) and 1 (exclusive). * * @example * randomNumber() // 0.123456789 (example output) * randomNumber() // 0.987654321 (different each time) * @group Math * * @returns A random floating-point number between 0 and 1. */ export const randomNumber = () => { return Math.random(); }; /** * Formats a number to a decimal representation as specified by the format string. * * @example * formatNumber(1234.567, "#,##0.00") // "1,234.57" * (1000)|formatNumber("0.00") // "1000.00" * formatNumber(42, "#,###") // "42" * @group Conversion * * @param input The input number to format. * @param format The format string specifying decimal places and grouping. * @returns The formatted number string, or empty string if input cannot be converted to a number. */ export const formatNumber = (input, format) => { const num = typeof input === "number" ? input : parseInt(toNumber(input).toString(), 10); return isNaN(num) ? "" : num.toLocaleString("en-us", { minimumFractionDigits: format.split(".")[1]?.length, maximumFractionDigits: format.split(".")[1]?.length, useGrouping: format.split(".")[0]?.includes(","), }); }; /** * Formats a number as a string in the specified base. * * @example * formatBase(255, 16) // "ff" * (10)|formatBase(2) // "1010" * formatBase(64, 8) // "100" * @group Conversion * * @param input The input number to format. * @param base The numeric base to convert to (2-36). * @returns The number formatted in the specified base, or empty string if input cannot be converted to a number. */ export const formatBase = (input, base) => { const num = typeof input === "number" ? input : parseInt(toNumber(input).toString(), 10); return isNaN(num) ? "" : num.toString(base); }; /** * Formats a number as an integer with zero padding. * * @example * formatInteger(42, "000") // "042" * (7)|formatInteger("0000") // "0007" * formatInteger(123, "00") // "123" * @group Conversion * * @param input The input number to format. * @param format The format string indicating the minimum number of digits. * @returns The zero-padded integer string, or empty string if input cannot be converted to a number. */ export const formatInteger = (input, format) => { const num = toNumber(input); return isNaN(num) ? "" : pad(Math.floor(num).toString(), -format.length, "0"); }; /** * Calculates the sum of an array of numbers. * * @example * sum([1, 2, 3, 4]) // 10 * [1.5, 2.5, 3.0]|sum // 7 * sum(1, 2, 3, 4) // 10 * @group Math * * @param input The input array of numbers or individual number arguments. * @returns The sum of all numbers, or NaN if input is not an array. */ export const sum = (...input) => { if (!Array.isArray(input)) return NaN; return input.flat().reduce((acc, val) => acc + toNumber(val), 0); }; /** * Finds the maximum value in an array of numbers. * * @example * max([1, 5, 3, 2]) // 5 * [10, 20, 15]|max // 20 * max(1, 5, 3, 2) // 5 * @group Math * * @param input The input array of numbers or individual number arguments. * @returns The maximum value, or NaN if input is not an array. */ export const max = (...input) => { if (!Array.isArray(input)) return NaN; return Math.max(...input.flat().map(toNumber)); }; /** * Finds the minimum value in an array of numbers. * * @example * min([1, 5, 3, 2]) // 1 * [10, 20, 15]|min // 10 * min(1, 5, 3, 2) // 1 * @group Math * * @param input The input array of numbers or individual number arguments. * @returns The minimum value, or NaN if input is not an array. */ export const min = (...input) => { if (!Array.isArray(input)) return NaN; return Math.min(...input.flat().map(toNumber)); }; /** * Calculates the average of an array of numbers. * * @example * average([1, 2, 3, 4]) // 2.5 * [10, 20, 30]|average // 20 * average(1, 2, 3, 4) // 2.5 * @group Math * * @param input The input array of numbers or individual number arguments. * @returns The average value, or NaN if input is not an array. */ export const average = (...input) => { if (!Array.isArray(input)) return NaN; const total = sum(...input); return total / input.flat().length; }; /** * Converts the input to a boolean. * * @example * toBoolean("true") // true * "false"|toBoolean // false * toBoolean(1) // true * toBoolean(0) // false * @group Conversion * * @param input The input to convert to a boolean. * @returns The boolean value, or undefined for ambiguous string values. */ export const toBoolean = (input) => { if (typeof input === "boolean") return input; if (typeof input === "number") return input !== 0; if (typeof input === "string") { if (input.trim().toLowerCase() === "true" || input.trim() === "1") return true; if (input.trim().toLowerCase() === "false" || input.trim() === "0") return false; else return undefined; } return Boolean(input); }; /** * Returns the logical NOT of the input. * * @example * not(true) // false * false|not // true * not(0) // true * not("") // true * @group Utility * * @param input The input to apply logical NOT to. * @returns The logical NOT of the input converted to boolean. */ export const not = (input) => { return !toBoolean(input); }; /** * Evaluates a list of predicates and returns the first result expression whose predicate is satisfied. * * @example * switch(expression, case1, result1, case2, result2, ..., default) * @group Utility * * @param args The arguments array where the first element is the expression to evaluate, followed by pairs of case and result, and optionally a default value. * @returns The result of the first case whose predicate is satisfied, or the default value if no case is satisfied. */ export const switchCase = (...args) => { if (args.length < 3) return null; const expressionResult = args[0]; for (let i = 1; i < args.length - 1; i += 2) { const caseResult = args[i]; if (JSON.stringify(expressionResult) === JSON.stringify(caseResult)) { return args[i + 1]; } } // Return default if (args.length % 2 === 0) { const defaultResult = args[args.length - 1]; return defaultResult; } // Return null if no default specified return null; }; /** * Returns a sub-array from start index to end index. * * @example * range([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4] * [10, 20, 30, 40]|range(0, 2) // [10, 20] * range(["a", "b", "c", "d"], 2) // ["c", "d"] * @group Array * * @param array The input array. * @param start The starting index (inclusive). * @param end The ending index (exclusive). If not provided, slices to the end of the array. * @returns The sub-array from start to end, or empty array if input is not an array. */ export const arrayRange = (array, start, end) => { if (!Array.isArray(array)) return []; return array.slice(start, end); }; /** * Appends elements to an array. * * @example * append([1, 2], 3) // [1, 2, 3] * [1, 2]|append(3, 4) // [1, 2, 3, 4] * append([], 1, 2, 3) // [1, 2, 3] * @group Array * * @param input The input values to append to an array. * @returns A new array with all inputs flattened and appended, or empty array if no valid input. */ export const arrayAppend = (...input) => { if (!Array.isArray(input)) return []; return [...input.flat()]; }; /** * Reverses the elements of an array. * * @example * reverse([1, 2, 3]) // [3, 2, 1] * [1, 2, 3]|reverse // [3, 2, 1] * reverse(["a", "b", "c"]) // ["c", "b", "a"] * @group Array * * @param input The input values to reverse. * @returns A new array with elements in reverse order, or empty array if no valid input. */ export const arrayReverse = (...input) => { if (!Array.isArray(input)) return []; return [...input.flat()].reverse(); }; /** * Shuffles the elements of an array randomly. * * @example * shuffle([1, 2, 3]) // [2, 1, 3] (random order) * [1, 2, 3]|shuffle // [3, 1, 2] (random order) * shuffle(["a", "b", "c"]) // ["c", "a", "b"] (random order) * @group Array * * @param input The input array to shuffle. * @returns The same array with elements randomly shuffled, or empty array if input is not an array. */ export const arrayShuffle = (input) => { if (!Array.isArray(input)) return []; for (let i = input.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [input[i], input[j]] = [input[j], input[i]]; } return input; }; /** * Sorts the elements of an array. * * @example * sort([3, 1, 2]) // [1, 2, 3] * [3, 1, 2]|sort // [1, 2, 3] * sort([{age: 30}, {age: 20}], "age") // [{age: 20}, {age: 30}] * sort([{age: 30}, {age: 20}], "age", true) // [{age: 30}, {age: 20}] * @group Array * * @param input The input array to sort. * @param expression Optional JEXL expression to determine sort value for objects. * @param descending Optional flag to sort in descending order. * @returns A new sorted array, or empty array if input is not an array. */ export const arraySort = (input, expression, descending) => { if (!Array.isArray(input)) return []; if (!expression) return [...input].sort(); const expr = jexl.compile(expression); const compareFunction = (a, b) => { const aValue = expr.evalSync(a); const bValue = expr.evalSync(b); if (aValue < bValue) return descending ? -1 : 1; if (aValue > bValue) return descending ? 1 : -1; return 0; }; return [...input].sort(compareFunction); }; /** * Returns a new array with duplicate elements removed. * * @example * distinct([1, 2, 2, 3, 1]) // [1, 2, 3] * [1, 2, 2, 3]|distinct // [1, 2, 3] * distinct(["a", "b", "a", "c"]) // ["a", "b", "c"] * @group Array * * @param input The input array to remove duplicates from. * @returns A new array with duplicates removed, or empty array if input is not an array. */ export const arrayDistinct = (input) => { if (!Array.isArray(input)) return []; return [...new Set(input)]; }; /** * Creates a new object based on key-value pairs or string keys. * * @example * toObject([["name", "John"], ["age", 30]]) // {name: "John", age: 30} * toObject("name", "John") // {name: "John"} * toObject(["key1", "key2"], "defaultValue") // {key1: "defaultValue", key2: "defaultValue"} * * @group Array * * @param input The input string key or array of key-value pairs. * @param val Optional default value for string keys or when array elements are strings. * @returns A new object created from the input, or empty object if input is invalid. */ export const arrayToObject = (input, val) => { if (typeof input === "string") return { [input]: val }; if (!Array.isArray(input)) return {}; return input.reduce((acc, kv) => { if (Array.isArray(kv) && kv.length === 2) { acc[kv[0]] = kv[1]; return acc; } else if (typeof kv === "string") { acc[kv] = val; return acc; } return acc; }, {}); }; /** * Returns a new array with elements transformed by extracting a specific field. * * @example * mapField([{name: "John"}, {name: "Jane"}], "name") // ["John", "Jane"] * [{age: 30}, {age: 25}]|mapField("age") // [30, 25] * mapField([{x: 1, y: 2}, {x: 3, y: 4}], "x") // [1, 3] * @group Array * * @param input The input array of objects to extract fields from. * @param field The field name to extract from each object. * @returns A new array with extracted field values, or empty array if input is not an array. */ export const mapField = (input, field) => { if (!Array.isArray(input)) return []; return input.map((item) => item[field]); }; /** * Returns an array containing the results of applying the expression parameter to each value in the array parameter. * The expression must be a valid JEXL expression string, which is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). * * @example * map([1, 2, 3], "value * 2") // [2, 4, 6] * [{name: "John"}, {name: "Jane"}]|map("value.name") // ["John", "Jane"] * map([1, 2, 3], "value + index") // [1, 3, 5] * @group Array * * @param input The input array to transform. * @param expression The JEXL expression to apply to each element. * @returns A new array with transformed elements, or undefined if input is not an array. */ export const arrayMap = (input, expression) => { if (!Array.isArray(input)) return undefined; const expr = jexl.compile(expression); return input.map((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * Checks whether the provided array has any elements that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). * * @example * any([1, 2, 3], "value > 2") // true * [{age: 25}, {age: 35}]|any("value.age > 30") // true * any([1, 2, 3], "value > 5") // false * @group Array * * @param input The input array to test. * @param expression The JEXL expression to test against each element (supports value, index and array as context). * @returns True if any element matches the expression, false otherwise or if input is not an array. */ export const arrayAny = (input, expression) => { if (!Array.isArray(input)) return false; const expr = jexl.compile(expression); return input.some((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * Checks whether the provided array has all elements that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). * * @example * every([2, 4, 6], "value % 2 == 0") // true * [{age: 25}, {age: 35}]|every("value.age > 20") // true * every([1, 2, 3], "value > 2") // false * @group Array * * @param input The input array to test. * @param expression The JEXL expression to test against each element (supports value, index and array as context). * @returns True if all elements match the expression, false otherwise or if input is not an array. */ export const arrayEvery = (input, expression) => { if (!Array.isArray(input)) return false; const expr = jexl.compile(expression); return input.every((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * Returns a new array with the elements of the input array that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). * * @example * filter([1, 2, 3, 4], "value > 2") // [3, 4] * [{age: 25}, {age: 35}]|filter("value.age > 30") // [{age: 35}] * filter([1, 2, 3, 4], "value % 2 == 0") // [2, 4] * @group Array * * @param input The input array to filter. * @param expression The JEXL expression to test against each element (supports value, index and array as context). * @returns A new array containing only elements that match the expression, or empty array if input is not an array. */ export const arrayFilter = (input, expression) => { if (!Array.isArray(input)) return []; const expr = jexl.compile(expression); return input.filter((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * Finds the first element in an array that matches the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). * * @example * find([1, 2, 3, 4], "value > 2") // 3 * [{name: "John"}, {name: "Jane"}]|find("value.name == 'Jane'") // {name: "Jane"} * find([1, 2, 3], "value > 5") // undefined * @group Array * * @param input The input array to search. * @param expression The JEXL expression to test against each element (supports value, index and array as context). * @returns The first element that matches the expression, or undefined if no match found or input is not an array. */ export const arrayFind = (input, expression) => { if (!Array.isArray(input)) return undefined; const expr = jexl.compile(expression); return input.find((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * * Finds the index of the first element in the input array that satisfies the given Jexl expression. * * @example * [1, 2, 3, 4]|findIndex('value > 2'); // returns 2 * @group Array * * @param input - The array to search through. * @param expression - A Jexl expression string to evaluate for each element. The expression has access to `value`, `index`, and `array`. * @returns The index of the first matching element, or `-1` if no element matches, or `undefined` if the input is not an array. */ export const arrayFindIndex = (input, expression) => { if (!Array.isArray(input)) return undefined; const expr = jexl.compile(expression); return input.findIndex((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; /** * Returns an aggregated value derived from applying the function parameter successively to each value in array in combination with the result of the previous application of the function. * The expression must be a valid JEXL expression string, and behaves like an infix operator between each value within the array. * The relative context provided to the expression is an object with the properties accumulator, value, index and array (the original array). * * @example * reduce([1, 2, 3, 4], "accumulator + value", 0) // 10 * [1, 2, 3]|reduce("accumulator * value", 1) // 6 * reduce(["a", "b", "c"], "accumulator + value", "") // "abc" * @group Array * * @param input The input array to reduce. * @param expression The JEXL expression to apply for each reduction step. * @param initialValue The initial value for the accumulator. * @returns The final accumulated value, or undefined if input is not an array. */ export const arrayReduce = (input, expression, initialValue) => { if (!Array.isArray(input)) return undefined; const expr = jexl.compile(expression); return input.reduce((accumulator, value, index, array) => { return expr.evalSync({ accumulator, value, index, array }); }, initialValue); }; /** * Returns the keys of an object as an array. * * @example * keys({name: "John", age: 30}) // ["name", "age"] * {a: 1, b: 2}|keys // ["a", "b"] * keys({}) // [] * @group Array * * @param input The input object to get keys from. * @returns An array of object keys, or undefined if input is not an object. */ export const objectKeys = (input) => { if (typeof input === "object" && input !== null) { return Object.keys(input); } return undefined; }; /** * Returns the values of an object as an array. * * @example * values({name: "John", age: 30}) // ["John", 30] * {a: 1, b: 2}|values // [1, 2] * values({}) // [] * @group Object * * @param input The input object to get values from. * @returns An array of object values, or undefined if input is not an object. */ export const objectValues = (input) => { if (typeof input === "object" && input !== null) { return Object.values(input); } return undefined; }; /** * Returns an array of key-value pairs from the input object. * * @example * entries({name: "John", age: 30}) // [["name", "John"], ["age", 30]] * {a: 1, b: 2}|entries // [["a", 1], ["b", 2]] * entries({}) // [] * @group Object * * @param input The input object to get entries from. * @returns An array of [key, value] pairs, or undefined if input is not an object. */ export const objectEntries = (input) => { if (typeof input === "object" && input !== null) { return Object.entries(input); } return undefined; }; /** * Returns a new object with the properties of the input objects merged together. * * @example * merge({a: 1}, {b: 2}) // {a: 1, b: 2} * {a: 1}|merge({b: 2}, {c: 3}) // {a: 1, b: 2, c: 3} * merge({a: 1}, {a: 2}) // {a: 2} (later values override) * @group Object * * @param args The input objects to merge. * @returns A new object with all properties merged together. */ export const objectMerge = (...args) => { return args.reduce((acc, obj) => { if (!Array.isArray(obj) && typeof obj === "object" && obj !== null) { return { ...acc, ...obj }; } if (Array.isArray(obj)) { for (const item of obj) { if (typeof item === "object" && item !== null) { acc = { ...acc, ...item }; } } } return acc; }, {}); }; /** * Returns the current date and time in the ISO 8601 format. * * @example * now() // "2023-12-25T10:30:00.000Z" * now() // "2023-12-25T14:45:30.123Z" (different time) * @group DateTime * * @returns The current date and time as an ISO 8601 string. */ export const now = () => { return new Date().toISOString(); }; /** * Returns the current date and time in milliseconds since the Unix epoch. * * @example * millis() // 1703505000000 * millis() // 1703505123456 (different time) * @group DateTime * * @returns The current timestamp in milliseconds. */ export const millis = () => { return Date.now(); }; /** * Parses the number of milliseconds since the Unix epoch or parses a string (with or without specified format) and returns the date and time in the ISO 8601 format. * * @example * toDateTime(1703505000000) // "2023-12-25T10:30:00.000Z" * toDateTime("2023-12-25") // "2023-12-25T00:00:00.000Z" * toDateTime("25/12/2023", "dd/MM/yyyy") // "2023-12-25T00:00:00.000Z" * toDateTime() // Current date/time (same as now()) * @group DateTime * * @param input Optional timestamp in milliseconds or date string. * @param format Optional format string for parsing date strings. * @returns The date and time as an ISO 8601 string, or undefined if parsing fails. */ export const toDateTime = (input, format) => { if (typeof input === "number") { return new Date(input).toISOString(); } if (typeof input === "string") { if (format) { // Add UTC as timezone if not provided const _format = format.includes("x") || format.includes("X") ? format : `${format} X`; const _input = format.includes("x") || format.includes("X") ? input : `${input} Z`; return dateParse(_input, _format, new Date()).toISOString(); } return new Date(input).toISOString(); } if (input === undefined) { return new Date().toISOString(); } return undefined; }; /** * Converts a date and time to a provided format. * * @example * dateTimeFormat(datetime, format) * datetime|dateTimeFormat(format) * @group DateTime * * @param input The input date and time, either as a string or number. * @param format The format to convert the date and time to. * @returns The date and time in the specified format. */ export const dateTimeFormat = (input, format) => { let dateTime; if (typeof input === "string") { dateTime = new Date(input); } else if (typeof input === "number") { dateTime = new Date(input); } else { return null; } // Convert to UTC const utcDateTime = new Date(dateTime.getTime() + dateTime.getTimezoneOffset() * 60000); // Format the date return dateFormat(utcDateTime, format); }; /** * Parses the date and time in the ISO 8601 format and returns the number of milliseconds since the Unix epoch. * * @example * dateTimeToMillis("2023-12-25T10:30:00.000Z") // 1703505000000 * "2023-01-01T00:00:00.000Z"|dateTimeToMillis // 1672531200000 * dateTimeToMillis("2023-12-25") // 1703462400000 * @group DateTime * * @param input The date and time string to parse. * @returns The timestamp in milliseconds since Unix epoch. */ export const dateTimeToMillis = (input) => { return new Date(input).getTime(); }; /** * Adds a time range to a date and time in the ISO 8601 format. * * @example * dateTimeAdd("2023-12-25T10:30:00.000Z", "day", 1) // "2023-12-26T10:30:00.000Z" * now()|dateTimeAdd("hour", -2) // Two hours ago * dateTimeAdd("2023-01-01T00:00:00.000Z", "month", 3) // "2023-04-01T00:00:00.000Z" * @group DateTime * * @param input The input date and time string in ISO 8601 format. * @param unit The time unit to add ("day", "hour", "minute", "second", "month", "year", etc.). * @param value The amount to add (can be negative to subtract). * @returns The new date and time as an ISO 8601 string. */ export const dateTimeAdd = (input, unit, value) => { // if unit doesn't end with 's' add it const _unit = unit.toLowerCase().endsWith("s") ? unit.toLowerCase() : `${unit.toLowerCase()}s`; const returnDate = dateAdd(new Date(input), { [_unit]: value }); return returnDate.toISOString(); // dateAdd(new Date(input), { [unit]: value }); }; /** * Converts an ISO datetime string to a target timezone, handling daylight savings, and returns an ISO string with the correct offset. * * @example * convertTimeZone('2025-06-26T12:00:00Z', 'Europe/Amsterdam') // 2025-06-26T14:00:00.0000000+02:00 * '2025-06-26T12:00:00Z'|convertTimeZone('Pacific Standard Time') // '2025-06-26T05:00:00.0000000-07:00' * @group DateTime * * @param input ISO datetime string * @param targetTimeZone Target timezone (IANA or Windows ID or fixed offset) * @returns ISO datetime string with correct offset */ export const convertTimeZone = (input, targetTimeZone) => { if (typeof input !== "string" || typeof targetTimeZone !== "string") return null; try { const date = new Date(input); if (isNaN(date.getTime())) return null; const tzStr = targetTimeZone.trim(); // Fixed offset: e.g. +02:00 or -08:00 const offsetMatch = /^([+-])(\d{2}):(\d{2})$/.exec(tzStr); if (offsetMatch) { const sign = offsetMatch[1] === "+" ? 1 : -1; const hours = parseInt(offsetMatch[2], 10); const minutes = parseInt(offsetMatch[3], 10); const offset = sign * (hours * 60 + minutes); const utcMillis = date.getTime(); const offsetMillis = offset * 60 * 1000; const converted = new Date(utcMillis + offsetMillis); const pad = (n, l = 2) => n.toString().padStart(l, "0"); const offsetStr = `${offsetMatch[1]}${pad(hours)}:${pad(minutes)}`; let iso = converted.toISOString().replace("Z", ""); const isoMatch = iso.match(/^(.*\.(\d+))/); if (isoMatch) { const frac = isoMatch[2].padEnd(7, "0").slice(0, 7); iso = iso.replace(/\.(\d+)/, `.${frac}`); } return iso + offsetStr; } // Windows timezone mapping let ianaTz = tzStr; if (!tzStr.includes("/") && tzStr.toLowerCase() !== "utc") { try { const iana = findIana(tzStr); if (iana && iana.length > 0 && typeof iana[0] === "string") { ianaTz = iana[0]; } } catch { } } if (tzStr.toLowerCase() === "utc" || tzStr === "Etc/UTC") { ianaTz = "UTC"; } // Use formatInTimeZone for robust formatting // yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX for ISO with 7 fractional digits and offset // SSSSSSS is not a standard token, so pad manually after formatting // Use SSS for milliseconds, then pad to 7 digits const { formatInTimeZone } = require("date-fns-tz"); // Use ESM import in actual code let formatted = formatInTimeZone(date, ianaTz, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); // Patch to 7 digits for fractional seconds formatted = formatted.replace(/\.(\d{3})/, (m, ms) => `.${ms.padEnd(7, "0")}`); // Patch UTC output to use +00:00 instead of Z if (ianaTz === "UTC" && formatted.endsWith("Z")) { formatted = formatted.slice(0, -1) + "+00:00"; } return formatted; } catch { return null; } }; /** * Converts a local time string in a specified timezone to an ISO datetime string with the correct offset. * * @example * localTimeToIsoWithOffset('2025-06-26 14:00:00', 'Europe/Amsterdam') // '2025-06-26T14:00:00.0000000+02:00' * '2025-06-26 05:00:00'|localTimeToIsoWithOffset('Pacific Standard Time') // '2025-06-26T05:00:00.0000000-08:00' * @group DateTime * * @param localTime Local time string * @param timeZone Timezone (IANA or Windows ID or fixed offset) * @returns ISO datetime string with correct offset */ export const localTimeToIsoWithOffset = (localTime, timeZone) => { try { const utcDate = fromZonedTime(localTime, timeZone); let formatted = formatInTimeZone(utcDate, timeZone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); formatted = formatted.replace(/\.(\d{3})/, (m, ms) => `.${ms.padEnd(7, "0")}`); return formatted; } catch { return null; } }; /** * Evaluates a JEXL expression and returns the result. * If only one argument is provided, it is expected that the first argument is a JEXL expression. * If two arguments are provided, the first argument is the context (must be an object) and the second argument is the JEXL expression. * The expression uses the default JEXL extended grammar and can't use any custom defined functions or transforms. * * @example * _eval("1 + 2") // 3 * _eval({x: 5, y: 10}, "x + y") // 15 * "2 * 3"|_eval // 6 * _eval({name: "John"}, "name") // "John" * @group Utility * * @param input Either a JEXL expression string or a context object. * @param expression Optional JEXL expression when first argument is context. * @returns The result of evaluating the expression, or undefined if evaluation fails. */ export const _eval = (input, expression) => { if (expression === undefined) { const _input = typeof input === "string" ? input : JSON.stringify(input); return jexl.evalSync(_input); } if (typeof input === "object") { return jexl.evalSync(expression, input); } return undefined; }; /** * Generates a new UUID (Universally Unique Identifier). * * @example * uuid() // "123e4567-e89b-12d3-a456-426614174000" * uuid() // "987fcdeb-51a2-43d7-b123-456789abcdef" (different each time) * @group Utility * * @returns A new UUID v4 string. */ export const uuid = () => { return uuidv4(); }; /** * Returns the type of the input value as a string. * * Supported return values: * - "string", "number", "boolean", "undefined", "array", "object" * - Only for JS: "function", "symbol", "bigint" * * @param input - The value to check the type of. * @returns {string} The type of the input value. * * @example * type(5); // "number" * foo|type; // "string" * type(true); // "boolean" * [1,2,3]|type; // "array" * {foo:1}|type; // "object" * undefined|type; // "undefined" */ export const getType = (input) => { if (input === null) return "null"; if (Array.isArray(input)) return "array"; return typeof input; };