фикс на функции

This commit is contained in:
Artem Tsyrulnikov
2026-03-26 10:44:11 +03:00
parent 5755b02b38
commit e5ca68423c
7 changed files with 990 additions and 18 deletions

View File

@@ -184,6 +184,156 @@ describe('Workbook — формулы', () => {
})
})
// ==================== ЕСЛИ с операторами сравнения ====================
describe('ЕСЛИ с операторами = >= <>', () => {
it('ЕСЛИ(A1=5, ...) — равенство числу (совпадает)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 5)
sheet.setCell('B1', '=ЕСЛИ(A1=5, "Пять", "Другое")')
expect(wb.getValue('Sheet1!B1')).toBe('Пять')
})
it('ЕСЛИ(A1=5, ...) — равенство числу (не совпадает)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 7)
sheet.setCell('B1', '=ЕСЛИ(A1=5, "Пять", "Другое")')
expect(wb.getValue('Sheet1!B1')).toBe('Другое')
})
it('ЕСЛИ(A1=B1, ...) — сравнение двух ячеек (равны)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 10)
sheet.setCell('B1', 10)
sheet.setCell('C1', '=ЕСЛИ(A1=B1, "Равны", "Не равны")')
expect(wb.getValue('Sheet1!C1')).toBe('Равны')
})
it('ЕСЛИ(A1=B1, ...) — сравнение двух ячеек (не равны)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 10)
sheet.setCell('B1', 20)
sheet.setCell('C1', '=ЕСЛИ(A1=B1, "Равны", "Не равны")')
expect(wb.getValue('Sheet1!C1')).toBe('Не равны')
})
it('ЕСЛИ(A1>=100, ...) — больше или равно (истина)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 100)
sheet.setCell('B1', '=ЕСЛИ(A1>=100, "Сотня+", A1)')
expect(wb.getValue('Sheet1!B1')).toBe('Сотня+')
})
it('ЕСЛИ(A1>=100, ...) — больше или равно (ложь)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 42)
sheet.setCell('B1', '=ЕСЛИ(A1>=100, "Сотня+", A1)')
expect(wb.getValue('Sheet1!B1')).toBe(42)
})
it('ЕСЛИ(A1>10, ...) — строгое больше', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 15)
sheet.setCell('B1', '=ЕСЛИ(A1>10, "Да", "Нет")')
expect(wb.getValue('Sheet1!B1')).toBe('Да')
})
it('ЕСЛИ с И и операторами >= <=', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 5)
sheet.setCell('B1', '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")')
expect(wb.getValue('Sheet1!B1')).toBe('В диапазоне')
})
it('ЕСЛИ с И и операторами >= <= — вне диапазона', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 15)
sheet.setCell('B1', '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")')
expect(wb.getValue('Sheet1!B1')).toBe('Вне')
})
it('ЕСЛИ(A1>10, ...) — строгое больше (ложь)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 5)
sheet.setCell('B1', '=ЕСЛИ(A1>10, "Да", "Нет")')
expect(wb.getValue('Sheet1!B1')).toBe('Нет')
})
it('ЕСЛИ с БОЛЬШЕ — истина', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('B1', '=ЕСЛИ(БОЛЬШЕ(5, 3), 1, 0)')
expect(wb.getValue('Sheet1!B1')).toBe(1)
})
it('ЕСЛИ с БОЛЬШЕ — ложь', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('B1', '=ЕСЛИ(БОЛЬШЕ(1, 3), 1, 0)')
expect(wb.getValue('Sheet1!B1')).toBe(0)
})
it('ЕСЛИ с равенством текста в ячейке', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 'Готово')
sheet.setCell('B1', '=ЕСЛИ(A1="Готово", "✓", "✗")')
expect(wb.getValue('Sheet1!B1')).toBe('✓')
})
it('ЕСЛИ с неравенством текста в ячейке', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 'В работе')
sheet.setCell('B1', '=ЕСЛИ(A1="Готово", "✓", "✗")')
expect(wb.getValue('Sheet1!B1')).toBe('✗')
})
})
describe('Оператор <> (не равно)', () => {
it('ЕСЛИ(A1<>0, ...) — не равно (истина)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 5)
sheet.setCell('B1', '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")')
expect(wb.getValue('Sheet1!B1')).toBe('Не ноль')
})
it('ЕСЛИ(A1<>0, ...) — не равно (ложь)', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 0)
sheet.setCell('B1', '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")')
expect(wb.getValue('Sheet1!B1')).toBe('Ноль')
})
it('ЕСЛИ(A1<>B1, ...) — две ячейки не равны', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 10)
sheet.setCell('B1', 20)
sheet.setCell('C1', '=ЕСЛИ(A1<>B1, "Разные", "Одинаковые")')
expect(wb.getValue('Sheet1!C1')).toBe('Разные')
})
it('ЕСЛИ(A1<>B1, ...) — две ячейки равны', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 10)
sheet.setCell('B1', 10)
sheet.setCell('C1', '=ЕСЛИ(A1<>B1, "Разные", "Одинаковые")')
expect(wb.getValue('Sheet1!C1')).toBe('Одинаковые')
})
it('<> внутри строкового литерала не трогается', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 1)
sheet.setCell('B1', '=ЕСЛИ(A1=1, "a<>b", "нет")')
expect(wb.getValue('Sheet1!B1')).toBe('a<>b')
})
})
describe('Операторы внутри строковых литералов не трогаются', () => {
it('= внутри строкового литерала остаётся как есть', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 1)
sheet.setCell('B1', '=ЕСЛИ(A1=1, "x=y", "нет")')
expect(wb.getValue('Sheet1!B1')).toBe('x=y')
})
})
// ==================== ЕПУСТО ====================
describe('ЕПУСТО в формуле', () => {
@@ -679,3 +829,130 @@ describe('CellUtils', () => {
expect(CellUtils.createCellName('B', 3)).toBe('B3')
})
})
// ==================== Edge Cases ====================
describe('Edge Cases функций через движок', () => {
// 1. ОКРУГЛ с отрицательными разрядами: ОКРУГЛ(1234, -2) = 1200 в Excel
describe('ОКРУГЛ с отрицательными разрядами', () => {
it('ОКРУГЛ(1234, -2) должно дать 1200', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=ОКРУГЛ(1234, -2)')
expect(wb.getValue('Sheet1!A1')).toBe(1200)
})
it('ОКРУГЛ(1550, -3) должно дать 2000', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=ОКРУГЛ(1550, -3)')
expect(wb.getValue('Sheet1!A1')).toBe(2000)
})
})
// 2. ЕСЛИ с числом как условие (0 = ложь, не-0 = истина)
describe('ЕСЛИ с числовым условием', () => {
it('0 как ложь', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 0)
sheet.setCell('B1', '=ЕСЛИ(A1, "да", "нет")')
expect(wb.getValue('Sheet1!B1')).toBe('нет')
})
it('ненулевое число как истина', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 42)
sheet.setCell('B1', '=ЕСЛИ(A1, "да", "нет")')
expect(wb.getValue('Sheet1!B1')).toBe('да')
})
})
// 3. СЦЕПИТЬ с числами
describe('СЦЕПИТЬ с числами', () => {
it('число конвертируется в строку', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 42)
sheet.setCell('B1', '=СЦЕПИТЬ("итого: ", A1)')
expect(wb.getValue('Sheet1!B1')).toBe('итого: 42')
})
})
// 4. Несколько = в одной формуле
describe('Несколько = в одной формуле', () => {
it('И(A1=1, B1=2) — оба true', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 1)
sheet.setCell('B1', 2)
sheet.setCell('C1', '=ЕСЛИ(И(A1=1, B1=2), "да", "нет")')
expect(wb.getValue('Sheet1!C1')).toBe('да')
})
it('И(A1=1, B1=2) — одно false', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 1)
sheet.setCell('B1', 99)
sheet.setCell('C1', '=ЕСЛИ(И(A1=1, B1=2), "да", "нет")')
expect(wb.getValue('Sheet1!C1')).toBe('нет')
})
})
// 5. Пустая ячейка в сравнении (движок подставляет 0)
describe('Пустая ячейка в сравнении с =', () => {
it('пустая ячейка = 0', () => {
const { wb, sheet } = createWorkbookWithSheet()
// A1 не установлена — пустая, движок подставит 0
sheet.setCell('B1', '=ЕСЛИ(A1=0, "пусто/ноль", "есть значение")')
expect(wb.getValue('Sheet1!B1')).toBe('пусто/ноль')
})
})
// 6. СТЕПЕНЬ(0, 0) — в Excel = 1
describe('СТЕПЕНЬ edge cases', () => {
it('СТЕПЕНЬ(0, 0) = 1', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=СТЕПЕНЬ(0, 0)')
expect(wb.getValue('Sheet1!A1')).toBe(1)
})
})
// 7. МАКС — вариадический (как в Excel)
describe('МАКС', () => {
// Обратная совместимость: 2 аргумента (как было раньше)
it('МАКС(3, 7) = 7 — обратная совместимость', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=МАКС(3, 7)')
expect(wb.getValue('Sheet1!A1')).toBe(7)
})
it('МАКС(10, 5) = 10 — обратная совместимость', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=МАКС(10, 5)')
expect(wb.getValue('Sheet1!A1')).toBe(10)
})
it('МАКС(-5, 3) = 3 — обратная совместимость с отрицательными', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=МАКС(-5, 3)')
expect(wb.getValue('Sheet1!A1')).toBe(3)
})
it('МАКС со ссылками на ячейки — обратная совместимость', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', 25)
sheet.setCell('A2', 10)
sheet.setCell('B1', '=МАКС(A1, A2)')
expect(wb.getValue('Sheet1!B1')).toBe(25)
})
// Новое: 3+ аргумента
it('МАКС(3, 7, 100) = 100 — вариадический', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=МАКС(3, 7, 100)')
expect(wb.getValue('Sheet1!A1')).toBe(100)
})
it('МАКС(1, 2, 3, 4, 5) = 5', () => {
const { wb, sheet } = createWorkbookWithSheet()
sheet.setCell('A1', '=МАКС(1, 2, 3, 4, 5)')
expect(wb.getValue('Sheet1!A1')).toBe(5)
})
})
})

