diff --git a/src/app/App.tsx b/src/app/App.tsx index 61bc06f..4ccec26 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -12,6 +12,7 @@ import { } from '@/lib/hooks/useToast' import { ElementsCreation, + FunctionsWikiPage, ProtocolCreation, StandardsManagementPage, TemplateEditPage, @@ -59,6 +60,10 @@ const App: FC = () => { path="standards-management" element={} /> + } + /> diff --git a/src/feature/excelEngine/excelEngine.test.ts b/src/feature/excelEngine/excelEngine.test.ts index 3c222f7..4d4d801 100644 --- a/src/feature/excelEngine/excelEngine.test.ts +++ b/src/feature/excelEngine/excelEngine.test.ts @@ -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) + }) + }) +}) diff --git a/src/feature/excelEngine/excelEngine.ts b/src/feature/excelEngine/excelEngine.ts index b65b745..90ccd51 100644 --- a/src/feature/excelEngine/excelEngine.ts +++ b/src/feature/excelEngine/excelEngine.ts @@ -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, diff --git a/src/feature/excelEngine/functions.test.ts b/src/feature/excelEngine/functions.test.ts index ff39656..41d4454 100644 --- a/src/feature/excelEngine/functions.test.ts +++ b/src/feature/excelEngine/functions.test.ts @@ -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('#ЗНАЧ!') }) }) diff --git a/src/feature/excelEngine/functions.ts b/src/feature/excelEngine/functions.ts index 0a81b14..2f0717a 100644 --- a/src/feature/excelEngine/functions.ts +++ b/src/feature/excelEngine/functions.ts @@ -67,18 +67,11 @@ export const DEFAULT_FUNCTIONS: Record = { 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() diff --git a/src/page/functions-wiki/Page.tsx b/src/page/functions-wiki/Page.tsx new file mode 100644 index 0000000..2c40388 --- /dev/null +++ b/src/page/functions-wiki/Page.tsx @@ -0,0 +1,669 @@ +import { ArrowLeft, BookOpen, Calculator, Hash, Search, Sigma, Type } from 'lucide-react' +import { FC, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' + +interface FunctionInfo { + name: string + category: string + description: string + syntax: string + args: { name: string; description: string; optional?: boolean }[] + examples: { formula: string; result: string }[] + notes?: string +} + +const FUNCTION_CATALOG: FunctionInfo[] = [ + // Случайные числа + { + name: 'СЛУЧ', + category: 'Случайные числа', + description: 'Возвращает случайное число от 0 до 1.', + syntax: 'СЛУЧ()', + args: [], + examples: [ + { formula: '=СЛУЧ()', result: '0.7231...' }, + ], + notes: 'Волатильная функция — пересчитывается при каждом изменении.', + }, + { + name: 'СЛУЧМЕЖДУ', + category: 'Случайные числа', + description: 'Возвращает случайное целое число в заданном диапазоне.', + syntax: 'СЛУЧМЕЖДУ(низ,верх)', + args: [ + { name: 'низ', description: 'Нижняя граница диапазона' }, + { name: 'верх', description: 'Верхняя граница диапазона' }, + ], + examples: [ + { formula: '=СЛУЧМЕЖДУ(1,10)', result: '7' }, + { formula: '=СЛУЧМЕЖДУ(100,200)', result: '142' }, + ], + notes: 'Волатильная функция.', + }, + { + name: 'СЛУЧДР', + category: 'Случайные числа', + description: 'Возвращает случайное дробное число в заданном диапазоне с указанным количеством знаков после запятой.', + syntax: 'СЛУЧДР(низ,верх,знаков)', + args: [ + { name: 'низ', description: 'Нижняя граница диапазона' }, + { name: 'верх', description: 'Верхняя граница диапазона' }, + { name: 'знаков', description: 'Количество знаков после запятой (0–15)', optional: true }, + ], + examples: [ + { formula: '=СЛУЧДР(1,10,2)', result: '4.73' }, + { formula: '=СЛУЧДР(0,1,4)', result: '0.5182' }, + ], + notes: 'Волатильная функция. Если низ > верх, значения меняются местами автоматически.', + }, + + // Математические + { + name: 'СУММ', + category: 'Математические', + description: 'Суммирует числовые значения. Нечисловые значения игнорируются.', + syntax: 'СУММ(число1,число2,...)', + args: [ + { name: 'число1,число2,...', description: 'Числа или диапазоны ячеек для суммирования' }, + ], + examples: [ + { formula: '=СУММ(1,2,3)', result: '6' }, + { formula: '=СУММ(A1:A5)', result: 'Сумма ячеек A1–A5' }, + ], + }, + { + name: 'СРЗНАЧ', + category: 'Математические', + description: 'Вычисляет среднее арифметическое числовых значений.', + syntax: 'СРЗНАЧ(число1,число2,...)', + args: [ + { name: 'число1,число2,...', description: 'Числа или диапазоны ячеек' }, + ], + examples: [ + { formula: '=СРЗНАЧ(10,20,30)', result: '20' }, + { formula: '=СРЗНАЧ(A1:A3)', result: 'Среднее значений A1–A3' }, + ], + }, + { + name: 'СЧЁТ', + category: 'Математические', + description: 'Подсчитывает количество числовых значений в аргументах.', + syntax: 'СЧЁТ(значение1,значение2,...)', + args: [ + { name: 'значение1,значение2,...', description: 'Значения или диапазоны ячеек' }, + ], + examples: [ + { formula: '=СЧЁТ(1,"текст",3,5)', result: '3' }, + ], + }, + { + name: 'МАКС', + category: 'Математические', + description: 'Возвращает наибольшее из двух чисел.', + syntax: 'МАКС(число1,число2)', + args: [ + { name: 'число1', description: 'Первое число' }, + { name: 'число2', description: 'Второе число' }, + ], + examples: [ + { formula: '=МАКС(10,25)', result: '25' }, + { formula: '=МАКС(-5,3)', result: '3' }, + ], + }, + { + name: 'МИН', + category: 'Математические', + description: 'Возвращает наименьшее числовое значение из аргументов.', + syntax: 'МИН(число1,число2,...)', + args: [ + { name: 'число1,число2,...', description: 'Числа или диапазоны ячеек' }, + ], + examples: [ + { formula: '=МИН(10,5,20)', result: '5' }, + { formula: '=МИН(A1:A3)', result: 'Минимальное из A1–A3' }, + ], + }, + { + name: 'ABS', + category: 'Математические', + description: 'Возвращает абсолютное значение (модуль) числа.', + syntax: 'ABS(число)', + args: [ + { name: 'число', description: 'Число, модуль которого нужно найти' }, + ], + examples: [ + { formula: '=ABS(-15)', result: '15' }, + { formula: '=ABS(7)', result: '7' }, + ], + }, + { + name: 'КОРЕНЬ', + category: 'Математические', + description: 'Возвращает квадратный корень числа.', + syntax: 'КОРЕНЬ(число)', + args: [ + { name: 'число', description: 'Неотрицательное число' }, + ], + examples: [ + { formula: '=КОРЕНЬ(25)', result: '5' }, + { formula: '=КОРЕНЬ(2)', result: '1.4142...' }, + ], + notes: 'Возвращает #ЧИСЛО! для отрицательных чисел.', + }, + { + name: 'СТЕПЕНЬ', + category: 'Математические', + description: 'Возводит число в указанную степень.', + syntax: 'СТЕПЕНЬ(число,степень)', + args: [ + { name: 'число', description: 'Основание' }, + { name: 'степень', description: 'Показатель степени' }, + ], + examples: [ + { formula: '=СТЕПЕНЬ(2,10)', result: '1024' }, + { formula: '=СТЕПЕНЬ(5,2)', result: '25' }, + ], + }, + + // Округление + { + name: 'ОКРУГЛ', + category: 'Округление', + description: 'Округляет число до указанного количества разрядов.', + syntax: 'ОКРУГЛ(число,разряды)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'разряды', description: 'Количество десятичных разрядов', optional: true }, + ], + examples: [ + { formula: '=ОКРУГЛ(3.456,2)', result: '3.46' }, + { formula: '=ОКРУГЛ(3.456,0)', result: '3' }, + ], + }, + { + name: 'ОКРУГЛВВЕРХ', + category: 'Округление', + description: 'Округляет число вверх (от нуля) до указанного количества разрядов.', + syntax: 'ОКРУГЛВВЕРХ(число,разряды)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'разряды', description: 'Количество десятичных разрядов', optional: true }, + ], + examples: [ + { formula: '=ОКРУГЛВВЕРХ(3.21,1)', result: '3.3' }, + { formula: '=ОКРУГЛВВЕРХ(3.1,0)', result: '4' }, + ], + }, + { + name: 'ОКРУГЛВНИЗ', + category: 'Округление', + description: 'Округляет число вниз (к нулю) до указанного количества разрядов.', + syntax: 'ОКРУГЛВНИЗ(число,разряды)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'разряды', description: 'Количество десятичных разрядов', optional: true }, + ], + examples: [ + { formula: '=ОКРУГЛВНИЗ(3.89,1)', result: '3.8' }, + { formula: '=ОКРУГЛВНИЗ(3.9,0)', result: '3' }, + ], + }, + { + name: 'ОКРВВЕРХ', + category: 'Округление', + description: 'Округляет число вверх до ближайшего кратного.', + syntax: 'ОКРВВЕРХ(число,кратное)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'кратное', description: 'Кратное, до которого округляется', optional: true }, + ], + examples: [ + { formula: '=ОКРВВЕРХ(7,5)', result: '10' }, + { formula: '=ОКРВВЕРХ(23,10)', result: '30' }, + ], + }, + { + name: 'ОКРВНИЗ', + category: 'Округление', + description: 'Округляет число вниз до ближайшего кратного.', + syntax: 'ОКРВНИЗ(число,кратное)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'кратное', description: 'Кратное, до которого округляется', optional: true }, + ], + examples: [ + { formula: '=ОКРВНИЗ(7,5)', result: '5' }, + { formula: '=ОКРВНИЗ(27,10)', result: '20' }, + ], + }, + { + name: 'ЦЕЛОЕ', + category: 'Округление', + description: 'Округляет число вниз до ближайшего целого.', + syntax: 'ЦЕЛОЕ(число)', + args: [ + { name: 'число', description: 'Число для округления' }, + ], + examples: [ + { formula: '=ЦЕЛОЕ(7.8)', result: '7' }, + { formula: '=ЦЕЛОЕ(-3.2)', result: '-4' }, + ], + }, + { + name: 'ОТБР', + category: 'Округление', + description: 'Отбрасывает дробную часть числа до указанного количества разрядов.', + syntax: 'ОТБР(число,разряды)', + args: [ + { name: 'число', description: 'Число для усечения' }, + { name: 'разряды', description: 'Количество десятичных разрядов', optional: true }, + ], + examples: [ + { formula: '=ОТБР(7.89,1)', result: '7.8' }, + { formula: '=ОТБР(-3.7,0)', result: '-3' }, + ], + }, + { + name: 'ОКРУГЛТ', + category: 'Округление', + description: 'Округляет число до ближайшего кратного.', + syntax: 'ОКРУГЛТ(число,кратное)', + args: [ + { name: 'число', description: 'Число для округления' }, + { name: 'кратное', description: 'Кратное, до которого округляется' }, + ], + examples: [ + { formula: '=ОКРУГЛТ(7,3)', result: '6' }, + { formula: '=ОКРУГЛТ(10,3)', result: '9' }, + ], + }, + + // Логические + { + name: 'ЕСЛИ', + category: 'Логические', + description: 'Возвращает одно значение, если условие истинно, и другое — если ложно.', + syntax: 'ЕСЛИ(условие,истина,ложь)', + args: [ + { name: 'условие', description: 'Логическое выражение для проверки' }, + { name: 'истина', description: 'Значение, если условие истинно' }, + { name: 'ложь', description: 'Значение, если условие ложно' }, + ], + examples: [ + { formula: '=ЕСЛИ(A1>10, "Да", "Нет")', result: '"Да" если A1 > 10' }, + { formula: '=ЕСЛИ(A1=5, "Пять", "Другое")', result: '"Пять" если A1 равно 5' }, + { formula: '=ЕСЛИ(A1=B1, "Равны", "Не равны")', result: 'Сравнение двух ячеек на равенство' }, + { formula: '=ЕСЛИ(A1>=100, "Сотня+", A1)', result: '"Сотня+" если A1 >= 100, иначе значение A1' }, + { formula: '=ЕСЛИ(БОЛЬШЕ(5, 3), 1, 0)', result: '1' }, + { formula: '=ЕСЛИ(A1="Готово", "✓", "✗")', result: '"✓" если A1 содержит текст "Готово"' }, + { formula: '=ЕСЛИ(A1<>0, "Не ноль", "Ноль")', result: '"Не ноль" если A1 не равно 0' }, + { formula: '=ЕСЛИ(И(A1>=0, A1<=10), "В диапазоне", "Вне")', result: 'Проверка попадания в диапазон 0–10' }, + ], + }, + { + name: 'И', + category: 'Логические', + description: 'Возвращает ИСТИНА, если все аргументы истинны.', + syntax: 'И(условие1,условие2,...)', + args: [ + { name: 'условие1,условие2,...', description: 'Логические выражения' }, + ], + examples: [ + { formula: '=И(БОЛЬШЕ(5;3),МЕНЬШЕ(2;4))', result: 'ИСТИНА' }, + { formula: '=ЕСЛИ(И(A1>0,A1<100),"ОК","Ошибка")', result: '"ОК" если 0 < A1 < 100' }, + ], + }, + { + name: 'ИЛИ', + category: 'Логические', + description: 'Возвращает ИСТИНА, если хотя бы один аргумент истинен.', + syntax: 'ИЛИ(условие1,условие2,...)', + args: [ + { name: 'условие1,условие2,...', description: 'Логические выражения' }, + ], + examples: [ + { formula: '=ИЛИ(БОЛЬШЕ(1;5),БОЛЬШЕ(10;5))', result: 'ИСТИНА' }, + ], + }, + { + name: 'НЕ', + category: 'Логические', + description: 'Инвертирует логическое значение.', + syntax: 'НЕ(значение)', + args: [ + { name: 'значение', description: 'Логическое значение для инверсии' }, + ], + examples: [ + { formula: '=НЕ(ИСТИНА)', result: 'ЛОЖЬ' }, + { formula: '=НЕ(БОЛЬШЕ(1;5))', result: 'ИСТИНА' }, + ], + }, + + // Сравнение + { + name: 'БОЛЬШЕ', + category: 'Сравнение', + description: 'Проверяет, больше ли первое число второго.', + syntax: 'БОЛЬШЕ(число1,число2)', + args: [ + { name: 'число1', description: 'Первое число' }, + { name: 'число2', description: 'Второе число' }, + ], + examples: [ + { formula: '=БОЛЬШЕ(10,5)', result: 'ИСТИНА' }, + { formula: '=БОЛЬШЕ(3,7)', result: 'ЛОЖЬ' }, + ], + }, + { + name: 'МЕНЬШЕ', + category: 'Сравнение', + description: 'Проверяет, меньше ли первое число второго.', + syntax: 'МЕНЬШЕ(число1,число2)', + args: [ + { name: 'число1', description: 'Первое число' }, + { name: 'число2', description: 'Второе число' }, + ], + examples: [ + { formula: '=МЕНЬШЕ(3,7)', result: 'ИСТИНА' }, + { formula: '=МЕНЬШЕ(10,5)', result: 'ЛОЖЬ' }, + ], + }, + { + name: 'ЕПУСТО', + category: 'Сравнение', + description: 'Проверяет, является ли значение пустым (null, undefined или пустая строка).', + syntax: 'ЕПУСТО(значение)', + args: [ + { name: 'значение', description: 'Значение для проверки' }, + ], + examples: [ + { formula: '=ЕПУСТО(A1)', result: 'ИСТИНА если A1 пуста' }, + { formula: '=ЕСЛИ(ЕПУСТО(A1),"Пусто",A1)', result: 'Значение A1 или "Пусто"' }, + ], + }, + + // Текстовые + { + name: 'СЦЕПИТЬ', + category: 'Текстовые', + description: 'Объединяет несколько текстовых строк в одну.', + syntax: 'СЦЕПИТЬ(текст1,текст2,...)', + args: [ + { name: 'текст1,текст2,...', description: 'Строки для объединения' }, + ], + examples: [ + { formula: '=СЦЕПИТЬ("Привет"," ","мир")', result: '"Привет мир"' }, + { formula: '=СЦЕПИТЬ(A1," - ",B1)', result: 'Значения A1 и B1 через дефис' }, + ], + }, + { + name: 'ДЛСТР', + category: 'Текстовые', + description: 'Возвращает длину текстовой строки.', + syntax: 'ДЛСТР(текст)', + args: [ + { name: 'текст', description: 'Строка для подсчёта длины' }, + ], + examples: [ + { formula: '=ДЛСТР("Привет")', result: '6' }, + ], + }, + { + name: 'ПРОПИСН', + category: 'Текстовые', + description: 'Преобразует текст в верхний регистр.', + syntax: 'ПРОПИСН(текст)', + args: [ + { name: 'текст', description: 'Строка для преобразования' }, + ], + examples: [ + { formula: '=ПРОПИСН("привет")', result: '"ПРИВЕТ"' }, + ], + }, + { + name: 'СТРОЧН', + category: 'Текстовые', + description: 'Преобразует текст в нижний регистр.', + syntax: 'СТРОЧН(текст)', + args: [ + { name: 'текст', description: 'Строка для преобразования' }, + ], + examples: [ + { formula: '=СТРОЧН("ПРИВЕТ")', result: '"привет"' }, + ], + }, + { + name: 'СЖПРОБЕЛЫ', + category: 'Текстовые', + description: 'Удаляет начальные и конечные пробелы из текста.', + syntax: 'СЖПРОБЕЛЫ(текст)', + args: [ + { name: 'текст', description: 'Строка для обработки' }, + ], + examples: [ + { formula: '=СЖПРОБЕЛЫ(" Привет ")', result: '"Привет"' }, + ], + }, +] + +const CATEGORY_ICONS: Record> = { + 'Случайные числа': Hash, + 'Математические': Calculator, + 'Округление': Sigma, + 'Логические': BookOpen, + 'Сравнение': Search, + 'Текстовые': Type, +} + +const CATEGORY_ORDER = [ + 'Математические', + 'Округление', + 'Случайные числа', + 'Логические', + 'Сравнение', + 'Текстовые', +] + +export const FunctionsWikiPage: FC = () => { + const navigate = useNavigate() + const [search, setSearch] = useState('') + const [activeCategory, setActiveCategory] = useState(null) + + const filtered = useMemo(() => { + const q = search.toLowerCase().trim() + return FUNCTION_CATALOG.filter(fn => { + const matchesSearch = !q || fn.name.toLowerCase().includes(q) || fn.description.toLowerCase().includes(q) + const matchesCategory = !activeCategory || fn.category === activeCategory + return matchesSearch && matchesCategory + }) + }, [search, activeCategory]) + + const grouped = useMemo(() => { + const map = new Map() + for (const fn of filtered) { + const list = map.get(fn.category) || [] + list.push(fn) + map.set(fn.category, list) + } + return CATEGORY_ORDER + .filter(cat => map.has(cat)) + .map(cat => ({ category: cat, functions: map.get(cat)! })) + }, [filtered]) + + return ( +
+ {/* Header */} +
+
+
+ +
+

Справочник функций

+

+ {FUNCTION_CATALOG.length} функций в {CATEGORY_ORDER.length} категориях +

+
+
+
+
+ +
+ {/* Operators reference */} +
+

Операторы сравнения (как в Excel)

+
+
= равно
+
{'<>'} не равно
+
{'>'} больше
+
{'<'} меньше
+
{'>='} больше или равно
+
{'<='} меньше или равно
+
+

Пример: =ЕСЛИ(A1{'<>'}0, "Не ноль", "Ноль")

+
+ + {/* Search & Filters */} +
+
+ + setSearch(e.target.value)} + className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> +
+
+ + {CATEGORY_ORDER.map(cat => { + const Icon = CATEGORY_ICONS[cat] + return ( + + ) + })} +
+
+ + {/* Results */} + {grouped.length === 0 ? ( +
+ Функции не найдены. Попробуйте изменить запрос. +
+ ) : ( +
+ {grouped.map(({ category, functions }) => { + const Icon = CATEGORY_ICONS[category] + return ( +
+
+ {Icon && } +

{category}

+ + {functions.length} + +
+
+ {functions.map(fn => ( + + ))} +
+
+ ) + })} +
+ )} +
+
+ ) +} + +const FunctionCard: FC<{ fn: FunctionInfo }> = ({ fn }) => { + return ( +
+
+ + {fn.name} + + {fn.notes && ( + + {fn.notes.length > 30 ? fn.notes.slice(0, 30) + '...' : fn.notes} + + )} +
+ +

{fn.description}

+ + {/* Syntax */} +
+
+ Синтаксис +
+ + ={fn.syntax} + +
+ + {/* Args */} + {fn.args.length > 0 && ( +
+
+ Аргументы +
+
+ {fn.args.map(arg => ( +
+ {arg.name} + {arg.optional && ( + (необяз.) + )} + — {arg.description} +
+ ))} +
+
+ )} + + {/* Examples */} +
+
+ Примеры +
+
+ {fn.examples.map((ex, i) => ( +
+ {ex.formula} + + {ex.result} +
+ ))} +
+
+
+ ) +} diff --git a/src/page/index.ts b/src/page/index.ts index 196bec5..e4b7a99 100644 --- a/src/page/index.ts +++ b/src/page/index.ts @@ -1,3 +1,4 @@ +import { FunctionsWikiPage } from './functions-wiki/Page' import { StandardsManagementPage } from './standards-management/Page' import { TemplateEditPage } from './templates/edit/Page' import { ElementsCreation } from './templates/elements/Page' @@ -6,6 +7,7 @@ import { ProtocolCreation } from './templates/protocols/Page' export { ElementsCreation, + FunctionsWikiPage, ProtocolCreation, StandardsManagementPage, TemplateEditPage,