From 70dbc40fdba54e785427afe5b98133816978cd6e Mon Sep 17 00:00:00 2001 From: John Brinkman Date: Wed, 6 Nov 2024 17:43:11 +0100 Subject: [PATCH 1/3] allow array parameters to all the functions where it makes sense --- README.md | 2 +- doc/spec.adoc | 2 +- src/functions.js | 810 +++++++++++++++++++++++------------------- test/functions.json | 384 +++++++++++++++++++- test/specSamples.json | 2 +- test/tests.json | 59 ++- 6 files changed, 853 insertions(+), 406 deletions(-) diff --git a/README.md b/README.md index c4e935ce..25004deb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Given: Visit the [Playground](https://opensource.adobe.com/json-formula/dist/index.html) # Documentation -Specification / Reference: [HTML](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-1.1.0.html) / [PDF](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-1.1.0.pdf) +Specification / Reference: [HTML](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-1.1.2.html) / [PDF](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-1.1.2.pdf) [JavaScript API](./doc/output/JSDOCS.md) diff --git a/doc/spec.adoc b/doc/spec.adoc index 2c5c5861..405fe0d7 100644 --- a/doc/spec.adoc +++ b/doc/spec.adoc @@ -91,7 +91,7 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ eval([1,2,3] ~ 4, {}) -> [1,2,3,4] eval(123 < "124", {}) -> true eval("23" > 111, {}) -> false - eval(abs("-2"), {}) -> 2 + eval(avg(["2", "3", "4"]), {}) -> 3 eval(1 == "1", {}) -> false ---- diff --git a/src/functions.js b/src/functions.js index 4c2039a7..6dcc9e5a 100644 --- a/src/functions.js +++ b/src/functions.js @@ -101,6 +101,247 @@ export default function functions( return JSON.stringify(value, null, offset); } + function balanceArrays(listOfArrays) { + const maxLen = Math.max(...listOfArrays.map(a => (Array.isArray(a) ? a.length : 0))); + const allArrays = listOfArrays.map(a => { + if (Array.isArray(a)) { + return a.concat(Array(maxLen - a.length).fill(null)); + } + return Array(maxLen).fill(a); + }); + // convolve allArrays + const arrays = []; + for (let i = 0; i < maxLen; i += 1) { + const row = []; + for (let j = 0; j < allArrays.length; j += 1) { + row.push(allArrays[j][i]); + } + arrays.push(row); + } + return arrays; + } + + function evaluate(args, fn) { + if (args.some(Array.isArray)) { + return balanceArrays(args).map(a => fn(...a)); + } + return fn(...args); + } + + function datedifFn(date1Arg, date2Arg, unitArg) { + const unit = toString(unitArg).toLowerCase(); + const date1 = getDateObj(date1Arg); + const date2 = getDateObj(date2Arg); + if (date2 === date1) return 0; + if (date2 < date1) throw functionError('end_date must be >= start_date in datedif()'); + + if (unit === 'd') return Math.floor(getDateNum(date2 - date1)); + const yearDiff = date2.getFullYear() - date1.getFullYear(); + let monthDiff = date2.getMonth() - date1.getMonth(); + const dayDiff = date2.getDate() - date1.getDate(); + + if (unit === 'y') { + let y = yearDiff; + if (monthDiff < 0) y -= 1; + if (monthDiff === 0 && dayDiff < 0) y -= 1; + return y; + } + if (unit === 'm') { + return yearDiff * 12 + monthDiff + (dayDiff < 0 ? -1 : 0); + } + if (unit === 'ym') { + if (dayDiff < 0) monthDiff -= 1; + if (monthDiff <= 0 && yearDiff > 0) return 12 + monthDiff; + return monthDiff; + } + if (unit === 'yd') { + if (dayDiff < 0) monthDiff -= 1; + if (monthDiff < 0) date2.setFullYear(date1.getFullYear() + 1); + else date2.setFullYear(date1.getFullYear()); + return Math.floor(getDateNum(date2 - date1)); + } + throw functionError(`Unrecognized unit parameter "${unit}" for datedif()`); + } + + function endsWithFn(searchArg, suffixArg) { + const searchStr = valueOf(searchArg); + const suffix = valueOf(suffixArg); + // make sure the comparison is based on code points + const search = Array.from(searchStr).reverse(); + const ending = Array.from(suffix).reverse(); + return ending.every((c, i) => c === search[i]); + } + + function eomonthFn(dateArg, monthsArg) { + const jsDate = getDateObj(dateArg); + const months = toInteger(monthsArg); + // We can give the constructor a month value > 11 and it will increment the years + // Since day is 1-based, giving zero will yield the last day of the previous month + const newDate = new Date(jsDate.getFullYear(), jsDate.getMonth() + months + 1, 0); + return getDateNum(newDate); + } + + function findFn(queryArg, textArg, offsetArg) { + const query = Array.from(toString(queryArg)); + const text = Array.from(toString(textArg)); + const offset = toInteger(offsetArg); + if (offset < 0) throw evaluationError('find() start position must be >= 0'); + if (query.length === 0) { + // allow an empty string to be found at any position -- including the end + if (offset > text.length) return null; + return offset; + } + for (let i = offset; i < text.length; i += 1) { + if (text.slice(i, i + query.length).every((c, j) => c === query[j])) { + return i; + } + } + return null; + } + + function properFn(arg) { + const capitalize = word => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; + const original = toString(arg); + // split the string by whitespace, punctuation, and numbers + const wordParts = original.match(/[\s\d\p{P}]+|[^\s\d\p{P}]+/gu); + if (wordParts !== null) return wordParts.map(w => capitalize(w)).join(''); + return capitalize(original); + } + + function reptFn(textArg, countArg) { + const text = toString(textArg); + const count = toInteger(countArg); + if (count < 0) throw evaluationError('rept() count must be greater than or equal to 0'); + return text.repeat(count); + } + + function searchFn(findTextString, withinTextString, startPosInt = 0) { + const findText = toString(findTextString); + const withinText = toString(withinTextString); + const startPos = toInteger(startPosInt); + if (startPos < 0) throw functionError('search() startPos must be greater than or equal to 0'); + if (findText === null || withinText === null || withinText.length === 0) return []; + + // Process as an array of code points + // Find escapes and wildcards + const globString = Array.from(findText).reduce((acc, cur) => { + if (acc.escape) return { escape: false, result: acc.result.concat(cur) }; + if (cur === '\\') return { escape: true, result: acc.result }; + if (cur === '?') return { escape: false, result: acc.result.concat('dot') }; + if (cur === '*') { + // consecutive * are treated as a single * + if (acc.result.slice(-1).pop() === 'star') return acc; + return { escape: false, result: acc.result.concat('star') }; + } + return { escape: false, result: acc.result.concat(cur) }; + }, { escape: false, result: [] }).result; + + const testMatch = (array, glob, match) => { + // we've consumed the entire glob, so we're done + if (glob.length === 0) return match; + // we've consumed the entire array, but there's still glob left -- no match + if (array.length === 0) return null; + const testChar = array[0]; + let [globChar, ...nextGlob] = glob; + const isStar = globChar === 'star'; + if (isStar) { + // '*' is at the end of the match -- so we're done matching + if (glob.length === 1) return match; + // we'll check for a match past the * and if not found, we'll process the * + [globChar, ...nextGlob] = glob.slice(1); + } + if (testChar === globChar || globChar === 'dot') { + return testMatch(array.slice(1), nextGlob, match.concat(testChar)); + } + // no match, so consume wildcard * + if (isStar) return testMatch(array.slice(1), glob, match.concat(testChar)); + + return null; + }; + // process code points + const within = Array.from(withinText); + for (let i = startPos; i < within.length; i += 1) { + const result = testMatch(within.slice(i), globString, []); + if (result !== null) return [i, result.join('')]; + } + return []; + } + + function splitFn(strArg, separatorArg) { + const str = toString(strArg); + const separator = toString(separatorArg); + // for empty separator, return an array of code points + return separator.length === 0 ? Array.from(str) : str.split(separator); + } + + function startsWithFn(subjectString, prefixString) { + const subject = Array.from(toString(subjectString)); + const prefix = Array.from(toString(prefixString)); + if (prefix.length > subject.length) return false; + for (let i = 0; i < prefix.length; i += 1) { + if (prefix[i] !== subject[i]) return false; + } + return true; + } + + function substituteFn(source, oldString, replacementString, nearest) { + const src = Array.from(toString(source)); + const old = Array.from(toString(oldString)); + const replacement = Array.from(toString(replacementString)); + + if (old.length === 0) return source; + + // no third parameter? replace all instances + let replaceAll = true; + let whch = 0; + if (nearest > -1) { + replaceAll = false; + whch = nearest + 1; + } + + let found = 0; + const result = []; + // find the instances to replace + for (let j = 0; j < src.length;) { + const match = old.every((c, i) => src[j + i] === c); + if (match) found += 1; + if (match && (replaceAll || found === whch)) { + result.push(...replacement); + j += old.length; + } else { + result.push(src[j]); + j += 1; + } + } + return result.join(''); + } + + function truncFn(number, d) { + const digits = toInteger(d); + + const method = number >= 0 ? Math.floor : Math.ceil; + return method(number * 10 ** digits) / 10 ** digits; + } + + function weekdayFn(date, type) { + const jsDate = getDateObj(date); + const day = jsDate.getDay(); + // day is in range [0-7) with 0 mapping to sunday + switch (toInteger(type)) { + case 1: + // range = [1, 7], sunday = 1 + return day + 1; + case 2: + // range = [1, 7] sunday = 7 + return ((day + 6) % 7) + 1; + case 3: + // range = [0, 6] sunday = 6 + return (day + 6) % 7; + default: + throw functionError(`Unsupported returnType: "${type}" for weekday()`); + } + } + const functionMap = { // name: [function, ] // The can be: @@ -124,8 +365,8 @@ export default function functions( * abs(-1) // returns 1 */ abs: { - _func: resolvedArgs => Math.abs(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.abs), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** * Compute the inverse cosine (in radians) of a number. @@ -137,8 +378,8 @@ export default function functions( * acos(0) => 1.5707963267948966 */ acos: { - _func: resolvedArgs => validNumber(Math.acos(resolvedArgs[0]), 'acos'), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, n => validNumber(Math.acos(n), 'acos')), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -161,7 +402,7 @@ export default function functions( }); return result; }, - _signature: [{ types: [dataTypes.TYPE_ANY], variadic: true }], + _signature: [{ types: [TYPE_ANY], variadic: true }], }, /** @@ -174,8 +415,8 @@ export default function functions( * Math.asin(0) => 0 */ asin: { - _func: resolvedArgs => validNumber(Math.asin(resolvedArgs[0]), 'asin'), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, n => validNumber(Math.asin(n), 'asin')), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -190,10 +431,10 @@ export default function functions( * atan2(20,10) => 1.1071487177940904 */ atan2: { - _func: resolvedArgs => Math.atan2(resolvedArgs[0], resolvedArgs[1]), + _func: args => evaluate(args, Math.atan2), _signature: [ - { types: [TYPE_NUMBER] }, - { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -229,12 +470,11 @@ export default function functions( * casefold("AbC") // returns "abc" */ casefold: { - _func: (args, _data, interpreter) => { - const str = toString(args[0]); - return str.toLocaleUpperCase(interpreter.language).toLocaleLowerCase(interpreter.language); - }, + _func: (args, _data, interpreter) => evaluate(args, s => toString(s) + .toLocaleUpperCase(interpreter.language) + .toLocaleLowerCase(interpreter.language)), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -250,8 +490,8 @@ export default function functions( */ ceil: { - _func: resolvedArgs => Math.ceil(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.ceil), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** * Retrieve the first code point from a string @@ -262,12 +502,12 @@ export default function functions( * codePoint("ABC") // 65 */ codePoint: { - _func: args => { - const text = toString(args[0]); + _func: args => evaluate(args, arg => { + const text = toString(arg); return text.length === 0 ? null : text.codePointAt(0); - }, + }), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -320,8 +560,8 @@ export default function functions( * cos(1.0471975512) => 0.4999999999970535 */ cos: { - _func: resolvedArgs => Math.cos(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.cos), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -353,44 +593,11 @@ export default function functions( * // 75 days between June 1 and August 15, ignoring the years of the dates (75) */ datedif: { - _func: args => { - const unit = toString(args[2]).toLowerCase(); - const date1 = getDateObj(args[0]); - const date2 = getDateObj(args[1]); - if (date2 === date1) return 0; - if (date2 < date1) throw functionError('end_date must be >= start_date in datedif()'); - - if (unit === 'd') return Math.floor(getDateNum(date2 - date1)); - const yearDiff = date2.getFullYear() - date1.getFullYear(); - let monthDiff = date2.getMonth() - date1.getMonth(); - const dayDiff = date2.getDate() - date1.getDate(); - - if (unit === 'y') { - let y = yearDiff; - if (monthDiff < 0) y -= 1; - if (monthDiff === 0 && dayDiff < 0) y -= 1; - return y; - } - if (unit === 'm') { - return yearDiff * 12 + monthDiff + (dayDiff < 0 ? -1 : 0); - } - if (unit === 'ym') { - if (dayDiff < 0) monthDiff -= 1; - if (monthDiff <= 0 && yearDiff > 0) return 12 + monthDiff; - return monthDiff; - } - if (unit === 'yd') { - if (dayDiff < 0) monthDiff -= 1; - if (monthDiff < 0) date2.setFullYear(date1.getFullYear() + 1); - else date2.setFullYear(date1.getFullYear()); - return Math.floor(getDateNum(date2 - date1)); - } - throw functionError(`Unrecognized unit parameter "${unit}" for datedif()`); - }, + _func: args => evaluate(args, datedifFn), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -435,13 +642,13 @@ export default function functions( return getDateNum(baseDate); }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -456,9 +663,9 @@ export default function functions( * day(datetime(2008,5,23)) // returns 23 */ day: { - _func: args => getDateObj(args[0]).getDate(), + _func: args => evaluate(args, a => getDateObj(a).getDate()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -491,8 +698,8 @@ export default function functions( return arg; }, _signature: [ - { types: [dataTypes.TYPE_ANY] }, - { types: [dataTypes.TYPE_ANY, dataTypes.TYPE_EXPREF], optional: true }, + { types: [TYPE_ANY] }, + { types: [TYPE_ANY, TYPE_EXPREF], optional: true }, ], }, @@ -532,8 +739,8 @@ export default function functions( return items; }, _signature: [ - { types: [dataTypes.TYPE_OBJECT, dataTypes.TYPE_ARRAY, dataTypes.TYPE_NULL] }, - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_NUMBER] }, + { types: [TYPE_OBJECT, TYPE_ARRAY, TYPE_NULL] }, + { types: [TYPE_STRING, TYPE_NUMBER] }, ], }, @@ -548,15 +755,11 @@ export default function functions( * endsWith("Abcd", "A") // returns false */ endsWith: { - _func: resolvedArgs => { - const searchStr = valueOf(resolvedArgs[0]); - const suffix = valueOf(resolvedArgs[1]); - // make sure the comparison is based on code points - const search = Array.from(searchStr).reverse(); - const ending = Array.from(suffix).reverse(); - return ending.every((c, i) => c === search[i]); - }, - _signature: [{ types: [TYPE_STRING] }, { types: [TYPE_STRING] }], + _func: args => evaluate(args, endsWithFn), + _signature: [ + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + ], }, /** @@ -578,8 +781,8 @@ export default function functions( _signature: [ { types: [ - dataTypes.TYPE_ARRAY, - dataTypes.TYPE_OBJECT, + TYPE_ARRAY, + TYPE_OBJECT, ], }, ], @@ -599,17 +802,10 @@ export default function functions( * eomonth(datetime(2011, 1, 1), -3) | [month(@), day(@)] // returns [10, 31] */ eomonth: { - _func: args => { - const jsDate = getDateObj(args[0]); - const months = toInteger(args[1]); - // We can give the constructor a month value > 11 and it will increment the years - // Since day is 1-based, giving zero will yield the last day of the previous month - const newDate = new Date(jsDate.getFullYear(), jsDate.getMonth() + months + 1, 0); - return getDateNum(newDate); - }, + _func: args => evaluate(args, eomonthFn), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -622,9 +818,9 @@ export default function functions( * exp(10) // returns 22026.465794806718 */ exp: { - _func: args => Math.exp(args[0]), + _func: args => evaluate(args, Math.exp), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -654,27 +850,15 @@ export default function functions( * find("M", "abMcdM", 2) // returns 2 */ find: { - _func: args => { - const query = Array.from(toString(args[0])); - const text = Array.from(toString(args[1])); - const offset = args.length > 2 ? toInteger(args[2]) : 0; - if (offset < 0) throw evaluationError('find() start position must be >= 0'); - if (query.length === 0) { - // allow an empty string to be found at any position -- including the end - if (offset > text.length) return null; - return offset; - } - for (let i = offset; i < text.length; i += 1) { - if (text.slice(i, i + query.length).every((c, j) => c === query[j])) { - return i; - } - } - return null; + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + if (args.length < 3) args.push(0); + return evaluate(args, findFn); }, _signature: [ - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true }, ], }, @@ -689,8 +873,8 @@ export default function functions( * floor(10) // returns 10 */ floor: { - _func: resolvedArgs => Math.floor(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.floor), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -705,15 +889,15 @@ export default function functions( */ fromCodePoint: { _func: args => { - const code = toInteger(args[0]); try { - return String.fromCodePoint(code); + const points = Array.isArray(args[0]) ? args[0] : [args[0]]; + return String.fromCodePoint(...points.map(toInteger)); } catch (e) { - throw evaluationError(`Invalid code point: "${code}"`); + throw evaluationError(`Invalid code point: "${args[0]}"`); } }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -744,7 +928,7 @@ export default function functions( return Object.fromEntries(array); }, _signature: [ - { types: [dataTypes.TYPE_ARRAY_ARRAY] }, + { types: [TYPE_ARRAY_ARRAY] }, ], }, @@ -758,8 +942,8 @@ export default function functions( * fround(100.44444444444444444444) => 100.44444274902344 */ fround: { - _func: resolvedArgs => Math.fround(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.fround), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -786,7 +970,7 @@ export default function functions( const obj = valueOf(args[0]); if (obj === null) return false; const isArray = isArrayType(obj); - if (!(isArray || getType(obj) === dataTypes.TYPE_OBJECT)) { + if (!(isArray || getType(obj) === TYPE_OBJECT)) { throw typeError('First parameter to hasProperty() must be either an object or array.'); } @@ -798,8 +982,8 @@ export default function functions( return result !== undefined; }, _signature: [ - { types: [dataTypes.TYPE_ANY] }, - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_NUMBER] }, + { types: [TYPE_ANY] }, + { types: [TYPE_STRING, TYPE_NUMBER] }, ], }, /** @@ -815,9 +999,9 @@ export default function functions( * hour(time(12, 0, 0)) // returns 12 */ hour: { - _func: args => getDateObj(args[0]).getHours(), + _func: args => evaluate(args, a => getDateObj(a).getHours()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -852,9 +1036,9 @@ export default function functions( return interpreter.visit(rightBranchNode, data); }, _signature: [ - { types: [dataTypes.TYPE_ANY] }, - { types: [dataTypes.TYPE_ANY] }, - { types: [dataTypes.TYPE_ANY] }], + { types: [TYPE_ANY] }, + { types: [TYPE_ANY] }, + { types: [TYPE_ANY] }], }, /** @@ -914,8 +1098,8 @@ export default function functions( return text.slice(0, numEntries).join(''); }, _signature: [ - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_ARRAY] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_STRING, TYPE_ARRAY] }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -956,8 +1140,8 @@ export default function functions( * log(10) // 2.302585092994046 */ log: { - _func: resolvedArgs => validNumber(Math.log(resolvedArgs[0]), 'log'), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, a => validNumber(Math.log(a), 'log')), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -969,8 +1153,8 @@ export default function functions( * log10(100000) // 5 */ log10: { - _func: resolvedArgs => validNumber(Math.log10(resolvedArgs[0]), 'log10'), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, a => validNumber(Math.log10(a), 'log10')), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -982,12 +1166,9 @@ export default function functions( * lower("E. E. Cummings") // returns e. e. cummings */ lower: { - _func: args => { - const value = toString(args[0]); - return value.toLowerCase(); - }, + _func: args => evaluate(args, a => toString(a).toLowerCase()), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -1101,9 +1282,9 @@ export default function functions( return text.slice(startPos, startPos + numEntries).join(''); }, _signature: [ - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_ARRAY] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_STRING, TYPE_ARRAY] }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER] }, ], }, @@ -1119,9 +1300,9 @@ export default function functions( * millisecond(datetime(2008, 5, 23, 12, 10, 53, 42)) // returns 42 */ millisecond: { - _func: args => getDateObj(args[0]).getMilliseconds(), + _func: args => evaluate(args, a => getDateObj(a).getMilliseconds()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1173,9 +1354,9 @@ export default function functions( * minute(time(12, 10, 0)) // returns 10 */ minute: { - _func: args => getDateObj(args[0]).getMinutes(), + _func: args => evaluate(args, a => getDateObj(a).getMinutes()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1192,16 +1373,14 @@ export default function functions( * mod(-3, 2) // returns -1 */ mod: { - _func: args => { - const p1 = args[0]; - const p2 = args[1]; - const result = p1 % p2; - if (Number.isNaN(result)) throw evaluationError(`Bad parameter for mod: '${p1} % ${p2}'`); + _func: args => evaluate(args, (a, b) => { + const result = a % b; + if (Number.isNaN(result)) throw evaluationError(`Bad parameter for mod: '${a} % ${b}'`); return result; - }, + }), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1218,9 +1397,9 @@ export default function functions( */ month: { // javascript months start from 0 - _func: args => getDateObj(args[0]).getMonth() + 1, + _func: args => evaluate(args, a => getDateObj(a).getMonth() + 1), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1239,7 +1418,7 @@ export default function functions( */ not: { _func: resolveArgs => !toBoolean(valueOf(resolveArgs[0])), - _signature: [{ types: [dataTypes.TYPE_ANY] }], + _signature: [{ types: [TYPE_ANY] }], }, /** @@ -1302,7 +1481,7 @@ export default function functions( }); return result; }, - _signature: [{ types: [dataTypes.TYPE_ANY], variadic: true }], + _signature: [{ types: [TYPE_ANY], variadic: true }], }, /** @@ -1315,10 +1494,10 @@ export default function functions( * power(10, 2) // returns 100 (10 raised to power 2) */ power: { - _func: args => validNumber(args[0] ** args[1], 'power'), + _func: args => evaluate(args, (a, b) => validNumber(a ** b, 'power')), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1337,16 +1516,9 @@ export default function functions( * proper("76BudGet") // returns "76Budget" */ proper: { - _func: args => { - const capitalize = word => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; - const original = toString(args[0]); - // split the string by whitespace, punctuation, and numbers - const wordParts = original.match(/[\s\d\p{P}]+|[^\s\d\p{P}]+/gu); - if (wordParts !== null) return wordParts.map(w => capitalize(w)).join(''); - return capitalize(original); - }, + _func: args => evaluate(args, properFn), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -1489,10 +1661,10 @@ export default function functions( return subject.join(''); }, _signature: [ - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_ARRAY] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_ANY] }, + { types: [TYPE_STRING, TYPE_ARRAY] }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_ANY] }, ], }, @@ -1508,15 +1680,10 @@ export default function functions( * rept("x", 5) // returns "xxxxx" */ rept: { - _func: args => { - const text = toString(args[0]); - const count = toInteger(args[1]); - if (count < 0) throw evaluationError('rept() count must be greater than or equal to 0'); - return text.repeat(count); - }, + _func: args => evaluate(args, reptFn), _signature: [ - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1567,8 +1734,8 @@ export default function functions( return text.slice(numEntries * -1).join(''); }, _signature: [ - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_ARRAY] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_STRING, TYPE_ARRAY] }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -1592,10 +1759,17 @@ export default function functions( * round(-1.5) // -1 */ round: { - _func: args => round(args[0], args.length > 1 ? toInteger(args[1]) : 0), + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + if (args.length < 2)args.push(0); + return evaluate(args, (a, n) => { + const digits = toInteger(n); + return round(a, digits); + }); + }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true }, ], }, @@ -1619,61 +1793,15 @@ export default function functions( * search("a?c", "acabc") // returns [2, "abc"] */ search: { - _func: args => { - const findText = toString(args[0]); - const withinText = toString(args[1]); - const startPos = args.length > 2 ? toInteger(args[2]) : 0; - if (startPos < 0) throw functionError('search() startPos must be greater than or equal to 0'); - if (findText === null || withinText === null || withinText.length === 0) return []; - - // Process as an array of code points - // Find escapes and wildcards - const globString = Array.from(findText).reduce((acc, cur) => { - if (acc.escape) return { escape: false, result: acc.result.concat(cur) }; - if (cur === '\\') return { escape: true, result: acc.result }; - if (cur === '?') return { escape: false, result: acc.result.concat('dot') }; - if (cur === '*') { - // consecutive * are treated as a single * - if (acc.result.slice(-1).pop() === 'star') return acc; - return { escape: false, result: acc.result.concat('star') }; - } - return { escape: false, result: acc.result.concat(cur) }; - }, { escape: false, result: [] }).result; - - const testMatch = (array, glob, match) => { - // we've consumed the entire glob, so we're done - if (glob.length === 0) return match; - // we've consumed the entire array, but there's still glob left -- no match - if (array.length === 0) return null; - const testChar = array[0]; - let [globChar, ...nextGlob] = glob; - const isStar = globChar === 'star'; - if (isStar) { - // '*' is at the end of the match -- so we're done matching - if (glob.length === 1) return match; - // we'll check for a match past the * and if not found, we'll process the * - [globChar, ...nextGlob] = glob.slice(1); - } - if (testChar === globChar || globChar === 'dot') { - return testMatch(array.slice(1), nextGlob, match.concat(testChar)); - } - // no match, so consume wildcard * - if (isStar) return testMatch(array.slice(1), glob, match.concat(testChar)); - - return null; - }; - // process code points - const within = Array.from(withinText); - for (let i = startPos; i < within.length; i += 1) { - const result = testMatch(within.slice(i), globString, []); - if (result !== null) return [i, result.join('')]; - } - return []; + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + if (args.length < 2) args.push(0); + return evaluate(args, searchFn); }, _signature: [ - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true }, ], }, @@ -1691,9 +1819,9 @@ export default function functions( * second(time(12, 10, 53)) // returns 53 */ second: { - _func: args => getDateObj(args[0]).getSeconds(), + _func: args => evaluate(args, a => getDateObj(a).getSeconds()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1709,8 +1837,8 @@ export default function functions( * sign(0) // 0 */ sign: { - _func: resolvedArgs => Math.sign(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.sign), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -1723,8 +1851,8 @@ export default function functions( * sin(1) // 0.8414709848078965 */ sin: { - _func: resolvedArgs => Math.sin(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.sin), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -1834,15 +1962,10 @@ export default function functions( * split("abcdef", "e") // returns ["abcd", "f"] */ split: { - _func: args => { - const str = toString(args[0]); - const separator = toString(args[1]); - // for empty separator, return an array of code points - return separator.length === 0 ? Array.from(str) : str.split(separator); - }, + _func: args => evaluate(args, splitFn), _signature: [ - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -1855,12 +1978,9 @@ export default function functions( * sqrt(4) // returns 2 */ sqrt: { - _func: args => { - const result = Math.sqrt(args[0]); - return validNumber(result, 'sqrt'); - }, + _func: args => evaluate(args, arg => validNumber(Math.sqrt(arg), 'sqrt')), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, @@ -1874,16 +1994,11 @@ export default function functions( * startsWith("jack is at home", "jack") // returns true */ startsWith: { - _func: resolvedArgs => { - const subject = Array.from(toString(resolvedArgs[0])); - const prefix = Array.from(toString(resolvedArgs[1])); - if (prefix.length > subject.length) return false; - for (let i = 0; i < prefix.length; i += 1) { - if (prefix[i] !== subject[i]) return false; - } - return true; - }, - _signature: [{ types: [TYPE_STRING] }, { types: [TYPE_STRING] }], + _func: args => evaluate(args, startsWithFn), + _signature: [ + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + ], }, /** * Estimates standard deviation based on a sample. @@ -1908,7 +2023,7 @@ export default function functions( return validNumber(result, 'stdev'); }, _signature: [ - { types: [dataTypes.TYPE_ARRAY_NUMBER] }, + { types: [TYPE_ARRAY_NUMBER] }, ], }, @@ -1936,7 +2051,7 @@ export default function functions( return validNumber(result, 'stdevp'); }, _signature: [ - { types: [dataTypes.TYPE_ARRAY_NUMBER] }, + { types: [TYPE_ARRAY_NUMBER] }, ], }, @@ -1959,44 +2074,26 @@ export default function functions( * substitute("Quarter 1, 2011", "1", "2", 2)" // returns "Quarter 1, 2012" */ substitute: { - _func: args => { - const src = Array.from(toString(args[0])); - const old = Array.from(toString(args[1])); - const replacement = Array.from(toString(args[2])); - - if (old.length === 0) return args[0]; - - // no third parameter? replace all instances - let replaceAll = true; - let whch = 0; + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + let n; if (args.length > 3) { - replaceAll = false; - whch = toInteger(args[3]); - if (whch < 0) throw evaluationError('substitute() which parameter must be greater than or equal to 0'); - whch += 1; - } - - let found = 0; - const result = []; - // find the instances to replace - for (let j = 0; j < src.length;) { - const match = old.every((c, i) => src[j + i] === c); - if (match) found += 1; - if (match && (replaceAll || found === whch)) { - result.push(...replacement); - j += old.length; + if (Array.isArray(args[3])) { + n = args[3].map(toInteger); + if (n.find(o => o < 0) !== undefined) throw evaluationError('substitute() which parameter must be greater than or equal to 0'); } else { - result.push(src[j]); - j += 1; + n = toInteger(args[3]); + if (n < 0) throw evaluationError('substitute() which parameter must be greater than or equal to 0'); } + args[3] = n; } - return result.join(''); + return evaluate(args, substituteFn); }, _signature: [ - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_STRING] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true }, ], }, @@ -2029,8 +2126,8 @@ export default function functions( * tan(1) // 1.5574077246549023 */ tan: { - _func: resolvedArgs => Math.tan(resolvedArgs[0]), - _signature: [{ types: [TYPE_NUMBER] }], + _func: args => evaluate(args, Math.tan), + _signature: [{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }], }, /** @@ -2060,9 +2157,9 @@ export default function functions( return getDateNum(epochTime); }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER] }, + { types: [TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -2216,7 +2313,7 @@ export default function functions( }, _signature: [ { types: [TYPE_ANY] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -2250,14 +2347,9 @@ export default function functions( * trim(" ab c ") // returns "ab c" */ trim: { - _func: args => { - const text = toString(args[0]); - // only removes the space character - // other whitespace characters like \t \n left intact - return text.split(' ').filter(x => x).join(' '); - }, + _func: args => evaluate(args, s => toString(s).split(' ').filter(x => x).join(' ')), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -2273,27 +2365,26 @@ export default function functions( }, /** - * Truncates a number to an integer by removing the fractional part of the number. - * i.e. it rounds towards zero. - * @param {number} numA number to truncate - * @param {integer} [numB=0] A number specifying the number of decimal digits to preserve. - * @return {number} Truncated value - * @function trunc - * @example - * trunc(8.9) // returns 8 - * trunc(-8.9) // returns -8 - * trunc(8.912, 2) // returns 8.91 - */ + * Truncates a number to an integer by removing the fractional part of the number. + * i.e. it rounds towards zero. + * @param {number} numA number to truncate + * @param {integer} [numB=0] A number specifying the number of decimal digits to preserve. + * @return {number} Truncated value + * @function trunc + * @example + * trunc(8.9) // returns 8 + * trunc(-8.9) // returns -8 + * trunc(8.912, 2) // returns 8.91 + */ trunc: { - _func: args => { - const number = args[0]; - const digits = args.length > 1 ? toInteger(args[1]) : 0; - const method = number >= 0 ? Math.floor : Math.ceil; - return method(number * 10 ** digits) / 10 ** digits; + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + if (args.length < 2) args.push(0); + return evaluate(args, truncFn); }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true }, ], }, @@ -2353,7 +2444,7 @@ export default function functions( ); }, _signature: [ - { types: [dataTypes.TYPE_ARRAY] }, + { types: [TYPE_ARRAY] }, ], }, @@ -2366,9 +2457,9 @@ export default function functions( * upper("abcd") // returns "ABCD" */ upper: { - _func: args => toString(args[0]).toUpperCase(), + _func: args => evaluate(args, a => toString(a).toUpperCase()), _signature: [ - { types: [dataTypes.TYPE_STRING] }, + { types: [TYPE_STRING, TYPE_ARRAY_STRING] }, ], }, @@ -2397,7 +2488,7 @@ export default function functions( } const obj = valueOf(args[0]); if (obj === null) return null; - if (!(getType(obj) === dataTypes.TYPE_OBJECT || subjectArray)) { + if (!(getType(obj) === TYPE_OBJECT || subjectArray)) { throw typeError('First parameter to value() must be one of: object, array, null.'); } if (subjectArray) { @@ -2418,8 +2509,8 @@ export default function functions( return result; }, _signature: [ - { types: [dataTypes.TYPE_ANY] }, - { types: [dataTypes.TYPE_STRING, dataTypes.TYPE_NUMBER] }, + { types: [TYPE_ANY] }, + { types: [TYPE_STRING, TYPE_NUMBER] }, ], }, @@ -2462,29 +2553,14 @@ export default function functions( * weekday(datetime(2006,5,21), 3) // 6 */ weekday: { - _func: args => { - const date = args[0]; - const type = args.length > 1 ? toInteger(args[1]) : 1; - const jsDate = getDateObj(date); - const day = jsDate.getDay(); - // day is in range [0-7) with 0 mapping to sunday - switch (type) { - case 1: - // range = [1, 7], sunday = 1 - return day + 1; - case 2: - // range = [1, 7] sunday = 7 - return ((day + 6) % 7) + 1; - case 3: - // range = [0, 6] sunday = 6 - return (day + 6) % 7; - default: - throw functionError(`Unsupported returnType: "${type}" for weekday()`); - } + _func: resolvedArgs => { + const args = resolvedArgs.slice(); + if (args.length < 2) args.push(1); + return evaluate(args, weekdayFn); }, _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, - { types: [dataTypes.TYPE_NUMBER], optional: true }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, + { types: [TYPE_NUMBER], optional: true }, ], }, @@ -2500,9 +2576,9 @@ export default function functions( * year(datetime(2008,5,23)) // returns 2008 */ year: { - _func: args => getDateObj(args[0]).getFullYear(), + _func: args => evaluate(args, a => getDateObj(a).getFullYear()), _signature: [ - { types: [dataTypes.TYPE_NUMBER] }, + { types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER] }, ], }, diff --git a/test/functions.json b/test/functions.json index 7a66a97f..7af7ce2e 100644 --- a/test/functions.json +++ b/test/functions.json @@ -138,8 +138,7 @@ }, { "expression": "abs(`false`)", - "result": 0, - "was": "TypeError" + "error": "TypeError" }, { "expression": "abs(`-24`)", @@ -264,8 +263,7 @@ }, { "expression": "endsWith(str, `0`)", - "result": false, - "was": "TypeError" + "error": "TypeError" }, { "expression": "floor(`1.2`)", @@ -733,8 +731,7 @@ }, { "expression": "startsWith(str, `0`)", - "result": false, - "was": "TypeError" + "error": "TypeError" }, { "expression": "sum(numbers)", @@ -1753,5 +1750,380 @@ "error": "TypeError" } ] + }, + { + "given": { + "num": 3, + "arrayNum": [-3.333, -2.22, -1.1, 0, 1.1, 2.22, 3.333], + "arrayInt": [1,2,3,4,5,6,7], + "str": "abcdefg", + "arrayStr": ["abc", "bcd", "cde", "def", "efg"] + }, + "cases": [ + { + "expression": "abs(arrayNum)", + "result": [3.333, 2.22, 1.1, 0, 1.1, 2.22, 3.333] + }, + { + "expression": "acos(arrayInt / (arrayInt + 1))", + "result": [ + 1.0471975511965979, 0.8410686705679303, 0.7227342478134157, + 0.6435011087932843, 0.5856855434571508, 0.5410995259571458, + 0.5053605102841573 + ] + }, + { + "expression": "asin(arrayInt / (arrayInt + 1))", + "result": [ + 0.5235987755982989, 0.7297276562269663, 0.848062078981481, + 0.9272952180016123, 0.9851107833377457, 1.0296968008377507, + 1.0654358165107394 + ] + }, + { + "expression": "atan2(arrayNum, 10)", + "result": [ + -0.32172055409664824, -0.21845717120535865, -0.10955952677394436, 0, + 0.10955952677394436, 0.21845717120535865, 0.32172055409664824 + ] + }, + { + "expression": "atan2(arrayNum, arrayInt)", + "result": [ + -1.2793120068559851, -0.837483712611627, -0.3514447940035517, 0, + 0.2165503049760893, 0.35437991912343786, 0.44438039217805514 + ] + }, + { + "expression": "casefold(arrayStr)", + "result": ["abc", "bcd", "cde", "def", "efg"] + }, + { + "expression": "ceil(arrayNum)", + "result": [-3, -2, -1, 0, 2, 3, 4] + }, + { + "expression": "codePoint(arrayStr)", + "result": [97, 98, 99, 100, 101] + }, + { + "expression": "cos(arrayNum)", + "result": [ + -0.9817374728267503, -0.6045522710579296, 0.4535961214255773, 1, + 0.4535961214255773, -0.6045522710579296, -0.9817374728267503 + ] + }, + { + "expression": "now() | datedif(@, @ + 1, [\"y\",\"m\",\"d\",\"ym\",\"yd\"])", + "result": [0, 0, 1, 0, 1] + }, + { + "expression": "now() | datedif([@, @], [@ + 1, @ + 2], \"d\")", + "result": [1, 2] + }, + + + + { + "expression": "datetime(2024, 10, 12) | day([@, @ + 1])", + "result": [12, 13] + }, + { + "expression": "endsWith(arrayStr, arrayStr)", + "result": [true, true, true, true, true] + }, + { + "expression": "endsWith(arrayStr, \"c\")", + "result": [true, false, false, false, false] + }, + { + "expression": "datetime(2024, 10, 12) | eomonth([@, @, @, @, @, @, @], 1)", + "result": [ + 20056.958333333332, 20056.958333333332, 20056.958333333332, + 20056.958333333332, 20056.958333333332, 20056.958333333332, + 20056.958333333332 + ] + }, + { + "expression": "{d: datetime(2024, 10, 12), n:arrayInt} | eomonth([@.d, @.d, @.d, @.d, @.d, @.d, @.d], @.n)", + "result": [ + 20056.958333333332, 20087.958333333332, 20118.958333333332, + 20146.958333333332, 20177.916666666668, 20207.916666666668, + 20238.916666666668 + ] + }, + { + "expression": "exp(arrayInt)", + "result": [ + 2.718281828459045, 7.38905609893065, 20.085536923187668, + 54.598150033144236, 148.4131591025766, 403.4287934927351, + 1096.6331584284585 + ] + }, + { + "expression": "find(arrayStr, arrayStr, [0,0,0,0,0,0])", + "result": [0, 0, 0, 0, 0, 0] + }, + { + "expression": "find(\"c\", [\"cc\",\"ccc\",\"cccc\",\"ccccc\",\"cccccc\",\"ccccccc\",\"cccccccc\"], arrayInt)", + "result": [1, 2, 3, 4, 5, 6, 7] + }, + { + "expression": "find(arrayStr, arrayStr, 0)", + "result": [0, 0, 0, 0, 0] + }, + { + "expression": "find(\"c\", \"abcdefg\", arrayInt)", + "result": [2, 2, null, null, null, null, null] + }, + { + "expression": "floor(arrayNum)", + "result": [-4, -3, -2, 0, 1, 2, 3] + }, + { + "expression": "fromCodePoint(64 + arrayInt)", + "result": "ABCDEFG" + }, + { + "expression": "fround(arrayNum)", + "result": [ + -3.3329999446868896, -2.2200000286102295, -1.100000023841858, 0, + 1.100000023841858, 2.2200000286102295, 3.3329999446868896 + ] + }, + { + "expression": "datetime(2024, 10, 12, 13) | hour([@, @])", + "result": [13, 13] + }, + { + "expression": "log(arrayInt)", + "result": [ + 0, 0.6931471805599453, 1.0986122886681096, 1.3862943611198906, + 1.6094379124341003, 1.791759469228055, 1.9459101490553132 + ] + }, + { + "expression": "log10(arrayInt)", + "result": [ + 0, 0.3010299956639812, 0.47712125471966244, 0.6020599913279624, + 0.6989700043360189, 0.7781512503836436, 0.8450980400142568 + ] + }, + { + "expression": "lower(arrayStr)", + "result": ["abc", "bcd", "cde", "def", "efg"] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | millisecond([@, @ + 1])", + "result": [16, 16] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | minute([@, @ + 1])", + "result": [14, 14] + }, + { + "expression": "mod(arrayInt, 2)", + "result": [1, 0, 1, 0, 1, 0, 1] + }, + { + "expression": "mod(arrayInt, arrayInt + 1)", + "result": [1, 2, 3, 4, 5, 6, 7] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | month([@, @])", + "result": [10, 10] + }, + { + "expression": "power(arrayInt, arrayInt)", + "result": [1, 4, 27, 256, 3125, 46656, 823543] + }, + { + "expression": "power(arrayInt, 2)", + "result": [1, 4, 9, 16, 25, 36, 49] + }, + { + "expression": "power(2, arrayInt)", + "result": [2, 4, 8, 16, 32, 64, 128] + }, + { + "expression": "proper(arrayStr)", + "result": ["Abc", "Bcd", "Cde", "Def", "Efg"] + }, + { + "expression": "rept(arrayStr, arrayInt)", + "result": [ + "abc", + "bcdbcd", + "cdecdecde", + "defdefdefdef", + "efgefgefgefgefg", + "", + "" + ] + }, + { + "expression": "rept(arrayStr, 2)", + "result": ["abcabc", "bcdbcd", "cdecde", "defdef", "efgefg"] + }, + { + "expression": "rept(\"a\", arrayInt)", + "result": ["a", "aa", "aaa", "aaaa", "aaaaa", "aaaaaa", "aaaaaaa"] + }, + { + "expression": "round(arrayNum)", + "result": [-3, -2, -1, 0, 1, 2, 3] + }, + { + "expression": "search(arrayStr, arrayStr, [0,0,0,0,0])", + "result": [ + [0, "abc"], + [0, "bcd"], + [0, "cde"], + [0, "def"], + [0, "efg"] + ] + }, + { + "expression": "search(\"c\", arrayStr, [0,1,2,3,4,5])", + "result": [[2, "c"], [1, "c"], [], [], [], []] + }, + { + "expression": "search(arrayStr, arrayStr, 0)", + "result": [ + [0, "abc"], + [0, "bcd"], + [0, "cde"], + [0, "def"], + [0, "efg"] + ] + }, + { + "expression": "search(arrayStr, \"abcdefg\", [0,1,2,3])", + "result": [ + [0, "abc"], + [1, "bcd"], + [2, "cde"], + [3, "def"], + [4, "efg"] + ] + }, + { + "expression": "search(\"b\", \"abcbebg\", [0,1,2,3])", + "result": [ + [1, "b"], + [1, "b"], + [3, "b"], + [3, "b"] + ] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | second([@, @])", + "result": [15, 15] + }, + { + "expression": "sign(arrayInt)", + "result": [1, 1, 1, 1, 1, 1, 1] + }, + { + "expression": "sin(arrayNum)", + "result": [ + 0.19024072762619915, -0.7965654722360865, -0.8912073600614354, 0, + 0.8912073600614354, 0.7965654722360865, -0.19024072762619915 + ] + }, + { + "expression": "split(arrayStr, \"\")", + "result": [ + ["a", "b", "c"], + ["b", "c", "d"], + ["c", "d", "e"], + ["d", "e", "f"], + ["e", "f", "g"] + ] + }, + { + "expression": "split(\"abcdefg\", [\"b\", \"c\"])", + "result": [ + ["a", "cdefg"], + ["ab", "defg"] + ] + }, + { + "expression": "split(arrayStr, [\"a\", \"b\"])", + "result": [ + ["", "bc"], + ["", "cd"], + ["c", "d", "e"], + ["d", "e", "f"], + ["e", "f", "g"] + ] + }, + { + "expression": "sqrt(arrayNum[?@ > 0])", + "result": [ + 1.0488088481701516, + 1.489966442575134, + 1.8256505689753448 + ] + }, + { + "expression": "startsWith(arrayStr, arrayStr)", + "result": [true, + true, + true, + true, + true] + }, + { + "expression": "startsWith(arrayStr, \"b\")", + "result": [ + false, + true, + false, + false, + false + ] + }, + { + "expression": "startsWith(\"abcdefg\", [\"a\", \"b\", \"c\"])", + "result": [ + true, + false, + false + ] + }, + { + "expression": "substitute(arrayStr, \"c\", \"C\", [0,0,0,0,0])", + "result": ["abC","bCd","Cde","def","efg"] + }, + { + "expression": "tan(arrayNum)", + "result": [-0.1937796334476594, 1.3176122402817965, -1.9647596572486523, 0, 1.9647596572486523, -1.3176122402817965, 0.1937796334476594] + }, + { + "expression": "trim(arrayStr)", + "result": ["abc", "bcd", "cde", "def", "efg"] + }, + { + "expression": "trunc(arrayNum, [1,1,0,0,0,1,1])", + "result": [-3.3, -2.2, -1, 0, 1, 2.2, 3.3] + }, + { + "expression": "trunc(arrayNum)", + "result": [-3, -2, -1, 0, 1, 2, 3] + }, + { + "expression": "upper(arrayStr)", + "result": ["ABC", "BCD", "CDE", "DEF", "EFG"] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | weekday([@, @ + 1], 1)", + "result": [7, 1] + }, + { + "expression": "datetime(2024, 10, 12, 13, 14, 15, 16) | year([@, @ + 1])", + "result": [2024, 2024] + } + ] } ] diff --git a/test/specSamples.json b/test/specSamples.json index f6bd89f4..61c50d13 100644 --- a/test/specSamples.json +++ b/test/specSamples.json @@ -12,7 +12,7 @@ { "expression": "[1,2,3] ~ 4", "result": [1, 2, 3, 4] }, { "expression": "123 < \"124\"", "result": true }, { "expression": "\"23\" > 111", "result": false }, - { "expression": "abs(\"-2\")", "result": 2 }, + { "expression": "avg([\"2\", \"3\", \"4\"])", "result": 3 }, { "expression": "1 == \"1\"", "result": false }, { "expression": "\"$123.00\" + 1", "error": "TypeError" }, { diff --git a/test/tests.json b/test/tests.json index e9a211ba..a307a383 100644 --- a/test/tests.json +++ b/test/tests.json @@ -349,7 +349,7 @@ { "data": "'purchase-order'", "expression": "lower(address.missing)", - "result": "" + "error": "TypeError" }, { "data": "'purchase-order'", @@ -359,11 +359,11 @@ { "expression": "lower(\"\")", "result": "" }, { "expression": "lower(\"abc\")", "result": "abc" }, { "expression": "lower(\"aBc\")", "result": "abc" }, - { "expression": "lower(42)", "result": "42" }, + { "expression": "lower(42)", "error": "TypeError" }, { "data": "'purchase-order'", "expression": "upper(address.missing)", - "result": "" + "error": "TypeError" }, { "data": "'purchase-order'", @@ -373,7 +373,7 @@ { "expression": "upper(\"\")", "result": "" }, { "expression": "upper(\"ABC\")", "result": "ABC" }, { "expression": "upper(\"aBc\")", "result": "ABC" }, - { "expression": "upper(42)", "result": "42" }, + { "expression": "upper(42)", "error": "TypeError" }, { "data": "'purchase-order'", "expression": "exp(items[0].quantity)", @@ -382,10 +382,10 @@ { "data": "'purchase-order'", "expression": "exp(missing)", - "result": 1 + "error": "TypeError" }, { "expression": "exp(0)", "result": 1 }, - { "expression": "exp(\"0\")", "result": 1 }, + { "expression": "exp(\"0\")", "error": "TypeError" }, { "expression": "exp(1)", "result": 2.718281828459045 }, { "data": "'purchase-order'", @@ -395,7 +395,7 @@ { "data": "'purchase-order'", "expression": "power(missing, 1)", - "result": 0 + "error": "TypeError" }, { "expression": "power(1, 1)", "result": 1 }, { "expression": "power(2, 3)", "result": 8 }, @@ -450,17 +450,17 @@ { "data": "'purchase-order'", "expression": "find(\"Oak\", missing)", - "result": null + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "find(missing, address.street)", - "result": 0 + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "find(missing, missing)", - "result": 0 + "error": "TypeError" }, { "expression": "left(\"abc\")", "result": "a" }, { "expression": "left(\"abc\", 1)", "result": "a" }, @@ -551,7 +551,7 @@ }, { "data": "'purchase-order'", - "expression": "left(split(address.street, ''))", + "expression": "left(split(address.street, \"\"))", "result": ["1"] }, { @@ -580,7 +580,7 @@ }, { "data": "'purchase-order'", - "expression": "right(split(address.street, ''))", + "expression": "right(split(address.street, \"\"))", "result": ["t"] }, { @@ -625,9 +625,9 @@ { "data": "'purchase-order'", "expression": "proper(missing)", - "result": "" + "error": "TypeError" }, - { "expression": "rept('', 10)", "result": "" }, + { "expression": "rept(\"\", 10)", "result": "" }, { "expression": "rept(\"a\", 2)", "result": "aa" }, { "expression": "rept(\"abc\", 2)", @@ -643,12 +643,12 @@ { "data": "'purchase-order'", "expression": "rept(address.country,missing)", - "result": "" + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "rept(missing,3)", - "result": "" + "error": "TypeError" }, { "expression": "replace(\"abcdefg\", 2, 2, \"yz\")", @@ -753,12 +753,12 @@ { "data": "'purchase-order'", "expression": "round(items[0].price, missing)", - "result": 3 + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "round(missing, 1)", - "result": 0 + "error": "TypeError" }, { "expression": "round(1.5)", "result": 2 }, { "expression": "round(-1.5)", "result": -1 }, @@ -779,7 +779,7 @@ { "data": "'purchase-order'", "expression": "sqrt(missing)", - "result": 0 + "error": "TypeError" }, { "expression": "stdev(`[1,\"2\",3]`)", "result": 1 }, { "expression": "stdev(`[1]`)", "error": "EvaluationError" }, @@ -823,7 +823,7 @@ { "data": "'purchase-order'", "expression": "trim(missing)", - "result": "" + "error": "TypeError" }, { "expression": "trunc(123.456)", "result": 123 }, { "expression": "trunc(123.456, 1)", "result": 123.4 }, @@ -853,12 +853,12 @@ { "data": "'purchase-order'", "expression": "trunc(items[0].price, missing)", - "result": 3 + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "trunc(missing, 1)", - "result": 0 + "error": "TypeError" }, { "expression": "fromCodePoint(13055)", @@ -876,7 +876,7 @@ { "data": "'purchase-order'", "expression": "fromCodePoint(missing)", - "result": "\u0000" + "error": "TypeError" }, { "expression": "codePoint(\"\\t\")", "result": 9 }, { "expression": "codePoint(\"㋿\")", "result": 13055 }, @@ -888,7 +888,7 @@ { "data": "'purchase-order'", "expression": "codePoint(missing)", - "result": null + "error": "TypeError" }, { "data": "'purchase-order'", @@ -1097,7 +1097,7 @@ { "data": "casefold", "expression": "casefold(notfound)", - "result": "" + "error": "TypeError" }, { "data": "casefold.test1", @@ -1210,18 +1210,18 @@ }, { "data": "'purchase-order'", - "expression": "split(address.country, '')", + "expression": "split(address.country, \"\")", "result": ["U", "S", "A"] }, { "data": "'purchase-order'", "expression": "split(address.country, `null`)", - "result": ["U", "S", "A"] + "error": "TypeError" }, { "data": "'purchase-order'", "expression": "split(no, where)", - "result": [] + "error": "TypeError" }, { "data": "'purchase-order'", @@ -1558,8 +1558,7 @@ "expression": "search(\".\\\\\\\\^$(+{]\", \"pada.\\\\^$(+{]b\")", "result": [4, ".\\^$(+{]"] }, - { "expression": "search(\"\", null)", "result": [] }, - { "expression": "search(\"\", null)", "result": [] }, + { "expression": "search(\"\", null)", "error": "TypeError" }, { "expression": "search(\"a**a\", \"pada\")", "result": [1, "ada"]}, { "expression": "search(\"a*?a\", \"pada\")", "result": [1, "ada"]}, { From 6322953b28031888ca2f1eaa38c2ee6ca77fc3c2 Mon Sep 17 00:00:00 2001 From: John Brinkman Date: Thu, 7 Nov 2024 13:15:51 +0100 Subject: [PATCH 2/3] update documentation for array functions --- doc/spec.adoc | 30 +++++-- src/functions.js | 220 ++++++++++++++++++++++++----------------------- 2 files changed, 135 insertions(+), 115 deletions(-) diff --git a/doc/spec.adoc b/doc/spec.adoc index 405fe0d7..7b7bb7ec 100644 --- a/doc/spec.adoc +++ b/doc/spec.adoc @@ -433,7 +433,7 @@ The numeric and concatenation operators (`+`, `-`, `{asterisk}`, `/`, `&`) have * When both operands are arrays, a new array is returned where the elements are populated by applying the operator on each element of the left operand array with the corresponding element from the right operand array * If both operands are arrays and they do not have the same size, the shorter array is padded with null values -* If one operand is an array and one is a scalar value, a new array is returned where the operator is applied with the scalar against each element in the array +* If one operand is an array and one is a scalar value, the scalar operand will be converted to an array by repeating the value to the same size array as the other operand [source%unbreakable] ---- @@ -1194,27 +1194,43 @@ output. return_type function_name2(type1|type2 $argname) ---- +=== Function parameters + Functions support the set of standard json-formula <>. If the resolved arguments cannot be coerced to match the types specified in the signature, a `TypeError` error occurs. As a shorthand, the type `any` is used to indicate that the function argument can be -of any of (`array|object|number|string|boolean|null`). +any of (`array|object|number|string|boolean|null`). The expression type, (denoted by `&expression`), is used to specify an expression that is not immediately evaluated. Instead, a reference to that expression is provided to the function being called. The function can then apply the expression reference as needed. It is semantically similar to an https://en.wikipedia.org/wiki/Anonymous_function[anonymous function]. See the <<_sortBy, sortBy()>> function for an example of the expression type. -The result of the `functionExpression` is the result returned by the -function call. If a `functionExpression` is evaluated for a function that -does not exist, a `FunctionError` error is raised. - -Functions can either have a specific arity or be variadic with a minimum +Function parameters can either have a specific arity or be variadic with a minimum number of arguments. If a `functionExpression` is encountered where the arity does not match, or the minimum number of arguments for a variadic function is not provided, or too many arguments are provided, then a `FunctionError` error is raised. +The result of the `functionExpression` is the result returned by the +function call. If a `functionExpression` is evaluated for a function that +does not exist, a `FunctionError` error is raised. + +Many functions that process scalar values also allow for the processing of arrays of values. For example, the `round()` function may be called to process a single value: `round(1.2345, 2)` or to process an array of values: `round([1.2345, 2.3456], 2)`. The first call will return a single value, the second call will return an array of values. +When processing arrays of values, and where there is more than one parameter, each parameter is converted to an array so that the function processes each value in the set of arrays. From our example above, the call to `round([1.2345, 2.3456], 2)` would be processed as if it were `round([1.2345, 2.3456], [2, 2])`, and the result would be the same as: `[round(1.2345, 2), round(2.3456, 2)]`. + +The rules for the treatment of parameters with arrays of values: + +When any parameter is an array then: + +* All parameters will be treated as arrays +* Any scalar parameters will be converted to an array by repeating the scalar value to the length of the longest array +* All array parameters will be padded to the length of the longest array by adding null values +* The function will return an array which is the result of iterating over the elements of the arrays and applying the function logic on the values at the same index. + +=== Function evaluation + Functions are evaluated in applicative order: - Each argument must be an expression - Each argument expression must be evaluated before evaluating the diff --git a/src/functions.js b/src/functions.js index 6dcc9e5a..bba13daa 100644 --- a/src/functions.js +++ b/src/functions.js @@ -358,8 +358,8 @@ export default function functions( // and if not provided is assumed to be false. /** * Find the absolute (non-negative) value of the provided argument `value`. - * @param {number} value a numeric value - * @return {number} If `value < 0`, returns `-value`, otherwise returns `value` + * @param {number|number[]} value A numeric value + * @return {number|number[]} If `value < 0`, returns `-value`, otherwise returns `value` * @function abs * @example * abs(-1) // returns 1 @@ -370,9 +370,9 @@ export default function functions( }, /** * Compute the inverse cosine (in radians) of a number. - * @param {number} cosine A number between -1 and 1, inclusive, + * @param {number|number[]} cosine A number between -1 and 1, inclusive, * representing the angle's cosine value. - * @return {number} The inverse cosine angle in radians between 0 and PI + * @return {number|number[]} The inverse cosine angle in radians between 0 and PI * @function acos * @example * acos(0) => 1.5707963267948966 @@ -407,9 +407,9 @@ export default function functions( /** * Compute the inverse sine (in radians) of a number. - * @param {number} sine A number between -1 and 1, inclusive, + * @param {number|number[]} sine A number between -1 and 1, inclusive, * representing the angle's sine value. - * @return {number} The inverse sine angle in radians between -PI/2 and PI/2 + * @return {number|number[]} The inverse sine angle in radians between -PI/2 and PI/2 * @function asin * @example * Math.asin(0) => 0 @@ -422,9 +422,9 @@ export default function functions( /** * Compute the angle in the plane (in radians) between the positive * x-axis and the ray from (0, 0) to the point (x, y) - * @param {number} y The y coordinate of the point - * @param {number} x The x coordinate of the point - * @return {number} The angle in radians (between -PI and PI), + * @param {number|number[]} y The y coordinate of the point + * @param {number|number[]} x The x coordinate of the point + * @return {number|number[]} The angle in radians (between -PI and PI), * between the positive x-axis and the ray from (0, 0) to the point (x, y). * @function atan2 * @example @@ -463,8 +463,8 @@ export default function functions( /** * Generates a lower-case string of the `input` string using locale-specific mappings. * e.g. Strings with German letter ß (eszett) can be compared to "ss" - * @param {string} input string to casefold - * @returns {string} A new string converted to lower case + * @param {string|string[]} input string to casefold + * @returns {string|string[]} A new string converted to lower case * @function casefold * @example * casefold("AbC") // returns "abc" @@ -481,8 +481,8 @@ export default function functions( /** * Finds the next highest integer value of the argument `num` by rounding up if necessary. * i.e. ceil() rounds toward positive infinity. - * @param {number} num numeric value - * @return {integer} The smallest integer greater than or equal to num + * @param {number|number[]} num numeric value + * @return {integer|integer[]} The smallest integer greater than or equal to num * @function ceil * @example * ceil(10) // returns 10 @@ -495,8 +495,9 @@ export default function functions( }, /** * Retrieve the first code point from a string - * @param {string} str source string. - * @return {integer} Unicode code point value. If the input string is empty, returns `null`. + * @param {string|string[]} str source string. + * @return {integer|integer[]} Unicode code point value. + * If the input string is empty, returns `null`. * @function codePoint * @example * codePoint("ABC") // 65 @@ -553,8 +554,8 @@ export default function functions( }, /** * Compute the cosine (in radians) of a number. - * @param {number} angle A number representing an angle in radians - * @return {number} The cosine of the angle, between -1 and 1, inclusive. + * @param {number|number[]} angle A number representing an angle in radians + * @return {number|number[]} The cosine of the angle, between -1 and 1, inclusive. * @function cos * @example * cos(1.0471975512) => 0.4999999999970535 @@ -575,15 +576,15 @@ export default function functions( * after subtracting whole years. * * `yd` the number of days between `start_date` and `end_date`, assuming `start_date` * and `end_date` were no more than one year apart - * @param {number} start_date The starting <<_date_and_time_values, date/time value>>. + * @param {number|number[]} start_date The starting <<_date_and_time_values, date/time value>>. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @param {number} end_date The end <<_date_and_time_values, date/time value>> -- must + * @param {number|number[]} end_date The end <<_date_and_time_values, date/time value>> -- must * be greater or equal to start_date. If not, an error will be thrown. - * @param {string} unit Case-insensitive string representing the unit of time to measure. - * An unrecognized unit will result in an error. - * @returns {integer} The number of days/months/years difference + * @param {string|string[]} unit Case-insensitive string representing the unit of + * time to measure. An unrecognized unit will result in an error. + * @returns {integer|integer[]} The number of days/months/years difference * @function datedif * @example * datedif(datetime(2001, 1, 1), datetime(2003, 1, 1), "y") // returns 2 @@ -654,10 +655,10 @@ export default function functions( /** * Finds the day of the month for a date value - * @param {number} date <<_date_and_time_values, date/time value>> generated using the + * @param {number|number[]} date <<_date_and_time_values, date/time value>> generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} The day of the month ranging from 1 to 31. + * @return {integer|integer[]} The day of the month ranging from 1 to 31. * @function day * @example * day(datetime(2008,5,23)) // returns 23 @@ -746,9 +747,9 @@ export default function functions( /** * Determines if the `subject` string ends with a specific `suffix` - * @param {string} subject source string in which to search - * @param {string} suffix search string - * @return {boolean} true if the `suffix` value is at the end of the `subject` + * @param {string|string[]} subject source string in which to search + * @param {string|string[]} suffix search string + * @return {boolean|boolean[]} true if the `suffix` value is at the end of the `subject` * @function endsWith * @example * endsWith("Abcd", "d") // returns true @@ -790,12 +791,12 @@ export default function functions( /** * Finds the date value of the end of a month, given `startDate` plus `monthAdd` months - * @param {number} startDate The base date to start from. + * @param {number|number[]} startDate The base date to start from. * <<_date_and_time_values, Date/time values>> can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @param {integer} monthAdd Number of months to add to start date - * @return {number} the date of the last day of the month + * @param {integer|integer[]} monthAdd Number of months to add to start date + * @return {number|number[]} the date of the last day of the month * @function eomonth * @example * eomonth(datetime(2011, 1, 1), 1) | [month(@), day(@)] // returns [2, 28] @@ -811,8 +812,8 @@ export default function functions( /** * Finds e (the base of natural logarithms) raised to a power. (i.e. e^x) - * @param {number} x A numeric expression representing the power of e. - * @returns {number} e (the base of natural logarithms) raised to power x + * @param {number|number[]} x A numeric expression representing the power of e. + * @returns {number|number[]} e (the base of natural logarithms) raised to power x * @function exp * @example * exp(10) // returns 22026.465794806718 @@ -837,11 +838,11 @@ export default function functions( /** * Finds and returns the index of query in text from a start position - * @param {string} findText string to search - * @param {string} withinText text to be searched - * @param {integer} [start=0] zero-based position to start searching. + * @param {string|string[]} findText string to search + * @param {string|string[]} withinText text to be searched + * @param {integer|integer[]} [start=0] zero-based position to start searching. * If specified, `start` must be greater than or equal to 0 - * @returns {integer|null} The position of the found string, null if not found. + * @returns {integer|null|integer[]} The position of the found string, null if not found. * @function find * @example * find("m", "abm") // returns 2 @@ -865,8 +866,8 @@ export default function functions( /** * Calculates the next lowest integer value of the argument `num` by rounding down if necessary. * i.e. floor() rounds toward negative infinity. - * @param {number} num numeric value - * @return {integer} The largest integer smaller than or equal to num + * @param {number|number[]} num numeric value + * @return {integer|integer[]} The largest integer smaller than or equal to num * @function floor * @example * floor(10.4) // returns 10 @@ -879,9 +880,9 @@ export default function functions( /** * Create a string from a code point. - * @param {integer} codePoint An integer between 0 and 0x10FFFF (inclusive) - * representing a Unicode code point. - * @return {string} A string from a given code point + * @param {integer|integer[]} codePoint An integer or array of integers + * between 0 and 0x10FFFF (inclusive) representing Unicode code point(s). + * @return {string} A string from the given code point(s) * @function fromCodePoint * @example * fromCodePoint(65) // "A" @@ -934,8 +935,8 @@ export default function functions( /** * Compute the nearest 32-bit single precision float representation of a number - * @param {number} num input to be rounded - * @return {number} The rounded representation of `num` + * @param {number|number[]} num input to be rounded + * @return {number|number[]} The rounded representation of `num` * @function fround * @example * fround(2147483650.987) => 2147483648 @@ -988,11 +989,11 @@ export default function functions( }, /** * Extract the hour from a <<_date_and_time_values, date/time value>> - * @param {number} date The datetime/time for which the hour is to be returned. + * @param {number|number[]} date The datetime/time for which the hour is to be returned. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} value between 0 and 23 + * @return {integer|integer[]} value between 0 and 23 * @function hour * @example * hour(datetime(2008,5,23,12, 0, 0)) // returns 12 @@ -1133,8 +1134,8 @@ export default function functions( /** * Compute the natural logarithm (base e) of a number - * @param {number} num A number greater than zero - * @return {number} The natural log value + * @param {number|number[]} num A number greater than zero + * @return {number|number[]} The natural log value * @function log * @example * log(10) // 2.302585092994046 @@ -1146,8 +1147,8 @@ export default function functions( /** * Compute the base 10 logarithm of a number. - * @param {number} num A number greater than or equal to zero - * @return {number} The base 10 log result + * @param {number|number[]} num A number greater than or equal to zero + * @return {number|number[]} The base 10 log result * @function log10 * @example * log10(100000) // 5 @@ -1159,8 +1160,8 @@ export default function functions( /** * Converts all the alphabetic code points in a string to lowercase. - * @param {string} input input string - * @returns {string} the lower case value of the input string + * @param {string|string[]} input input string + * @returns {string|string[]} the lower case value of the input string * @function lower * @example * lower("E. E. Cummings") // returns e. e. cummings @@ -1290,11 +1291,11 @@ export default function functions( /** * Extract the milliseconds of the time value in a <<_date_and_time_values, date/time value>>. - * @param {number} date datetime/time for which the millisecond is to be returned. + * @param {number|number[]} date datetime/time for which the millisecond is to be returned. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} The number of milliseconds: 0 through 999 + * @return {integer|integer[]} The number of milliseconds: 0 through 999 * @function millisecond * @example * millisecond(datetime(2008, 5, 23, 12, 10, 53, 42)) // returns 42 @@ -1343,11 +1344,11 @@ export default function functions( /** * Extract the minute (0 through 59) from a <<_date_and_time_values, date/time value>> - * @param {number} date A datetime/time value. + * @param {number|number[]} date A datetime/time value. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} Number of minutes in the time portion of the date/time value + * @return {integer|integer[]} Number of minutes in the time portion of the date/time value * @function minute * @example * minute(datetime(2008,5,23,12, 10, 0)) // returns 10 @@ -1362,9 +1363,9 @@ export default function functions( /** * Return the remainder when one number is divided by another number. - * @param {number} dividend The number for which to find the remainder. - * @param {number} divisor The number by which to divide number. - * @return {number} Computes the remainder of `dividend`/`divisor`. + * @param {number|number[]} dividend The number for which to find the remainder. + * @param {number|number[]} divisor The number by which to divide number. + * @return {number|number[]} Computes the remainder of `dividend`/`divisor`. * If `dividend` is negative, the result will also be negative. * If `dividend` is zero, an error is thrown. * @function mod @@ -1386,11 +1387,11 @@ export default function functions( /** * Finds the month of a date. - * @param {number} date source <<_date_and_time_values, date/time value>>. + * @param {number|number[]} date source <<_date_and_time_values, date/time value>>. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} The month number value, ranging from 1 (January) to 12 (December). + * @return {integer|integer[]} The month number value, ranging from 1 (January) to 12 (December) * @function month * @example * month(datetime(2008,5,23)) // returns 5 @@ -1486,9 +1487,9 @@ export default function functions( /** * Computes `a` raised to a power `x`. (a^x) - * @param {number} a The base number -- can be any real number. - * @param {number} x The exponent to which the base number is raised. - * @return {number} + * @param {number|number[]} a The base number -- can be any real number. + * @param {number|number[]} x The exponent to which the base number is raised. + * @return {number|number[]} * @function power * @example * power(10, 2) // returns 100 (10 raised to power 2) @@ -1507,8 +1508,8 @@ export default function functions( * uppercase letter and the rest of the letters in the word converted to lowercase. * Words are demarcated by whitespace, punctuation, or numbers. * Specifically, any character(s) matching the regular expression: `[\s\d\p{P}]+`. - * @param {string} text source string - * @returns {string} source string with proper casing applied. + * @param {string|string[]} text source string + * @returns {string|string[]} source string with proper casing applied. * @function proper * @example * proper("this is a TITLE") // returns "This Is A Title" @@ -1670,10 +1671,10 @@ export default function functions( /** * Return text repeated `count` times. - * @param {string} text text to repeat - * @param {integer} count number of times to repeat the text. + * @param {string|string[]} text text to repeat + * @param {integer|integer[]} count number of times to repeat the text. * Must be greater than or equal to 0. - * @returns {string} Text generated from the repeated text. + * @returns {string|string[]} Text generated from the repeated text. * if `count` is zero, returns an empty string. * @function rept * @example @@ -1745,9 +1746,9 @@ export default function functions( * * If `precision` is greater than zero, round to the specified number of decimal places. * * If `precision` is 0, round to the nearest integer. * * If `precision` is less than 0, round to the left of the decimal point. - * @param {number} num number to round - * @param {integer} [precision=0] precision to use for the rounding operation. - * @returns {number} rounded value. Rounding a half value will round up. + * @param {number|number[]} num number to round + * @param {integer|integer[]} [precision=0] precision to use for the rounding operation. + * @returns {number|number[]} rounded value. Rounding a half value will round up. * @function round * @example * round(2.15, 1) // returns 2.2 @@ -1780,9 +1781,10 @@ export default function functions( * precede them with an escape (`{backslash}`) character. * Note that the wildcard search is not greedy. * e.g. `search("a{asterisk}b", "abb")` will return `[0, "ab"]` Not `[0, "abb"]` - * @param {string} findText the search string -- which may include wild cards. - * @param {string} withinText The string to search. - * @param {integer} [startPos=0] The zero-based position of withinText to start searching. + * @param {string|string[]} findText the search string -- which may include wild cards. + * @param {string|string[]} withinText The string to search. + * @param {integer|integer[]} [startPos=0] The zero-based position of withinText + * to start searching. * A negative value is not allowed. * @returns {array} returns an array with two values: * @@ -1808,11 +1810,11 @@ export default function functions( /** * Extract the seconds of the time value in a <<_date_and_time_values, date/time value>>. - * @param {number} date datetime/time for which the second is to be returned. + * @param {number|number[]} date datetime/time for which the second is to be returned. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} The number of seconds: 0 through 59 + * @return {integer|integer[]} The number of seconds: 0 through 59 * @function second * @example * second(datetime(2008,5,23,12, 10, 53)) // returns 53 @@ -1827,8 +1829,8 @@ export default function functions( /** * Computes the sign of a number passed as argument. - * @param {number} num any number - * @return {number} returns 1 or -1, indicating the sign of `num`. + * @param {number|number[]} num any number + * @return {number|number[]} returns 1 or -1, indicating the sign of `num`. * If the `num` is 0, it will return 0. * @function sign * @example @@ -1843,8 +1845,8 @@ export default function functions( /** * Computes the sine of a number in radians - * @param {number} angle A number representing an angle in radians. - * @return {number} The sine of `angle`, between -1 and 1, inclusive + * @param {number|number[]} angle A number representing an angle in radians. + * @return {number|number[]} The sine of `angle`, between -1 and 1, inclusive * @function sin * @example * sin(0) // 0 @@ -1953,9 +1955,9 @@ export default function functions( /** * Split a string into an array, given a separator - * @param {string} string string to split - * @param {string} separator separator where the split(s) should occur - * @return {string[]} The array of separated strings + * @param {string|string[]} string string to split + * @param {string|string[]} separator separator where the split(s) should occur + * @return {string[]|string[][]} The array of separated strings * @function split * @example * split("abcdef", "") // returns ["a", "b", "c", "d", "e", "f"] @@ -1971,8 +1973,8 @@ export default function functions( /** * Find the square root of a number - * @param {number} num source number - * @return {number} The calculated square root value + * @param {number|number[]} num source number + * @return {number|number[]} The calculated square root value * @function sqrt * @example * sqrt(4) // returns 2 @@ -1986,9 +1988,9 @@ export default function functions( /** * Determine if a string starts with a prefix. - * @param {string} subject string to search - * @param {string} prefix prefix to search for - * @return {boolean} true if `prefix` matches the start of `subject` + * @param {string|string[]} subject string to search + * @param {string|string[]} prefix prefix to search for + * @return {boolean|boolean[]} true if `prefix` matches the start of `subject` * @function startsWith * @example * startsWith("jack is at home", "jack") // returns true @@ -2060,13 +2062,14 @@ export default function functions( * with text `old` replaced by text `new` (when searching from the left). * If there is no match, or if `old` has length 0, `text` is returned unchanged. * Note that `old` and `new` may have different lengths. - * @param {string} text The text for which to substitute code points. - * @param {string} old The text to replace. - * @param {string} new The text to replace `old` with. If `new` is an empty string, then - * occurrences of `old` are removed from `text`. - * @param {integer} [which] The zero-based occurrence of `old` text to replace with `new` text. + * @param {string|string[]} text The text for which to substitute code points. + * @param {string|string[]} old The text to replace. + * @param {string|string[]} new The text to replace `old` with. + * If `new` is an empty string, then occurrences of `old` are removed from `text`. + * @param {integer|integer[]} [which] + * The zero-based occurrence of `old` text to replace with `new` text. * If `which` parameter is omitted, every occurrence of `old` is replaced with `new`. - * @returns {string} replaced string + * @returns {string|string[]} replaced string * @function substitute * @example * substitute("Sales Data", "Sales", "Cost") // returns "Cost Data" @@ -2118,8 +2121,8 @@ export default function functions( }, /** * Computes the tangent of a number in radians - * @param {number} angle A number representing an angle in radians. - * @return {number} The tangent of `angle` + * @param {number|number[]} angle A number representing an angle in radians. + * @return {number|number[]} The tangent of `angle` * @function tan * @example * tan(0) // 0 @@ -2340,8 +2343,8 @@ export default function functions( /** * Remove leading and trailing spaces (U+0020), and replace all internal multiple spaces * with a single space. Note that other whitespace characters are left intact. - * @param {string} text string to trim - * @return {string} trimmed string + * @param {string|string[]} text string to trim + * @return {string|string[]} trimmed string * @function trim * @example * trim(" ab c ") // returns "ab c" @@ -2367,9 +2370,10 @@ export default function functions( /** * Truncates a number to an integer by removing the fractional part of the number. * i.e. it rounds towards zero. - * @param {number} numA number to truncate - * @param {integer} [numB=0] A number specifying the number of decimal digits to preserve. - * @return {number} Truncated value + * @param {number|number[]} numA number to truncate + * @param {integer|integer[]} [numB=0] + * A number specifying the number of decimal digits to preserve. + * @return {number|number[]} Truncated value * @function trunc * @example * trunc(8.9) // returns 8 @@ -2450,8 +2454,8 @@ export default function functions( /** * Converts all the alphabetic code points in a string to uppercase. - * @param {string} input input string - * @returns {string} the upper case value of the input string + * @param {string|string[]} input input string + * @returns {string|string[]} the upper case value of the input string * @function upper * @example * upper("abcd") // returns "ABCD" @@ -2537,15 +2541,15 @@ export default function functions( * * 1 : Sunday (1), Monday (2), ..., Saturday (7) * * 2 : Monday (1), Tuesday (2), ..., Sunday(7) * * 3 : Monday (0), Tuesday (1), ...., Sunday(6) - * @param {number} date <<_date_and_time_values, date/time value>> for + * @param {number|number[]} date <<_date_and_time_values, date/time value>> for * which the day of the week is to be returned. * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @param {integer} [returnType=1] Determines the + * @param {integer|integer[]} [returnType=1] Determines the * representation of the result. * An unrecognized returnType will result in a error. - * @returns {integer} day of the week + * @returns {integer|integer[]} day of the week * @function weekday * @example * weekday(datetime(2006,5,21)) // 1 @@ -2566,11 +2570,11 @@ export default function functions( /** * Finds the year of a datetime value - * @param {number} date input <<_date_and_time_values, date/time value>> + * @param {number|number[]} date input <<_date_and_time_values, date/time value>> * Date/time values can be generated using the * [datetime]{@link datetime}, [toDate]{@link todate}, [today]{@link today}, [now]{@link now} * and [time]{@link time} functions. - * @return {integer} The year value + * @return {integer|integer[]} The year value * @function year * @example * year(datetime(2008,5,23)) // returns 2008 From 161c0a96ede22f1f73621c9b3a2705c8b0e8bff8 Mon Sep 17 00:00:00 2001 From: John Brinkman Date: Mon, 11 Nov 2024 11:50:16 -0800 Subject: [PATCH 3/3] Allow more lenient coercion rules for function parameters --- doc/spec.adoc | 6 ++++-- src/matchType.js | 49 +++++++++++++++++++++++++++++++++++++++++-- test/functions.json | 22 +++++++------------ test/specSamples.json | 2 +- test/tests.json | 10 ++++----- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/doc/spec.adoc b/doc/spec.adoc index 7b7bb7ec..0f005909 100644 --- a/doc/spec.adoc +++ b/doc/spec.adoc @@ -73,7 +73,7 @@ If the supplied data is not correct for the execution context, json-formula will * Operands of the union operator (`~`) shall be coerced to an array * The left-hand operand of ordering comparison operators (`>`, `>=`, `<`, `\<=`) must be a string or number. Any other type shall be coerced to a number. * If the operands of an ordering comparison are different, they shall both be coerced to a number -* Parameters to functions shall be coerced to the expected type as long as the expected type is a single choice. If the function signature allows multiple types for a parameter e.g. either string or array, then coercion will not occur. +* Parameters to functions shall be coerced when there is just a single viable coercion available. For example, if a null value is provided to a function that accepts a number or string, then coercion shall not happen, since a null value can be coerced to both types. Conversely if a string is provided to a function that accepts a number or array of numbers, then the string shall be coerced to a number, since there is no supported coercion to convert it to an array of numbers. The equality and inequality operators (`=`, `==`, `!=`, `<>`) do **not** perform type coercion. If operands are different types, the values are considered not equal. @@ -127,6 +127,8 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ | null | boolean | false |=== +An array may be coerced to another type of array as long as there is a supported coercion for the array content. e.g. just as a string can be coerced to a number, an array of strings may be coerced to an array of numbers. + [discrete] ==== Examples @@ -135,7 +137,7 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ eval("\"$123.00\" + 1", {}) -> TypeError eval("truth is " & `true`, {}) -> "truth is true" eval(2 + `true`, {}) -> 3 - eval(avg("20"), {}) -> 20 + eval(avg(["20", "30"]), {}) -> 25 ---- === Date and Time Values diff --git a/src/matchType.js b/src/matchType.js index 8d041072..439e2112 100644 --- a/src/matchType.js +++ b/src/matchType.js @@ -92,6 +92,46 @@ export function getTypeName(arg) { return typeNameTable[getType(arg)]; } +function supportedConversion(from, to) { + const pairs = { + [TYPE_NUMBER]: [ + TYPE_STRING, + TYPE_ARRAY, + TYPE_ARRAY_NUMBER, + TYPE_BOOLEAN, + ], + [TYPE_BOOLEAN]: [ + TYPE_STRING, + TYPE_NUMBER, + TYPE_ARRAY, + ], + [TYPE_ARRAY]: [TYPE_BOOLEAN, TYPE_ARRAY_STRING, TYPE_ARRAY_NUMBER], + [TYPE_ARRAY_NUMBER]: [TYPE_BOOLEAN, TYPE_ARRAY_STRING, TYPE_ARRAY], + [TYPE_ARRAY_STRING]: [TYPE_BOOLEAN, TYPE_ARRAY_NUMBER, TYPE_ARRAY], + [TYPE_ARRAY_ARRAY]: [TYPE_BOOLEAN], + [TYPE_EMPTY_ARRAY]: [TYPE_BOOLEAN], + + [TYPE_OBJECT]: [TYPE_BOOLEAN], + [TYPE_NULL]: [ + TYPE_STRING, + TYPE_NUMBER, + TYPE_EMPTY_ARRAY, + TYPE_ARRAY_STRING, + TYPE_ARRAY_NUMBER, + TYPE_ARRAY_ARRAY, + TYPE_ARRAY, + TYPE_OBJECT, + TYPE_BOOLEAN, + ], + [TYPE_STRING]: [ + TYPE_NUMBER, + TYPE_ARRAY_STRING, + TYPE_ARRAY, + TYPE_BOOLEAN], + }; + return pairs[from].includes(to); +} + export function matchType(expectedList, argValue, context, toNumber, toString) { const actual = getType(argValue); if (argValue?.jmespathType === TOK_EXPREF && !expectedList.includes(TYPE_EXPREF)) { @@ -106,8 +146,13 @@ export function matchType(expectedList, argValue, context, toNumber, toString) { if (expectedList.some(type => match(type, actual))) return argValue; // if the function allows multiple types, we can't coerce the type and we need an exact match - const exactMatch = expectedList.length > 1; - const expected = expectedList[0]; + // Of the set of expected types, filter out the ones that can be coerced from the actual type + const filteredList = expectedList.filter(t => supportedConversion(actual, t)); + if (filteredList.length === 0) { + throw typeError(`${context} expected argument to be type ${typeNameTable[expectedList[0]]} but received type ${typeNameTable[actual]} instead.`); + } + const exactMatch = filteredList.length > 1; + const expected = filteredList[0]; let wrongType = false; // Can't coerce objects and arrays to any other type diff --git a/test/functions.json b/test/functions.json index 7af7ce2e..4f2b3487 100644 --- a/test/functions.json +++ b/test/functions.json @@ -138,7 +138,7 @@ }, { "expression": "abs(`false`)", - "error": "TypeError" + "result": 0 }, { "expression": "abs(`-24`)", @@ -263,7 +263,7 @@ }, { "expression": "endsWith(str, `0`)", - "error": "TypeError" + "result": false }, { "expression": "floor(`1.2`)", @@ -731,7 +731,7 @@ }, { "expression": "startsWith(str, `0`)", - "error": "TypeError" + "result": false }, { "expression": "sum(numbers)", @@ -1837,20 +1837,12 @@ "result": [true, false, false, false, false] }, { - "expression": "datetime(2024, 10, 12) | eomonth([@, @, @, @, @, @, @], 1)", - "result": [ - 20056.958333333332, 20056.958333333332, 20056.958333333332, - 20056.958333333332, 20056.958333333332, 20056.958333333332, - 20056.958333333332 - ] + "expression": "datetime(2024, 10, 12) | eomonth([@, @, @, @, @, @, @], 1)| [month(@) & day(@)][]", + "result": ["1130", "1130", "1130", "1130", "1130", "1130", "1130"] }, { - "expression": "{d: datetime(2024, 10, 12), n:arrayInt} | eomonth([@.d, @.d, @.d, @.d, @.d, @.d, @.d], @.n)", - "result": [ - 20056.958333333332, 20087.958333333332, 20118.958333333332, - 20146.958333333332, 20177.916666666668, 20207.916666666668, - 20238.916666666668 - ] + "expression": "{d: datetime(2024, 10, 12), n:arrayInt} | eomonth([@.d, @.d, @.d, @.d, @.d, @.d, @.d], @.n) | [month(@) & day(@)][]", + "result": ["1130", "1231", "131", "228", "331", "430", "531"] }, { "expression": "exp(arrayInt)", diff --git a/test/specSamples.json b/test/specSamples.json index 61c50d13..f50ed094 100644 --- a/test/specSamples.json +++ b/test/specSamples.json @@ -65,7 +65,7 @@ "result": { "month": 1, "day": 21, "hour": 12 } }, { "expression": "2 + `true`", "result": 3 }, - { "expression": "avg(\"20\")", "result": 20 }, + { "expression": "avg([\"20\", \"30\"])", "result": 25 }, { "expression": "left + right", "data": { "left": 8, "right": 12 }, diff --git a/test/tests.json b/test/tests.json index a307a383..5f697ed9 100644 --- a/test/tests.json +++ b/test/tests.json @@ -359,7 +359,7 @@ { "expression": "lower(\"\")", "result": "" }, { "expression": "lower(\"abc\")", "result": "abc" }, { "expression": "lower(\"aBc\")", "result": "abc" }, - { "expression": "lower(42)", "error": "TypeError" }, + { "expression": "lower(42)", "result": "42" }, { "data": "'purchase-order'", "expression": "upper(address.missing)", @@ -373,7 +373,7 @@ { "expression": "upper(\"\")", "result": "" }, { "expression": "upper(\"ABC\")", "result": "ABC" }, { "expression": "upper(\"aBc\")", "result": "ABC" }, - { "expression": "upper(42)", "error": "TypeError" }, + { "expression": "upper(42)", "result": "42" }, { "data": "'purchase-order'", "expression": "exp(items[0].quantity)", @@ -385,7 +385,7 @@ "error": "TypeError" }, { "expression": "exp(0)", "result": 1 }, - { "expression": "exp(\"0\")", "error": "TypeError" }, + { "expression": "exp(\"0\")", "result": 1 }, { "expression": "exp(1)", "result": 2.718281828459045 }, { "data": "'purchase-order'", @@ -1151,7 +1151,7 @@ { "data": "'purchase-order'", "expression": "entries(address.country)", - "error": "TypeError" + "result": [["0", "USA"]] }, { "expression": "fromEntries([[\"a\", 1], [\"b\", 2, 4], [\"a\", 3]])", @@ -1440,7 +1440,7 @@ ] }, { "expression": "zip([1,2,3])", "result": [[1], [2], [3]] }, - { "expression": "zip([])", "result": [] }, + { "expression": "zip(`[]`)", "result": [] }, { "expression": "null() == `null`", "result": true }, { "expression": "null() == notfound", "result": true }, {