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

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