ручной рефакторинг
This commit is contained in:
676
src/feature/excelEngine/excelEngine.ts
Normal file
676
src/feature/excelEngine/excelEngine.ts
Normal file
@@ -0,0 +1,676 @@
|
||||
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
|
||||
const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g // A1, B2, etc.
|
||||
const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // A1:B2, D5:D7, etc.
|
||||
const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // SheetName!A1:B2
|
||||
|
||||
// Типы для значений ячеек
|
||||
export type CellValue = string | number | boolean | null | undefined
|
||||
|
||||
// Утилитная функция для устранения машинной ошибки в числах с плавающей точкой
|
||||
// Применяется только при финальном выводе значений
|
||||
function fixFloatingPointError(value: CellValue): CellValue {
|
||||
if (typeof value === 'number' && isFinite(value)) {
|
||||
// Проверяем, является ли число "почти целым" или "почти простой дробью"
|
||||
const tolerance = 1e-10
|
||||
|
||||
// Проверка на целое число
|
||||
if (Math.abs(value - Math.round(value)) < tolerance) {
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
// Проверка на простые дроби (до 6 знаков после запятой)
|
||||
for (let precision = 1; precision <= 6; precision++) {
|
||||
const factor = Math.pow(10, precision)
|
||||
const rounded = Math.round(value * factor) / factor
|
||||
if (Math.abs(value - rounded) < tolerance) {
|
||||
return rounded
|
||||
}
|
||||
}
|
||||
|
||||
// Для остальных случаев ограничиваем до разумного количества знаков
|
||||
return Math.round(value * 1e10) / 1e10
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export class Cell {
|
||||
sheet: Sheet
|
||||
name: string
|
||||
raw: CellValue = null
|
||||
value: CellValue = null
|
||||
precedents: Set<string> = new Set()
|
||||
dependents: Set<string> = new Set()
|
||||
isVolatile: boolean = false
|
||||
|
||||
constructor(sheet: Sheet, name: string) {
|
||||
this.sheet = sheet
|
||||
this.name = name
|
||||
}
|
||||
|
||||
get qualifiedName(): string {
|
||||
return `${this.sheet.name}!${this.name}`
|
||||
}
|
||||
|
||||
set(raw: CellValue): void {
|
||||
// Удаляем старые зависимости
|
||||
for (const ref of this.precedents) {
|
||||
this.sheet.workbook.getCell(ref).dependents.delete(this.qualifiedName)
|
||||
}
|
||||
|
||||
// Убираем ячейку из списка волатильных если она там была
|
||||
if (this.isVolatile) {
|
||||
this.sheet.workbook.removeVolatileCell(this.qualifiedName)
|
||||
this.isVolatile = false
|
||||
}
|
||||
|
||||
this.raw = raw
|
||||
this.precedents.clear()
|
||||
|
||||
// Помечаем себя и всех зависимых ячеек "грязными"
|
||||
this.sheet.workbook.markDirty(this.qualifiedName)
|
||||
}
|
||||
|
||||
evaluate(): CellValue {
|
||||
// Если не формула — просто возвращаем значение
|
||||
if (typeof this.raw !== 'string' || !this.raw.startsWith('=')) {
|
||||
this.value = this.raw
|
||||
return this.value
|
||||
}
|
||||
|
||||
// Убираем знак равенства
|
||||
const expr = this.raw.substring(1)
|
||||
|
||||
// Проверяем, содержит ли формула волатильные функции
|
||||
this.checkVolatileFunctions(expr)
|
||||
|
||||
// Регистрируем зависимости на другие ячейки
|
||||
// Сначала обрабатываем квалифицированные ссылки (SheetName!A1)
|
||||
const qualifiedMatches = expr.match(QUALIFIED_REF_RE)
|
||||
if (qualifiedMatches) {
|
||||
for (const ref of qualifiedMatches) {
|
||||
this.precedents.add(ref)
|
||||
const cell = this.sheet.workbook.getCell(ref)
|
||||
cell.dependents.add(this.qualifiedName)
|
||||
}
|
||||
}
|
||||
|
||||
// Затем обрабатываем локальные ссылки (A1, B2)
|
||||
const localMatches = expr.match(LOCAL_REF_RE)
|
||||
if (localMatches) {
|
||||
for (const ref of localMatches) {
|
||||
// Избегаем конфликтов с функциями (например, SUM, MAX)
|
||||
if (!this.isFunction(ref)) {
|
||||
const qualifiedRef = `${this.sheet.name}!${ref}`
|
||||
this.precedents.add(qualifiedRef)
|
||||
const cell = this.sheet.workbook.getCell(qualifiedRef)
|
||||
cell.dependents.add(this.qualifiedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Заменяем ссылки на вычисленные значения
|
||||
let code = expr
|
||||
|
||||
// ВАЖНО: Сначала заменяем диапазоны ячеек на массивы значений,
|
||||
// затем отдельные ячейки, чтобы избежать конфликтов
|
||||
code = code.replace(QUALIFIED_RANGE_RE, match => {
|
||||
const [sheetName, range] = match.split('!')
|
||||
const values = this.expandRange(range, sheetName)
|
||||
return `[${values.join(',')}]`
|
||||
})
|
||||
|
||||
code = code.replace(RANGE_RE, match => {
|
||||
const values = this.expandRange(match)
|
||||
return `[${values.join(',')}]`
|
||||
})
|
||||
|
||||
// Заменяем квалифицированные ссылки на ячейки
|
||||
code = code.replace(QUALIFIED_REF_RE, match => {
|
||||
try {
|
||||
const [sheetName] = match.split('!')
|
||||
const sheet = this.sheet.workbook.getSheet(sheetName)
|
||||
if (!sheet) {
|
||||
return '"#REF!"'
|
||||
}
|
||||
const cellValue = this.sheet.workbook.getCell(match).evaluate()
|
||||
// Только строки оборачиваем в кавычки, числа и булевы значения - нет
|
||||
if (typeof cellValue === 'string') {
|
||||
return `"${cellValue}"`
|
||||
} else if (
|
||||
typeof cellValue === 'number' ||
|
||||
typeof cellValue === 'boolean'
|
||||
) {
|
||||
return String(cellValue)
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
} catch (error) {
|
||||
return '"#REF!"'
|
||||
}
|
||||
})
|
||||
|
||||
// Заменяем локальные ссылки на ячейки
|
||||
code = code.replace(LOCAL_REF_RE, match => {
|
||||
// Избегаем замены функций
|
||||
if (this.isFunction(match)) {
|
||||
return match
|
||||
}
|
||||
try {
|
||||
const qualifiedRef = `${this.sheet.name}!${match}`
|
||||
const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate()
|
||||
// Только строки оборачиваем в кавычки, числа и булевы значения - нет
|
||||
if (typeof cellValue === 'string') {
|
||||
return `"${cellValue}"`
|
||||
} else if (
|
||||
typeof cellValue === 'number' ||
|
||||
typeof cellValue === 'boolean'
|
||||
) {
|
||||
return String(cellValue)
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
} catch (error) {
|
||||
return '"#REF!"'
|
||||
}
|
||||
})
|
||||
|
||||
// Заменяем функции Excel
|
||||
const functions = this.sheet.workbook.getFunctions()
|
||||
|
||||
for (const [funcName] of Object.entries(functions)) {
|
||||
// Экранируем специальные символы в названии функции для регулярного выражения
|
||||
const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
// Для кириллических символов \b может не работать, используем более универсальное решение
|
||||
const funcRegex = new RegExp(
|
||||
`(?:^|[^\\w])(${escapedFuncName})\\s*\\(`,
|
||||
'gi'
|
||||
)
|
||||
const before = code
|
||||
code = code.replace(funcRegex, (match, p1) => {
|
||||
// p1 - это название функции, нужно сохранить символ перед функцией если он есть
|
||||
const prefix = match.substring(0, match.indexOf(p1))
|
||||
return `${prefix}__functions__.${funcName}(`
|
||||
})
|
||||
|
||||
if (before !== code && expr.includes('ТЕСТ')) {
|
||||
// console.log(`Replaced ${funcName}:`, before, '->', code)
|
||||
}
|
||||
}
|
||||
|
||||
// Создаём контекст для выполнения
|
||||
const context = {
|
||||
__functions__: functions,
|
||||
wb: this.sheet.workbook,
|
||||
}
|
||||
|
||||
// Выполняем код в безопасном контексте
|
||||
this.value = this.evaluateExpression(code, context)
|
||||
return this.value
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating formula in ${this.qualifiedName}:`, error)
|
||||
this.value = '#ERROR!'
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
private checkVolatileFunctions(expr: string): void {
|
||||
// Проверяем, содержит ли формула волатильные функции
|
||||
const functions = this.sheet.workbook.getFunctions()
|
||||
let hasVolatileFunction = false
|
||||
|
||||
for (const funcName of Object.keys(functions)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasVolatileFunction && !this.isVolatile) {
|
||||
this.isVolatile = true
|
||||
this.sheet.workbook.addVolatileCell(this.qualifiedName)
|
||||
} else if (!hasVolatileFunction && this.isVolatile) {
|
||||
this.isVolatile = false
|
||||
this.sheet.workbook.removeVolatileCell(this.qualifiedName)
|
||||
}
|
||||
}
|
||||
|
||||
private isFunction(ref: string): boolean {
|
||||
const functions = this.sheet.workbook.getFunctions()
|
||||
return Object.keys(functions).includes(ref.toUpperCase())
|
||||
}
|
||||
|
||||
private evaluateExpression(expr: string, context: any): CellValue {
|
||||
try {
|
||||
// Создаём функцию с контекстом
|
||||
const func = new Function(...Object.keys(context), `return ${expr}`)
|
||||
|
||||
const result = func(...Object.values(context))
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid expression: ${expr}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для развертывания диапазона в массив значений
|
||||
private expandRange(range: string, sheetName?: string): number[] {
|
||||
const parts = range.split(':')
|
||||
if (parts.length !== 2) return []
|
||||
|
||||
const startCell = parts[0]
|
||||
const endCell = parts[1]
|
||||
|
||||
const startCol = startCell.match(/[A-Z]+/)?.[0] || ''
|
||||
const startRow = parseInt(startCell.match(/[0-9]+/)?.[0] || '0')
|
||||
const endCol = endCell.match(/[A-Z]+/)?.[0] || ''
|
||||
const endRow = parseInt(endCell.match(/[0-9]+/)?.[0] || '0')
|
||||
|
||||
const startColIndex = startCol.charCodeAt(0) - 65
|
||||
const endColIndex = endCol.charCodeAt(0) - 65
|
||||
|
||||
const values: number[] = []
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColIndex; col <= endColIndex; col++) {
|
||||
const cellName = String.fromCharCode(65 + col) + row
|
||||
const qualifiedName = sheetName
|
||||
? `${sheetName}!${cellName}`
|
||||
: `${this.sheet.name}!${cellName}`
|
||||
const cellValue = this.sheet.workbook.getCell(qualifiedName).evaluate()
|
||||
const numValue =
|
||||
typeof cellValue === 'number'
|
||||
? cellValue
|
||||
: parseFloat(String(cellValue)) || 0
|
||||
values.push(numValue)
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
export class Sheet {
|
||||
name: string
|
||||
workbook: Workbook
|
||||
cells: Map<string, Cell> = new Map()
|
||||
|
||||
constructor(name: string, workbook: Workbook) {
|
||||
this.name = name
|
||||
this.workbook = workbook
|
||||
}
|
||||
|
||||
getCell(name: string): Cell {
|
||||
if (!this.cells.has(name)) {
|
||||
this.cells.set(name, new Cell(this, name))
|
||||
}
|
||||
return this.cells.get(name)!
|
||||
}
|
||||
|
||||
setCell(name: string, raw: CellValue): void {
|
||||
const cell = this.getCell(name)
|
||||
cell.set(raw)
|
||||
}
|
||||
|
||||
getCellValue(name: string): CellValue {
|
||||
return this.getCell(name).value
|
||||
}
|
||||
|
||||
getAllCells(): Map<string, Cell> {
|
||||
return this.cells
|
||||
}
|
||||
|
||||
toJSON(): Record<string, CellValue> {
|
||||
const result: Record<string, CellValue> = {}
|
||||
for (const [name, cell] of this.cells) {
|
||||
if (cell.raw !== null && cell.raw !== undefined && cell.raw !== '') {
|
||||
result[name] = cell.raw
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fromJSON(data: Record<string, CellValue>): void {
|
||||
for (const [name, value] of Object.entries(data)) {
|
||||
this.setCell(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Workbook {
|
||||
sheets: Map<string, Sheet> = new Map()
|
||||
private _dirty: Set<string> = new Set()
|
||||
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 {
|
||||
const sheet = new Sheet(name, this)
|
||||
this.sheets.set(name, sheet)
|
||||
return sheet
|
||||
}
|
||||
|
||||
getSheet(name: string): Sheet | undefined {
|
||||
return this.sheets.get(name)
|
||||
}
|
||||
|
||||
getCell(qualifiedName: string): Cell {
|
||||
const [sheetName, cellName] = qualifiedName.split('!')
|
||||
const sheet = this.sheets.get(sheetName)
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet "${sheetName}" not found`)
|
||||
}
|
||||
return sheet.getCell(cellName)
|
||||
}
|
||||
|
||||
setCell(qualifiedName: string, raw: CellValue): void {
|
||||
const cell = this.getCell(qualifiedName)
|
||||
cell.set(raw)
|
||||
}
|
||||
|
||||
markDirty(qualifiedName: string): void {
|
||||
const toVisit = new Set([qualifiedName])
|
||||
|
||||
while (toVisit.size > 0) {
|
||||
const current = toVisit.values().next().value!
|
||||
toVisit.delete(current)
|
||||
|
||||
if (!this._dirty.has(current)) {
|
||||
this._dirty.add(current)
|
||||
// Добавляем всех зависимых ячеек
|
||||
for (const dep of this.getCell(current).dependents) {
|
||||
toVisit.add(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Также помечаем все волатильные ячейки как "грязные"
|
||||
for (const volatileCell of this._volatileCells) {
|
||||
if (!this._dirty.has(volatileCell)) {
|
||||
this._dirty.add(volatileCell)
|
||||
// Добавляем всех зависимых от волатильных ячеек
|
||||
for (const dep of this.getCell(volatileCell).dependents) {
|
||||
toVisit.add(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем зависимых от волатильных ячеек
|
||||
while (toVisit.size > 0) {
|
||||
const current = toVisit.values().next().value!
|
||||
toVisit.delete(current)
|
||||
|
||||
if (!this._dirty.has(current)) {
|
||||
this._dirty.add(current)
|
||||
// Добавляем всех зависимых ячеек
|
||||
for (const dep of this.getCell(current).dependents) {
|
||||
toVisit.add(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private topologicalSort(cells: Set<string>): string[] {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const result: string[] = []
|
||||
const hasCycle = new Set<string>()
|
||||
|
||||
const visit = (cellName: string): boolean => {
|
||||
if (visiting.has(cellName)) {
|
||||
// Обнаружена циклическая зависимость
|
||||
hasCycle.add(cellName)
|
||||
return false
|
||||
}
|
||||
|
||||
if (visited.has(cellName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
visiting.add(cellName)
|
||||
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
|
||||
// Посещаем всех предшественников (ячейки, от которых зависит текущая)
|
||||
for (const precedent of cell.precedents) {
|
||||
if (cells.has(precedent)) {
|
||||
if (!visit(precedent)) {
|
||||
hasCycle.add(cellName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ячейка не существует или другая ошибка - игнорируем
|
||||
}
|
||||
|
||||
visiting.delete(cellName)
|
||||
visited.add(cellName)
|
||||
result.push(cellName)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Посещаем все ячейки
|
||||
for (const cellName of cells) {
|
||||
if (!visited.has(cellName)) {
|
||||
visit(cellName)
|
||||
}
|
||||
}
|
||||
|
||||
// Если обнаружены циклы, выводим предупреждение
|
||||
if (hasCycle.size > 0) {
|
||||
console.warn(
|
||||
'Обнаружены циклические зависимости в ячейках:',
|
||||
Array.from(hasCycle)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private detectCycle(startCell: string): boolean {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
|
||||
const visit = (cellName: string): boolean => {
|
||||
if (visiting.has(cellName)) {
|
||||
return true // Цикл найден
|
||||
}
|
||||
|
||||
if (visited.has(cellName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
visiting.add(cellName)
|
||||
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
for (const precedent of cell.precedents) {
|
||||
if (visit(precedent)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ячейка не существует - игнорируем
|
||||
}
|
||||
|
||||
visiting.delete(cellName)
|
||||
visited.add(cellName)
|
||||
return false
|
||||
}
|
||||
|
||||
return visit(startCell)
|
||||
}
|
||||
|
||||
recalc(): void {
|
||||
if (this._dirty.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Выполняем топологическую сортировку для определения правильного порядка пересчета
|
||||
const sortedCells = this.topologicalSort(this._dirty)
|
||||
|
||||
// Очищаем множество "грязных" ячеек
|
||||
this._dirty.clear()
|
||||
|
||||
// Пересчитываем ячейки в правильном порядке
|
||||
for (const cellName of sortedCells) {
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
|
||||
// Проверяем на циклические зависимости перед вычислением
|
||||
if (this.detectCycle(cellName)) {
|
||||
console.error(
|
||||
`Циклическая зависимость обнаружена для ячейки ${cellName}`
|
||||
)
|
||||
cell.value = '#CYCLE!'
|
||||
} else {
|
||||
cell.evaluate()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при пересчете ячейки ${cellName}:`, error)
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
cell.value = '#ERROR!'
|
||||
} catch (getError) {
|
||||
// Ячейка не существует - игнорируем
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(qualifiedName: string): CellValue {
|
||||
if (this._dirty.has(qualifiedName)) {
|
||||
this.recalc()
|
||||
}
|
||||
const value = this.getCell(qualifiedName).value
|
||||
|
||||
return fixFloatingPointError(value)
|
||||
}
|
||||
|
||||
// Управление волатильными ячейками
|
||||
addVolatileCell(qualifiedName: string): void {
|
||||
this._volatileCells.add(qualifiedName)
|
||||
}
|
||||
|
||||
removeVolatileCell(qualifiedName: string): void {
|
||||
this._volatileCells.delete(qualifiedName)
|
||||
}
|
||||
|
||||
getVolatileCells(): Set<string> {
|
||||
return new Set(this._volatileCells)
|
||||
}
|
||||
|
||||
// Управление функциями
|
||||
addFunction(name: string, func: ExcelFunction): void {
|
||||
this._functions[name.toUpperCase()] = func
|
||||
|
||||
// Если добавляем волатильную функцию, обновляем список
|
||||
if (VOLATILE_FUNCTIONS.has(name.toUpperCase())) {
|
||||
this.markAllVolatileCellsDirty()
|
||||
}
|
||||
}
|
||||
|
||||
removeFunction(name: string): void {
|
||||
delete this._functions[name.toUpperCase()]
|
||||
}
|
||||
|
||||
getFunctions(): Record<string, ExcelFunction> {
|
||||
return this._functions
|
||||
}
|
||||
|
||||
// Помечаем все волатильные ячейки как "грязные"
|
||||
private markAllVolatileCellsDirty(): void {
|
||||
for (const volatileCell of this._volatileCells) {
|
||||
this.markDirty(volatileCell)
|
||||
}
|
||||
}
|
||||
|
||||
// Сериализация
|
||||
toJSON(): Record<string, Record<string, CellValue>> {
|
||||
const result: Record<string, Record<string, CellValue>> = {}
|
||||
for (const [name, sheet] of this.sheets) {
|
||||
result[name] = sheet.toJSON()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fromJSON(data: Record<string, Record<string, CellValue>>): void {
|
||||
for (const [sheetName, sheetData] of Object.entries(data)) {
|
||||
if (!this.sheets.has(sheetName)) {
|
||||
this.addSheet(sheetName)
|
||||
}
|
||||
this.sheets.get(sheetName)!.fromJSON(sheetData)
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка всех данных
|
||||
clear(): void {
|
||||
this.sheets.clear()
|
||||
this._dirty.clear()
|
||||
this._volatileCells.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Утилиты для работы с координатами ячеек
|
||||
export class CellUtils {
|
||||
static columnToIndex(column: string): number {
|
||||
let result = 0
|
||||
for (let i = 0; i < column.length; i++) {
|
||||
result = result * 26 + (column.charCodeAt(i) - 65 + 1)
|
||||
}
|
||||
return result - 1
|
||||
}
|
||||
|
||||
static indexToColumn(index: number): string {
|
||||
let result = ''
|
||||
while (index >= 0) {
|
||||
result = String.fromCharCode(65 + (index % 26)) + result
|
||||
index = Math.floor(index / 26) - 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static parseCellName(cellName: string): { column: string; row: number } {
|
||||
const match = cellName.match(/^([A-Z]+)(\d+)$/)
|
||||
if (!match) {
|
||||
throw new Error(`Invalid cell name: ${cellName}`)
|
||||
}
|
||||
return {
|
||||
column: match[1],
|
||||
row: parseInt(match[2], 10),
|
||||
}
|
||||
}
|
||||
|
||||
static createCellName(column: string | number, row: number): string {
|
||||
const col =
|
||||
typeof column === 'number' ? CellUtils.indexToColumn(column) : column
|
||||
return `${col}${row}`
|
||||
}
|
||||
}
|
||||
|
||||
// Экспорт для удобства использования
|
||||
export { DEFAULT_FUNCTIONS, VOLATILE_FUNCTIONS }
|
||||
121
src/feature/excelEngine/functions.ts
Normal file
121
src/feature/excelEngine/functions.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export type CellValue = string | number | boolean | null | undefined
|
||||
|
||||
export interface ExcelFunction {
|
||||
(...args: any[]): CellValue
|
||||
}
|
||||
|
||||
// Волатильные функции - пересчитываются при любом изменении
|
||||
export const VOLATILE_FUNCTIONS = new Set(['СЛУЧ', 'СЛУЧМЕЖДУ', 'СЛУЧДР'])
|
||||
|
||||
export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
|
||||
СЛУЧ: () => Math.random(),
|
||||
СЛУЧМЕЖДУ: (низ: number, верх: number) =>
|
||||
Math.floor(Math.random() * (верх - низ + 1)) + низ,
|
||||
СЛУЧДР: (низ: number, верх: number, знаков: number = 0) => {
|
||||
// Проверка валидности входных параметров
|
||||
if (
|
||||
typeof низ !== 'number' ||
|
||||
typeof верх !== 'number' ||
|
||||
typeof знаков !== 'number'
|
||||
) {
|
||||
return '#ЗНАЧ!' // Ошибка значения
|
||||
}
|
||||
|
||||
if (!isFinite(низ) || !isFinite(верх) || !isFinite(знаков)) {
|
||||
return '#ЧИСЛО!' // Ошибка числа
|
||||
}
|
||||
|
||||
// Если низ больше верха, меняем местами
|
||||
if (низ > верх) {
|
||||
;[низ, верх] = [верх, низ]
|
||||
}
|
||||
|
||||
// Если низ равен верху, возвращаем это значение
|
||||
if (низ === верх) {
|
||||
const множитель = Math.pow(10, Math.max(0, Math.floor(знаков)))
|
||||
return Math.round(низ * множитель) / множитель
|
||||
}
|
||||
|
||||
// Ограничиваем количество знаков после запятой (максимум 15 для точности JavaScript)
|
||||
const ограниченныеЗнаки = Math.max(0, Math.min(15, Math.floor(знаков)))
|
||||
|
||||
// Генерируем случайное число
|
||||
const случайное = Math.random() * (верх - низ) + низ
|
||||
const множитель = Math.pow(10, ограниченныеЗнаки)
|
||||
|
||||
// Округляем и возвращаем результат
|
||||
const результат = Math.round(случайное * множитель) / множитель
|
||||
|
||||
// Проверяем, что результат в допустимых пределах
|
||||
return Math.max(низ, Math.min(верх, результат))
|
||||
},
|
||||
СУММ: (...args: any[]) => {
|
||||
const flatArgs = args.flat()
|
||||
return flatArgs.reduce(
|
||||
(sum, val) => sum + (typeof val === 'number' ? val : 0),
|
||||
0
|
||||
)
|
||||
},
|
||||
СРЗНАЧ: (...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
|
||||
},
|
||||
СЧЁТ: (...args: any[]) => {
|
||||
const flatArgs = args.flat()
|
||||
return flatArgs.filter(val => typeof val === 'number').length
|
||||
},
|
||||
МАКС: (...args: any[]) => {
|
||||
const flatArgs = args.flat()
|
||||
const validArgs = flatArgs.filter(val => typeof val === 'number')
|
||||
return validArgs.length > 0 ? Math.max(...validArgs) : 0
|
||||
},
|
||||
МИН: (...args: any[]) => {
|
||||
const flatArgs = args.flat()
|
||||
const validArgs = flatArgs.filter(val => typeof val === 'number')
|
||||
return validArgs.length > 0 ? Math.min(...validArgs) : 0
|
||||
},
|
||||
ЕСЛИ: (условие: 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() : ''),
|
||||
|
||||
// Функции округления
|
||||
ОКРУГЛ: (число: number, разряды: number = 0) => {
|
||||
const factor = Math.pow(10, разряды)
|
||||
return Math.round(число * factor) / factor
|
||||
},
|
||||
ОКРУГЛВВЕРХ: (число: number, разряды: number = 0) => {
|
||||
const factor = Math.pow(10, разряды)
|
||||
return Math.ceil(число * factor) / factor
|
||||
},
|
||||
ОКРУГЛВНИЗ: (число: number, разряды: number = 0) => {
|
||||
const factor = Math.pow(10, разряды)
|
||||
return Math.floor(число * factor) / factor
|
||||
},
|
||||
ОКРВВЕРХ: (число: number, кратное: number = 1) => {
|
||||
if (кратное === 0) return 0
|
||||
return Math.ceil(число / кратное) * кратное
|
||||
},
|
||||
ОКРВНИЗ: (число: number, кратное: number = 1) => {
|
||||
if (кратное === 0) return 0
|
||||
return Math.floor(число / кратное) * кратное
|
||||
},
|
||||
ЦЕЛОЕ: (число: number) => Math.floor(число),
|
||||
ОТБР: (число: number, разряды: number = 0) => {
|
||||
const factor = Math.pow(10, разряды)
|
||||
return Math.trunc(число * factor) / factor
|
||||
},
|
||||
ОКРУГЛТ: (число: number, кратное: number) => {
|
||||
if (кратное === 0) return 0
|
||||
return Math.round(число / кратное) * кратное
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user