Browse Source

Merge pull request #2090 from nocodb/enhancement/formula

enhancement: formula
pull/2124/head
աɨռɢӄաօռɢ 3 years ago committed by GitHub
parent
commit
99954d1fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 342
      packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue
  2. 35
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/FormulaCell.vue
  3. 490
      packages/nc-gui/helpers/formulaList.js
  4. 18
      packages/noco-docs/content/en/setup-and-usages/formulas.md

342
packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue

@ -37,7 +37,9 @@
> >
<!-- Function --> <!-- Function -->
<template v-if="it.type ==='function'"> <template v-if="it.type ==='function'">
<v-list-item-content> <v-tooltip right offset-x nudge-right="100">
<template #activator="{ on }">
<v-list-item-content v-on="on">
<span <span
class="caption primary--text text--lighten-2 font-weight-bold" class="caption primary--text text--lighten-2 font-weight-bold"
> >
@ -50,6 +52,17 @@
</span> </span>
</v-list-item-action> </v-list-item-action>
</template> </template>
<div>
{{ it.description }} <br><br>
Syntax: <br>
{{ it.syntax }} <br><br>
Examples: <br>
<div v-for="(example, idx) in it.examples" :key="idx">
<pre>({{ idx + 1 }}): {{ example }}</pre>
</div>
</div>
</v-tooltip>
</template>
<!-- Column --> <!-- Column -->
<template v-if="it.type ==='column'"> <template v-if="it.type ==='column'">
@ -96,7 +109,7 @@
import debounce from 'debounce' import debounce from 'debounce'
import jsep from 'jsep' import jsep from 'jsep'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk' import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import formulaList, { validations } from '../../../../../helpers/formulaList' import formulaList, { formulas, formulaTypes } from '../../../../../helpers/formulaList'
import { getWordUntilCaret, insertAtCursor } from '@/helpers' import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'
@ -125,7 +138,10 @@ export default {
return [ return [
...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({ ...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({
text: fn + '()', text: fn + '()',
type: 'function' type: 'function',
description: formulas[fn].description,
syntax: formulas[fn].syntax,
examples: formulas[fn].examples
})), })),
...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({
text: c.title, text: c.title,
@ -209,44 +225,314 @@ export default {
}, },
parseAndValidateFormula(formula) { parseAndValidateFormula(formula) {
try { try {
const pt = jsep(formula) const parsedTree = jsep(formula)
const err = this.validateAgainstMeta(pt) const metaErrors = this.validateAgainstMeta(parsedTree)
if (err.length) { if (metaErrors.size) {
return err.join(', ') return [...metaErrors].join(', ')
} }
return true return true
} catch (e) { } catch (e) {
return e.message return e.message
} }
}, },
validateAgainstMeta(pt, arr = []) { validateAgainstMeta(parsedTree, errors = new Set(), typeErrors = new Set()) {
if (pt.type === 'CallExpression') { if (parsedTree.type === jsep.CALL_EXP) {
if (!this.availableFunctions.includes(pt.callee.name)) { // validate function name
arr.push(`'${pt.callee.name}' function is not available`) if (!this.availableFunctions.includes(parsedTree.callee.name)) {
errors.add(`'${parsedTree.callee.name}' function is not available`)
} }
const validation = validations[pt.callee.name] && validations[pt.callee.name].validation // validate arguments
const validation = formulas[parsedTree.callee.name] && formulas[parsedTree.callee.name].validation
if (validation && validation.args) { if (validation && validation.args) {
if (validation.args.rqd !== undefined && validation.args.rqd !== pt.arguments.length) { if (validation.args.rqd !== undefined && validation.args.rqd !== parsedTree.arguments.length) {
arr.push(`'${pt.callee.name}' required ${validation.args.rqd} arguments`) errors.add(`'${parsedTree.callee.name}' required ${validation.args.rqd} arguments`)
} else if (validation.args.min !== undefined && validation.args.min > pt.arguments.length) { } else if (validation.args.min !== undefined && validation.args.min > parsedTree.arguments.length) {
arr.push(`'${pt.callee.name}' required minimum ${validation.args.min} arguments`) errors.add(`'${parsedTree.callee.name}' required minimum ${validation.args.min} arguments`)
} else if (validation.args.max !== undefined && validation.args.max < pt.arguments.length) { } else if (validation.args.max !== undefined && validation.args.max < parsedTree.arguments.length) {
arr.push(`'${pt.callee.name}' required maximum ${validation.args.max} arguments`) errors.add(`'${parsedTree.callee.name}' required maximum ${validation.args.max} arguments`)
}
}
parsedTree.arguments.map(arg => this.validateAgainstMeta(arg, errors))
// validate data type
if (parsedTree.callee.type === jsep.IDENTIFIER) {
const expectedType = formulas[parsedTree.callee.name].type
if (
expectedType === formulaTypes.NUMERIC ||
expectedType === formulaTypes.STRING
) {
parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors))
} else if (expectedType === formulaTypes.DATE) {
if (parsedTree.callee.name === 'DATEADD') {
// parsedTree.arguments[0] = date
this.validateAgainstType(parsedTree.arguments[0], formulaTypes.DATE, (v) => {
if (!(v instanceof Date)) {
typeErrors.add('The first parameter of DATEADD() should have date value')
}
}, typeErrors)
// parsedTree.arguments[1] = numeric
this.validateAgainstType(parsedTree.arguments[1], formulaTypes.NUMERIC, (v) => {
if (typeof v !== 'number') {
typeErrors.add('The second parameter of DATEADD() should have numeric value')
}
}, typeErrors)
// parsedTree.arguments[2] = ["day" | "week" | "month" | "year"]
this.validateAgainstType(parsedTree.arguments[2], formulaTypes.STRING, (v) => {
if (!['day', 'week', 'month', 'year'].includes(v)) {
typeErrors.add('The third parameter of DATEADD() should have the value either "day", "week", "month" or "year"')
}
}, typeErrors)
}
}
}
errors = new Set([...errors, ...typeErrors])
} else if (parsedTree.type === jsep.IDENTIFIER) {
if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== parsedTree.name)) {
errors.add(`Column '${parsedTree.name}' is not available`)
}
// check circular reference
// e.g. formula1 -> formula2 -> formula1 should return circular reference error
// get all formula columns excluding itself
const formulaPaths = this.meta.columns.filter(c => c.id !== this.column.id && c.uidt === UITypes.Formula).reduce((res, c) => {
// in `formula`, get all the target neighbours
// i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type
const neighbours = (c.colOptions.formula.match(/cl_\w{14}/g) || []).filter(colId => (this.meta.columns.filter(col => (col.id === colId && col.uidt === UITypes.Formula)).length))
if (neighbours.length > 0) {
// e.g. formula column 1 -> [formula column 2, formula column3]
res.push({ [c.id]: neighbours })
}
return res
}, [])
// include target formula column (i.e. the one to be saved if applicable)
const targetFormulaCol = this.meta.columns.find(c => c.title === parsedTree.name && c.uidt === UITypes.Formula)
if (targetFormulaCol) {
formulaPaths.push({
[this.column.id]: [targetFormulaCol.id]
})
}
const vertices = formulaPaths.length
if (vertices > 0) {
// perform kahn's algo for cycle detection
const adj = new Map()
const inDegrees = new Map()
// init adjacency list & indegree
for (const [_, v] of Object.entries(formulaPaths)) {
const src = Object.keys(v)[0]
const neighbours = v[src]
inDegrees.set(src, inDegrees.get(src) || 0)
for (const neighbour of neighbours) {
adj.set(src, (adj.get(src) || new Set()).add(neighbour))
inDegrees.set(neighbour, (inDegrees.get(neighbour) || 0) + 1)
}
}
const queue = []
// put all vertices with in-degree = 0 (i.e. no incoming edges) to queue
inDegrees.forEach((inDegree, col) => {
if (inDegree === 0) {
// in-degree = 0 means we start traversing from this node
queue.push(col)
}
})
// init count of visited vertices
let visited = 0
// BFS
while (queue.length !== 0) {
// remove a vertex from the queue
const src = queue.shift()
// if this node has neighbours, increase visited by 1
const neighbours = adj.get(src) || new Set()
if (neighbours.size > 0) {
visited += 1
}
// iterate each neighbouring nodes
neighbours.forEach((neighbour) => {
// decrease in-degree of its neighbours by 1
inDegrees.set(neighbour, inDegrees.get(neighbour) - 1)
// if in-degree becomes 0
if (inDegrees.get(neighbour) === 0) {
// then put the neighboring node to the queue
queue.push(neighbour)
}
})
}
// vertices not same as visited = cycle found
if (vertices !== visited) {
errors.add('Can’t save field because it causes a circular reference')
} }
} }
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) } else if (parsedTree.type === jsep.BINARY_EXP) {
} else if (pt.type === 'Identifier') { if (!this.availableBinOps.includes(parsedTree.operator)) {
if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) { errors.add(`'${parsedTree.operator}' operation is not available`)
arr.push(`Column '${pt.name}' is not available`)
} }
} else if (pt.type === 'BinaryExpression') { this.validateAgainstMeta(parsedTree.left, errors)
if (!this.availableBinOps.includes(pt.operator)) { this.validateAgainstMeta(parsedTree.right, errors)
arr.push(`'${pt.operator}' operation is not available`) } else if (parsedTree.type === jsep.LITERAL || parsedTree.type === jsep.UNARY_EXP) {
// do nothing
} else if (parsedTree.type === jsep.COMPOUND) {
if (parsedTree.body.length) {
errors.add('Can’t save field because the formula is invalid')
} }
this.validateAgainstMeta(pt.left, arr) } else {
this.validateAgainstMeta(pt.right, arr) errors.add('Can’t save field because the formula is invalid')
}
return errors
},
validateAgainstType(parsedTree, expectedType, func, typeErrors = new Set()) {
if (parsedTree === false || typeof parsedTree === 'undefined') { return typeErrors }
if (parsedTree.type === jsep.LITERAL) {
if (typeof func === 'function') {
func(parsedTree.value)
} else if (expectedType === formulaTypes.NUMERIC) {
if (typeof parsedTree.value !== 'number') {
typeErrors.add('Numeric type is expected')
}
} else if (expectedType === formulaTypes.STRING) {
if (typeof parsedTree.value !== 'string') {
typeErrors.add('string type is expected')
}
}
} else if (parsedTree.type === jsep.IDENTIFIER) {
const col = this.meta.columns.find(c => c.title === parsedTree.name)
if (col === undefined) { return }
if (col.uidt === UITypes.Formula) {
const foundType = this.getRootDataType(jsep(col.colOptions.formula_raw))
if (foundType === 'N/A') {
typeErrors.add(`Not supported to reference column ${c.title}`)
} else if (expectedType !== foundType) {
typeErrors.add(`Type ${expectedType} is expected but found Type ${foundType}`)
}
} else {
switch (col.uidt) {
// string
case UITypes.SingleLineText:
case UITypes.LongText:
case UITypes.MultiSelect:
case UITypes.SingleSelect:
case UITypes.PhoneNumber:
case UITypes.Email:
case UITypes.URL:
if (expectedType !== formulaTypes.STRING) {
typeErrors.add(`Column '${parsedTree.name}' with ${formulaTypes.STRING} type is found but ${expectedType} type is expected`)
}
break
// numeric
case UITypes.Year:
case UITypes.Number:
case UITypes.Decimal:
case UITypes.Rating:
case UITypes.Count:
case UITypes.AutoNumber:
if (expectedType !== formulaTypes.NUMERIC) {
typeErrors.add(`Column '${parsedTree.name}' with ${formulaTypes.NUMERIC} type is found but ${expectedType} type is expected`)
}
break
// date
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.LastModifiedTime:
if (expectedType !== formulaTypes.DATE) {
typeErrors.add(`Column '${parsedTree.name}' with ${formulaTypes.DATE} type is found but ${expectedType} type is expected`)
}
break
// not supported
case UITypes.ForeignKey:
case UITypes.Attachment:
case UITypes.ID:
case UITypes.Time:
case UITypes.Currency:
case UITypes.Percent:
case UITypes.Duration:
case UITypes.Rollup:
case UITypes.Lookup:
case UITypes.Barcode:
case UITypes.Button:
case UITypes.Checkbox:
case UITypes.Collaborator:
default:
typeErrors.add(`Not supported to reference column '${parsedTree.name}'`)
break
}
}
} else if (parsedTree.type === jsep.UNARY_EXP || parsedTree.type === jsep.BINARY_EXP) {
if (expectedType !== formulaTypes.NUMERIC) {
// parsedTree.name won't be available here
typeErrors.add(`${formulaTypes.NUMERIC} type is found but ${expectedType} type is expected`)
}
} else if (parsedTree.type === jsep.CALL_EXP) {
if (formulas[parsedTree.callee.name]?.type && expectedType !== formulas[parsedTree.callee.name].type) {
typeErrors.add(`${expectedType} not matched with ${formulas[parsedTree.callee.name].type}`)
}
}
return typeErrors
},
getRootDataType(parsedTree) {
// given a parse tree, return the data type of it
if (parsedTree.type === jsep.CALL_EXP) {
return formulas[parsedTree.callee.name].type
} else if (parsedTree.type === jsep.IDENTIFIER) {
const col = this.meta.columns.find(c => c.title === parsedTree.name)
if (col.uidt === UITypes.Formula) {
return this.getRootDataType(jsep(col.colOptions.formula_raw))
} else {
switch (col.uidt) {
// string
case UITypes.SingleLineText:
case UITypes.LongText:
case UITypes.MultiSelect:
case UITypes.SingleSelect:
case UITypes.PhoneNumber:
case UITypes.Email:
case UITypes.URL:
return 'string'
// numeric
case UITypes.Year:
case UITypes.Number:
case UITypes.Decimal:
case UITypes.Rating:
case UITypes.Count:
case UITypes.AutoNumber:
return 'number'
// date
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.LastModifiedTime:
return 'date'
// not supported
case UITypes.ForeignKey:
case UITypes.Attachment:
case UITypes.ID:
case UITypes.Time:
case UITypes.Currency:
case UITypes.Percent:
case UITypes.Duration:
case UITypes.Rollup:
case UITypes.Lookup:
case UITypes.Barcode:
case UITypes.Button:
case UITypes.Checkbox:
case UITypes.Collaborator:
default:
return 'N/A'
}
}
} else if (parsedTree.type === jsep.BINARY_EXP || parsedTree.type === jsep.UNARY_EXP) {
return 'number'
} else if (parsedTree.type === jsep.LITERAL) {
return typeof parsedTree.value
} else {
return 'N/A'
} }
return arr
}, },
appendText(it) { appendText(it) {
const text = it.text const text = it.text

35
packages/nc-gui/components/project/spreadsheet/components/virtualCell/FormulaCell.vue

@ -1,6 +1,11 @@
<template> <template>
<div>
<v-tooltip <v-tooltip
v-if="column && column.colOptions&& column.colOptions.error" v-if="
column
&&
column.colOptions&&
column.colOptions.error"
bottom bottom
color="error" color="error"
> >
@ -9,10 +14,17 @@
</template> </template>
<span class=" font-weight-bold">{{ column.colOptions.error }}</span> <span class=" font-weight-bold">{{ column.colOptions.error }}</span>
</v-tooltip> </v-tooltip>
<div v-else-if="urls" v-html="urls" /> <div class="formula-cell-wrapper" @dblclick="showEditFormulaWarningMessage">
<div v-if="urls" v-html="urls" />
<div v-else> <div v-else>
{{ result }} {{ result }}
</div> </div>
<div v-if="showEditFormulaWarning == true" class="edit-warning">
<!-- TODO: i18n -->
Warning: Formula fields should be configured in the field menu dropdown.
</div>
</div>
</div>
</template> </template>
<script> <script>
@ -21,6 +33,9 @@ import dayjs from 'dayjs'
export default { export default {
name: 'FormulaCell', name: 'FormulaCell',
props: { column: Object, row: Object, client: String }, props: { column: Object, row: Object, client: String },
data: () => ({
showEditFormulaWarning: false
}),
computed: { computed: {
result() { result() {
if (this.client === 'pg') { if (this.client === 'pg') {
@ -53,11 +68,25 @@ export default {
return val.replace(/((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[01]|0[1-9]|[12][0-9])T(?:2[0-3]|[01][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9]))/g, (i, v) => { return val.replace(/((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[01]|0[1-9]|[12][0-9])T(?:2[0-3]|[01][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9]))/g, (i, v) => {
return dayjs(v).format('YYYY-MM-DD HH:mm') return dayjs(v).format('YYYY-MM-DD HH:mm')
}) })
},
showEditFormulaWarningMessage() {
this.showEditFormulaWarning = true
setTimeout(() => {
this.showEditFormulaWarning = false
}, 3000)
} }
} }
} }
</script> </script>
<style scoped>/**/ <style scoped>
.formula-cell-wrapper {
padding: 10px;
}
.edit-warning {
text-align: left;
margin-top: 10px;
color: #E65100;
}
</style> </style>

490
packages/nc-gui/helpers/formulaList.js

@ -1,62 +1,478 @@
const validations = { const formulaTypes = {
NUMERIC: "numeric",
STRING: "string",
DATE: "date",
LOGICAL: "logical",
COND_EXP: "conditional_expression"
}
const formulas = {
AVG: { AVG: {
type: formulaTypes.NUMERIC,
validation: { validation: {
args: { min: 1 } args: {
min: 1
} }
}, },
description: 'Average of input parameters',
syntax: 'AVG(value1, [value2, ...])',
examples: [
'AVG(10, 5) => 7.5',
'AVG({column1}, {column2})',
'AVG({column1}, {column2}, {column3})'
]
},
ADD: { ADD: {
type: formulaTypes.NUMERIC,
validation: {
args: {
min: 1
}
},
description: 'Sum of input parameters',
syntax: 'ADD(value1, [value2, ...])',
examples: [
'ADD(5, 5) => 10',
'ADD({column1}, {column2})',
'ADD({column1}, {column2}, {column3})'
]
},
DATEADD: {
type: formulaTypes.DATE,
validation: { validation: {
args: { min: 1 } args: {
rqd: 3
} }
}, },
DATEADD: { validation: { args: { rqd: 3 } } }, description: 'Adds a "count" units to Datetime.',
syntax: 'DATEADD(date | datetime, value, ["day" | "week" | "month" | "year"])',
examples: [
'DATEADD({column1}, 2, "day")',
'DATEADD({column1}, -2, "day")',
'DATEADD({column1}, 2, "week")',
'DATEADD({column1}, -2, "week")',
'DATEADD({column1}, 2, "month")',
'DATEADD({column1}, -2, "month")',
'DATEADD({column1}, 2, "year")',
'DATEADD({column1}, -2, "year")'
]
},
AND: { AND: {
type: formulaTypes.COND_EXP,
validation: { validation: {
args: { min: 1 } args: {
min: 1
} }
}, },
description: 'TRUE if all expr evaluate to TRUE',
syntax: 'AND(expr1, [expr2, ...])',
examples: [
'AND(5 > 2, 5 < 10) => 1',
'AND({column1} > 2, {column2} < 10)'
]
},
OR: { OR: {
type: formulaTypes.COND_EXP,
validation: { validation: {
args: { min: 1 } args: {
min: 1
} }
}, },
description: 'TRUE if at least one expr evaluates to TRUE',
syntax: 'OR(expr1, [expr2, ...])',
examples: [
'OR(5 > 2, 5 < 10) => 1',
'OR({column1} > 2, {column2} < 10)'
]
},
CONCAT: { CONCAT: {
validation: { args: { min: 1 } } type: formulaTypes.STRING,
validation: {
args: {
min: 1
}
},
description: 'Concatenated string of input parameters',
syntax: 'CONCAT(str1, [str2, ...])',
examples: [
'CONCAT("AA", "BB", "CC") => "AABBCC"',
'CONCAT({column1}, {column2}, {column3})'
]
}, },
TRIM: { TRIM: {
validation: { args: { min: 1 } } type: formulaTypes.STRING,
validation: {
args: {
rqd: 1
}
},
description: 'Remove trailing and leading whitespaces from input parameter',
syntax: 'TRIM(str)',
examples: [
'TRIM(" HELLO WORLD ") => "HELLO WORLD"',
'TRIM({column1})'
]
}, },
UPPER: { UPPER: {
validation: { args: { rqd: 1 } } type: formulaTypes.STRING,
}, validation: {
LOWER: { validation: { args: { rqd: 1 } } }, args: {
LEN: { validation: { args: { rqd: 1 } } }, rqd: 1
MIN: { validation: { args: { min: 1 } } }, }
MAX: { validation: { args: { min: 1 } } }, },
CEILING: { validation: { args: { rqd: 1 } } }, description: 'Upper case converted string of input parameter',
FLOOR: { validation: { args: { rqd: 1 } } }, syntax: 'UPPER(str)',
ROUND: { validation: { args: { rqd: 1 } } }, examples: [
MOD: { validation: { args: { rqd: 2 } } }, 'UPPER("nocodb") => "NOCODB"',
REPEAT: { validation: { args: { rqd: 2 } } }, 'UPPER({column1})'
LOG: { validation: {} }, ]
EXP: { validation: {} }, },
POWER: { validation: { args: { rqd: 2 } } }, LOWER: {
SQRT: { validation: { args: { rqd: 1 } } }, type: formulaTypes.STRING,
ABS: { validation: { args: { rqd: 1 } } }, validation: {
NOW: { validation: { args: { rqd: 0 } } }, args: {
REPLACE: { validation: { args: { rqd: 3 } } }, rqd: 1
SEARCH: { validation: { args: { rqd: 2 } } }, }
INT: { validation: { args: { rqd: 1 } } }, },
RIGHT: { validation: { args: { rqd: 2 } } }, description: 'Lower case converted string of input parameter',
syntax: 'LOWER(str)',
examples: [
'LOWER("NOCODB") => "nocodb"',
'LOWER({column1})'
]
},
LEN: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 1
}
},
description: 'Input parameter character length',
syntax: 'LEN(value)',
examples: [
'LEN("NocoDB") => 6',
'LEN({column1})'
]
},
MIN: {
type: formulaTypes.NUMERIC,
validation: {
args: {
min: 1
}
},
description: 'Minimum value amongst input parameters',
syntax: 'MIN(value1, [value2, ...])',
examples: [
'MIN(1000, 2000) => 1000',
'MIN({column1}, {column2})'
]
},
MAX: {
type: formulaTypes.NUMERIC,
validation: {
args: {
min: 1
}
},
description: 'Maximum value amongst input parameters',
syntax: 'MAX(value1, [value2, ...])',
examples: [
'MAX(1000, 2000) => 2000',
'MAX({column1}, {column2})'
]
},
CEILING: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Rounded next largest integer value of input parameter',
syntax: 'CEILING(value)',
examples: [
'CEILING(1.01) => 2',
'CEILING({column1})'
]
},
FLOOR: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Rounded largest integer less than or equal to input parameter',
syntax: 'FLOOR(value)',
examples: [
'FLOOR(3.1415) => 3',
'FLOOR({column1})'
]
},
ROUND: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Nearest integer to the input parameter',
syntax: 'ROUND(value)',
examples: [
'ROUND(3.1415) => 3',
'ROUND({column1})'
]
},
MOD: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 2
}
},
description: 'Remainder after integer division of input parameters',
syntax: 'MOD(value1, value2)',
examples: [
'MOD(1024, 1000) => 24',
'MOD({column}, 2)'
]
},
REPEAT: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 2
}
},
description: 'Specified copies of the input parameter string concatenated together',
syntax: 'REPEAT(str, count)',
examples: [
'REPEAT("A", 5) => "AAAAA"',
'REPEAT({column}, 5)'
]
},
LOG: {
type: formulaTypes.NUMERIC,
validation: {},
description: 'Logarithm of input parameter to the base (default = e) specified',
syntax: 'LOG([base], value)',
examples: [
'LOG(2, 1024) => 10',
'LOG(2, {column1})'
]
},
EXP: {
type: formulaTypes.NUMERIC,
validation: {},
description: 'Exponential value of input parameter (e ^ power)',
syntax: 'EXP(power)',
examples: [
'EXP(1) => 2.718281828459045',
'EXP({column1})'
]
},
POWER: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 2
}
},
description: 'base to the exponent power, as in base ^ exponent',
syntax: 'POWER(base, exponent)',
examples: [
'POWER(2, 10) => 1024',
'POWER({column1}, 10)'
]
},
SQRT: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Square root of the input parameter',
syntax: 'SQRT(value)',
examples: [
'SQRT(100) => 10',
'SQRT({column1})'
]
},
ABS: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Absolute value of the input parameter',
syntax: 'ABS(value)',
examples: [
'ABS({column1})'
]
},
NOW: {
type: formulaTypes.DATE,
validation: {
args: {
rqd: 0
}
},
description: 'Returns the current time and day',
syntax: 'NOW()',
examples: [
'NOW() => 2022-05-19 17:20:43'
]
},
REPLACE: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 3
}
},
description: 'String, after replacing all occurrences of srchStr with rplcStr',
syntax: 'REPLACE(str, srchStr, rplcStr)',
examples: [
'REPLACE("AABBCC", "AA", "BB") => "BBBBCC"',
'REPLACE({column1}, {column2}, {column3})'
]
},
SEARCH: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 2
}
},
description: 'Index of srchStr specified if found, 0 otherwise',
syntax: 'SEARCH(str, srchStr)',
examples: [
'SEARCH("HELLO WORLD", "WORLD") => 7',
'SEARCH({column1}, "abc")'
]
},
INT: {
type: formulaTypes.NUMERIC,
validation: {
args: {
rqd: 1
}
},
description: 'Integer value of input parameter',
syntax: 'INT(value)',
examples: [
'INT(3.1415) => 3',
'INT({column1})'
]
},
RIGHT: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 2
}
},
description: 'n characters from the end of input parameter',
syntax: 'RIGHT(str, n)',
examples: [
'RIGHT("HELLO WORLD", 5) => WORLD',
'RIGHT({column1}, 3)'
]
},
LEFT: { LEFT: {
validation: { args: { rqd: 1 } } type: formulaTypes.STRING,
validation: {
args: {
rqd: 2
}
},
description: 'n characters from the beginning of input parameter',
syntax: 'LEFT(str, n)',
examples: [
'LEFT({column1}, 2)',
'LEFT("ABCD", 2) => "AB"'
]
},
SUBSTR: {
type: formulaTypes.STRING,
validation: {
args: {
min: 2,
max: 3
}
},
description: 'Substring of length n of input string from the postition specified',
syntax: ' SUBTR(str, position, [n])',
examples: [
'SUBSTR("HELLO WORLD", 7) => WORLD',
'SUBSTR("HELLO WORLD", 7, 3) => WOR',
'SUBSTR({column1}, 7, 5)'
]
},
MID: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 3
}
},
description: 'Alias for SUBSTR',
syntax: 'MID(str, position, [count])',
examples: [
'MID("NocoDB", 3, 2) => "co"',
'MID({column1}, 3, 2)'
]
},
IF: {
type: formulaTypes.COND_EXP,
validation: {
args: {
min: 2,
max: 3
}
},
description: 'SuccessCase if expr evaluates to TRUE, elseCase otherwise',
syntax: 'IF(expr, successCase, elseCase)',
examples: [
'IF(5 > 1, "YES", "NO") => "YES"',
'IF({column} > 1, "YES", "NO")'
]
},
SWITCH: {
type: formulaTypes.COND_EXP,
validation: {
args: {
min: 3
}
}, },
SUBSTR: { validation: { args: { min: 2, max: 3 } } }, description: 'Switch case value based on expr output',
MID: { validation: { args: { rqd: 1 } } }, syntax: 'SWITCH(expr, [pattern, value, ..., default])',
IF: { validation: { args: { min: 2, max: 3 } } }, examples: [
SWITCH: { validation: { args: { min: 3 } } }, 'SWITCH(1, 1, "One", 2, "Two", "N/A") => "One""',
URL: { validation: { args: { rqd: 1 } } } 'SWITCH(2, 1, "One", 2, "Two", "N/A") => "Two"',
'SWITCH(3, 1, "One", 2, "Two", "N/A") => "N/A"',
'SWITCH({column1}, 1, "One", 2, "Two", "N/A")'
]
},
URL: {
type: formulaTypes.STRING,
validation: {
args: {
rqd: 1
}
},
description: 'Convert to a hyperlink if it is a valid URL',
syntax: 'URL(str)',
examples: [
'URL("https://github.com/nocodb/nocodb")',
'URL({column1})'
]
}
} }
export default Object.keys(validations) export default Object.keys(formulas)
export { validations } export { formulas, formulaTypes }

