русские функции

This commit is contained in:
2025-07-18 16:04:53 +03:00
parent 81a2f91dc4
commit 78643f94b7
2 changed files with 111 additions and 53 deletions

View File

@@ -6,83 +6,97 @@ export interface ExcelFunction {
// Волатильные функции - пересчитываются при любом изменении
export const VOLATILE_FUNCTIONS = new Set([
'RAND',
'RANDBETWEEN',
'NOW',
'TODAY',
'СЛУЧ',
'СЛУЧМЕЖДУ',
'СЕЙЧАС',
'СЕГОДНЯ',
])
export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
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 значение
},
}

View File

@@ -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<string, Sheet> = new Map()
private _dirty: Set<string> = new Set()
private _functions: Record<string, ExcelFunction> = { ...DEFAULT_FUNCTIONS }
private _functions: Record<string, ExcelFunction> = {}
constructor() {
// Инициализируем функции через метод addFunction, чтобы обеспечить единообразие
Object.entries(DEFAULT_FUNCTIONS).forEach(([name, func]) => {
this.addFunction(name, func)
})
}
private _volatileCells: Set<string> = 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()
}