diff --git a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue index e5b0056a82..19cd252e8e 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -100,6 +100,7 @@ import jsep from 'jsep'; import { UITypes, jsepCurlyHook } from 'nocodb-sdk'; import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'; import formulaList, { formulas, formulaTypes } from '@/helpers/formulaList'; +import { validateDateWithUnknownFormat } from '@/helpers/dateFormatHelper'; import { getWordUntilCaret, insertAtCursor } from '@/helpers'; import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'; @@ -108,7 +109,6 @@ export default { props: ['nodes', 'column', 'meta', 'isSQLite', 'alias', 'value', 'sqlUi'], data: () => ({ formula: {}, - // formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()'] availableFunctions: formulaList, availableBinOps: ['+', '-', '*', '/', '>', '<', '==', '<=', '>=', '!='], autocomplete: false, @@ -260,7 +260,39 @@ export default { if (parsedTree.callee.type === jsep.IDENTIFIER) { const expectedType = formulas[parsedTree.callee.name].type; if (expectedType === formulaTypes.NUMERIC) { - parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors)); + if (parsedTree.callee.name === 'WEEKDAY') { + // parsedTree.arguments[0] = date + this.validateAgainstType( + parsedTree.arguments[0], + formulaTypes.DATE, + v => { + if (!validateDateWithUnknownFormat(v)) { + typeErrors.add('The first parameter of WEEKDAY() should have date value'); + } + }, + typeErrors + ); + // parsedTree.arguments[1] = startDayOfWeek (optional) + this.validateAgainstType( + parsedTree.arguments[1], + formulaTypes.STRING, + v => { + if ( + typeof v !== 'string' || + !['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'].includes( + v.toLowerCase() + ) + ) { + typeErrors.add( + 'The second parameter of WEEKDAY() should have the value either "sunday", "monday", "tuesday", "wednesday", "thursday", "friday" or "saturday"' + ); + } + }, + typeErrors + ); + } else { + parsedTree.arguments.map(arg => this.validateAgainstType(arg, expectedType, null, typeErrors)); + } } else if (expectedType === formulaTypes.DATE) { if (parsedTree.callee.name === 'DATEADD') { // parsedTree.arguments[0] = date @@ -268,7 +300,7 @@ export default { parsedTree.arguments[0], formulaTypes.DATE, v => { - if (!(v instanceof Date)) { + if (!validateDateWithUnknownFormat(v)) { typeErrors.add('The first parameter of DATEADD() should have date value'); } }, diff --git a/packages/nc-gui/helpers/dateFormatHelper.js b/packages/nc-gui/helpers/dateFormatHelper.js index 032e44f829..4142dc3ea6 100644 --- a/packages/nc-gui/helpers/dateFormatHelper.js +++ b/packages/nc-gui/helpers/dateFormatHelper.js @@ -1,3 +1,7 @@ +import dayjs from 'dayjs'; +const customParseFormat = require('dayjs/plugin/customParseFormat'); +dayjs.extend(customParseFormat); + export const dateFormat = [ 'DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY-MM-DD', 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY/MM/DD', @@ -6,4 +10,12 @@ export const dateFormat = [ export function validateDateFormat(v) { return dateFormat.includes(v) +} + +export function validateDateWithUnknownFormat(v) { + let res = 0; + for (const format of dateFormat) { + res |= dayjs(v.toString(), format, true).isValid(); + } + return res; } \ No newline at end of file diff --git a/packages/nc-gui/helpers/formulaList.js b/packages/nc-gui/helpers/formulaList.js index 044764630c..4364a475fe 100644 --- a/packages/nc-gui/helpers/formulaList.js +++ b/packages/nc-gui/helpers/formulaList.js @@ -471,6 +471,21 @@ const formulas = { 'URL("https://github.com/nocodb/nocodb")', 'URL({column1})' ] + }, + WEEKDAY: { + type: formulaTypes.NUMERIC, + validation: { + args: { + min: 1, + max: 2, + } + }, + description: 'Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default', + syntax: 'WEEKDAY(date, [startDayOfWeek])', + examples: [ + 'WEEKDAY("2021-06-09")', + 'WEEKDAY(NOW(), "sunday")' + ] } } diff --git a/packages/noco-docs/content/en/setup-and-usages/formulas.md b/packages/noco-docs/content/en/setup-and-usages/formulas.md index 53044134a6..be5dbaa939 100644 --- a/packages/noco-docs/content/en/setup-and-usages/formulas.md +++ b/packages/noco-docs/content/en/setup-and-usages/formulas.md @@ -90,6 +90,10 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ )) | | | `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. | +| | | `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. | +| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument | +| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | + ### Logical Operators | Operator | Sample | Description | diff --git a/packages/nocodb-sdk/src/lib/formulaHelpers.ts b/packages/nocodb-sdk/src/lib/formulaHelpers.ts index 6666c097b8..237e1ac4af 100644 --- a/packages/nocodb-sdk/src/lib/formulaHelpers.ts +++ b/packages/nocodb-sdk/src/lib/formulaHelpers.ts @@ -129,6 +129,7 @@ export function jsepTreeToFormula(node) { 'AVG', 'ADD', 'DATEADD', + 'WEEKDAY', 'AND', 'OR', 'CONCAT', diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts index a95f69e6c4..1358a87411 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts @@ -1,5 +1,7 @@ +import dayjs from 'dayjs'; import { MapFnArgs } from '../mapFunctionName'; import commonFns from './commonFns'; +import { getWeekdayByText } from '../helpers/formulaFnHelper'; const mssql = { ...commonFns, @@ -108,6 +110,19 @@ const mssql = { END${colAlias}` ); }, + WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { + // DATEPART(WEEKDAY, DATE): sunday = 1, monday = 2, ..., saturday = 7 + // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday + return knex.raw( + `(DATEPART(WEEKDAY, ${ + pt.arguments[0].type === 'Literal' + ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'` + : fn(pt.arguments[0]) + }) - 2 - ${getWeekdayByText( + pt?.arguments[1]?.value + )} % 7 + 7) % 7 ${colAlias}` + ); + }, }; export default mssql; diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts index 9b34375394..babad78588 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts @@ -1,5 +1,7 @@ +import dayjs from 'dayjs'; import { MapFnArgs } from '../mapFunctionName'; import commonFns from './commonFns'; +import { getWeekdayByText } from '../helpers/formulaFnHelper'; const mysql2 = { ...commonFns, @@ -55,6 +57,18 @@ const mysql2 = { END${colAlias}` ); }, + WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { + // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday + return knex.raw( + `(WEEKDAY(${ + pt.arguments[0].type === 'Literal' + ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'` + : fn(pt.arguments[0]) + }) - ${getWeekdayByText( + pt?.arguments[1]?.value + )} % 7 + 7) % 7 ${colAlias}` + ); + }, }; export default mysql2; diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts index 21d8ff53ea..4e34629bca 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts @@ -1,5 +1,7 @@ +import dayjs from 'dayjs'; import { MapFnArgs } from '../mapFunctionName'; import commonFns from './commonFns'; +import { getWeekdayByText } from '../helpers/formulaFnHelper'; const pg = { ...commonFns, @@ -42,6 +44,19 @@ const pg = { )}')::interval${colAlias}` ); }, + WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { + // isodow: the day of the week as Monday (1) to Sunday (7) + // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday + return knex.raw( + `(EXTRACT(ISODOW FROM ${ + pt.arguments[0].type === 'Literal' + ? `date '${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'` + : fn(pt.arguments[0]) + }) - 1 - ${getWeekdayByText( + pt?.arguments[1]?.value + )} % 7 + 7) % 7 ${colAlias}` + ); + }, }; export default pg; diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts index e5ba36ed32..4a12f5ee4b 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts @@ -1,5 +1,7 @@ +import dayjs from 'dayjs'; import { MapFnArgs } from '../mapFunctionName'; import commonFns from './commonFns'; +import { getWeekdayByText } from '../helpers/formulaFnHelper'; const sqlite3 = { ...commonFns, @@ -75,6 +77,20 @@ const sqlite3 = { END${colAlias}` ); }, + + WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { + // strftime('%w', date) - day of week 0 - 6 with Sunday == 0 + // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday + return knex.raw( + `(strftime('%w', ${ + pt.arguments[0].type === 'Literal' + ? `'${dayjs(fn(pt.arguments[0])).format('YYYY-MM-DD')}'` + : fn(pt.arguments[0]) + }) - 1 - ${getWeekdayByText( + pt?.arguments[1]?.value + )} % 7 + 7) % 7 ${colAlias}` + ); + }, }; export default sqlite3; diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts new file mode 100644 index 0000000000..f8057a2473 --- /dev/null +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts @@ -0,0 +1,23 @@ +export function getWeekdayByText(v: string) { + return { + monday: 0, + tuesday: 1, + wednesday: 2, + thursday: 3, + friday: 4, + saturday: 5, + sunday: 6, + }[v?.toLowerCase() || 'monday']; +} + +export function getWeekdayByIndex(idx: number): string { + return { + 0: 'monday', + 1: 'tuesday', + 2: 'wednesday', + 3: 'thursday', + 4: 'friday', + 5: 'saturday', + 6: 'sunday', + }[idx || 0]; +} diff --git a/scripts/cypress/integration/common/3b_formula_column.js b/scripts/cypress/integration/common/3b_formula_column.js index 9fba2ab366..d8251535a7 100644 --- a/scripts/cypress/integration/common/3b_formula_column.js +++ b/scripts/cypress/integration/common/3b_formula_column.js @@ -153,6 +153,8 @@ export const genTest = (apiType, dbType) => { let RESULT_MATH_0 = []; let RESULT_MATH_1 = []; let RESULT_MATH_2 = []; + let RESULT_WEEKDAY_0 = []; + let RESULT_WEEKDAY_1 = []; for (let i = 0; i < 10; i++) { // CONCAT, LOWER, UPPER, TRIM @@ -184,6 +186,11 @@ export const genTest = (apiType, dbType) => { Math.pow(cityId[i], 3) + Math.sqrt(countryId[i]) ); + + // WEEKDAY: starts from Monday + RESULT_WEEKDAY_0[i] = 1; + // WEEKDAY: starts from Sunday + RESULT_WEEKDAY_1[i] = 2; } it("Formula: ADD, AVG, LEN", () => { @@ -194,9 +201,25 @@ export const genTest = (apiType, dbType) => { rowValidation("NC_MATH_0", RESULT_MATH_0); }); - it("Formula: CONCAT, LOWER, UPPER, TRIM", () => { + it("Formula: WEEKDAY", () => { editColumnByName( "NC_MATH_0", + "NC_WEEKDAY_0", + `WEEKDAY("2022-07-19")` + ); + rowValidation("NC_WEEKDAY_0", RESULT_WEEKDAY_0); + + editColumnByName( + "NC_WEEKDAY_0", + "NC_WEEKDAY_1", + `WEEKDAY("2022-07-19", "sunday")` + ); + rowValidation("NC_WEEKDAY_1", RESULT_WEEKDAY_1); + }); + + it("Formula: CONCAT, LOWER, UPPER, TRIM", () => { + editColumnByName( + "NC_WEEKDAY_1", "NC_STR_1", `CONCAT(UPPER({City}), LOWER({City}), TRIM(' trimmed '))` );