Translate QExcell shared formulas on import

This commit is contained in:
Курнат Андрей
2026-06-01 08:09:23 +03:00
parent 9ad29a184b
commit 66406c2c0c
3 changed files with 92 additions and 8 deletions
+31
View File
@@ -28,6 +28,37 @@ describe("excellOffice OOXML helpers", () => {
expect(xlsxCellToText({ r: "C2", f: { "#text": "A2*2" }, v: "8" }, [])).toBe("=A2*2");
});
it("сдвигает относительные ссылки XLSX shared formulas при импорте", () => {
const cells = worksheetXmlToCells(
{
worksheet: {
sheetData: {
row: [
{
r: "1",
c: [{ r: "C1", f: { t: "shared", si: "0", ref: "C1:D2", "#text": "A1+B$1+$A1+$B$2+'A1 Sheet'!A1&\"A1\"" }, v: "0" }]
},
{
r: "2",
c: [
{ r: "C2", f: { t: "shared", si: "0" }, v: "0" },
{ r: "D2", f: { t: "shared", si: "0" }, v: "0" }
]
}
]
}
}
},
[]
);
expect(cells).toEqual({
C1: "=A1+B$1+$A1+$B$2+'A1 Sheet'!A1&\"A1\"",
C2: "=A2+B$1+$A2+$B$2+'A1 Sheet'!A2&\"A1\"",
D2: "=B2+C$1+$A2+$B$2+'A1 Sheet'!B2&\"A1\""
});
});
it("читает shared string, inline string, число и boolean", () => {
expect(xlsxCellToText({ r: "A1", t: "s", v: "1" }, ["Первый", "Второй"])).toBe("Второй");
expect(xlsxCellToText({ r: "A2", t: "inlineStr", is: { t: "Текст" } }, [])).toBe("Текст");
+59 -6
View File
@@ -64,6 +64,13 @@ interface WorksheetCellRecord {
cell: XmlRecord;
}
interface SharedFormulaDefinition {
formula: string;
baseAddress: string;
}
type SharedFormulaMap = Record<string, string | SharedFormulaDefinition>;
export interface CellStyleRegistry {
formats: CellFormat[];
idsByKey: Record<string, number>;
@@ -223,20 +230,20 @@ export function parsedWorksheetToSheet(
export function worksheetXmlToCells(worksheetXml: unknown, sharedStrings: string[]) {
const cells: Record<string, string> = {};
const sharedFormulas: Record<string, string> = {};
const sharedFormulas: SharedFormulaMap = {};
const parsedCells = worksheetCellRecords(worksheetXml);
parsedCells.forEach(({ cell }) => {
parsedCells.forEach(({ address, cell }) => {
const formulaRecord = asRecord(cell.f);
const sharedFormulaId = sharedFormulaKey(formulaRecord);
const formulaText = formulaNodeText(cell.f);
if (sharedFormulaId && formulaText) {
sharedFormulas[sharedFormulaId] = formulaText;
sharedFormulas[sharedFormulaId] = { formula: formulaText, baseAddress: address };
}
});
parsedCells.forEach(({ address, cell }) => {
const text = xlsxCellToText(cell, sharedStrings, sharedFormulas);
const text = xlsxCellToText(cell, sharedStrings, sharedFormulas, address);
if (text !== "") {
cells[address] = text;
@@ -435,7 +442,7 @@ function chartRangeFromFormula(formula: string, sheetName: string) {
return normalizeChartRange(withoutSheet.replace(/\$/g, ""));
}
export function xlsxCellToText(cell: XmlRecord, sharedStrings: string[], sharedFormulas: Record<string, string> = {}) {
export function xlsxCellToText(cell: XmlRecord, sharedStrings: string[], sharedFormulas: SharedFormulaMap = {}, address = "") {
const formula = formulaNodeText(cell.f);
if (formula) {
return formula.startsWith("=") ? formula : `=${formula}`;
@@ -443,7 +450,12 @@ export function xlsxCellToText(cell: XmlRecord, sharedStrings: string[], sharedF
const sharedFormulaId = sharedFormulaKey(asRecord(cell.f));
if (sharedFormulaId && sharedFormulas[sharedFormulaId]) {
return `=${sharedFormulas[sharedFormulaId]}`;
const sharedFormula = sharedFormulas[sharedFormulaId];
const formulaText =
typeof sharedFormula === "string"
? sharedFormula
: translateSharedFormula(sharedFormula.formula, sharedFormula.baseAddress, normalizeCellAddress(address || String(cell.r || "")));
return formulaText.startsWith("=") ? formulaText : `=${formulaText}`;
}
const type = typeof cell.t === "string" ? cell.t : "";
@@ -1271,6 +1283,47 @@ function normalizeCellAddress(address: string) {
return isCellAddress(trimmed) ? trimmed : "";
}
function translateSharedFormula(formula: string, baseAddress: string, targetAddress: string) {
const normalizedBase = normalizeCellAddress(baseAddress);
const normalizedTarget = normalizeCellAddress(targetAddress);
if (!normalizedBase || !normalizedTarget || normalizedBase === normalizedTarget) {
return formula;
}
const columnOffset = cellColumnNumber(normalizedTarget) - cellColumnNumber(normalizedBase);
const rowOffset = cellRowNumber(normalizedTarget) - cellRowNumber(normalizedBase);
return formula
.split(/("[^"]*(?:""[^"]*)*"|'[^']*(?:''[^']*)*')/g)
.map((segment, index) => (index % 2 === 1 ? segment : translateFormulaReferences(segment, columnOffset, rowOffset)))
.join("");
}
function translateFormulaReferences(formula: string, columnOffset: number, rowOffset: number) {
return formula.replace(/(^|[^A-Z0-9_])(\$?[A-Z]{1,3}\$?[1-9][0-9]*)(?![A-Z0-9_])/gi, (match, prefix: string, reference: string) => {
const translated = translateFormulaReference(reference, columnOffset, rowOffset);
return translated ? `${prefix}${translated}` : match;
});
}
function translateFormulaReference(reference: string, columnOffset: number, rowOffset: number) {
const match = /^(\$?)([A-Z]{1,3})(\$?)([1-9][0-9]*)$/i.exec(reference);
if (!match) {
return "";
}
const [, columnLock, columnNameValue, rowLock, rowValue] = match;
const columnNumber = cellColumnNumber(`${columnNameValue}1`);
const rowNumber = Number(rowValue);
const translatedColumn = columnLock ? columnNumber : columnNumber + columnOffset;
const translatedRow = rowLock ? rowNumber : rowNumber + rowOffset;
if (translatedColumn < 1 || translatedRow < 1) {
return reference;
}
return `${columnLock}${columnName(translatedColumn)}${rowLock}${translatedRow}`;
}
function normalizeChartRange(range: string) {
const raw = range.trim().replace(/\$/g, "").toUpperCase();
const [start, end = start] = raw.split(":");