From 78643f94b736bf516984016aa1df2cdd96c90c53 Mon Sep 17 00:00:00 2001 From: tlartem Date: Fri, 18 Jul 2025 16:04:53 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/spreadsheet-engine/functions.ts | 102 ++++++++++-------- .../spreadsheet-engine/spreadsheet-engine.ts | 62 +++++++++-- 2 files changed, 111 insertions(+), 53 deletions(-) diff --git a/src/lib/spreadsheet-engine/functions.ts b/src/lib/spreadsheet-engine/functions.ts index 53f2ef4..8bbe2ce 100644 --- a/src/lib/spreadsheet-engine/functions.ts +++ b/src/lib/spreadsheet-engine/functions.ts @@ -6,83 +6,97 @@ export interface ExcelFunction { // Волатильные функции - пересчитываются при любом изменении export const VOLATILE_FUNCTIONS = new Set([ - 'RAND', - 'RANDBETWEEN', - 'NOW', - 'TODAY', + 'СЛУЧ', + 'СЛУЧМЕЖДУ', + 'СЕЙЧАС', + 'СЕГОДНЯ', ]) export const DEFAULT_FUNCTIONS: Record = { - RAND: () => Math.random(), - RANDBETWEEN: (bottom: number, top: number) => - Math.floor(Math.random() * (top - bottom + 1)) + bottom, - SUM: (...args: any[]) => { + СЛУЧ: () => Math.random(), + СЛУЧМЕЖДУ: (низ: number, верх: number) => + Math.floor(Math.random() * (верх - низ + 1)) + низ, + СУММ: (...args: any[]) => { const flatArgs = args.flat() return flatArgs.reduce( (sum, val) => sum + (typeof val === 'number' ? val : 0), 0, ) }, - AVERAGE: (...args: any[]) => { + СРЗНАЧ: (...args: any[]) => { const flatArgs = args.flat() const validArgs = flatArgs.filter((val) => typeof val === 'number') return validArgs.length > 0 ? validArgs.reduce((sum, val) => sum + val, 0) / validArgs.length : 0 }, - COUNT: (...args: any[]) => { + СЧЁТ: (...args: any[]) => { const flatArgs = args.flat() return flatArgs.filter((val) => typeof val === 'number').length }, - MAX: (...args: any[]) => { + МАКС: (...args: any[]) => { const flatArgs = args.flat() const validArgs = flatArgs.filter((val) => typeof val === 'number') return validArgs.length > 0 ? Math.max(...validArgs) : 0 }, - MIN: (...args: any[]) => { + МИН: (...args: any[]) => { const flatArgs = args.flat() const validArgs = flatArgs.filter((val) => typeof val === 'number') return validArgs.length > 0 ? Math.min(...validArgs) : 0 }, - IF: (condition: boolean, trueValue: CellValue, falseValue: CellValue) => - condition ? trueValue : falseValue, - AND: (...args: boolean[]) => args.every(Boolean), - OR: (...args: boolean[]) => args.some(Boolean), - NOT: (value: boolean) => !value, - CONCATENATE: (...args: string[]) => args.join(''), - LEN: (text: string) => (text ? text.length : 0), - UPPER: (text: string) => (text ? text.toUpperCase() : ''), - LOWER: (text: string) => (text ? text.toLowerCase() : ''), - TRIM: (text: string) => (text ? text.trim() : ''), + ЕСЛИ: (условие: boolean, истина: CellValue, ложь: CellValue) => + условие ? истина : ложь, + И: (...args: boolean[]) => args.every(Boolean), + ИЛИ: (...args: boolean[]) => args.some(Boolean), + НЕ: (value: boolean) => !value, + СЦЕПИТЬ: (...args: string[]) => args.join(''), + ДЛСТР: (текст: string) => (текст ? текст.length : 0), + ПРОПИСН: (текст: string) => (текст ? текст.toUpperCase() : ''), + СТРОЧН: (текст: string) => (текст ? текст.toLowerCase() : ''), + СЖПРОБЕЛЫ: (текст: string) => (текст ? текст.trim() : ''), // Функции округления - ROUND: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits) - return Math.round(number * factor) / factor + ОКРУГЛ: (число: number, разряды: number = 0) => { + const factor = Math.pow(10, разряды) + return Math.round(число * factor) / factor }, - ROUNDUP: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits) - return Math.ceil(number * factor) / factor + ОКРУГЛВВЕРХ: (число: number, разряды: number = 0) => { + const factor = Math.pow(10, разряды) + return Math.ceil(число * factor) / factor }, - ROUNDDOWN: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits) - return Math.floor(number * factor) / factor + ОКРУГЛВНИЗ: (число: number, разряды: number = 0) => { + const factor = Math.pow(10, разряды) + return Math.floor(число * factor) / factor }, - CEILING: (number: number, significance: number = 1) => { - if (significance === 0) return 0 - return Math.ceil(number / significance) * significance + ОКРВВЕРХ: (число: number, кратное: number = 1) => { + if (кратное === 0) return 0 + return Math.ceil(число / кратное) * кратное }, - FLOOR: (number: number, significance: number = 1) => { - if (significance === 0) return 0 - return Math.floor(number / significance) * significance + ОКРВНИЗ: (число: number, кратное: number = 1) => { + if (кратное === 0) return 0 + return Math.floor(число / кратное) * кратное }, - INT: (number: number) => Math.floor(number), - TRUNC: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits) - return Math.trunc(number * factor) / factor + ЦЕЛОЕ: (число: number) => Math.floor(число), + ОТБР: (число: number, разряды: number = 0) => { + const factor = Math.pow(10, разряды) + return Math.trunc(число * factor) / factor }, - MROUND: (number: number, multiple: number) => { - if (multiple === 0) return 0 - return Math.round(number / multiple) * multiple + ОКРУГЛТ: (число: number, кратное: number) => { + if (кратное === 0) return 0 + return Math.round(число / кратное) * кратное + }, + + // Функции даты и времени + СЕЙЧАС: () => Date.now(), + СЕГОДНЯ: () => { + const date = new Date() + date.setHours(0, 0, 0, 0) + return date.getTime() + }, + + // Отладочная функция + ТЕСТ: (значение: any = 'Функция работает!') => { + console.log('ТЕСТ вызван с:', значение) + return значение }, } diff --git a/src/lib/spreadsheet-engine/spreadsheet-engine.ts b/src/lib/spreadsheet-engine/spreadsheet-engine.ts index b269625..7abb884 100644 --- a/src/lib/spreadsheet-engine/spreadsheet-engine.ts +++ b/src/lib/spreadsheet-engine/spreadsheet-engine.ts @@ -1,4 +1,8 @@ -import { DEFAULT_FUNCTIONS, ExcelFunction, VOLATILE_FUNCTIONS } from './functions' +import { + DEFAULT_FUNCTIONS, + ExcelFunction, + VOLATILE_FUNCTIONS, +} from './functions' // Регулярные выражения для ссылок const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g // SheetName!A1 @@ -136,9 +140,36 @@ export class Cell { // Заменяем функции Excel const functions = this.sheet.workbook.getFunctions() + + // Отладка + if (expr.includes('ТЕСТ')) { + console.log('Processing ТЕСТ function') + console.log('Available functions:', Object.keys(functions)) + console.log('Original code:', code) + } + for (const [funcName] of Object.entries(functions)) { - const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi') - code = code.replace(funcRegex, `__functions__.${funcName}(`) + // Экранируем специальные символы в названии функции для регулярного выражения + const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Для кириллических символов \b может не работать, используем более универсальное решение + const funcRegex = new RegExp( + `(?:^|[^\\w])(${escapedFuncName})\\s*\\(`, + 'gi', + ) + const before = code + code = code.replace(funcRegex, (match, p1, offset) => { + // p1 - это название функции, нужно сохранить символ перед функцией если он есть + const prefix = match.substring(0, match.indexOf(p1)) + return `${prefix}__functions__.${funcName}(` + }) + + if (before !== code && expr.includes('ТЕСТ')) { + console.log(`Replaced ${funcName}:`, before, '->', code) + } + } + + if (expr.includes('ТЕСТ')) { + console.log('Final code:', code) } // Создаём контекст для выполнения @@ -163,8 +194,14 @@ export class Cell { let hasVolatileFunction = false for (const funcName of Object.keys(functions)) { - if (VOLATILE_FUNCTIONS.has(funcName.toUpperCase())) { - const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi') + if (VOLATILE_FUNCTIONS.has(funcName)) { + // Экранируем специальные символы для регулярного выражения + const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Для кириллических символов используем более универсальное решение + const funcRegex = new RegExp( + `(?:^|[^\\w])${escapedFuncName}\\s*\\(`, + 'gi', + ) if (funcRegex.test(expr)) { hasVolatileFunction = true break @@ -288,7 +325,14 @@ export class Sheet { export class Workbook { sheets: Map = new Map() private _dirty: Set = new Set() - private _functions: Record = { ...DEFAULT_FUNCTIONS } + private _functions: Record = {} + + constructor() { + // Инициализируем функции через метод addFunction, чтобы обеспечить единообразие + Object.entries(DEFAULT_FUNCTIONS).forEach(([name, func]) => { + this.addFunction(name, func) + }) + } private _volatileCells: Set = new Set() // Список волатильных ячеек addSheet(name: string): Sheet { @@ -319,7 +363,7 @@ export class Workbook { const toVisit = new Set([qualifiedName]) while (toVisit.size > 0) { - const current = toVisit.values().next().value + const current = toVisit.values().next().value! toVisit.delete(current) if (!this._dirty.has(current)) { @@ -344,7 +388,7 @@ export class Workbook { // Обрабатываем зависимых от волатильных ячеек while (toVisit.size > 0) { - const current = toVisit.values().next().value + const current = toVisit.values().next().value! toVisit.delete(current) if (!this._dirty.has(current)) { @@ -360,7 +404,7 @@ export class Workbook { recalc(): void { // Пересчёт всех "грязных" ячеек while (this._dirty.size > 0) { - const current = this._dirty.values().next().value + const current = this._dirty.values().next().value! this._dirty.delete(current) this.getCell(current).evaluate() }