18
packages/noco-docs/content/en/setup-and-usages/formulas.md

@ -66,13 +66,13 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| Name | Syntax | Sample | Output | | Name | Syntax | Sample | Output |
|-------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------| |-------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------|
| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT({Column1}, ' ', {Column2})` | Concatenated string of input parameters | | **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT({Column1}, ' ', {Column2})` | Concatenated string of input parameters |
| **LEFT** | `LEFT(str1, [str2,...])` | `LEFT({Column}, 3)` | `n` characters from the beginning of input parameter | | **LEFT** | `LEFT(str1, n)` | `LEFT({Column}, 3)` | `n` characters from the beginning of input parameter |
| **LEN** | `LEN(str)` | `LEN({Column})` | Input parameter character length | | **LEN** | `LEN(str)` | `LEN({Column})` | Input parameter character length |
| **LOWER** | `LOWER(str)` | `LOWER({Column})` | Lower case converted string of input parameter | | **LOWER** | `LOWER(str)` | `LOWER({Column})` | Lower case converted string of input parameter |
| **MID** | `SUBTR(str, position, [count])` | `MID({Column}, 3, 2)` | Alias for `SUBSTR` | | **MID** | `MID(str, position, [count])` | `MID({Column}, 3, 2)` | Alias for `SUBSTR` |
| **REPEAT** | `REPEAT(str, count)` | `REPEAT({Column}, 2)` | Specified copies of the input parameter string concatenated together | | **REPEAT** | `REPEAT(str, count)` | `REPEAT({Column}, 2)` | Specified copies of the input parameter string concatenated together |
| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE({Column}, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` | | **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE({Column}, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` |
| **RIGHT** | `RIGHT(str, count)` | `RIGHT({Column}, 3)` | `n` characters from the end of input parameter | | **RIGHT** | `RIGHT(str, n)` | `RIGHT({Column}, 3)` | `n` characters from the end of input parameter |
| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH({Column}, 'str')` | Index of `srchStr` specified if found, 0 otherwise | | **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH({Column}, 'str')` | Index of `srchStr` specified if found, 0 otherwise |
| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR({Column}, 3, 2)` | Substring of length 'count' of input string, from the postition specified | | **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR({Column}, 3, 2)` | Substring of length 'count' of input string, from the postition specified |
| **TRIM** | `TRIM(str)` | `TRIM({Column})` | Remove trailing and leading whitespaces from input parameter | | **TRIM** | `TRIM(str)` | `TRIM({Column})` | Remove trailing and leading whitespaces from input parameter |
@ -83,11 +83,13 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| Name | Syntax | Sample | Output | Remark | | Name | Syntax | Sample | Output | Remark |
|---|---|---|---|---| |---|---|---|---|---|
| **DATEADD** | `DATEADD({DATE_COL}, 1, 'day')` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | | **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `DATEADD({DATE_COL}, 2, 'month')` | `DATEADD(date, 2, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-05-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -2, 'month')` |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | `IF(NOW() < DATEADD({DATE_COL},10,'day'), "true", "false")` | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
### Logical Operators ### Logical Operators
| Operator | Sample | Description | | Operator | Sample | Description |
@ -104,7 +106,7 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| Name | Syntax | Sample | Output | | Name | Syntax | Sample | Output |
|------------|------------------------------------------------|---------------------------------------------|-------------------------------------------------------------| |------------|------------------------------------------------|---------------------------------------------|-------------------------------------------------------------|
| **IF** | `IF(expr, successCase, [failCase])` | `IF({Column} > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | | **IF** | `IF(expr, successCase, elseCase)` | `IF({Column} > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise |
| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH({Column}, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | | **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH({Column}, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output |
| **AND** | `AND(expr1, [expr2,...])` | `AND({Column} > 2, {Column} < 10)` | TRUE if all `expr` evaluate to TRUE | | **AND** | `AND(expr1, [expr2,...])` | `AND({Column} > 2, {Column} < 10)` | TRUE if all `expr` evaluate to TRUE |
| **OR** | `OR(expr1, [expr2,...])` | `OR({Column} > 2, {Column} < 10)` | TRUE if at least one `expr` evaluates to TRUE | | **OR** | `OR(expr1, [expr2,...])` | `OR({Column} > 2, {Column} < 10)` | TRUE if at least one `expr` evaluates to TRUE |

Loading…
Cancel
Save