View File

@@ -131,7 +131,9 @@ export class Cell {
try {
const cell = this.sheet.workbook.getCell(qualifiedRef)
const raw = cell.raw
return (raw === null || raw === undefined || raw === '') ? 'true' : 'false'
return raw === null || raw === undefined || raw === ''
? 'true'
: 'false'
} catch {
return 'true'
}
@@ -230,6 +232,21 @@ export class Cell {
}
}
// Конвертируем Excel-операторы сравнения в JS-операторы
// Сохраняем строковые литералы, чтобы не трогать операторы внутри них
const strings: string[] = []
code = code.replace(/"[^"]*"/g, m => {
strings.push(m)
return `__STR${strings.length - 1}__`
})
// <> → !== (не равно, как в Excel)
code = code.replace(/<>/g, '!==')
// >= и <= уже валидны в JS, пропускаем
// Одиночное = (не ==, !=, >=, <=) → === (равенство вместо присваивания)
code = code.replace(/(?<![<>!=])=(?!=)/g, '===')
// Восстанавливаем строковые литералы
code = code.replace(/__STR(\d+)__/g, (_, i) => strings[Number(i)])
// Создаём контекст для выполнения
const context = {
__functions__: functions,

View File

@@ -41,14 +41,23 @@ describe('DEFAULT_FUNCTIONS', () => {
})
describe('МАКС', () => {
it('возвращает максимум', () => {
it('возвращает максимум из двух', () => {
expect(DEFAULT_FUNCTIONS.МАКС(3, 7)).toBe(7)
})
it('ошибка при не-числах', () => {
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 1)).toBe('#ЗНАЧ!')
it('возвращает максимум из нескольких', () => {
expect(DEFAULT_FUNCTIONS.МАКС(3, 7, 100)).toBe(100)
})
it('ошибка при Infinity', () => {
expect(DEFAULT_FUNCTIONS.МАКС(Infinity, 1)).toBe('#ЧИСЛО!')
it('работает с массивом', () => {
expect(DEFAULT_FUNCTIONS.МАКС([3, 7, 1])).toBe(7)
})
it('игнорирует не-числа', () => {
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 1)).toBe(1)
})
it('игнорирует Infinity', () => {
expect(DEFAULT_FUNCTIONS.МАКС(Infinity, 1)).toBe(1)
})
it('ошибка без валидных чисел', () => {
expect(DEFAULT_FUNCTIONS.МАКС('a' as any, 'b' as any)).toBe('#ЗНАЧ!')
})
})

View File

@@ -67,18 +67,11 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
const flatArgs = args.flat()
return flatArgs.filter(val => typeof val === 'number').length
},
МАКС: (число1: number, число2: number) => {
// Проверка валидности входных параметров
if (typeof число1 !== 'number' || typeof число2 !== 'number') {
return '#ЗНАЧ!' // Ошибка значения
}
if (!isFinite(число1) || !isFinite(число2)) {
return '#ЧИСЛО!' // Ошибка числа
}
// Возвращаем максимальное из двух чисел
return Math.max(число1, число2)
МАКС: (...args: any[]) => {
const flatArgs = args.flat()
const validArgs = flatArgs.filter(val => typeof val === 'number' && isFinite(val))
if (validArgs.length === 0) return '#ЗНАЧ!'
return Math.max(...validArgs)
},
МИН: (...args: any[]) => {
const flatArgs = args.flat()