русские функции
This commit is contained in:
@@ -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 значение
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user