@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/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 parseTables(pkg, entry.partPath)) {
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=90056589614CAA9A64756E2164756E21
1852
+ //# debugId=D5F9B2A423DC266864756E2164756E21