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

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

@@ -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={<StandardsManagementPage />}
/>
<Route
path="functions-wiki"
element={<FunctionsWikiPage />}
/>
</Route>
</Routes>
</TemplateProvider>

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()

View File

@@ -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: 'Количество знаков после запятой (015)', 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: 'Сумма ячеек A1A5' },
],
},
{
name: 'СРЗНАЧ',
category: 'Математические',
description: 'Вычисляет среднее арифметическое числовых значений.',
syntax: 'СРЗНАЧ(число1,число2,...)',
args: [
{ name: 'число1,число2,...', description: 'Числа или диапазоны ячеек' },
],
examples: [
{ formula: '=СРЗНАЧ(10,20,30)', result: '20' },
{ formula: '=СРЗНАЧ(A1:A3)', result: 'Среднее значений A1A3' },
],
},
{
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: 'Минимальное из A1A3' },
],
},
{
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: 'Проверка попадания в диапазон 010' },
],
},
{
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<string, FC<{ className?: string }>> = {
'Случайные числа': Hash,
'Математические': Calculator,
'Округление': Sigma,
'Логические': BookOpen,
'Сравнение': Search,
'Текстовые': Type,
}
const CATEGORY_ORDER = [
'Математические',
'Округление',
'Случайные числа',
'Логические',
'Сравнение',
'Текстовые',
]
export const FunctionsWikiPage: FC = () => {
const navigate = useNavigate()
const [search, setSearch] = useState('')
const [activeCategory, setActiveCategory] = useState<string | null>(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<string, FunctionInfo[]>()
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 (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="sticky top-0 z-10 border-b bg-white shadow-sm">
<div className="mx-auto max-w-5xl px-4 py-3">
<div className="flex items-center gap-3">
<button
onClick={() => navigate(-1)}
className="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div>
<h1 className="text-lg font-semibold text-gray-900">Справочник функций</h1>
<p className="text-sm text-gray-500">
{FUNCTION_CATALOG.length} функций в {CATEGORY_ORDER.length} категориях
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto max-w-5xl px-4 py-6">
{/* Operators reference */}
<div className="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
<h3 className="mb-2 text-sm font-semibold text-blue-800">Операторы сравнения (как в Excel)</h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-3">
<div><code className="font-medium text-blue-700">=</code> <span className="text-gray-600">равно</span></div>
<div><code className="font-medium text-blue-700">{'<>'}</code> <span className="text-gray-600">не равно</span></div>
<div><code className="font-medium text-blue-700">{'>'}</code> <span className="text-gray-600">больше</span></div>
<div><code className="font-medium text-blue-700">{'<'}</code> <span className="text-gray-600">меньше</span></div>
<div><code className="font-medium text-blue-700">{'>='}</code> <span className="text-gray-600">больше или равно</span></div>
<div><code className="font-medium text-blue-700">{'<='}</code> <span className="text-gray-600">меньше или равно</span></div>
</div>
<p className="mt-2 text-xs text-gray-500">Пример: <code className="text-blue-700">=ЕСЛИ(A1{'<>'}0, "Не ноль", "Ноль")</code></p>
</div>
{/* Search & Filters */}
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Поиск функций..."
value={search}
onChange={e => 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"
/>
</div>
<div className="flex flex-wrap gap-1.5">
<button
onClick={() => setActiveCategory(null)}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
!activeCategory
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
Все
</button>
{CATEGORY_ORDER.map(cat => {
const Icon = CATEGORY_ICONS[cat]
return (
<button
key={cat}
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
className={`flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors ${
activeCategory === cat
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{Icon && <Icon className="h-3 w-3" />}
{cat}
</button>
)
})}
</div>
</div>
{/* Results */}
{grouped.length === 0 ? (
<div className="py-12 text-center text-gray-500">
Функции не найдены. Попробуйте изменить запрос.
</div>
) : (
<div className="space-y-8">
{grouped.map(({ category, functions }) => {
const Icon = CATEGORY_ICONS[category]
return (
<section key={category}>
<div className="mb-3 flex items-center gap-2">
{Icon && <Icon className="h-5 w-5 text-gray-400" />}
<h2 className="text-base font-semibold text-gray-800">{category}</h2>
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500">
{functions.length}
</span>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{functions.map(fn => (
<FunctionCard key={fn.name} fn={fn} />
))}
</div>
</section>
)
})}
</div>
)}
</div>
</div>
)
}
const FunctionCard: FC<{ fn: FunctionInfo }> = ({ fn }) => {
return (
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md">
<div className="mb-2 flex items-start justify-between gap-2">
<code className="rounded bg-blue-50 px-2 py-0.5 text-sm font-bold text-blue-700">
{fn.name}
</code>
{fn.notes && (
<span className="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-600">
{fn.notes.length > 30 ? fn.notes.slice(0, 30) + '...' : fn.notes}
</span>
)}
</div>
<p className="mb-2 text-sm text-gray-600">{fn.description}</p>
{/* Syntax */}
<div className="mb-2">
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
Синтаксис
</div>
<code className="block rounded bg-gray-50 px-2 py-1 text-xs text-gray-700">
={fn.syntax}
</code>
</div>
{/* Args */}
{fn.args.length > 0 && (
<div className="mb-2">
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
Аргументы
</div>
<div className="space-y-0.5">
{fn.args.map(arg => (
<div key={arg.name} className="flex gap-1.5 text-xs">
<code className="shrink-0 font-medium text-gray-700">{arg.name}</code>
{arg.optional && (
<span className="shrink-0 text-[10px] text-gray-400">(необяз.)</span>
)}
<span className="text-gray-500"> {arg.description}</span>
</div>
))}
</div>
</div>
)}
{/* Examples */}
<div>
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-gray-400">
Примеры
</div>
<div className="space-y-1">
{fn.examples.map((ex, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
<code className="rounded bg-green-50 px-1.5 py-0.5 text-green-700">{ex.formula}</code>
<span className="text-gray-400"></span>
<span className="text-gray-600">{ex.result}</span>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -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,