/**
*
* ## Formula module
*
* **Handle specific formulae that can be used inside computed fields.**
*
* @namespace
*/
kiss.formula = {
//--------------------
//
// WORKING WITH TEXT
//
//--------------------
/**
* Returns the left part of a string
*
* @param {string} text
* @param {number} n
* @returns {text}
*
* @example
* LEFT("San Francisco", 3) // San
*/
LEFT: (text, n) => (n > 0) ? text.slice(0, n) : "",
LEFT_HELP:
`LEFT( {{field}}, 3)
The left part of a TEXT field`,
/**
* Returns the right part of a string
*
* @param {string} text
* @param {number} n
* @returns {text}
*
* @example
* RIGHT("San Francisco", 9) // Francisco
*/
RIGHT: (text, n) => (n > 0) ? text.slice(-n) : "",
RIGHT_HELP:
`RIGHT( {{field}}, 3)
The right part of a TEXT field`,
/**
* Returns the middle part of a string
*
* @param {string} text
* @param {number} n
* @returns {text}
*
* @example
* MIDDLE("San Francisco", 4, 8) // Fran
*/
MIDDLE: (text, from, to) => text.slice(from, to),
MIDDLE_HELP:
`MIDDLE( {{field}}, 4, 8)
The middle part of a TEXT field`,
/**
* Returns the left part of a string, given a separator
*
* @param {string} text
* @param {string} separator
* @returns {text}
*
* @example
* STRLEFT("San Francisco", " ") // San
*/
STRLEFT: (text, separator) => text.split(separator)[0],
STRLEFT_HELP:
`STRLEFT( {{field}}, "@")
The part of a TEXT field at the left of a given string`,
/**
* Returns the right part of a string, given a separator
*
* @param {string} text
* @param {string} separator
* @returns {text}
*
* @example
* STRRIGHT("San Francisco", " ") // Francisco
*/
STRRIGHT: (text, separator) => text.split(separator).pop(),
STRRIGHT_HELP:
`STRRIGHT( {{field}}, "@")
The part of a TEXT field at the right of a given string`,
/**
* Convert a string to uppercase
*
* @param {string} text
* @returns {text}
*
* @example
* UPPERCASE("San Francisco") // SAN FRANCISCO
*/
UPPERCASE: (text) => text.toUpperCase(),
UPPERCASE_HELP:
`UPPERCASE( {{field}})
Returns a TEXT field in uppercase`,
/**
* Convert a string to lowercase
*
* @param {string} text
* @returns {text}
*
* @example
* LOWERCASE("San Francisco") // san francisco
*/
LOWERCASE: (text) => text.toLowerCase(),
LOWERCASE_HELP:
`LOWERCASE( {{field}} )
Returns a TEXT field in lowercase`,
/**
* Convert a string to titlecase
*
* @param {string} text
* @returns {text}
*
* @example
* TITLECASE("paris") // Paris
*/
TITLECASE: (text) => text.toTitleCase(),
TITLECASE_HELP:
`TITLECASE( {{field}} )
Returns a TEXT field in titlecase`,
/**
* Replace a string inside another string
*
* @param {string} text
* @param {string} oldText
* @param {string} newText
* @returns {text}
*
* @example
* REPLACE("New York is great", "New York", "Paris") // Paris is great
*/
REPLACE: (text, oldText, newText) => text.replaceAll(oldText, newText),
REPLACE_HELP:
`REPLACE( {{field}}, "New York", "Paris")
Replaces one string with another inside a TEXT field`,
/**
* Generates a slug from a plain title
*
* @param {string} title
* @returns {string} The generated slug
*
* @example
* SLUG("My article about dogs") // Returns "my-article-about-dogs"
*/
SLUG: (text) => kiss.tools.generateSlug(text),
SLUG_HELP:
`SLUG( {{field}} )
Transforms a TEXT field into a slug. Ex: "my-article-about-this"`,
/**
* Concatenate any number of strings
*
* @param {...any} strings
* @returns {string}
*
* @example
* CONCATENATE("Bob", " ", "Wilson") // "Bob Wilson"
*/
CONCATENATE: (...strings) => strings.filter(string => string).join(""),
CONCATENATE_HELP:
`CONCATENATE( {{field1}}, " - ", {{field2}} )
Concatenate multiple TEXT fields or texts together`,
/**
* Check if a string contains a value
*
* @param {string} string
* @param {string} value
* @returns {boolean}
*
* @example
* CONTAINS("San Francisco", "San") // true
* CONTAINS("Paris", "San") // false
*/
CONTAINS: (string, value) => string.includes(value),
CONTAINS_HELP:
`CONTAINS( {{field}}, "San")
Returns true if a TEXT field contains the given string`,
//--------------------
//
// WORKING WITH NUMBERS
//
//--------------------
/**
* MIN
* @param {...any} numbers
* @returns {number}
*
* @example
* MIN(42, 666, 1515, 7) // 7
*/
MIN: (...numbers) => Math.min(...numbers),
MIN_HELP:
`MIN( {{field1}}, {{field2}}, ... )
The min value of multiple NUMBER fields`,
/**
* MAX
*
* @param {...any} numbers
* @returns {number}
*
* @example
* MAX(42, 666, 1515, 7) // 1515
*/
MAX: (...numbers) => Math.max(...numbers),
MAX_HELP:
`MAX( {{field1}}, {{field2}}, ... )
The max value of multiple NUMBER fields`,
/**
* AVERAGE
*
* @param {...any} numbers
* @returns {number}
*
* @example
* AVERAGE(10, 20, 30) // 20
*/
AVERAGE: (...numbers) => kiss.formula.SUM(...numbers) / numbers.length,
AVERAGE_HELP:
`AVERAGE( {{field1}}, {{field2}}, ... )
The average value of multiple NUMBER fields`,
/**
* ROUND
*
* @param {number} number
* @param {number} precision
* @returns {number}
*
* @example
* ROUND(12.367891, 3) // 12.378
*/
ROUND: (number, precision) => number.round(precision),
ROUND_HELP:
`ROUND( {{field}}, 3)
The rounded value of a NUMBER field`,
/**
* Returns the square root of a number
*
* @param {number} number
* @returns {number}
*
* @example
* SQRT(16) // 4
*/
SQRT: (number) => Math.sqrt(number),
SQRT_HELP:
`SQRT( {{field}} )
The square root of a NUMBER field`,
/**
* POW
*
* @param {number} number
* @param {number} power
* @returns {number}
*
* @example
* POW(4, 2) // 16
*/
POW: (number, power) => Math.pow(number ?? 0, power ?? 1),
POW_HELP:
`POW( {{field}}, 2)
Raise a NUMBER field to the specified power`,
/**
* PI
*
* @returns {number}
*
* @example
* PI() // 3.1415927...
*/
PI: () => Math.PI,
PI_HELP:
`PI()
PI number: 3.1415927...`,
/**
* Returns the COSINUS of a number
*
* @param {number} number
* @returns {number}
*
* @example
* COS(2 * PI()) // 1
*/
COS: (number) => Math.cos(number),
COS_HELP:
`COS( {{field}} )
The cosinus of a NUMBER field`,
/**
* Returns the SINUS of a number
*
* @param {number} number
* @returns {number}
*
* @example
* SIN(PI() / 2) // 1
*/
SIN: (number) => Math.sin(number),
SIN_HELP:
`SIN( {{field}} )
The sinus of a NUMBER field`,
/**
* Returns the TANGENT of a number
*
* @param {number} number
* @returns {number}
*
* @example
* TAN(PI() / 4) // 1
*/
TAN: (number) => Math.tan(number),
TAN_HELP:
`TAN( {{field}} )
The tangent of a NUMBER field`,
//--------------------
//
// WORKING WITH DATES
//
//--------------------
/**
* Get the YEAR of an ISO date
*
* @param {string} strDateISO
* @returns {string}
*
* @example
* YEAR("2022-12-24") // "2022"
*/
YEAR: (strDateISO) => strDateISO.substring(0, 4),
YEAR_HELP:
`YEAR( {{field}} )
The year of a DATE field`,
/**
* Get the MONTH of an ISO date
*
* @param {string} strDateISO
* @returns {string}
*
* @example
* MONTH("2022-12-24") // "12"
*/
MONTH: (strDateISO) => strDateISO.substring(5, 7),
MONTH_HELP:
`MONTH( {{field}} )
The month of a DATE field`,
/**
* Get the DAY of an ISO date
*
* @param {string} strDateISO
* @returns {string}
*
* @example
* DAY("2022-12-24") // "24"
*/
DAY: (strDateISO) => strDateISO.substring(8, 10),
DAY_HELP:
`DAY( {{field}} )
The day of a DATE field`,
/**
* Get the YEAR and MONTH of an ISO date
*
* @param {string} strDateISO
* @returns {string} The year
*
* @example
* YEAR_MONTH("2022-12-24") // "2022-12"
*/
YEAR_MONTH: (strDateISO) => strDateISO.substring(0, 7),
YEAR_MONTH_HELP:
`YEAR_MONTH( {{field}} )
The year and month of a DATE field, like "2020-07"`,
/**
* Compute the time difference between 2 dates
*
* @param {string} fromISODate - As an ISO date string like "2023-02-14T15:44:05.886Z" or "2023-02-14"
* @param {string} toISODate - As an ISO date string like "2023-02-14T15:44:05.886Z" or "2023-02-14"
* @param {string} unit - "d" for days, "h" for hours... "mn", "s", "ms"
* @returns {integer} Time diffence in the required unit of time
*
* @example
* TIME_DIFFERENCE("2023-02-14T15:44:05.886Z", "2023-02-14T18:44:26.316Z", "h") // 3
* TIME_DIFFERENCE("2023-02-10", "2023-02-20", "d") // 10
*/
TIME_DIFFERENCE: (fromISODate, toISODate, unit = "d") => {
try {
const fromDate = new Date(fromISODate)
const toDate = new Date(toISODate)
let coef
switch(unit) {
case "d":
coef = 1000 * 60 * 60 * 24
break
case "h":
coef = 1000 * 60 * 60
break
case "mn":
coef = 1000 * 60
break
case "s":
coef = 1000
case "ms":
coef = 1
}
return Math.round((toDate.getTime() - fromDate.getTime()) / coef)
} catch (err) {
return 0
}
},
TIME_DIFFERENCE_HELP:
`TIME_DIFFERENCE( {{field1}}, {{field2}}, "h")
The time difference between 2 DATE fields, using the given unit (d, h, mn, s, or ms)`,
/**
* Compute the number of days between 2 dates
*
* @param {string} fromISODate - As an ISO date string like "2023-02-14T15:44:05.886Z" or "2023-02-14"
* @param {string} toISODate - As an ISO date string like "2023-02-14T15:44:05.886Z" or "2023-02-14"
* @returns {integer} Number of days
*
* @example
* DAYS_DIFFERENCE("2023-01-01T15:44:05.886Z", "2023-01-15T18:44:26.316Z") // 14
* DAYS_DIFFERENCE("2023-01-01", "2023-01-10") // 9
*/
DAYS_DIFFERENCE: (fromISODate, toISODate) => {
return kiss.formula.TIME_DIFFERENCE(fromISODate, toISODate, "d")
},
DAYS_DIFFERENCE_HELP:
`DAYS_DIFFERENCE( {{field1}}, {{field2}} )
The number of days between 2 DATE fields`,
/**
* Compute the number of hours between 2 dates
*
* @param {string} fromISODate
* @param {string} toISODate
* @returns {integer} Number of hours
*
* @example
* HOURS_DIFFERENCE("2023-01-01T15:00:00.000Z", "2023-01-02T16:00:00.000Z") // 25
*/
HOURS_DIFFERENCE: (fromISODate, toISODate) => {
return kiss.formula.TIME_DIFFERENCE(fromISODate, toISODate, "h")
},
HOURS_DIFFERENCE_HELP:
`HOURS_DIFFERENCE( {{field1}}, {{field2}} )
The number of hours between 2 DATE fields`,
/**
* Adjust a date to a new date, passing the number of years, months, days, hours, minutes and seconds to add or subtract.
* If the hours, minutes and seconds are not specified, they are set to 0.
*
* @param {date|string} date - Date or ISO date string like "2023-12-01"
* @param {number} [years]
* @param {number} [months]
* @param {number} [days]
* @param {number} [hours]
* @param {number} [minutes]
* @param {number} [seconds]
* @param {string} [format] - "string" (default) or "date" to return a date object
* @returns {string} The adjusted date, as an ISO string like "2023-01-01"
*
* @example
* ADJUST_DATE("2023-01-01", 0, 1, 0) // "2023-02-01"
* ADJUST_DATE("2023-01-01", 0, 0, 0, 48, 0, 0) // "2023-01-03"
*/
ADJUST_DATE(date, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, format = "ISO") {
// Create a new date object by cloning the original date
let newDate = (date) ? new Date(date) : new Date()
// Add or subtract seconds, minutes, hours first using UTC methods
newDate.setUTCSeconds(newDate.getUTCSeconds() + seconds)
newDate.setUTCMinutes(newDate.getUTCMinutes() + minutes)
newDate.setUTCHours(newDate.getUTCHours() + hours)
// Then adjust days, months, years using UTC methods
newDate.setUTCDate(newDate.getUTCDate() + days)
newDate.setUTCMonth(newDate.getUTCMonth() + months)
newDate.setUTCFullYear(newDate.getUTCFullYear() + years)
// Return the adjusted date
if (format == "ISO") return newDate.toISO()
return newDate
},
ADJUST_DATE_HELP:
`ADJUST_DATE( {{field}}, 0, 1, 0, 0, 0, 0)
Adjust a DATE field by the number of given years, months, days, hours, minutes and seconds, and output the result like "2023-01-01"`,
/**
* Adjust a date to a new date and time, passing the number of years, months, days, hours, minutes and seconds to add or subtract.
* If the hours, minutes and seconds are not specified, they are set to 0.
*
* @param {date|string} date - Date or ISO date string like "2023-12-01"
* @param {number} [years]
* @param {number} [months]
* @param {number} [days]
* @param {number} [hours]
* @param {number} [minutes]
* @param {number} [seconds]
* @param {string} [format] - "string" (default) or "date" to return a date object
* @returns {string|date} The adjusted date and time, as an ISO string like "2023-01-01 12:34:56" or a date object
*
* @example
* ADJUST_DATE_AND_TIME("2023-01-01", 0, 1, 0) // "2023-02-01 00:00:00"
* ADJUST_DATE_AND_TIME("2023-01-01", 0, 1, 0, 3, 0, 0) // "2023-02-01 03:00:00"
* ADJUST_DATE_AND_TIME("2023-01-01 03:45:00", 0, 1, 0, 3, 0, 0) // "2023-02-01 06:45:00"
* ADJUST_DATE_AND_TIME(new Date(), 0, 1, 0, 3, 0, 0) // "2023-01-01 06:45:00"
*/
ADJUST_DATE_AND_TIME(date, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, format = "ISO") {
// Create a new date object by cloning the original date
let newDate = (date) ? new Date(date) : new Date()
log(">>NEW DATE")
log(newDate)
// Add or subtract the specified number of years, months, and days
newDate.setFullYear(newDate.getFullYear() + years)
newDate.setMonth(newDate.getMonth() + months)
newDate.setDate(newDate.getDate() + days)
newDate.setHours(newDate.getHours() + hours)
newDate.setMinutes(newDate.getMinutes() + minutes)
newDate.setSeconds(newDate.getSeconds() + seconds)
// Return the adjusted date and time
if (format == "ISO") return newDate.toISODateTime()
return newDate
},
ADJUST_DATE_AND_TIME_HELP:
`ADJUST_DATE_AND_TIME( {{field}}, 0, 0, 1, 12, 30, 0)
Adjust a DATE field by the number of given years, months, days, hours, minutes and seconds, and output the result like "2023-01-01 12:30:00"`,
/**
* Convert a date to a formatted string
*
* @param {date|string} date - Date or ISO date string like "2023-12-01"
* @param {string} format - Ex: "yyyy-mm-dd", "yyyy-mm-dd hh:MM:ss", "yyyy-mm-dd hh:MM", "yy-mm-dd hh:MM"
* @returns {string}
*
* @example
* FORMAT_DATE(new Date(), "yyyy-mm-dd") // "2023-01-01"
* FORMAT_DATE(new Date(), "yyyy-mm-dd hh:MM:ss") // "2023-01-01 00:00:00"
* FORMAT_DATE("2023-12-31", "yy-mm-dd") // "23-12-31"
*/
FORMAT_DATE: (date, format) => {
try {
if (!date) return ""
if (typeof date == "string") date = new Date(date)
const pad = (n) => n < 10 ? "0" + n : n
let year = date.getFullYear()
let month = pad(date.getMonth() + 1)
let day = pad(date.getDate())
let hour = pad(date.getHours())
let minute = pad(date.getMinutes())
let second = pad(date.getSeconds())
return format.replace(/yyyy/g, year)
.replace(/yy/g, year.toString().substring(2, 4))
.replace(/mm/g, month)
.replace(/dd/g, day)
.replace(/hh/g, hour)
.replace(/MM/g, minute)
.replace(/ss/g, second)
} catch (err) {
return ""
}
},
FORMAT_DATE_HELP:
`FORMAT_DATE( DATE, "yyyy-mm-dd"), or FORMAT_DATE( DATE, "yyyy-mm-dd hh:MM")
Convert a date to a formatted string. Usefull to display dates in a specific format, or to set another date field using ISO, like "2023-01-01"`,
//--------------------
//
// WORKING WITH MULTI-VALUE FIELDS
//
//--------------------
/**
* SUM
*
* @param {...any} numbers
* @returns {number}
*
* @example
* SUM(1, 2, 3) // 6
*/
SUM: (...numbers) => numbers.reduce((a, b) => Number(a||0) + Number(b||0), 0),
SUM_HELP:
`SUM( {{field1}}, {{field2}}, ... )
The sum of multiple NUMBER fields`,
/**
* Returns the LENGTH of an array or a string
*
* @param {array|string} array
* @returns {number}
*
* @example
* LENGTH([0, 1, 2, 3]) // 4
* LENGTH("Satori") // 6
*/
LENGTH: (array) => {
if (!array) return 0
if (!Array.isArray(array)) {
if (typeof array == "string") return array.length
else return 0
}
return array.length
},
LENGTH_HELP:
`LENGTH( {{field}} )
The length of a TEXT, or the number of elements in a MULTI-VALUE field`,
/**
* Returns the NTH element of an array or a string
*
* @param {array|string} array
* @param {number} index
* @returns {*}
*
* @example
* NTH(["low", "medium", "high"], 1) // "medium"
*/
NTH: (array, index) => {
if (!array) return ""
if (Array.isArray(array) || typeof array == "string") return array[index]
return ""
},
NTH_HELP:
`NTH( {{field}} )
The nth element of a MULTI-VALUE field`,
/**
* Join an array of strings into a single string, given a separator
*
* @param {string[]} array
* @param {string} separator
* @returns {string} The resulting string
*
* @example
* JOIN(["Paris", "San Diego", "Ajaccio"], " & ") // "Paris & San Diego & Ajaccio"
*/
JOIN: (array, separator) => array.join(separator),
JOIN_HELP:
`JOIN( {{field}}, " & ")
Transform a MULTI-VALUE field into a text with separators`,
/**
* Converts a list of values into an array, to feed MULTI-VALUE fields.
*
* @param {*} values
* @returns {string[]|number[]|date[]} The resulting array
*
* @example
* ARRAY( "a", "b", "c" ) // [ "a", "b", "c" ]
*/
ARRAY: (...values) => values,
ARRAY_HELP:
`ARRAY( "a", "b", "c" )
Transform a list of values into an array, to feed MULTI-VALUE fields`,
//--------------------
//
// TESTING FIELD VALUE
//
//--------------------
/**
* Check if a field value is empty
*
* @param {*} value
* @returns {boolean}
*
* @example
* IS_EMPTY([0, 1, 2, 3]) // false
* IS_EMPTY([]) // true
* IS_EMPTY("abc") // false
* IS_EMPTY("") // true
* IS_EMPTY(123) // false
* IS_EMPTY(0) // false
*/
IS_EMPTY: (value) => {
if (value === 0) return false
if (typeof value === "string" && value.trim() === "") return true
if (!value) return true
if (Array.isArray(value) && value.length == 0) return true
return false
},
IS_EMPTY_HELP:
`IS_EMPTY( {{field}} )
Returns true if the field is empty`,
/**
* Check if a field value is not empty
*
* @param {*} value
* @returns {boolean}
*
* @example
* IS_NOT_EMPTY([0, 1, 2, 3]) // true
* IS_NOT_EMPTY([]) // false
* IS_NOT_EMPTY("abc") // true
* IS_NOT_EMPTY("") // false
* IS_NOT_EMPTY(123) // true
* IS_NOT_EMPTY(0) // true
*/
IS_NOT_EMPTY: (value) => {
return !kiss.formula.IS_EMPTY(value)
},
IS_NOT_EMPTY_HELP:
`IS_NOT_EMPTY( {{field}} )
Returns true if the field is not empty`,
/**
* Test a set of conditions, and returns the value of the first expression that matches the test.
* If no condition matches, returns the value of the last (default) expression.
* Always has an odd number of parameters:
*
* ```
* IF(<condition 1>, <expression 1>, ..., ..., <condition N>, <expression N>, <expression Else>)
* ```
*
* @param {...any} params
* @returns {any} Value of the first expression that matches the condition
*
* @example
* // Returns "Good" if the field "amount" = 65, or "Poor" if the field "amout" = 20
* IF({{amount}} > 80, "Great", {{amount}} > 60, "Good", {{amount}} > 40, "Not bad", "Poor")
*/
IF: (...params) => {
try {
if (params.length < 3 || params.length % 2 == 0) return false
for (let i = 0; i <= params.length - 2; i = i + 2) {
if (params[i] == true) return params[i + 1]
}
return params[params.length - 1]
} catch (err) {
return false
}
},
IF_HELP:
`IF( {{field}} == 100, "Good", {{field}} > 50, "OK", "Poor" )
Returns the value where the test is true, or defaults to the last value`,
//--------------------
//
// TESTING WORKFLOW STATE
//
//--------------------
/**
* Check the current workflow step
*
* IMPORTANT: only valid on the client, for "hideWhen" formulae
*
* @param {string} stepName
* @returns {boolean} true if the given step name is the current step
*/
IS_WORKFLOW_STEP: (stepName) => {
const stepId = kiss.context.record["workflow-stepId"]
if (!stepId) return false
const step = kiss.global.workflowSteps[stepId]
if (!step) return false
return step.name == stepName
},
IS_WORKFLOW_STEP_HELP:
`IS_WORKFLOW_STEP("Analysis")
Returns true if the given workflow step name is the active one. Useful to show/hide form fields or sections depending on the workflow step.`,
/**
* Check if a workflow has started
*
* IMPORTANT: only valid on the client, for "hideWhen" formulae
*
* @param {string} stepName
* @returns {boolean} true if the given step name is the current step
*/
IS_WORKFLOW_STARTED: () => {
const stepId = kiss.context.record["workflow-stepId"]
return !!stepId
},
IS_WORKFLOW_STARTED_HELP:
`IS_WORKFLOW_STARTED()
Returns true if a workflow has started. Useful to show/hide form fields or sections depending on the workflow status.`,
//--------------------
//
// MISC TOOLS
//
//--------------------
/**
* Generates a unique id
*
* @returns {string}
*
* @example
* UID() // "01844399-988f-7974-a68f-92d35fc702cc"
*/
UID: () => kiss.tools.uid(),
UID_HELP:
`UID()
A unique ID like "01844399-988f-7974-a68f-92d35fc702cc"`,
/**
* Generates a short id
*
* @returns {string}
*
* @example
* SHORT_ID() // "A84F007X"
*/
SHORT_ID: () => kiss.tools.shortUid().toUpperCase(),
SHORT_ID_HELP:
`SHORT_ID()
A short ID like "A84F007X"`,
/**
* List of formulae which are not available for the user
*
* @ignore
*/
system: [
"system",
"hideWhen",
"execute",
"_parser",
"COUNT",
"COUNT_EMPTY",
"COUNT_NON_EMPTY",
"LIST",
"LIST_NAMES",
"SPLIT",
"TODAY",
"TIME",
"NOW"
],
/**
* List of formulae which are only valid for "hideWhen" context
*
* @ignore
*/
hideWhen: [
"IS_WORKFLOW_STEP",
"IS_WORKFLOW_STARTED"
],
/**
* COUNT the number of items passed as parameters
*
* @param {...any} items
* @returns {number}
*
* @example
* COUNT(1, "B", "C", 7) // 4
*/
COUNT: (...items) => {
return items.length
},
/**
* COUNT the number of empty items passed as parameters
*
* @param {...any} items
* @returns {number}
*
* @example
* COUNT_EMPTY(1, "B", "C", 7, "", []) // 2
*/
COUNT_EMPTY: (...items) => {
const empty = items.filter(item => {
return (item === undefined) || (item === "") || (Array.isArray(item) && item.length == 0)
})
return empty.length
},
/**
* COUNT the number of non-empty items passed as parameters
*
* @param {...any} items
* @returns {number}
*
* @example
* COUNT_NON_EMPTY(1, "B", "C", 7, "", []) // 4
*/
COUNT_NON_EMPTY: (...items) => {
const nonEmpty = items.filter(item => {
return (item !== undefined) && (item !== "") || (Array.isArray(item) && item.length != 0)
})
return nonEmpty.length
},
/**
* Concatenate any number of strings as a comma separated text
*
* @param {...any} strings
* @returns {string}
*
* @example
* LIST("A", "B", "C") // "A, B, C"
*/
LIST: (...strings) => strings.filter(string => string !== undefined && string != "").unique().join(", "),
/**
* Merge a list of names into an array of names
*
* @param {...string} names
* @returns {string[]}
*
* @example
* LIST_NAMES("John", "Bob", "Steve") // ["John, Bob, Steve"]
*/
LIST_NAMES: (...names) => {
return names.filter(name => name !== undefined && name != "").flat().unique()
},
/**
* Split a string into an array of strings, given a separator
*
* @param {string} text
* @param {string} separator
* @returns {string[]} The resulting array of strings
*
* @example
* SPLIT("Paris,San Diego,Ajaccio", ",") // ["Paris", "San Diego", "Ajaccio"]
*/
SPLIT: (text, separator) => text.split(separator),
/**
* Get the current date as an ISO date string
*
* @param {string} strDateISO
* @returns {string}
*
* @example
* TODAY() // "2022-12-24"
*/
TODAY: () => (new Date()).toISO(),
/**
* Get the current time as an ISO string (without time shift)
*
* @returns {string}
*
* @example
* TIME() // "14:53:28"
*/
TIME: () => (new Date()).toISOString().substring(11,16),
/**
* Get the current date and time as a simple readable string (without time shift)
*
* @returns {string}
*
* @example
* NOW() // "2022-12-24 14:53:28"
*/
NOW: () => (new Date()).toISOString().substring(0,19).replace("T", " "),
/**
* Execute a formula that is an arithmetic expression mixed or not with functions calls and return the result
*
* @param {string} formula - The formula to parse and execute
* @param {Object} record - A record object. Each object property will be available in the formula with the following syntax: "{{property_name}}" if the corresponding entry is listed in fields.
* @param {Array<{label: string, id: string}>} fields - A list of record properties sorted in a way such as {{index}} will be resolved as a record property.
* @returns {*} - The formula result
*
* @example
* kiss.formula.execute("SUM(4, 6)") // returns 10
* kiss.formula.execute("'Test: ' + (2 * SUM(3, 4))") // returns "Test: 14"
* kiss.formula.execute("true && !false") // returns true
* kiss.formula.execute("{{amount}} + {{vat}}") // returns 12 with record `{ amount: 10, vat: 2 }`
*/
execute(formula, record, fields = undefined) {
if (!this._parser) {
// We must create the parser here, doing it in the upper scope, it would lose the reference to kiss.formula for somewhat reason.
this._parser = new kiss.lib.formula.Parser({ availableFunctions: this })
}
const normalizedRecord = {}
const propertiesList = fields.map(({ id, label }) => {
normalizedRecord[label] = record[id]
return label
})
// log("-------------------- FORMULA ------------------------")
// log(formula)
return this._parser.parse(formula, normalizedRecord, propertiesList)
}
}
;
Source