diff --git a/packages/engine-formula/src/engine/utils/check-variant-error.ts b/packages/engine-formula/src/engine/utils/check-variant-error.ts index dac96f7c2a5..45ead3a35ec 100644 --- a/packages/engine-formula/src/engine/utils/check-variant-error.ts +++ b/packages/engine-formula/src/engine/utils/check-variant-error.ts @@ -39,6 +39,26 @@ export function checkVariantErrorIsArray(variant: BaseValueObject): BaseValueObj return _variant; } +export function checkVariantsErrorIsArray(...variants: BaseValueObject[]) { + for (let i = 0; i < variants.length; i++) { + const variant = checkVariantErrorIsArray(variants[i]); + + if (variant.isError()) { + return { + isError: true, + errorObject: variant, + }; + } + + variants[i] = variant; + } + + return { + isError: false, + variants, + }; +} + export function checkVariantsErrorIsArrayOrBoolean(...variants: BaseValueObject[]) { for (let i = 0; i < variants.length; i++) { const variant = checkVariantErrorIsArray(variants[i]); diff --git a/packages/engine-formula/src/engine/utils/regexp-check.ts b/packages/engine-formula/src/engine/utils/regexp-check.ts new file mode 100644 index 00000000000..3926de60535 --- /dev/null +++ b/packages/engine-formula/src/engine/utils/regexp-check.ts @@ -0,0 +1,737 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum CharTypes { + ROOT, + GROUP, + POSITION, + SET, + RANGE, + REPETITION, + REFERENCE, + CHAR, +}; + +type CharRange = { + type: CharTypes.RANGE; + from: number; + to: number; +} | { + type: CharTypes.CHAR; + value: number; +}; + +interface ICharSet { + type: CharTypes.SET; + set: CharRange[]; + not: boolean; +} + +interface IToken { + type?: CharTypes; + value?: number | string | IToken; + set?: Array; + stack?: IToken[]; + options?: IToken[][]; + remember?: boolean; + followedBy?: boolean; + notFollowedBy?: boolean; + not?: boolean; + min?: number; + max?: number; +} + +interface IReference { + reference: IToken; + stack: IToken[]; + index: number; +} + +interface IOptions { + limit?: number; +} + +// 定义数字字符集(0-9) +const numberRange = (): CharRange[] => [ + { + type: CharTypes.RANGE, + from: 48, + to: 57, + }, +]; + +// 定义字母和数字字符集(包含下划线、a-z、A-Z、0-9) +const alphaNumericChars = (): CharRange[] => [ + { + type: CharTypes.CHAR, + value: 95, + }, + { + type: CharTypes.RANGE, + from: 97, + to: 122, + }, + { + type: CharTypes.RANGE, + from: 65, + to: 90, + }, + { + type: CharTypes.RANGE, + from: 48, + to: 57, + }, +]; + +// 定义空白字符集 +const whitespaceChars = (): CharRange[] => [ + { + type: CharTypes.CHAR, + value: 9, + }, + { + type: CharTypes.CHAR, + value: 10, + }, + { + type: CharTypes.CHAR, + value: 11, + }, + { + type: CharTypes.CHAR, + value: 12, + }, + { + type: CharTypes.CHAR, + value: 13, + }, + { + type: CharTypes.CHAR, + value: 32, + }, + { + type: CharTypes.CHAR, + value: 160, + }, + { + type: CharTypes.CHAR, + value: 5760, + }, + { + type: CharTypes.RANGE, + from: 8192, + to: 8202, + }, + { + type: CharTypes.CHAR, + value: 8232, + }, + { + type: CharTypes.CHAR, + value: 8233, + }, + { + type: CharTypes.CHAR, + value: 8239, + }, + { + type: CharTypes.CHAR, + value: 8287, + }, + { + type: CharTypes.CHAR, + value: 12288, + }, + { + type: CharTypes.CHAR, + value: 65279, + }, +]; + +// 定义 words 函数,返回字母数字字符集的 SET 类型 +const words = (): ICharSet => ({ + type: CharTypes.SET, + set: alphaNumericChars(), + not: false, +}); + +// 定义 notWords 函数,返回字母数字字符集的 SET 类型,但取反 +const notWords = (): ICharSet => ({ + type: CharTypes.SET, + set: alphaNumericChars(), + not: true, +}); + +// 定义 ints 函数,返回数字字符集的 SET 类型 +const ints = (): ICharSet => ({ + type: CharTypes.SET, + set: numberRange(), + not: false, +}); + +// 定义 notInts 函数,返回数字字符集的 SET 类型,但取反 +const notInts = (): ICharSet => ({ + type: CharTypes.SET, + set: numberRange(), + not: true, +}); + +// 定义 whitespace 函数,返回空白字符集的 SET 类型 +const whitespace = (): ICharSet => ({ + type: CharTypes.SET, + set: whitespaceChars(), + not: false, +}); + +// 定义 notWhitespace 函数,返回空白字符集的 SET 类型,但取反 +const notWhitespace = (): ICharSet => ({ + type: CharTypes.SET, + set: whitespaceChars(), + not: true, +}); + +// 定义 anyChar 函数,返回特定字符集的 SET 类型,但取反 +const anyChar = (): ICharSet => ({ + type: CharTypes.SET, + set: [ + { + type: CharTypes.CHAR, + value: 10, + }, + { + type: CharTypes.CHAR, + value: 13, + }, + { + type: CharTypes.CHAR, + value: 8232, + }, + { + type: CharTypes.CHAR, + value: 8233, + }, + ], + not: true, +}); + +// eslint-disable-next-line +function tokenizer(regExpString: string) { + const root: IToken = { + type: CharTypes.ROOT, + stack: [], + }; + let currentScope: IToken = root; + let stack: IToken[] = root.stack as IToken[]; + const groups: IToken[] = []; + const references: IReference[] = []; + let captureCount = 0; + + const throwError = (index: number) => { + throw new SyntaxError(`Invalid regular expression: /${regExpString}/: Nothing to repeat at column ${index - 1}`); + }; + + const strChars = strToChars(regExpString); + + let i = 0; + let char; + + while (i < strChars.length) { + char = strChars[i++]; + + switch (char) { + case '\\': + if (i === strChars.length) { + throw new SyntaxError(`Invalid regular expression: /${regExpString}/: \\ at end of pattern`); + } + + char = strChars[i++]; + + switch (char) { + case 'b': + stack.push({ + type: CharTypes.POSITION, + value: 'b', + }); + break; + case 'B': + stack.push({ + type: CharTypes.POSITION, + value: 'B', + }); + break; + case 'w': + stack.push(words()); + break; + case 'W': + stack.push(notWords()); + break; + case 'd': + stack.push(ints()); + break; + case 'D': + stack.push(notInts()); + break; + case 's': + stack.push(whitespace()); + break; + case 'S': + stack.push(notWhitespace()); + break; + default: + if (/\d/.test(char)) { + while (/\d/.test(strChars[i]) && i < strChars.length) { + char += strChars[i++]; + } + + const num = Number.parseInt(char, 10); + + stack.push({ + type: CharTypes.REFERENCE, + value: num, + }); + + references.push({ + reference: { + type: CharTypes.REFERENCE, + value: num, + }, + stack, + index: stack.length - 1, + }); + } else { + stack.push({ + type: CharTypes.CHAR, + value: char.charCodeAt(0), + }); + } + } + + break; + case '^': + stack.push({ + type: CharTypes.POSITION, + value: '^', + }); + break; + case '$': + stack.push({ + type: CharTypes.POSITION, + value: '$', + }); + break; + case '[': + { + const negated = (strChars[i] === '^'); // 检查是否是否定字符类(例如 `[^a-z]`) + if (negated) { + i++; // 如果是否定的,则跳过 "^" 字符 + } + + // 提取字符类的内容,并计算结束位置 + const classTokens = tokenizeClass(strChars.slice(i), regExpString); + + // 更新读取位置 + i += classTokens[1]; + + // 将解析后的字符类添加到结果数组中 + stack.push({ + type: CharTypes.SET, + set: classTokens[0], + not: negated, + }); + + break; + } + case '.': + stack.push(anyChar()); + break; + case '(': + { + // 初始化一个对象来表示捕获组 + const group: IToken = { + type: CharTypes.GROUP, + stack: [], + remember: true, // 默认情况下,捕获组是需要记住(捕获)的 + }; + + // 检查是否存在 '?' 来指定组的特殊行为 + if (strChars[i] === '?') { + const nextChar = strChars[i + 1]; // 获取 '?' 后面的字符 + i += 2; // 跳过 '?' 和后面的字符 + + // 根据后面的字符确定组的类型 + if (nextChar === '=') { + group.followedBy = true; // 正向前瞻 + } else if (nextChar === '!') { + group.notFollowedBy = true; // 负向前瞻 + } else if (nextChar !== ':') { + throw new SyntaxError(`Invalid regular expression: /${regExpString}/: Invalid group, character '${nextChar}' after '?' at column ${i - 1}`); + } + + group.remember = false; + } else { + captureCount += 1; // 增加捕获组的计数 + } + + // 将捕获组对象添加到主栈中 + stack.push(group); + + // 保存当前状态并开始新的栈用于捕获组 + groups.push(currentScope); + currentScope = group; + stack = group.stack as IToken[]; + + break; + } + case ')': + if (groups.length === 0) { + throw new SyntaxError(`Invalid regular expression: /${regExpString}/: Unmatched ) at column ${i - 1}`); + } + + currentScope = groups.pop()!; + stack = (currentScope.options ? currentScope.options[currentScope.options.length - 1] : currentScope.stack) as IToken[]; + break; + case '|': + { + // 如果当前作用域没有 'options' 属性,则初始化它 + if (!currentScope.options) { + currentScope.options = [currentScope.stack] as IToken[][]; + delete currentScope.stack; + } + // 创建新的选项分支并添加到 'options' + const newOption: IToken[] = []; + currentScope.options.push(newOption); + // 更新当前的解析栈为新的选项分支 + stack = newOption; + break; + } + case '{': + { + const match = /^(\d+)(,(\d+)?)?\}/.exec(strChars.slice(i)); + + if (match) { + if (stack.length === 0) { + throwError(i); + } + + const min = Number.parseInt(match[1], 10); + const max = match[2] ? (match[3] ? Number.parseInt(match[3], 10) : Infinity) : min; + i += match[0].length; + + stack.push({ + type: CharTypes.REPETITION, + min, + max, + value: stack.pop()!, + }); + } else { + stack.push({ // Assuming 123 is the character code for '{' + type: CharTypes.CHAR, + value: 123, + }); + } + + break; + } + case '?': + if (stack.length === 0) { + throwError(i); + } + + stack.push({ + type: CharTypes.REPETITION, + min: 0, + max: 1, + value: stack.pop()!, + }); + break; + case '+': + if (stack.length === 0) { + throwError(i); + } + + stack.push({ + type: CharTypes.REPETITION, + min: 1, + max: Infinity, + value: stack.pop()!, + }); + break; + case '*': + if (stack.length === 0) { + throwError(i); + } + + stack.push({ + type: CharTypes.REPETITION, + min: 0, + max: Infinity, + value: stack.pop()!, + }); + break; + default: + stack.push({ + type: CharTypes.CHAR, + value: char.charCodeAt(0), + }); + } + } + + if (groups.length > 0) { + throw new SyntaxError(`Invalid regular expression: /${regExpString}/: Unterminated group`); + } + + processReferences(references, captureCount); + + return root; +} + +function tokenizeClass(input: string, pattern: string): [Array, number] { + let match; + const tokens: Array = []; + const regex = /\\(?:(w)|(d)|(s)|(W)|(D)|(S))|((?:(?:\\)(.)|([^\]\\]))-(((?:\\)])|(((?:\\)?([^\]])))))|(\])|(?:\\)?([^])/g; + + // eslint-disable-next-line + while ((match = regex.exec(input)) !== null) { + let token: CharRange | ICharSet | null = null; + const [, _words, _digit, _space, _notWords, _notDigit, _notSpace, range, rangeFrom, rangeTo, char] = match; + + if (_words || _digit || _space || _notWords || _notDigit || _notSpace) { + token = ((): ICharSet => { + let result: ICharSet = { + type: CharTypes.SET, + set: [], + not: false, + }; + + if (_words) { // \w + result = words(); + } else if (_digit) { // \d + result = ints(); + } else if (_space) { // \s + result = whitespace(); + } else if (_notWords) { // \W + result = notWords(); + } else if (_notDigit) { // \D + result = notInts(); + } else if (_notSpace) { // \S + result = notWhitespace(); + } + + return result; + })(); + } else if (range && char) { + token = { + type: CharTypes.RANGE, + from: (rangeFrom || rangeTo).charCodeAt(0), + to: char.charCodeAt(char.length - 1), + }; + } else if (match[16]) { + token = { + type: CharTypes.CHAR, + value: match[16].charCodeAt(0), + }; + } + + if (!token) { + return [tokens, regex.lastIndex]; + } + + tokens.push(token as CharRange | ICharSet); + } + + throw new SyntaxError(`Invalid regular expression: /${pattern}/: Unterminated character class`); +}; + +function strToChars(str: string): string { + return str.replace( + /(\[\\b\])|(\\)?\\(?:u([A-F0-9]{4})|x([A-F0-9]{2})|c([@A-Z[\\\]^?])|([0tnvfr]))/g, + (match, backspace, escapeChar, unicode, hex, ctrl, special) => { + if (escapeChar) { + return match; + } + + let charCode; + + if (backspace) { + charCode = 8; + } else if (unicode) { + charCode = Number.parseInt(unicode, 16); + } else if (hex) { + charCode = Number.parseInt(hex, 16); + } else if (ctrl) { + charCode = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^ ?'.indexOf(ctrl); + } else { + const specialMode: { [key: string]: number } = { + 0: 0, + t: 9, + n: 10, + v: 11, + f: 12, + r: 13, + }; + charCode = specialMode[special]; + } + + const char = String.fromCharCode(charCode); + + return /[[\]{}^$.|?*+()]/.test(char) ? `\\${char}` : char; + } + ); +}; + +function processReferences(references: IReference[], captureCount: number): void { + for (const ref of references.reverse()) { + const value = ref.reference.value! as number; + + if (captureCount < value) { + ref.reference.type = CharTypes.CHAR; + const valueStr = value.toString(); + ref.reference.value = Number.parseInt(valueStr, 8); + + if (!/^[0-7]+$/.test(valueStr)) { + let startIndex = 0; + + while (valueStr[startIndex] !== '8' && valueStr[startIndex] !== '9' && startIndex < valueStr.length) { + startIndex += 1; + } + + if (startIndex === 0) { + ref.reference.value = valueStr.charCodeAt(0); + startIndex += 1; + } else { + ref.reference.value = Number.parseInt(valueStr.slice(0, startIndex), 8); + } + + if (valueStr.length > startIndex) { + const remainingStack = ref.stack.splice(ref.index + 1); + + for (const char of valueStr.slice(startIndex)) { + ref.stack.push({ + type: CharTypes.CHAR, + value: char.charCodeAt(0), + }); + } + + ref.stack.push(...remainingStack); + } + } + } + } +}; + +export function handleRegExp(regExpString: string, isGlobal: boolean) { + if (!isValidRegExp(regExpString)) { + return { + isError: true, + regExp: null, + }; + } + + try { + const regExp = new RegExp(regExpString, isGlobal ? 'ug' : 'u'); + + if (!isSafeRegExp(regExp)) { + return { + isError: true, + regExp: null, + }; + } + + return { + isError: false, + regExp, + }; + } catch (error) { // eslint-disable-line + return { + isError: true, + regExp: null, + }; + } +} + +function isValidRegExp(regExpString: string): boolean { + return !( + (/\(\?<=.*?\)/g.test(regExpString) && !/\[.*?(\?<=.*?)\]/g.test(regExpString)) || + (/\(\? { + let _depth = depth; + + if (token.type === CharTypes.REPETITION) { + _depth++; + count++; + + if (_depth > 1) { + return false; + } + + if (count > limit) { + return false; + } + } + + if (token.options) { + for (const option of token.options) { + const result = validateToken({ stack: option }, _depth); + + if (!result) { + return false; + } + } + } + + const stack = token.stack || (token.value && (token.value as IToken).stack); + + if (!stack) { + return true; + } + + for (const item of stack) { + const result = validateToken(item, _depth); + + if (!result) { + return false; + } + } + + return true; + }; + + return validateToken(tokens, 0); +} diff --git a/packages/engine-formula/src/functions/text/function-map.ts b/packages/engine-formula/src/functions/text/function-map.ts index d5d0e33f287..bd1f549037b 100644 --- a/packages/engine-formula/src/functions/text/function-map.ts +++ b/packages/engine-formula/src/functions/text/function-map.ts @@ -22,6 +22,9 @@ import { Len } from './len'; import { Lenb } from './lenb'; import { Lower } from './lower'; import { Mid } from './mid'; +import { Regexextract } from './regexextract'; +import { Regexmatch } from './regexmatch'; +import { Regexreplace } from './regexreplace'; import { Rept } from './rept'; import { Text } from './text'; import { Textafter } from './textafter'; @@ -35,6 +38,9 @@ export const functionText = [ [Lenb, FUNCTION_NAMES_TEXT.LENB], [Lower, FUNCTION_NAMES_TEXT.LOWER], [Mid, FUNCTION_NAMES_TEXT.MID], + [Regexextract, FUNCTION_NAMES_TEXT.REGEXEXTRACT], + [Regexmatch, FUNCTION_NAMES_TEXT.REGEXMATCH], + [Regexreplace, FUNCTION_NAMES_TEXT.REGEXREPLACE], [Rept, FUNCTION_NAMES_TEXT.REPT], [Text, FUNCTION_NAMES_TEXT.TEXT], [Textafter, FUNCTION_NAMES_TEXT.TEXTAFTER], diff --git a/packages/engine-formula/src/functions/text/function-names.ts b/packages/engine-formula/src/functions/text/function-names.ts index d5d878278e6..e42405c0dcc 100644 --- a/packages/engine-formula/src/functions/text/function-names.ts +++ b/packages/engine-formula/src/functions/text/function-names.ts @@ -39,6 +39,9 @@ export enum FUNCTION_NAMES_TEXT { NUMBERVALUE = 'NUMBERVALUE', PHONETIC = 'PHONETIC', PROPER = 'PROPER', + REGEXEXTRACT = 'REGEXEXTRACT', + REGEXMATCH = 'REGEXMATCH', + REGEXREPLACE = 'REGEXREPLACE', REPLACE = 'REPLACE', REPLACEB = 'REPLACEB', REPT = 'REPT', diff --git a/packages/engine-formula/src/functions/text/regexextract/__test__/index.spec.ts b/packages/engine-formula/src/functions/text/regexextract/__test__/index.spec.ts new file mode 100644 index 00000000000..794e45500f6 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexextract/__test__/index.spec.ts @@ -0,0 +1,142 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { ErrorType } from '../../../../basics/error-type'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; +import { BooleanValueObject, NullValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { getObjectValue } from '../../../__tests__/create-function-test-bed'; +import { FUNCTION_NAMES_TEXT } from '../../function-names'; +import { Regexextract } from '../index'; + +describe('Test regexextract function', () => { + const testFunction = new Regexextract(FUNCTION_NAMES_TEXT.REGEXEXTRACT); + + describe('Regexextract', () => { + it('Value is normal', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual('cdef'); + }); + + it('RegularExpression is maybe backtrace', () => { + const text = StringValueObject.create('https://www.example.com'); + const regularExpression = StringValueObject.create('^(https?://)?([a-z0-9.-]+).*'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + }); + + it('Value is boolean', () => { + const text = BooleanValueObject.create(true); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.NA); + + const text2 = StringValueObject.create('abcdefg'); + const regularExpression2 = BooleanValueObject.create(true); + const result2 = testFunction.calculate(text2, regularExpression2); + expect(getObjectValue(result2)).toStrictEqual(ErrorType.NA); + }); + + it('Value is blank cell', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = NullValueObject.create(); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(''); + + const text2 = NullValueObject.create(); + const regularExpression2 = StringValueObject.create('c.*f'); + const result2 = testFunction.calculate(text2, regularExpression2); + expect(getObjectValue(result2)).toStrictEqual(ErrorType.NA); + }); + + it('Value is error', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = ErrorValueObject.create(ErrorType.NAME); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.NAME); + }); + + it('Value is array', () => { + const text = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, true, false, null, '***TRUETRUEFALSE'], + ]), + rowCount: 1, + columnCount: 5, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.VALUE); + }); + + it('Result length > 1', () => { + let text = StringValueObject.create('111aaa111'); + let regularExpression = StringValueObject.create('(\\d+)([a-z]+)(\\d+)'); + let result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual([ + ['111', 'aaa', '111'], + ]); + + text = StringValueObject.create('abcd'); + regularExpression = StringValueObject.create('((((a)b)c)d)'); + result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual([ + ['abcd', 'abc', 'ab', 'a'], + ]); + }); + + it('Is not valid or safe RegExp', () => { + let text = StringValueObject.create(''); + let regularExpression = StringValueObject.create('[a-Z]+'); + let result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + + text = StringValueObject.create('abcd'); + regularExpression = StringValueObject.create('((((a'); + result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + }); + + it('More test', () => { + let text = StringValueObject.create('我的生日20220104'); + let regularExpression = StringValueObject.create('生日(2022\\d+)'); + let result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual('20220104'); + + regularExpression = StringValueObject.create('生日(?:2022)(\\d+)'); + result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual('0104'); + + text = StringValueObject.create('The total is 123.45 or € 987.65'); + regularExpression = StringValueObject.create('[€]\d{1,3}(,\d{3})*(.\d{2})?'); + result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + + text = StringValueObject.create('Visit our website at https://www.example.com'); + regularExpression = StringValueObject.create('https?://[^\s]+'); + result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual('https://www.example.com'); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/text/regexextract/index.ts b/packages/engine-formula/src/functions/text/regexextract/index.ts new file mode 100644 index 00000000000..311c9f13ee9 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexextract/index.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorType } from '../../../basics/error-type'; +import { checkVariantsErrorIsArray } from '../../../engine/utils/check-variant-error'; +import { handleRegExp } from '../../../engine/utils/regexp-check'; +import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import { type BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { StringValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; + +export class Regexextract extends BaseFunction { + override minParams = 2; + + override maxParams = 2; + + override calculate(text: BaseValueObject, regularExpression: BaseValueObject): BaseValueObject { + const { isError, errorObject, variants } = checkVariantsErrorIsArray(text, regularExpression); + + if (isError) { + return errorObject as ErrorValueObject; + } + + const [textObject, regularExpressionObject] = variants as BaseValueObject[]; + + let textValue = textObject.getValue(); + + if (textObject.isNull()) { + textValue = ''; + } + + if (textObject.isBoolean()) { + textValue = textValue ? 'TRUE' : 'FALSE'; + } + + textValue = `${textValue}`; + + let regularExpressionValue = regularExpressionObject.getValue(); + + if (regularExpressionObject.isNull()) { + regularExpressionValue = ''; + } + + if (regularExpressionObject.isBoolean()) { + regularExpressionValue = regularExpressionValue ? 'TRUE' : 'FALSE'; + } + + regularExpressionValue = `${regularExpressionValue}`; + + const { isError: isError_regExp, regExp } = handleRegExp(regularExpressionValue, false); + + if (isError_regExp) { + return ErrorValueObject.create(ErrorType.REF); + } + + const result = textValue.match(regExp as RegExp); + + if (result === null) { + return ErrorValueObject.create(ErrorType.NA); + } + + if (result.length > 1) { + const resultArray = result.slice(1).map((item) => StringValueObject.create(item)); + + if (resultArray.length > 1) { + return ArrayValueObject.create({ + calculateValueList: [resultArray], + rowCount: 1, + columnCount: resultArray.length, + unitId: this.unitId as string, + sheetId: this.subUnitId as string, + row: this.row, + column: this.column, + }); + } + + return resultArray[0]; + } + + return StringValueObject.create(result[0]); + } +} diff --git a/packages/engine-formula/src/functions/text/regexmatch/__test__/index.spec.ts b/packages/engine-formula/src/functions/text/regexmatch/__test__/index.spec.ts new file mode 100644 index 00000000000..d4114ab32e7 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexmatch/__test__/index.spec.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { ErrorType } from '../../../../basics/error-type'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; +import { BooleanValueObject, NullValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { getObjectValue } from '../../../__tests__/create-function-test-bed'; +import { FUNCTION_NAMES_TEXT } from '../../function-names'; +import { Regexmatch } from '../index'; + +describe('Test regexmatch function', () => { + const testFunction = new Regexmatch(FUNCTION_NAMES_TEXT.REGEXMATCH); + + describe('Regexmatch', () => { + it('Value is normal', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(true); + }); + + it('RegularExpression is maybe backtrace', () => { + const text = StringValueObject.create('https://www.example.com'); + const regularExpression = StringValueObject.create('^(https?://)?([a-z0-9.-]+).*'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + }); + + it('Value is boolean', () => { + const text = BooleanValueObject.create(true); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(false); + + const text2 = StringValueObject.create('abcdefg'); + const regularExpression2 = BooleanValueObject.create(true); + const result2 = testFunction.calculate(text2, regularExpression2); + expect(getObjectValue(result2)).toStrictEqual(false); + }); + + it('Value is blank cell', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = NullValueObject.create(); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(true); + + const text2 = NullValueObject.create(); + const regularExpression2 = StringValueObject.create('c.*f'); + const result2 = testFunction.calculate(text2, regularExpression2); + expect(getObjectValue(result2)).toStrictEqual(false); + }); + + it('Value is error', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = ErrorValueObject.create(ErrorType.NAME); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.NAME); + }); + + it('Value is array', () => { + const text = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, true, false, null, '***TRUETRUEFALSE'], + ]), + rowCount: 1, + columnCount: 5, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const regularExpression = StringValueObject.create('c.*f'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(ErrorType.VALUE); + }); + + it('More test', () => { + const text = StringValueObject.create('13912345678'); + const regularExpression = StringValueObject.create('^1[3456789]\\d{9}$'); + const result = testFunction.calculate(text, regularExpression); + expect(getObjectValue(result)).toStrictEqual(true); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/text/regexmatch/index.ts b/packages/engine-formula/src/functions/text/regexmatch/index.ts new file mode 100644 index 00000000000..b0d588a8784 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexmatch/index.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorType } from '../../../basics/error-type'; +import { checkVariantsErrorIsArray } from '../../../engine/utils/check-variant-error'; +import { handleRegExp } from '../../../engine/utils/regexp-check'; +import type { BaseValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { BooleanValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; + +export class Regexmatch extends BaseFunction { + override minParams = 2; + + override maxParams = 2; + + override calculate(text: BaseValueObject, regularExpression: BaseValueObject): BaseValueObject { + const { isError, errorObject, variants } = checkVariantsErrorIsArray(text, regularExpression); + + if (isError) { + return errorObject as ErrorValueObject; + } + + const [textObject, regularExpressionObject] = variants as BaseValueObject[]; + + let textValue = textObject.getValue(); + + if (textObject.isNull()) { + textValue = ''; + } + + if (textObject.isBoolean()) { + textValue = textValue ? 'TRUE' : 'FALSE'; + } + + textValue = `${textValue}`; + + let regularExpressionValue = regularExpressionObject.getValue(); + + if (regularExpressionObject.isNull()) { + regularExpressionValue = ''; + } + + if (regularExpressionObject.isBoolean()) { + regularExpressionValue = regularExpressionValue ? 'TRUE' : 'FALSE'; + } + + regularExpressionValue = `${regularExpressionValue}`; + + const { isError: isError_regExp, regExp } = handleRegExp(regularExpressionValue, false); + + if (isError_regExp) { + return ErrorValueObject.create(ErrorType.REF); + } + + const result = textValue.match(regExp as RegExp); + + if (result === null) { + return BooleanValueObject.create(false); + } + + return BooleanValueObject.create(true); + } +} diff --git a/packages/engine-formula/src/functions/text/regexreplace/__test__/index.spec.ts b/packages/engine-formula/src/functions/text/regexreplace/__test__/index.spec.ts new file mode 100644 index 00000000000..6c48babcfc9 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexreplace/__test__/index.spec.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { ErrorType } from '../../../../basics/error-type'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; +import { BooleanValueObject, NullValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { getObjectValue } from '../../../__tests__/create-function-test-bed'; +import { FUNCTION_NAMES_TEXT } from '../../function-names'; +import { Regexreplace } from '../index'; + +describe('Test regexreplace function', () => { + const testFunction = new Regexreplace(FUNCTION_NAMES_TEXT.REGEXREPLACE); + + describe('Regexreplace', () => { + it('Value is normal', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = StringValueObject.create('c.*f'); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual('abxyzg'); + }); + + it('RegularExpression is maybe backtrace', () => { + const text = StringValueObject.create('https://www.example.com'); + const regularExpression = StringValueObject.create('^(https?://)?([a-z0-9.-]+).*'); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual(ErrorType.REF); + }); + + it('Value is boolean', () => { + const text = BooleanValueObject.create(true); + const regularExpression = StringValueObject.create('c.*f'); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual('TRUE'); + + const text2 = StringValueObject.create('abcdefg'); + const regularExpression2 = BooleanValueObject.create(true); + const result2 = testFunction.calculate(text2, regularExpression2, replacement); + expect(getObjectValue(result2)).toStrictEqual('abcdefg'); + + const replacement2 = BooleanValueObject.create(true); + const result3 = testFunction.calculate(text2, regularExpression, replacement2); + expect(getObjectValue(result3)).toStrictEqual('abTRUEg'); + }); + + it('Value is blank cell', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = NullValueObject.create(); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual('xyzaxyzbxyzcxyzdxyzexyzfxyzgxyz'); + + const text2 = NullValueObject.create(); + const regularExpression2 = StringValueObject.create('c.*f'); + const result2 = testFunction.calculate(text2, regularExpression2, replacement); + expect(getObjectValue(result2)).toStrictEqual(''); + + const replacement2 = NullValueObject.create(); + const result3 = testFunction.calculate(text, regularExpression2, replacement2); + expect(getObjectValue(result3)).toStrictEqual('abg'); + }); + + it('Value is error', () => { + const text = StringValueObject.create('abcdefg'); + const regularExpression = ErrorValueObject.create(ErrorType.NAME); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual(ErrorType.NAME); + }); + + it('Value is array', () => { + const text = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, true, false, null, '***TRUETRUEFALSE'], + ]), + rowCount: 1, + columnCount: 5, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const regularExpression = StringValueObject.create('c.*f'); + const replacement = StringValueObject.create('xyz'); + const result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual(ErrorType.VALUE); + }); + + it('More test', () => { + let text = StringValueObject.create('Sheets'); + let regularExpression = StringValueObject.create('e{2}'); + let replacement = StringValueObject.create('X'); + let result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual('ShXts'); + + text = StringValueObject.create('The event will be held on June 15, 2024.'); + regularExpression = StringValueObject.create('\\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\s+\\d{1,2},\\s+\\d{4}\\b'); + replacement = StringValueObject.create('[REDACTED]'); + result = testFunction.calculate(text, regularExpression, replacement); + expect(getObjectValue(result)).toStrictEqual('The event will be held on [REDACTED].'); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/text/regexreplace/index.ts b/packages/engine-formula/src/functions/text/regexreplace/index.ts new file mode 100644 index 00000000000..dfeeb6e0da5 --- /dev/null +++ b/packages/engine-formula/src/functions/text/regexreplace/index.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorType } from '../../../basics/error-type'; +import { checkVariantsErrorIsArray } from '../../../engine/utils/check-variant-error'; +import { handleRegExp } from '../../../engine/utils/regexp-check'; +import type { BaseValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { StringValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; + +export class Regexreplace extends BaseFunction { + override minParams = 3; + + override maxParams = 3; + + override calculate(text: BaseValueObject, regularExpression: BaseValueObject, replacement: BaseValueObject): BaseValueObject { + const { isError, errorObject, variants } = checkVariantsErrorIsArray(text, regularExpression, replacement); + + if (isError) { + return errorObject as ErrorValueObject; + } + + const [textObject, regularExpressionObject, replacementObject] = variants as BaseValueObject[]; + + let textValue = textObject.getValue(); + + if (textObject.isNull()) { + textValue = ''; + } + + if (textObject.isBoolean()) { + textValue = textValue ? 'TRUE' : 'FALSE'; + } + + textValue = `${textValue}`; + + let regularExpressionValue = regularExpressionObject.getValue(); + + if (regularExpressionObject.isNull()) { + regularExpressionValue = ''; + } + + if (regularExpressionObject.isBoolean()) { + regularExpressionValue = regularExpressionValue ? 'TRUE' : 'FALSE'; + } + + regularExpressionValue = `${regularExpressionValue}`; + + let replacementValue = replacementObject.getValue(); + + if (replacementObject.isNull()) { + replacementValue = ''; + } + + if (replacementObject.isBoolean()) { + replacementValue = replacementValue ? 'TRUE' : 'FALSE'; + } + + replacementValue = `${replacementValue}`; + + const { isError: isError_regExp, regExp } = handleRegExp(regularExpressionValue, true); + + if (isError_regExp) { + return ErrorValueObject.create(ErrorType.REF); + } + + const result = textValue.replace(regExp as RegExp, replacementValue); + + return StringValueObject.create(result); + } +} diff --git a/packages/sheets-formula-ui/src/locale/function-list/text/en-US.ts b/packages/sheets-formula-ui/src/locale/function-list/text/en-US.ts index 0e50467f46a..3c1e2476958 100644 --- a/packages/sheets-formula-ui/src/locale/function-list/text/en-US.ts +++ b/packages/sheets-formula-ui/src/locale/function-list/text/en-US.ts @@ -352,6 +352,49 @@ export default { number2: { name: 'number2', detail: 'second' }, }, }, + REGEXEXTRACT: { + description: 'Extracts the first matching substrings according to a regular expression.', + abstract: 'Extracts the first matching substrings according to a regular expression.', + links: [ + { + title: 'Instruction', + url: 'https://support.google.com/docs/answer/3098244?sjid=5628197291201472796-AP&hl=en', + }, + ], + functionParameter: { + text: { name: 'text', detail: 'The input text.' }, + regularExpression: { name: 'regular_expression', detail: 'The first part of text that matches this expression will be returned.' }, + }, + }, + REGEXMATCH: { + description: 'Whether a piece of text matches a regular expression.', + abstract: 'Whether a piece of text matches a regular expression.', + links: [ + { + title: 'Instruction', + url: 'https://support.google.com/docs/answer/3098292?sjid=5628197291201472796-AP&hl=en', + }, + ], + functionParameter: { + text: { name: 'text', detail: 'The text to be tested against the regular expression.' }, + regularExpression: { name: 'regular_expression', detail: 'The regular expression to test the text against.' }, + }, + }, + REGEXREPLACE: { + description: 'Replaces part of a text string with a different text string using regular expressions.', + abstract: 'Replaces part of a text string with a different text string using regular expressions.', + links: [ + { + title: 'Instruction', + url: 'https://support.google.com/docs/answer/3098245?sjid=5628197291201472796-AP&hl=en', + }, + ], + functionParameter: { + text: { name: 'text', detail: 'The text, a part of which will be replaced.' }, + regularExpression: { name: 'regular_expression', detail: 'The regular expression. All matching instances in text will be replaced.' }, + replacement: { name: 'replacement', detail: 'The text which will be inserted into the original text.' }, + }, + }, REPLACE: { description: 'Replaces characters within text', abstract: 'Replaces characters within text', diff --git a/packages/sheets-formula-ui/src/locale/function-list/text/ja-JP.ts b/packages/sheets-formula-ui/src/locale/function-list/text/ja-JP.ts index fcc8db1c79b..4d822b457d3 100644 --- a/packages/sheets-formula-ui/src/locale/function-list/text/ja-JP.ts +++ b/packages/sheets-formula-ui/src/locale/function-list/text/ja-JP.ts @@ -353,6 +353,49 @@ export default { number2: { name: 'number2', detail: 'second' }, }, }, + REGEXEXTRACT: { + description: '正規表現と最初に一致する部分文字列を抽出します。', + abstract: '正規表現と最初に一致する部分文字列を抽出します。', + links: [ + { + title: '指導', + url: 'https://support.google.com/docs/answer/3098244?sjid=5628197291201472796-AP&hl=ja', + }, + ], + functionParameter: { + text: { name: 'テキスト', detail: '代入するテキストです。' }, + regularExpression: { name: '正規表現', detail: 'この正規表現に一致する最初のテキスト部分が返されます。' }, + }, + }, + REGEXMATCH: { + description: '正規表現に一致するテキストの一部を検索します。', + abstract: '正規表現に一致するテキストの一部を検索します。', + links: [ + { + title: '指導', + url: 'https://support.google.com/docs/answer/3098292?sjid=5628197291201472796-AP&hl=ja', + }, + ], + functionParameter: { + text: { name: 'テキスト', detail: '正規表現に対して検証するテキストです。' }, + regularExpression: { name: '正規表現', detail: 'テキストを検証する正規表現です。' }, + }, + }, + REGEXREPLACE: { + description: '正規表現を使用して、テキスト文字列の一部を別のテキスト文字列に置き換えます。', + abstract: '正規表現を使用して、テキスト文字列の一部を別のテキスト文字列に置き換えます。', + links: [ + { + title: '指導', + url: 'https://support.google.com/docs/answer/3098245?sjid=5628197291201472796-AP&hl=ja', + }, + ], + functionParameter: { + text: { name: 'テキスト', detail: '一部を置換する対象のテキストです。' }, + regularExpression: { name: '正規表現', detail: 'この正規表現に一致するテキスト内のすべてのインスタンスが置き換えられます。' }, + replacement: { name: '置換', detail: '元のテキストに挿入されるテキストです。' }, + }, + }, REPLACE: { description: '文字列中の指定された数の文字を他の文字に置き換えます。', abstract: '文字列中の指定された数の文字を他の文字に置き換えます。', diff --git a/packages/sheets-formula-ui/src/locale/function-list/text/vi-VN.ts b/packages/sheets-formula-ui/src/locale/function-list/text/vi-VN.ts index e57e6361355..4fa3802540c 100644 --- a/packages/sheets-formula-ui/src/locale/function-list/text/vi-VN.ts +++ b/packages/sheets-formula-ui/src/locale/function-list/text/vi-VN.ts @@ -295,46 +295,46 @@ export default { }, }, REGEXEXTRACT: { - description: 'Trích xuất văn bản phù hợp với biểu thức chính quy (REGEX)', - abstract: 'Trích xuất văn bản phù hợp với biểu thức chính quy (REGEX)', + description: 'Trích xuất chuỗi con khớp đầu tiên theo một biểu thức chính quy.', + abstract: 'Trích xuất chuỗi con khớp đầu tiên theo một biểu thức chính quy.', links: [ { title: 'Hướng dẫn', - url: 'https://support.microsoft.com/vi-vn/office/regexextract-%E5%87%BD%E6%95%B0-917b74b6-0d5c-4c7b-883e-24b39a7a20e2', + url: 'https://support.google.com/docs/answer/3098244?sjid=5628197291201472796-AP&hl=vi', }, ], functionParameter: { - text: { name: 'text', detail: 'Văn bản mà bạn muốn tìm kiếm biểu thức chính quy (REGEX).' }, - regex: { name: 'regex', detail: 'Biểu thức chính quy (REGEX) bạn muốn khớp.' }, + text: { name: 'văn bản', detail: 'Văn bản nhập vào.' }, + regularExpression: { name: 'biểu thức chính quy', detail: 'Phần đầu tiên văn_bản khớp với biểu thức này sẽ được trả về.' }, }, }, REGEXMATCH: { - description: 'Trả về giá trị đúng hoặc sai để chỉ ra xem văn bản có khớp với một biểu thức chính quy (REGEX) hay không', - abstract: 'Trả về giá trị đúng hoặc sai để chỉ ra xem văn bản có khớp với một biểu thức chính quy (REGEX) hay không', + description: 'Xem một đoạn văn bản có khớp với một biểu thức chính quy hay không.', + abstract: 'Xem một đoạn văn bản có khớp với một biểu thức chính quy hay không.', links: [ { title: 'Hướng dẫn', - url: 'https://support.microsoft.com/vi-vn/office/regexmatch-%E5%87%BD%E6%95%B0-69e4f84a-0f8c-4ff0-92a4-41e6d3ad960d', + url: 'https://support.google.com/docs/answer/3098292?sjid=5628197291201472796-AP&hl=vi', }, ], functionParameter: { - text: { name: 'text', detail: 'Văn bản mà bạn muốn tìm kiếm biểu thức chính quy (REGEX).' }, - regex: { name: 'regex', detail: 'Biểu thức chính quy (REGEX) bạn muốn khớp.' }, + text: { name: 'văn bản', detail: 'Văn bản cần thử nghiệm theo biểu thức chính quy.' }, + regularExpression: { name: 'biểu thức chính quy', detail: 'Biểu thức chính quy dùng để thử nghiệm văn bản.' }, }, }, REGEXREPLACE: { - description: 'Thay thế một phần văn bản khớp với biểu thức chính quy (REGEX)', - abstract: 'Thay thế một phần văn bản khớp với biểu thức chính quy (REGEX)', + description: 'Thay thế một phần của một chuỗi văn bản bằng một chuỗi văn bản khác bằng cách sử dụng các biểu thức chính quy.', + abstract: 'Thay thế một phần của một chuỗi văn bản bằng một chuỗi văn bản khác bằng cách sử dụng các biểu thức chính quy.', links: [ { title: 'Hướng dẫn', - url: 'https://support.microsoft.com/vi-vn/office/regexreplace-%E5%87%BD%E6%95%B0-e6108146-9ba1-4b3d-a5f2-f3b9a8618f8d', + url: 'https://support.google.com/docs/answer/3098245?sjid=5628197291201472796-AP&hl=vi', }, ], functionParameter: { - text: { name: 'text', detail: 'Văn bản mà bạn muốn thay thế biểu thức chính quy (REGEX).' }, - regex: { name: 'regex', detail: 'Biểu thức chính quy (REGEX) bạn muốn thay thế.' }, - replacement: { name: 'replacement', detail: 'Văn bản thay thế.' }, + text: { name: 'văn bản', detail: 'Văn bản, một phần của văn bản này sẽ được thay thế.' }, + regularExpression: { name: 'biểu thức chính quy', detail: 'Biểu thức chính quy. Tất cả trường hợp phù hợp trong văn_bản sẽ được thay thế.' }, + replacement: { name: 'thay thế', detail: 'Văn bản sẽ được chèn vào văn bản gốc.' }, }, }, REPLACE: { diff --git a/packages/sheets-formula-ui/src/locale/function-list/text/zh-CN.ts b/packages/sheets-formula-ui/src/locale/function-list/text/zh-CN.ts index e11fbd96eda..0163ac142e7 100644 --- a/packages/sheets-formula-ui/src/locale/function-list/text/zh-CN.ts +++ b/packages/sheets-formula-ui/src/locale/function-list/text/zh-CN.ts @@ -352,6 +352,49 @@ export default { number2: { name: 'number2', detail: 'second' }, }, }, + REGEXEXTRACT: { + description: '根据正则表达式提取第一个匹配的字符串。', + abstract: '根据正则表达式提取第一个匹配的字符串。', + links: [ + { + title: '教学', + url: 'https://support.google.com/docs/answer/3098244?sjid=5628197291201472796-AP&hl=zh-Hans', + }, + ], + functionParameter: { + text: { name: '文本', detail: '输入文本' }, + regularExpression: { name: '正则表达式', detail: '此函数将返回 text 中符合此表达式的第一个字符串。' }, + }, + }, + REGEXMATCH: { + description: '判断一段文本是否与正则表达式相匹配。', + abstract: '判断一段文本是否与正则表达式相匹配。', + links: [ + { + title: '教学', + url: 'https://support.google.com/docs/answer/3098292?sjid=5628197291201472796-AP&hl=zh-Hans', + }, + ], + functionParameter: { + text: { name: '文本', detail: '要用正则表达式测试的文本。' }, + regularExpression: { name: '正则表达式', detail: '用来测试文本的正则表达式。' }, + }, + }, + REGEXREPLACE: { + description: '使用正则表达式将文本字符串中的一部分替换为其他文本字符串。', + abstract: '使用正则表达式将文本字符串中的一部分替换为其他文本字符串。', + links: [ + { + title: '教学', + url: 'https://support.google.com/docs/answer/3098245?sjid=5628197291201472796-AP&hl=zh-Hans', + }, + ], + functionParameter: { + text: { name: '文本', detail: '其中一部分将被替换的文本。' }, + regularExpression: { name: '正则表达式', detail: '正则表达式。text 中所有匹配的实例都将被替换。' }, + replacement: { name: '替换内容', detail: '要插入到原有文本中的文本。' }, + }, + }, REPLACE: { description: '替换文本中的字符', abstract: '替换文本中的字符', diff --git a/packages/sheets-formula-ui/src/locale/function-list/text/zh-TW.ts b/packages/sheets-formula-ui/src/locale/function-list/text/zh-TW.ts index 110300e4fba..6ad7a425154 100644 --- a/packages/sheets-formula-ui/src/locale/function-list/text/zh-TW.ts +++ b/packages/sheets-formula-ui/src/locale/function-list/text/zh-TW.ts @@ -351,6 +351,49 @@ export default { number2: { name: 'number2', detail: 'second' }, }, }, + REGEXEXTRACT: { + description: '根據規則運算式擷取第一個符合規則的字串。', + abstract: '根據規則運算式擷取第一個符合規則的字串。', + links: [ + { + title: '教導', + url: 'https://support.google.com/docs/answer/3098244?sjid=5628197291201472796-AP&hl=zh-Hant', + }, + ], + functionParameter: { + text: { name: '文字', detail: '輸入文字' }, + regularExpression: { name: '規則運算式', detail: '指定規則運算式,系統就會傳回 text 中第一個符合此運算式的字串。' }, + }, + }, + REGEXMATCH: { + description: '某段文字是否符合規則運算式。', + abstract: '某段文字是否符合規則運算式。', + links: [ + { + title: '教導', + url: 'https://support.google.com/docs/answer/3098292?sjid=5628197291201472796-AP&hl=zh-Hant', + }, + ], + functionParameter: { + text: { name: '文字', detail: '系統會根據規則運算式測試此文字。' }, + regularExpression: { name: '規則運算式', detail: '用來測試文字的規則運算式。' }, + }, + }, + REGEXREPLACE: { + description: '利用規則運算式將文字字串的一部分取代成其他文字字串。', + abstract: '利用規則運算式將文字字串的一部分取代成其他文字字串。', + links: [ + { + title: '教導', + url: 'https://support.google.com/docs/answer/3098245?sjid=5628197291201472796-AP&hl=zh-Hant', + }, + ], + functionParameter: { + text: { name: '文字', detail: '系統會取代這段文字的部分區段。' }, + regularExpression: { name: '規則運算式', detail: '規則運算式。系統將替換 text 中所有相符的項目。' }, + replacement: { name: '取代文字', detail: '系統會將這段文字插入原來的文字。' }, + }, + }, REPLACE: { description: '替換文字中的字元', abstract: '替換文字中的字元', diff --git a/packages/sheets-formula/src/services/function-list/text.ts b/packages/sheets-formula/src/services/function-list/text.ts index d4feb01a161..d10c3f1f2a4 100644 --- a/packages/sheets-formula/src/services/function-list/text.ts +++ b/packages/sheets-formula/src/services/function-list/text.ts @@ -531,6 +531,79 @@ export const FUNCTION_LIST_TEXT: IFunctionInfo[] = [ }, ], }, + { + functionName: FUNCTION_NAMES_TEXT.REGEXEXTRACT, + functionType: FunctionType.Text, + description: 'formula.functionList.REGEXEXTRACT.description', + abstract: 'formula.functionList.REGEXEXTRACT.abstract', + functionParameter: [ + { + name: 'formula.functionList.REGEXEXTRACT.functionParameter.text.name', + detail: 'formula.functionList.REGEXEXTRACT.functionParameter.text.detail', + example: '"abcdefg"', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.REGEXEXTRACT.functionParameter.regularExpression.name', + detail: 'formula.functionList.REGEXEXTRACT.functionParameter.regularExpression.detail', + example: '"c.*f"', + require: 1, + repeat: 0, + }, + ], + }, + { + functionName: FUNCTION_NAMES_TEXT.REGEXMATCH, + functionType: FunctionType.Text, + description: 'formula.functionList.REGEXMATCH.description', + abstract: 'formula.functionList.REGEXMATCH.abstract', + functionParameter: [ + { + name: 'formula.functionList.REGEXMATCH.functionParameter.text.name', + detail: 'formula.functionList.REGEXMATCH.functionParameter.text.detail', + example: '"Spreadsheets"', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.REGEXMATCH.functionParameter.regularExpression.name', + detail: 'formula.functionList.REGEXMATCH.functionParameter.regularExpression.detail', + example: '"S.r"', + require: 1, + repeat: 0, + }, + ], + }, + { + functionName: FUNCTION_NAMES_TEXT.REGEXREPLACE, + functionType: FunctionType.Text, + description: 'formula.functionList.REGEXREPLACE.description', + abstract: 'formula.functionList.REGEXREPLACE.abstract', + functionParameter: [ + { + name: 'formula.functionList.REGEXREPLACE.functionParameter.text.name', + detail: 'formula.functionList.REGEXREPLACE.functionParameter.text.detail', + example: '"abcedfg"', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.REGEXREPLACE.functionParameter.regularExpression.name', + detail: 'formula.functionList.REGEXREPLACE.functionParameter.regularExpression.detail', + example: '"a.*d"', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.REGEXREPLACE.functionParameter.replacement.name', + detail: 'formula.functionList.REGEXREPLACE.functionParameter.replacement.detail', + example: '"xyz"', + require: 1, + repeat: 0, + }, + ], + }, { functionName: FUNCTION_NAMES_TEXT.REPLACE, functionType: FunctionType.Text,