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