@ricsam/excel-to-formula-engine 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/dist/index.cjs +309 -5
- package/dist/index.cjs.map +8 -6
- package/dist/index.mjs +309 -5
- package/dist/index.mjs.map +8 -6
- package/dist/types/index.d.ts +27 -2
- package/dist/types/src/csv/convert.d.ts +31 -0
- package/dist/types/src/csv/parse.d.ts +40 -0
- package/dist/types/src/translate/formula.d.ts +14 -0
- package/dist/types/src/types.d.ts +42 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -905,12 +905,14 @@ function getSupportedFunctions() {
|
|
|
905
905
|
return supportedFunctionNames;
|
|
906
906
|
}
|
|
907
907
|
var XLFN_PREFIX = /_xlfn\.(_xlws\.)?/g;
|
|
908
|
-
var IMPLICIT_INTERSECTION = /(?<![\w$"'\]])@(?=[A-Za-z_\\])/g;
|
|
908
|
+
var IMPLICIT_INTERSECTION = /(?<![\w$"'\]\[])@(?=[A-Za-z_\\])/g;
|
|
909
|
+
var THIS_ROW_REFERENCE = /(?:'((?:[^']|'')+)'|([A-Za-z_\\][\w.\\]*))?\[\[#This ?Row\],\s*\[([^\]]*)\](?:\s*:\s*\[([^\]]*)\])?\]/gi;
|
|
909
910
|
function translateFormula(rawFormula, options = {}) {
|
|
910
911
|
const diagnostics = [];
|
|
911
912
|
let body = rawFormula.startsWith("=") ? rawFormula.slice(1) : rawFormula;
|
|
912
913
|
body = body.replace(XLFN_PREFIX, "");
|
|
913
914
|
body = body.replace(IMPLICIT_INTERSECTION, "");
|
|
915
|
+
body = contractThisRowReferences(body, options.containingTableName);
|
|
914
916
|
for (const name of findUnsupportedFunctions(body)) {
|
|
915
917
|
diagnostics.push({
|
|
916
918
|
severity: "warning",
|
|
@@ -1023,6 +1025,22 @@ function columnIndexToLabel2(index) {
|
|
|
1023
1025
|
}
|
|
1024
1026
|
return label;
|
|
1025
1027
|
}
|
|
1028
|
+
function contractThisRowReferences(formulaBody, containingTableName) {
|
|
1029
|
+
return formulaBody.replace(THIS_ROW_REFERENCE, (whole, quotedTable, bareTable, startCol, endCol) => {
|
|
1030
|
+
const tableName = quotedTable ? String(quotedTable).replace(/''/g, "'") : bareTable ? String(bareTable) : undefined;
|
|
1031
|
+
const columns = endCol === undefined ? `[${startCol}]` : `[${startCol}]:[${endCol}]`;
|
|
1032
|
+
const isPlainSingleColumn = endCol === undefined && /^[^[\]@#'"]+$/.test(String(startCol));
|
|
1033
|
+
const reference = isPlainSingleColumn ? `@${startCol}` : `@${columns}`;
|
|
1034
|
+
if (tableName && containingTableName && tableName.toLowerCase() === containingTableName.toLowerCase()) {
|
|
1035
|
+
return `[${reference}]`;
|
|
1036
|
+
}
|
|
1037
|
+
if (!tableName) {
|
|
1038
|
+
return `[${reference}]`;
|
|
1039
|
+
}
|
|
1040
|
+
const quoted = quotedTable ? `'${String(quotedTable)}'` : tableName;
|
|
1041
|
+
return `${quoted}[${reference}]`;
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1026
1044
|
|
|
1027
1045
|
// src/translate/conditional-format.ts
|
|
1028
1046
|
import {
|
|
@@ -1255,6 +1273,9 @@ async function convertWorkbook(source, options = {}) {
|
|
|
1255
1273
|
for (const entry of selected) {
|
|
1256
1274
|
const worksheet = parseWorksheet(pkg, entry.partPath, sharedStrings);
|
|
1257
1275
|
importedSheetNames.add(entry.name);
|
|
1276
|
+
const sheetTables = importTables ? parseTables(pkg, entry.partPath) : [];
|
|
1277
|
+
const tableRanges = sheetTables.map((table) => ({ name: table.displayName, range: parseA1Range(table.ref) })).filter((entryTable) => entryTable.range !== undefined);
|
|
1278
|
+
const tableAt = (colIndex, rowIndex) => tableRanges.find(({ range }) => colIndex >= range.start.colIndex && colIndex <= range.end.colIndex && rowIndex >= range.start.rowIndex && rowIndex <= range.end.rowIndex)?.name;
|
|
1258
1279
|
const content = new Map;
|
|
1259
1280
|
const cellMetadata = new Map;
|
|
1260
1281
|
const styleGroups = new Map;
|
|
@@ -1264,7 +1285,8 @@ async function convertWorkbook(source, options = {}) {
|
|
|
1264
1285
|
convertDates,
|
|
1265
1286
|
date1904,
|
|
1266
1287
|
sheetName: entry.name,
|
|
1267
|
-
diagnostics
|
|
1288
|
+
diagnostics,
|
|
1289
|
+
containingTableName: tableAt(cell.colIndex, cell.rowIndex)
|
|
1268
1290
|
});
|
|
1269
1291
|
if (value !== undefined) {
|
|
1270
1292
|
content.set(cell.reference, value);
|
|
@@ -1319,7 +1341,7 @@ async function convertWorkbook(source, options = {}) {
|
|
|
1319
1341
|
}
|
|
1320
1342
|
}
|
|
1321
1343
|
if (importTables) {
|
|
1322
|
-
for (const table of
|
|
1344
|
+
for (const table of sheetTables) {
|
|
1323
1345
|
const converted = convertTable(table, entry.name, diagnostics);
|
|
1324
1346
|
if (converted) {
|
|
1325
1347
|
tables.push(converted);
|
|
@@ -1369,7 +1391,8 @@ function cellValue(cell, format, context) {
|
|
|
1369
1391
|
const body = cell.formulaOffset ? offsetFormulaReferences(cell.formula, cell.formulaOffset.cols, cell.formulaOffset.rows) : cell.formula;
|
|
1370
1392
|
const translated = translateFormula(body, {
|
|
1371
1393
|
sheetName: context.sheetName,
|
|
1372
|
-
cellReference: cell.reference
|
|
1394
|
+
cellReference: cell.reference,
|
|
1395
|
+
containingTableName: context.containingTableName
|
|
1373
1396
|
});
|
|
1374
1397
|
context.diagnostics.push(...translated.diagnostics);
|
|
1375
1398
|
return translated.formula;
|
|
@@ -1515,6 +1538,238 @@ function coalesceCellsIntoAreas(sheetName, cells) {
|
|
|
1515
1538
|
return areas;
|
|
1516
1539
|
}
|
|
1517
1540
|
|
|
1541
|
+
// src/csv/parse.ts
|
|
1542
|
+
var CANDIDATE_DELIMITERS = [",", "\t", ";", "|"];
|
|
1543
|
+
function parseDelimitedText(text, delimiter) {
|
|
1544
|
+
const rows = [];
|
|
1545
|
+
let row = [];
|
|
1546
|
+
let field = "";
|
|
1547
|
+
let inQuotes = false;
|
|
1548
|
+
let fieldWasQuoted = false;
|
|
1549
|
+
let rowHasContent = false;
|
|
1550
|
+
const endField = () => {
|
|
1551
|
+
row.push(field);
|
|
1552
|
+
field = "";
|
|
1553
|
+
fieldWasQuoted = false;
|
|
1554
|
+
rowHasContent = true;
|
|
1555
|
+
};
|
|
1556
|
+
const endRow = () => {
|
|
1557
|
+
endField();
|
|
1558
|
+
rows.push(row);
|
|
1559
|
+
row = [];
|
|
1560
|
+
rowHasContent = false;
|
|
1561
|
+
};
|
|
1562
|
+
for (let i = 0;i < text.length; i++) {
|
|
1563
|
+
const char = text[i];
|
|
1564
|
+
if (inQuotes) {
|
|
1565
|
+
if (char === '"') {
|
|
1566
|
+
if (text[i + 1] === '"') {
|
|
1567
|
+
field += '"';
|
|
1568
|
+
i++;
|
|
1569
|
+
} else {
|
|
1570
|
+
inQuotes = false;
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
field += char;
|
|
1574
|
+
}
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (char === '"' && field.length === 0) {
|
|
1578
|
+
inQuotes = true;
|
|
1579
|
+
fieldWasQuoted = true;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (char === delimiter) {
|
|
1583
|
+
endField();
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (char === "\r") {
|
|
1587
|
+
if (text[i + 1] === `
|
|
1588
|
+
`)
|
|
1589
|
+
i++;
|
|
1590
|
+
endRow();
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
if (char === `
|
|
1594
|
+
`) {
|
|
1595
|
+
endRow();
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
field += char;
|
|
1599
|
+
}
|
|
1600
|
+
if (field.length > 0 || fieldWasQuoted || rowHasContent) {
|
|
1601
|
+
endRow();
|
|
1602
|
+
}
|
|
1603
|
+
return rows;
|
|
1604
|
+
}
|
|
1605
|
+
function detectDelimiter(text, candidates = CANDIDATE_DELIMITERS) {
|
|
1606
|
+
const lines = takeSampleLines(text, 20);
|
|
1607
|
+
if (lines.length === 0)
|
|
1608
|
+
return ",";
|
|
1609
|
+
let best;
|
|
1610
|
+
for (const delimiter of candidates) {
|
|
1611
|
+
const counts = lines.map((line) => countOutsideQuotes(line, delimiter));
|
|
1612
|
+
const present = counts.filter((count) => count > 0);
|
|
1613
|
+
if (present.length === 0)
|
|
1614
|
+
continue;
|
|
1615
|
+
const first = counts[0];
|
|
1616
|
+
const consistent = counts.filter((count) => count === first).length;
|
|
1617
|
+
const score = consistent / counts.length + Math.min(first, 50) / 1000;
|
|
1618
|
+
if (!best || score > best.score) {
|
|
1619
|
+
best = { delimiter, score };
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return best?.delimiter ?? ",";
|
|
1623
|
+
}
|
|
1624
|
+
function takeSampleLines(text, limit) {
|
|
1625
|
+
const lines = [];
|
|
1626
|
+
let current = "";
|
|
1627
|
+
let inQuotes = false;
|
|
1628
|
+
for (let i = 0;i < text.length && lines.length < limit; i++) {
|
|
1629
|
+
const char = text[i];
|
|
1630
|
+
if (char === '"') {
|
|
1631
|
+
if (inQuotes && text[i + 1] === '"') {
|
|
1632
|
+
current += '""';
|
|
1633
|
+
i++;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
inQuotes = !inQuotes;
|
|
1637
|
+
current += char;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (!inQuotes && (char === `
|
|
1641
|
+
` || char === "\r")) {
|
|
1642
|
+
if (char === "\r" && text[i + 1] === `
|
|
1643
|
+
`)
|
|
1644
|
+
i++;
|
|
1645
|
+
if (current.trim().length > 0)
|
|
1646
|
+
lines.push(current);
|
|
1647
|
+
current = "";
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
current += char;
|
|
1651
|
+
}
|
|
1652
|
+
if (current.trim().length > 0 && lines.length < limit)
|
|
1653
|
+
lines.push(current);
|
|
1654
|
+
return lines;
|
|
1655
|
+
}
|
|
1656
|
+
function countOutsideQuotes(line, delimiter) {
|
|
1657
|
+
let count = 0;
|
|
1658
|
+
let inQuotes = false;
|
|
1659
|
+
for (let i = 0;i < line.length; i++) {
|
|
1660
|
+
const char = line[i];
|
|
1661
|
+
if (char === '"') {
|
|
1662
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
1663
|
+
i++;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
inQuotes = !inQuotes;
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
if (!inQuotes && char === delimiter)
|
|
1670
|
+
count++;
|
|
1671
|
+
}
|
|
1672
|
+
return count;
|
|
1673
|
+
}
|
|
1674
|
+
function stripBom(text) {
|
|
1675
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/csv/convert.ts
|
|
1679
|
+
var MAX_CELLS = 2000000;
|
|
1680
|
+
function convertDelimitedText(text, options = {}) {
|
|
1681
|
+
const {
|
|
1682
|
+
sheetName = "Sheet1",
|
|
1683
|
+
fileName,
|
|
1684
|
+
parseNumbers = true,
|
|
1685
|
+
parseBooleans = true,
|
|
1686
|
+
trimFields = false
|
|
1687
|
+
} = options;
|
|
1688
|
+
const diagnostics = [];
|
|
1689
|
+
const body = stripBom(text);
|
|
1690
|
+
const delimiter = options.delimiter ?? detectDelimiter(body);
|
|
1691
|
+
const rows = parseDelimitedText(body, delimiter);
|
|
1692
|
+
const widest = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
1693
|
+
if (rows.length * widest > MAX_CELLS) {
|
|
1694
|
+
throw new Error(`The file has about ${rows.length * widest} cells, more than the ` + `${MAX_CELLS} this importer will load at once.`);
|
|
1695
|
+
}
|
|
1696
|
+
const content = new Map;
|
|
1697
|
+
for (let rowIndex = 0;rowIndex < rows.length; rowIndex++) {
|
|
1698
|
+
const row = rows[rowIndex];
|
|
1699
|
+
for (let colIndex = 0;colIndex < row.length; colIndex++) {
|
|
1700
|
+
const raw = trimFields ? row[colIndex].trim() : row[colIndex];
|
|
1701
|
+
if (raw.length === 0)
|
|
1702
|
+
continue;
|
|
1703
|
+
const reference = `${columnIndexToLabel(colIndex)}${rowIndex + 1}`;
|
|
1704
|
+
content.set(reference, toCellValue(raw, {
|
|
1705
|
+
parseNumbers,
|
|
1706
|
+
parseBooleans,
|
|
1707
|
+
sheetName,
|
|
1708
|
+
reference,
|
|
1709
|
+
diagnostics
|
|
1710
|
+
}));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const data = {
|
|
1714
|
+
sheets: [{ name: sheetName, content }],
|
|
1715
|
+
workbookMetadata: {
|
|
1716
|
+
fileName,
|
|
1717
|
+
sheetOrder: [sheetName],
|
|
1718
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
return { data, diagnostics };
|
|
1722
|
+
}
|
|
1723
|
+
function toCellValue(raw, context) {
|
|
1724
|
+
if (raw.startsWith("=") && raw.length > 1) {
|
|
1725
|
+
const translated = translateFormula(raw, {
|
|
1726
|
+
sheetName: context.sheetName,
|
|
1727
|
+
cellReference: context.reference
|
|
1728
|
+
});
|
|
1729
|
+
context.diagnostics.push(...translated.diagnostics);
|
|
1730
|
+
return translated.formula;
|
|
1731
|
+
}
|
|
1732
|
+
if (context.parseBooleans) {
|
|
1733
|
+
const upper = raw.toUpperCase();
|
|
1734
|
+
if (upper === "TRUE")
|
|
1735
|
+
return true;
|
|
1736
|
+
if (upper === "FALSE")
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
if (context.parseNumbers) {
|
|
1740
|
+
const number = toNumber(raw);
|
|
1741
|
+
if (number !== undefined)
|
|
1742
|
+
return number;
|
|
1743
|
+
}
|
|
1744
|
+
return raw;
|
|
1745
|
+
}
|
|
1746
|
+
function toNumber(raw) {
|
|
1747
|
+
const text = raw.trim();
|
|
1748
|
+
if (text.length === 0)
|
|
1749
|
+
return;
|
|
1750
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(text)) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const digits = text.replace(/^[+-]/, "");
|
|
1754
|
+
if (/^0\d/.test(digits))
|
|
1755
|
+
return;
|
|
1756
|
+
const value = Number(text);
|
|
1757
|
+
if (!Number.isFinite(value))
|
|
1758
|
+
return;
|
|
1759
|
+
if (!text.includes(".") && !/[eE]/.test(text) && !Number.isSafeInteger(value)) {
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
return value;
|
|
1763
|
+
}
|
|
1764
|
+
async function readTextFrom(source) {
|
|
1765
|
+
if (typeof source === "string")
|
|
1766
|
+
return source;
|
|
1767
|
+
if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
|
|
1768
|
+
return new TextDecoder("utf-8").decode(source);
|
|
1769
|
+
}
|
|
1770
|
+
return source.text();
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1518
1773
|
// index.ts
|
|
1519
1774
|
async function excelToFormulaEngine(source, options) {
|
|
1520
1775
|
const { data } = await convertWorkbook(source, withFileName(source, options));
|
|
@@ -1530,10 +1785,55 @@ function withFileName(source, options) {
|
|
|
1530
1785
|
const name = typeof File !== "undefined" && source instanceof File ? source.name : undefined;
|
|
1531
1786
|
return name ? { ...options, fileName: name } : { ...options };
|
|
1532
1787
|
}
|
|
1788
|
+
async function csvToFormulaEngine(source, options) {
|
|
1789
|
+
const { data } = await csvToFormulaEngineDetailed(source, options);
|
|
1790
|
+
return data;
|
|
1791
|
+
}
|
|
1792
|
+
async function csvToFormulaEngineDetailed(source, options) {
|
|
1793
|
+
const text = await readTextFrom(source);
|
|
1794
|
+
const resolved = withCsvFileName(source, options);
|
|
1795
|
+
return convertDelimitedText(text, resolved);
|
|
1796
|
+
}
|
|
1797
|
+
async function spreadsheetToFormulaEngine(source, options) {
|
|
1798
|
+
const { data } = await spreadsheetToFormulaEngineDetailed(source, options);
|
|
1799
|
+
return data;
|
|
1800
|
+
}
|
|
1801
|
+
async function spreadsheetToFormulaEngineDetailed(source, options) {
|
|
1802
|
+
const bytes = await toBytes2(source);
|
|
1803
|
+
if (looksLikeZip(bytes)) {
|
|
1804
|
+
return convertWorkbook(bytes, withFileName(source, options));
|
|
1805
|
+
}
|
|
1806
|
+
return csvToFormulaEngineDetailed(bytes, withCsvFileName(source, options));
|
|
1807
|
+
}
|
|
1808
|
+
function looksLikeZip(bytes) {
|
|
1809
|
+
return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
|
|
1810
|
+
}
|
|
1811
|
+
async function toBytes2(source) {
|
|
1812
|
+
if (source instanceof Uint8Array)
|
|
1813
|
+
return source;
|
|
1814
|
+
if (source instanceof ArrayBuffer)
|
|
1815
|
+
return new Uint8Array(source);
|
|
1816
|
+
return new Uint8Array(await source.arrayBuffer());
|
|
1817
|
+
}
|
|
1818
|
+
function withCsvFileName(source, options) {
|
|
1819
|
+
const name = typeof File !== "undefined" && source instanceof File ? source.name : undefined;
|
|
1820
|
+
const fileName = options?.fileName ?? name;
|
|
1821
|
+
if (!fileName)
|
|
1822
|
+
return { ...options };
|
|
1823
|
+
return {
|
|
1824
|
+
...options,
|
|
1825
|
+
fileName,
|
|
1826
|
+
sheetName: options?.sheetName ?? (fileName.replace(/\.(csv|tsv|tab|txt)$/i, "").trim() || "Sheet1")
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1533
1829
|
export {
|
|
1534
1830
|
columnIndexToLabel,
|
|
1535
1831
|
columnLabelToIndex,
|
|
1832
|
+
convertDelimitedText,
|
|
1536
1833
|
convertWorkbook,
|
|
1834
|
+
csvToFormulaEngine,
|
|
1835
|
+
csvToFormulaEngineDetailed,
|
|
1836
|
+
detectDelimiter,
|
|
1537
1837
|
excelSerialToIso,
|
|
1538
1838
|
excelToFormulaEngine,
|
|
1539
1839
|
excelToFormulaEngineDetailed,
|
|
@@ -1541,8 +1841,12 @@ export {
|
|
|
1541
1841
|
offsetFormulaReferences,
|
|
1542
1842
|
parseA1,
|
|
1543
1843
|
parseA1Range,
|
|
1844
|
+
parseDelimitedText,
|
|
1845
|
+
spreadsheetToFormulaEngine,
|
|
1846
|
+
spreadsheetToFormulaEngineDetailed,
|
|
1847
|
+
stripBom,
|
|
1544
1848
|
toA1,
|
|
1545
1849
|
translateFormula
|
|
1546
1850
|
};
|
|
1547
1851
|
|
|
1548
|
-
//# debugId=
|
|
1852
|
+
//# debugId=D5F9B2A423DC266864756E2164756E21
|