@fileverse-dev/formulajs 4.4.20-mod-1 → 4.4.20-mod-3

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/lib/cjs/index.cjs CHANGED
@@ -1441,115 +1441,392 @@ function VLOOKUP(lookup_value, table_array, col_index_num, range_lookup) {
1441
1441
  return result
1442
1442
  }
1443
1443
 
1444
- function XLOOKUP(lookup_value, lookup_array, return_array, if_not_found, match_mode, search_mode) {
1445
- // Handle case where error object might not be defined
1446
- const ERROR_NA = '#N/A';
1447
- const ERROR_REF = '#REF!';
1444
+ function XLOOKUP(search_key, lookup_range, result_range, missing_value, isCol,match_mode, search_mode) {
1445
+ console.log('XLOOKUP parameters:', { search_key, lookup_range, result_range, missing_value, match_mode, search_mode, isCol });
1446
+
1447
+ // Validate required parameters
1448
+ if (search_key === undefined || search_key === null) {
1449
+ console.log('Error: search_key is required');
1450
+ return new Error('Error: search_key is required')
1451
+ }
1452
+
1453
+ if (!lookup_range || !result_range) {
1454
+ console.log('Error: lookup_range and result_range are required');
1455
+ return new Error('Error: lookup_range and result_range are required')
1456
+ }
1457
+
1458
+ // Validate and normalize lookup_range (must be singular row or column)
1459
+ let lookup_array = normalizeLookupRange(lookup_range);
1460
+ if (!lookup_array) {
1461
+ console.log('Error: lookup_range must be a singular row or column');
1462
+ return new Error('Error: lookup_range must be a singular row or column')
1463
+ }
1464
+
1465
+ // Validate and normalize result_range
1466
+ let result_array = normalizeResultRange(result_range);
1467
+ if (!result_array) {
1468
+ console.log('Error: Invalid result_range');
1469
+ return new Error('Error: Invalid result_range')
1470
+ }
1471
+
1472
+ // Validate that lookup and result ranges have compatible dimensions
1473
+ // Exception: if result_range is a single row, it can be returned regardless of lookup_range length
1474
+ result_array.map((row) => {
1475
+ if (row.length !== lookup_array.length) {
1476
+ console.log('Error: lookup_range and result_range must have the same number of columns');
1477
+ return new Error('Error: lookup_range and result_range must have the same number of columns/rows')
1478
+ }
1479
+ });
1480
+
1481
+ // Set default parameter values
1482
+ missing_value = missing_value !== undefined ? missing_value : new Error("Error: Didn't find value in XLOOKUP evaluation");
1483
+ match_mode = match_mode !== undefined ? match_mode : 0;
1484
+ search_mode = search_mode !== undefined ? search_mode : 1;
1485
+ isCol = isCol !== undefined ? isCol : false;
1486
+
1487
+ // Validate match_mode
1488
+ if (![0, 1, -1, 2].includes(match_mode)) {
1489
+ console.log('Error: match_mode must be 0, 1, -1, or 2');
1490
+ return new Error('Error: match_mode must be 0, 1, -1, or 2')
1491
+ }
1492
+
1493
+ // Validate search_mode
1494
+ if (![1, -1, 2, -2].includes(search_mode)) {
1495
+ console.log('Error: search_mode must be 1, -1, 2, or -2');
1496
+ return new Error('Error: search_mode must be 1, -1, 2, or -2')
1497
+ }
1498
+
1499
+ // Validate binary search requirements
1500
+ if (Math.abs(search_mode) === 2 && match_mode === 2) {
1501
+ console.log('Error: Binary search (search_mode ±2) cannot be used with wildcard matching (match_mode 2)');
1502
+ return new Error('Error: Binary search (search_mode ±2) cannot be used with wildcard matching (match_mode 2)')
1503
+ }
1504
+
1505
+ console.log('Normalized arrays:', { lookup_array, result_array });
1506
+
1507
+ let res = performLookup(search_key, lookup_array, result_array, missing_value, match_mode, search_mode, isCol);
1508
+ res = isCol ? Array.isArray(res)?res.map((item) => [item.toString()]):res : res;
1509
+ return res
1510
+ }
1511
+
1512
+ function normalizeLookupRange(lookup_range) {
1513
+ if (!Array.isArray(lookup_range)) {
1514
+ return null
1515
+ }
1516
+
1517
+ // If it's a 1D array, it's already a column
1518
+ if (!Array.isArray(lookup_range[0])) {
1519
+ return lookup_range
1520
+ }
1521
+
1522
+ // If it's a 2D array, check if it's a single row or single column
1523
+ const rows = lookup_range.length;
1524
+ const cols = lookup_range[0].length;
1525
+
1526
+ if (rows === 1) {
1527
+ // Single row - extract as array
1528
+ return lookup_range[0]
1529
+ } else if (cols === 1) {
1530
+ // Single column - extract first element of each row
1531
+ return lookup_range.map(row => row[0])
1532
+ } else {
1533
+ // Multiple rows and columns - not allowed
1534
+ return null
1535
+ }
1536
+ }
1537
+
1538
+ function normalizeResultRange(result_range) {
1539
+ if (!Array.isArray(result_range)) {
1540
+ return null
1541
+ }
1542
+
1543
+ // If it's a 1D array, convert to 2D single column for consistency
1544
+ if (!Array.isArray(result_range[0])) {
1545
+ return result_range.map(value => [value])
1546
+ }
1547
+
1548
+ // If it's already 2D, return as is
1549
+ return result_range
1550
+ }
1448
1551
 
1449
- console.log('XLOOKUP parameters:', { lookup_value, lookup_array, return_array, if_not_found, match_mode, search_mode });
1552
+ function performLookup(search_key, lookup_array, result_array, missing_value, match_mode, search_mode, isCol) {
1450
1553
 
1451
- console.log('XLOOKUP called with:', { lookup_value, lookup_array, return_array, if_not_found, match_mode, search_mode });
1554
+ console.log('performLookup called with:', { search_key, lookup_array, result_array, missing_value, match_mode, search_mode, isCol });
1452
1555
 
1453
- // Handle VLOOKUP-style call: XLOOKUP("a", F7:G9, 2, "false")
1454
- if (typeof return_array === 'number' && Array.isArray(lookup_array) && Array.isArray(lookup_array[0])) {
1455
- console.log('Detected VLOOKUP-style call');
1456
- const table_array = lookup_array;
1457
- const col_index_num = return_array;
1458
- const range_lookup = !(if_not_found === "false" || if_not_found === false || if_not_found === 0);
1556
+ let foundIndex = -1;
1557
+ const isSingleResultRow = result_array.length === 1;
1558
+
1559
+ // Handle different match modes
1560
+ switch (match_mode) {
1561
+ case 0: // Exact match
1562
+ foundIndex = findExactMatch(search_key, lookup_array, search_mode);
1563
+ break
1564
+ case 1: // Exact match or next larger
1565
+ foundIndex = findExactOrNextLarger(search_key, lookup_array, search_mode);
1566
+ break
1567
+ case -1: // Exact match or next smaller
1568
+ foundIndex = findExactOrNextSmaller(search_key, lookup_array, search_mode);
1569
+ break
1570
+ case 2: // Wildcard match
1571
+ foundIndex = findWildcardMatch(search_key, lookup_array, search_mode);
1572
+ break
1573
+ }
1574
+
1575
+ if (foundIndex === -1) {
1576
+ // Return missing_value (single value): "yoo"
1577
+ return missing_value
1578
+ }
1579
+
1580
+ // Return the result
1581
+ if (isSingleResultRow) {
1582
+ // Single result row - return the entire row regardless of where match was found
1583
+ const resultRow = result_array[0];
1584
+ if (isCol) {
1585
+ return resultRow.map(val => [val])
1586
+ } else {
1587
+ return resultRow
1588
+ }
1589
+ } else {
1590
+ // Multiple result rows
1591
+ if (isCol) {
1592
+ // Return the foundIndex column from all rows: ["e", "r"]
1593
+ const columnValues = result_array.map(row => row[foundIndex]);
1594
+ return columnValues
1595
+ } else {
1596
+ // Return the entire matched row: ["e", 3, "s", "hj"]
1597
+ return result_array[foundIndex]
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ function findExactMatch(search_key, lookup_array, search_mode) {
1603
+ const processedSearchKey = typeof search_key === 'string' ? search_key.toLowerCase().trim() : search_key;
1604
+
1605
+ if (Math.abs(search_mode) === 2) {
1606
+ // Binary search
1607
+ return binarySearchExact(processedSearchKey, lookup_array, search_mode > 0)
1608
+ } else {
1609
+ // Linear search
1610
+ const indices = getSearchIndices(lookup_array.length, search_mode);
1459
1611
 
1460
- // Validate column index
1461
- if (col_index_num < 1 || col_index_num > table_array[0].length) {
1462
- return ERROR_REF
1612
+ for (const i of indices) {
1613
+ const value = lookup_array[i];
1614
+ const processedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;
1615
+
1616
+ if (processedValue === processedSearchKey) {
1617
+ console.log(`Exact match found at index ${i}:`, value);
1618
+ return i
1619
+ }
1463
1620
  }
1621
+ }
1622
+
1623
+ return -1
1624
+ }
1625
+
1626
+ function findExactOrNextLarger(search_key, lookup_array, search_mode) {
1627
+ const isNumber = typeof search_key === 'number';
1628
+ const processedSearchKey = typeof search_key === 'string' ? search_key.toLowerCase().trim() : search_key;
1629
+
1630
+ if (Math.abs(search_mode) === 2) {
1631
+ // Binary search for exact or next larger
1632
+ return binarySearchNextLarger(processedSearchKey, lookup_array, search_mode > 0)
1633
+ }
1634
+
1635
+ const indices = getSearchIndices(lookup_array.length, search_mode);
1636
+ let bestIndex = -1;
1637
+
1638
+ for (const i of indices) {
1639
+ const value = lookup_array[i];
1640
+ const processedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;
1464
1641
 
1465
- // Extract lookup and return columns
1466
- const lookup_col = table_array.map(row => row[0]);
1467
- const return_col = table_array.map(row => row[col_index_num - 1]);
1642
+ // Exact match
1643
+ if (processedValue === processedSearchKey) {
1644
+ return i
1645
+ }
1468
1646
 
1469
- return performLookup(lookup_value, lookup_col, return_col, ERROR_NA, range_lookup ? -1 : 0)
1647
+ // Next larger value
1648
+ if (isNumber && typeof value === 'number' && value > search_key) {
1649
+ if (bestIndex === -1 || value < lookup_array[bestIndex]) {
1650
+ bestIndex = i;
1651
+ }
1652
+ } else if (!isNumber && typeof value === 'string' && processedValue > processedSearchKey) {
1653
+ if (bestIndex === -1 || processedValue < (typeof lookup_array[bestIndex] === 'string' ? lookup_array[bestIndex].toLowerCase().trim() : lookup_array[bestIndex])) {
1654
+ bestIndex = i;
1655
+ }
1656
+ }
1470
1657
  }
1471
1658
 
1472
- // Standard XLOOKUP call: XLOOKUP("a", F7:F9, G7:G9, "hahaha")
1473
- console.log('Detected standard XLOOKUP call');
1659
+ return bestIndex
1660
+ }
1661
+
1662
+ function findExactOrNextSmaller(search_key, lookup_array, search_mode) {
1663
+ const isNumber = typeof search_key === 'number';
1664
+ const processedSearchKey = typeof search_key === 'string' ? search_key.toLowerCase().trim() : search_key;
1474
1665
 
1475
- if (!lookup_array || !return_array) {
1476
- console.log('Missing lookup_array or return_array');
1477
- return ERROR_NA
1666
+ if (Math.abs(search_mode) === 2) {
1667
+ // Binary search for exact or next smaller
1668
+ return binarySearchNextSmaller(processedSearchKey, lookup_array, search_mode > 0)
1478
1669
  }
1479
1670
 
1480
- // Handle case where arrays might be 2D (from Excel ranges)
1481
- let lookup_col = lookup_array;
1482
- let return_col = return_array;
1671
+ const indices = getSearchIndices(lookup_array.length, search_mode);
1672
+ let bestIndex = -1;
1483
1673
 
1484
- // If lookup_array is 2D, extract first column
1485
- if (Array.isArray(lookup_array) && Array.isArray(lookup_array[0])) {
1486
- lookup_col = lookup_array.map(row => row[0]);
1487
- console.log('Extracted lookup column from 2D array:', lookup_col);
1674
+ for (const i of indices) {
1675
+ const value = lookup_array[i];
1676
+ const processedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;
1677
+
1678
+ // Exact match
1679
+ if (processedValue === processedSearchKey) {
1680
+ return i
1681
+ }
1682
+
1683
+ // Next smaller value
1684
+ if (isNumber && typeof value === 'number' && value < search_key) {
1685
+ if (bestIndex === -1 || value > lookup_array[bestIndex]) {
1686
+ bestIndex = i;
1687
+ }
1688
+ } else if (!isNumber && typeof value === 'string' && processedValue < processedSearchKey) {
1689
+ if (bestIndex === -1 || processedValue > (typeof lookup_array[bestIndex] === 'string' ? lookup_array[bestIndex].toLowerCase().trim() : lookup_array[bestIndex])) {
1690
+ bestIndex = i;
1691
+ }
1692
+ }
1488
1693
  }
1489
1694
 
1490
- // If return_array is 2D, extract first column
1491
- if (Array.isArray(return_array) && Array.isArray(return_array[0])) {
1492
- return_col = return_array.map(row => row[0]);
1493
- console.log('Extracted return column from 2D array:', return_col);
1695
+ return bestIndex
1696
+ }
1697
+
1698
+ function findWildcardMatch(search_key, lookup_array, search_mode) {
1699
+ if (typeof search_key !== 'string') {
1700
+ return -1 // Wildcard only works with strings
1494
1701
  }
1495
1702
 
1496
- if (lookup_col.length !== return_col.length) {
1497
- console.log('Array length mismatch:', lookup_col.length, 'vs', return_col.length);
1498
- return ERROR_NA
1499
- }
1703
+ // Convert wildcard pattern to regex
1704
+ const pattern = search_key
1705
+ .toLowerCase()
1706
+ .replace(/\*/g, '.*') // * matches any sequence of characters
1707
+ .replace(/\?/g, '.') // ? matches any single character
1708
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape other regex chars
1709
+ .replace(/\\\.\*/g, '.*') // Restore our wildcards
1710
+ .replace(/\\\./g, '.');
1711
+
1712
+ const regex = new RegExp(`^${pattern}$`, 'i');
1500
1713
 
1501
- // Default parameters
1502
- if_not_found = if_not_found !== undefined ? if_not_found : ERROR_NA;
1503
- match_mode = match_mode || 0; // 0 = exact match, -1 = exact or next smallest
1504
- search_mode = search_mode || 1; // 1 = first to last, -1 = last to first
1714
+ const indices = getSearchIndices(lookup_array.length, search_mode);
1505
1715
 
1506
- return performLookup(lookup_value, lookup_col, return_col, if_not_found, match_mode, search_mode)
1716
+ for (const i of indices) {
1717
+ const value = lookup_array[i];
1718
+ if (typeof value === 'string' && regex.test(value)) {
1719
+ console.log(`Wildcard match found at index ${i}:`, value);
1720
+ return i
1721
+ }
1722
+ }
1723
+
1724
+ return -1
1507
1725
  }
1508
1726
 
1727
+ function getSearchIndices(length, search_mode) {
1728
+ if (search_mode === -1) {
1729
+ // Last to first
1730
+ return Array.from({ length }, (_, i) => length - 1 - i)
1731
+ } else {
1732
+ // First to last (default)
1733
+ return Array.from({ length }, (_, i) => i)
1734
+ }
1735
+ }
1509
1736
 
1510
- function performLookup(lookup_value, lookup_array, return_array, if_not_found, match_mode, search_mode) {
1511
- const isNumberLookup = typeof lookup_value === 'number';
1512
- const lookupValue = typeof lookup_value === 'string' ? lookup_value.toLowerCase().trim() : lookup_value;
1513
- let bestMatchIndex = -1;
1737
+ function binarySearchExact(search_key, lookup_array, ascending) {
1738
+ let left = 0;
1739
+ let right = lookup_array.length - 1;
1514
1740
 
1515
- // Debug: Log what we're looking for and what we have
1516
- console.log('XLOOKUP Debug:', {
1517
- looking_for: lookup_value,
1518
- processed_lookup: lookupValue,
1519
- lookup_array: lookup_array,
1520
- return_array: return_array,
1521
- match_mode: match_mode
1522
- });
1741
+ while (left <= right) {
1742
+ const mid = Math.floor((left + right) / 2);
1743
+ const midValue = lookup_array[mid];
1744
+ const processedMidValue = typeof midValue === 'string' ? midValue.toLowerCase().trim() : midValue;
1745
+
1746
+ if (processedMidValue === search_key) {
1747
+ return mid
1748
+ }
1749
+
1750
+ const comparison = ascending ?
1751
+ (processedMidValue < search_key) :
1752
+ (processedMidValue > search_key);
1753
+
1754
+ if (comparison) {
1755
+ left = mid + 1;
1756
+ } else {
1757
+ right = mid - 1;
1758
+ }
1759
+ }
1523
1760
 
1524
- // Determine search direction
1525
- const startIndex = search_mode === -1 ? lookup_array.length - 1 : 0;
1526
- const endIndex = search_mode === -1 ? -1 : lookup_array.length;
1527
- const step = search_mode === -1 ? -1 : 1;
1761
+ return -1
1762
+ }
1763
+
1764
+ function binarySearchNextLarger(search_key, lookup_array, ascending) {
1765
+ let left = 0;
1766
+ let right = lookup_array.length - 1;
1767
+ let result = -1;
1528
1768
 
1529
- for (let i = startIndex; i !== endIndex; i += step) {
1530
- const rawValue = lookup_array[i];
1531
- const arrayValue = typeof rawValue === 'string' ? rawValue.toLowerCase().trim() : rawValue;
1769
+ while (left <= right) {
1770
+ const mid = Math.floor((left + right) / 2);
1771
+ const midValue = lookup_array[mid];
1772
+ const processedMidValue = typeof midValue === 'string' ? midValue.toLowerCase().trim() : midValue;
1532
1773
 
1533
- console.log(`Comparing: "${lookupValue}" === "${arrayValue}" (types: ${typeof lookupValue} vs ${typeof arrayValue})`);
1774
+ if (processedMidValue === search_key) {
1775
+ return mid // Exact match
1776
+ }
1534
1777
 
1535
- // Exact match
1536
- if (arrayValue === lookupValue) {
1537
- console.log(`Found match at index ${i}: ${return_array[i]}`);
1538
- return return_array[i]
1778
+ if (ascending) {
1779
+ if (processedMidValue > search_key) {
1780
+ result = mid;
1781
+ right = mid - 1;
1782
+ } else {
1783
+ left = mid + 1;
1784
+ }
1785
+ } else {
1786
+ if (processedMidValue < search_key) {
1787
+ result = mid;
1788
+ left = mid + 1;
1789
+ } else {
1790
+ right = mid - 1;
1791
+ }
1539
1792
  }
1793
+ }
1794
+
1795
+ return result
1796
+ }
1797
+
1798
+ function binarySearchNextSmaller(search_key, lookup_array, ascending) {
1799
+ let left = 0;
1800
+ let right = lookup_array.length - 1;
1801
+ let result = -1;
1802
+
1803
+ while (left <= right) {
1804
+ const mid = Math.floor((left + right) / 2);
1805
+ const midValue = lookup_array[mid];
1806
+ const processedMidValue = typeof midValue === 'string' ? midValue.toLowerCase().trim() : midValue;
1540
1807
 
1541
- // Approximate match (match_mode = -1, similar to VLOOKUP range_lookup = true)
1542
- if (match_mode === -1) {
1543
- if ((isNumberLookup && typeof arrayValue === 'number' && arrayValue <= lookup_value) ||
1544
- (!isNumberLookup && typeof arrayValue === 'string' && arrayValue.localeCompare(lookupValue) <= 0)) {
1545
- bestMatchIndex = i;
1546
- console.log(`Approximate match candidate at index ${i}: ${arrayValue}`);
1808
+ if (processedMidValue === search_key) {
1809
+ return mid // Exact match
1810
+ }
1811
+
1812
+ if (ascending) {
1813
+ if (processedMidValue < search_key) {
1814
+ result = mid;
1815
+ left = mid + 1;
1816
+ } else {
1817
+ right = mid - 1;
1818
+ }
1819
+ } else {
1820
+ if (processedMidValue > search_key) {
1821
+ result = mid;
1822
+ right = mid - 1;
1823
+ } else {
1824
+ left = mid + 1;
1547
1825
  }
1548
1826
  }
1549
1827
  }
1550
1828
 
1551
- console.log(`No exact match found. Returning: ${bestMatchIndex !== -1 ? return_array[bestMatchIndex] : if_not_found}`);
1552
- return bestMatchIndex !== -1 ? return_array[bestMatchIndex] : if_not_found
1829
+ return result
1553
1830
  }
1554
1831
 
1555
1832
  /**