This commit is contained in:
Artyom Tsyrulnikov
2025-07-13 16:51:51 +03:00
parent 36bdb4b2be
commit e1fe966bcd
8 changed files with 1227 additions and 10 deletions

View File

@@ -1,6 +1,8 @@
// Регулярные выражения для ссылок
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;
@@ -18,14 +20,29 @@ const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
RAND: () => Math.random(),
RANDBETWEEN: (bottom: number, top: number) =>
Math.floor(Math.random() * (top - bottom + 1)) + bottom,
SUM: (...args: number[]) => args.reduce((sum, val) => sum + (val || 0), 0),
AVERAGE: (...args: number[]) => {
const validArgs = args.filter(val => val !== null && val !== undefined);
SUM: (...args: any[]) => {
const flatArgs = args.flat();
return flatArgs.reduce((sum, val) => sum + (typeof val === 'number' ? val : 0), 0);
},
AVERAGE: (...args: any[]) => {
const flatArgs = args.flat();
const validArgs = flatArgs.filter(val => typeof val === 'number');
return validArgs.length > 0 ? validArgs.reduce((sum, val) => sum + val, 0) / validArgs.length : 0;
},
COUNT: (...args: any[]) => args.filter(val => typeof val === 'number').length,
MAX: (...args: number[]) => Math.max(...args.filter(val => val !== null && val !== undefined)),
MIN: (...args: number[]) => Math.min(...args.filter(val => val !== null && val !== undefined)),
COUNT: (...args: any[]) => {
const flatArgs = args.flat();
return flatArgs.filter(val => typeof val === 'number').length;
},
MAX: (...args: any[]) => {
const flatArgs = args.flat();
const validArgs = flatArgs.filter(val => typeof val === 'number');
return validArgs.length > 0 ? Math.max(...validArgs) : 0;
},
MIN: (...args: any[]) => {
const flatArgs = args.flat();
const validArgs = flatArgs.filter(val => typeof val === 'number');
return validArgs.length > 0 ? Math.min(...validArgs) : 0;
},
IF: (condition: boolean, trueValue: CellValue, falseValue: CellValue) =>
condition ? trueValue : falseValue,
AND: (...args: boolean[]) => args.every(Boolean),
@@ -83,6 +100,37 @@ export class Cell {
this.name = name;
}
// Функция для развертывания диапазона в массив значений
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;
}
get qualifiedName(): string {
return `${this.sheet.name}!${this.name}`;
}
@@ -147,6 +195,19 @@ export class Cell {
// Заменяем ссылки на вычисленные значения
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) => {
const cellValue = this.sheet.workbook.getCell(match).evaluate();

63
src/lib/template-data.ts Normal file
View File

@@ -0,0 +1,63 @@
// Пример шаблонных данных для отчета (только простые значения из Excel)
export const exampleTemplateData = {
// Заголовки
A1: 'Отчет о продажах',
A2: 'Период: Январь 2024',
A3: '',
// Таблица данных
A4: 'Товар',
B4: 'Количество',
C4: 'Цена',
D4: 'Сумма',
A5: 'Товар A',
B5: 100,
C5: 1500,
D5: 150000, // Простое значение вместо формулы
A6: 'Товар B',
B6: 50,
C6: 2000,
D6: 100000, // Простое значение вместо формулы
A7: 'Товар C',
B7: 75,
C7: 1200,
D7: 90000, // Простое значение вместо формулы
A8: '',
A9: 'Итого:',
D9: 340000, // Простое значение вместо формулы
// Дополнительные поля для расчетов
A11: 'НДС (20%):',
D11: 68000, // Простое значение вместо формулы
A12: 'Всего к оплате:',
D12: 408000, // Простое значение вместо формулы
// Поля для ссылок на правый лист (пока пустые, формулы можно добавить поверх)
A14: 'Скидка из расчетов:',
D14: '', // Пустое значение, формулу можно добавить поверх
A15: 'Итого со скидкой:',
D15: '' // Пустое значение, формулу можно добавить поверх
};
// Пример данных для листа вычислений
export const exampleCalculationsData = {
A1: 'Расчеты',
A2: 'Скидка:',
B2: '=Report!D9*0.05', // 5% скидка от общей суммы
A4: 'Дополнительные расчеты:',
A5: 'Средняя цена:',
B5: '=AVERAGE(Report!C5:C7)',
A6: 'Максимальная сумма:',
B6: '=MAX(Report!D5:D7)',
A7: 'Минимальная сумма:',
B7: '=MIN(Report!D5:D7)',
};