@supercmd/calculator 1.0.0 → 1.0.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/dist/index.js +327 -60
- package/dist/index.mjs +327 -60
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -931,7 +931,7 @@ for (const cat of UNIT_CATEGORIES) {
|
|
|
931
931
|
}
|
|
932
932
|
}
|
|
933
933
|
function lookupUnit(token) {
|
|
934
|
-
return _unitIndex.get(token
|
|
934
|
+
return _unitIndex.get(normalizeUnitToken(token));
|
|
935
935
|
}
|
|
936
936
|
function convertUnit(value, from, to) {
|
|
937
937
|
if (from.category !== to.category) {
|
|
@@ -946,6 +946,9 @@ function convertUnit(value, from, to) {
|
|
|
946
946
|
const baseValue = value * from.def.factor;
|
|
947
947
|
return baseValue / to.def.factor;
|
|
948
948
|
}
|
|
949
|
+
function normalizeUnitToken(token) {
|
|
950
|
+
return token.toLowerCase().trim().replace(/\s+/g, " ").replace(/²/g, "2").replace(/³/g, "3");
|
|
951
|
+
}
|
|
949
952
|
|
|
950
953
|
// src/data/timezones.ts
|
|
951
954
|
var TIMEZONE_MAP = {
|
|
@@ -1018,8 +1021,11 @@ var TIMEZONE_MAP = {
|
|
|
1018
1021
|
"buenos aires": "America/Argentina/Buenos_Aires",
|
|
1019
1022
|
argentina: "America/Argentina/Buenos_Aires",
|
|
1020
1023
|
lima: "America/Lima",
|
|
1024
|
+
peru: "America/Lima",
|
|
1021
1025
|
bogota: "America/Bogota",
|
|
1026
|
+
colombia: "America/Bogota",
|
|
1022
1027
|
santiago: "America/Santiago",
|
|
1028
|
+
chile: "America/Santiago",
|
|
1023
1029
|
caracas: "America/Caracas",
|
|
1024
1030
|
quito: "America/Guayaquil",
|
|
1025
1031
|
// ─── Europe ───────────────────────────────────────────
|
|
@@ -1303,6 +1309,9 @@ var COUNTRY_TZ = {
|
|
|
1303
1309
|
canada: "America/Toronto",
|
|
1304
1310
|
mexico: "America/Mexico_City",
|
|
1305
1311
|
brazil: "America/Sao_Paulo",
|
|
1312
|
+
colombia: "America/Bogota",
|
|
1313
|
+
peru: "America/Lima",
|
|
1314
|
+
chile: "America/Santiago",
|
|
1306
1315
|
uk: "Europe/London",
|
|
1307
1316
|
"united kingdom": "Europe/London",
|
|
1308
1317
|
britain: "Europe/London",
|
|
@@ -1373,6 +1382,58 @@ function resolveTimezone(name) {
|
|
|
1373
1382
|
}
|
|
1374
1383
|
|
|
1375
1384
|
// src/providers/default.ts
|
|
1385
|
+
var STATIC_FIAT_PER_USD = {
|
|
1386
|
+
USD: 1,
|
|
1387
|
+
EUR: 0.92,
|
|
1388
|
+
GBP: 0.79,
|
|
1389
|
+
INR: 83.1,
|
|
1390
|
+
JPY: 150,
|
|
1391
|
+
CNY: 7.2,
|
|
1392
|
+
AUD: 1.52,
|
|
1393
|
+
CAD: 1.35,
|
|
1394
|
+
CHF: 0.88,
|
|
1395
|
+
KRW: 1330,
|
|
1396
|
+
SGD: 1.34,
|
|
1397
|
+
HKD: 7.8,
|
|
1398
|
+
NZD: 1.64,
|
|
1399
|
+
SEK: 10.5,
|
|
1400
|
+
NOK: 10.7,
|
|
1401
|
+
DKK: 6.85,
|
|
1402
|
+
MXN: 17.1,
|
|
1403
|
+
BRL: 5,
|
|
1404
|
+
THB: 35.8,
|
|
1405
|
+
PHP: 56.2,
|
|
1406
|
+
IDR: 15700,
|
|
1407
|
+
MYR: 4.48,
|
|
1408
|
+
ZAR: 18.4,
|
|
1409
|
+
AED: 3.67,
|
|
1410
|
+
SAR: 3.75,
|
|
1411
|
+
TRY: 32.1,
|
|
1412
|
+
PLN: 3.95,
|
|
1413
|
+
CZK: 23.2,
|
|
1414
|
+
RUB: 92
|
|
1415
|
+
};
|
|
1416
|
+
var STATIC_CRYPTO_USD = {
|
|
1417
|
+
BTC: 92e3,
|
|
1418
|
+
ETH: 3500,
|
|
1419
|
+
SOL: 180,
|
|
1420
|
+
XRP: 2.3,
|
|
1421
|
+
DOGE: 0.18,
|
|
1422
|
+
ADA: 0.8,
|
|
1423
|
+
MATIC: 0.95,
|
|
1424
|
+
DOT: 7.5,
|
|
1425
|
+
LTC: 95,
|
|
1426
|
+
AVAX: 42,
|
|
1427
|
+
LINK: 20,
|
|
1428
|
+
UNI: 12,
|
|
1429
|
+
ATOM: 10,
|
|
1430
|
+
XLM: 0.12,
|
|
1431
|
+
TON: 6,
|
|
1432
|
+
SHIB: 25e-6,
|
|
1433
|
+
BNB: 650,
|
|
1434
|
+
USDT: 1,
|
|
1435
|
+
USDC: 1
|
|
1436
|
+
};
|
|
1376
1437
|
async function fetchJSON(url, timeoutMs = 8e3) {
|
|
1377
1438
|
const controller = new AbortController();
|
|
1378
1439
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -1395,6 +1456,30 @@ async function withFallback(fns) {
|
|
|
1395
1456
|
}
|
|
1396
1457
|
throw lastError ?? new Error("All providers failed");
|
|
1397
1458
|
}
|
|
1459
|
+
async function staticFiatRate(base, target) {
|
|
1460
|
+
const from = STATIC_FIAT_PER_USD[base];
|
|
1461
|
+
const to = STATIC_FIAT_PER_USD[target];
|
|
1462
|
+
if (from === void 0 || to === void 0) {
|
|
1463
|
+
throw new Error(`Static fiat rate unavailable for ${base}/${target}`);
|
|
1464
|
+
}
|
|
1465
|
+
return to / from;
|
|
1466
|
+
}
|
|
1467
|
+
async function staticCryptoRate(from, to) {
|
|
1468
|
+
const fromCrypto = STATIC_CRYPTO_USD[from];
|
|
1469
|
+
const toCrypto = STATIC_CRYPTO_USD[to];
|
|
1470
|
+
if (fromCrypto !== void 0 && toCrypto !== void 0) {
|
|
1471
|
+
return fromCrypto / toCrypto;
|
|
1472
|
+
}
|
|
1473
|
+
if (fromCrypto !== void 0) {
|
|
1474
|
+
const usdToTarget = await staticFiatRate("USD", to);
|
|
1475
|
+
return fromCrypto * usdToTarget;
|
|
1476
|
+
}
|
|
1477
|
+
if (toCrypto !== void 0) {
|
|
1478
|
+
const fromToUsd = await staticFiatRate(from, "USD");
|
|
1479
|
+
return fromToUsd / toCrypto;
|
|
1480
|
+
}
|
|
1481
|
+
throw new Error(`Static crypto rate unavailable for ${from}/${to}`);
|
|
1482
|
+
}
|
|
1398
1483
|
async function frankfurterRate(base, target) {
|
|
1399
1484
|
const data = await fetchJSON(
|
|
1400
1485
|
`https://api.frankfurter.dev/v1/latest?base=${base}&symbols=${target}`
|
|
@@ -1644,7 +1729,8 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1644
1729
|
return await withFallback([
|
|
1645
1730
|
() => coingeckoRate(from, to),
|
|
1646
1731
|
() => binanceRate(from, to),
|
|
1647
|
-
() => coincapRate(from, to)
|
|
1732
|
+
() => coincapRate(from, to),
|
|
1733
|
+
() => staticCryptoRate(from, to)
|
|
1648
1734
|
]);
|
|
1649
1735
|
} catch {
|
|
1650
1736
|
}
|
|
@@ -1652,7 +1738,8 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1652
1738
|
const cryptoToUsd = await withFallback([
|
|
1653
1739
|
() => coingeckoRate(from, "USD"),
|
|
1654
1740
|
() => binanceRate(from, "USDT"),
|
|
1655
|
-
() => coincapRate(from, "USD")
|
|
1741
|
+
() => coincapRate(from, "USD"),
|
|
1742
|
+
() => staticCryptoRate(from, "USD")
|
|
1656
1743
|
]);
|
|
1657
1744
|
if (to === "USD" || to === "USDT" || to === "USDC") return cryptoToUsd;
|
|
1658
1745
|
const usdToFiat = await getFiat("USD", to);
|
|
@@ -1669,11 +1756,12 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1669
1756
|
async () => {
|
|
1670
1757
|
const p = await coincapRate(to, "USD");
|
|
1671
1758
|
return p === 0 ? Promise.reject("zero") : 1 / p;
|
|
1672
|
-
}
|
|
1759
|
+
},
|
|
1760
|
+
() => staticCryptoRate("USD", to)
|
|
1673
1761
|
]);
|
|
1674
1762
|
return fiatToUsd * usdToCrypto;
|
|
1675
1763
|
}
|
|
1676
|
-
|
|
1764
|
+
return staticCryptoRate(from, to);
|
|
1677
1765
|
}
|
|
1678
1766
|
function createDefaultProvider() {
|
|
1679
1767
|
return {
|
|
@@ -1681,14 +1769,16 @@ function createDefaultProvider() {
|
|
|
1681
1769
|
return withFallback([
|
|
1682
1770
|
() => frankfurterRate(base, target),
|
|
1683
1771
|
() => exchangeRateApiRate(base, target),
|
|
1684
|
-
() => fawazCurrencyRate(base, target)
|
|
1772
|
+
() => fawazCurrencyRate(base, target),
|
|
1773
|
+
() => staticFiatRate(base, target)
|
|
1685
1774
|
]);
|
|
1686
1775
|
},
|
|
1687
1776
|
async getCryptoRate(base, target) {
|
|
1688
1777
|
const getFiat = (b, t) => withFallback([
|
|
1689
1778
|
() => frankfurterRate(b, t),
|
|
1690
1779
|
() => exchangeRateApiRate(b, t),
|
|
1691
|
-
() => fawazCurrencyRate(b, t)
|
|
1780
|
+
() => fawazCurrencyRate(b, t),
|
|
1781
|
+
() => staticFiatRate(b, t)
|
|
1692
1782
|
]);
|
|
1693
1783
|
return getCryptoRateWithFiatFallback(base, target, getFiat);
|
|
1694
1784
|
}
|
|
@@ -1697,28 +1787,46 @@ function createDefaultProvider() {
|
|
|
1697
1787
|
|
|
1698
1788
|
// src/parser/intent.ts
|
|
1699
1789
|
function detectIntent(input) {
|
|
1700
|
-
const trimmed = input
|
|
1790
|
+
const trimmed = normalizeWhitespace(input);
|
|
1701
1791
|
if (!trimmed) {
|
|
1702
1792
|
return { kind: "math", expression: "0" };
|
|
1703
1793
|
}
|
|
1704
1794
|
return tryTimeIntent(trimmed) ?? tryDateIntent(trimmed) ?? tryCurrencyOrCryptoIntent(trimmed) ?? tryUnitIntent(trimmed) ?? { kind: "math", expression: trimmed };
|
|
1705
1795
|
}
|
|
1706
|
-
var
|
|
1707
|
-
|
|
1796
|
+
var TIME_IN_PATTERNS = [
|
|
1797
|
+
/^(?:what(?:'s| is) )?(?:the )?(?:current )?time (?:in|at) (.+)$/i,
|
|
1798
|
+
/^(?:what time is it|whats the time|what is the time|current time|time now) (?:in|at) (.+)$/i,
|
|
1799
|
+
/^(?:now|current time) in (.+)$/i
|
|
1800
|
+
];
|
|
1801
|
+
var TIME_EXPLICIT_CONVERT_PATTERN = /^(midnight|noon|\d{1,2}(?::\d{2})?(?:\s*(?:am|pm))?)\s+(?:in\s+)?(.+?)\s+to\s+(.+?)(?:\s+time)?$/i;
|
|
1802
|
+
var TIME_CONVERT_PATTERN = /^(.+?) to (.+?)(?: time)?$/i;
|
|
1708
1803
|
var TIME_NOW_PATTERN = /^(?:what(?:'s| is) )?(?:the )?(?:current )?time(?: now)?$/i;
|
|
1709
1804
|
function tryTimeIntent(input) {
|
|
1710
1805
|
let match;
|
|
1711
|
-
|
|
1806
|
+
for (const pattern of TIME_IN_PATTERNS) {
|
|
1807
|
+
match = input.match(pattern);
|
|
1808
|
+
if (match) {
|
|
1809
|
+
const place = normalizeTimePlace(match[1]);
|
|
1810
|
+
const tz2 = resolveTimezone(place);
|
|
1811
|
+
if (tz2) return { kind: "time", query: input, to: tz2 };
|
|
1812
|
+
return { kind: "time", query: input, to: place };
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
match = input.match(TIME_EXPLICIT_CONVERT_PATTERN);
|
|
1712
1816
|
if (match) {
|
|
1713
|
-
const
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1817
|
+
const time = match[1].trim();
|
|
1818
|
+
const from = normalizeTimePlace(match[2]);
|
|
1819
|
+
const to = normalizeTimePlace(match[3]);
|
|
1820
|
+
const fromTz = resolveTimezone(from);
|
|
1821
|
+
const toTz = resolveTimezone(to);
|
|
1822
|
+
if (fromTz && toTz) {
|
|
1823
|
+
return { kind: "time", query: input, from: fromTz, to: toTz, time };
|
|
1824
|
+
}
|
|
1717
1825
|
}
|
|
1718
1826
|
match = input.match(TIME_CONVERT_PATTERN);
|
|
1719
1827
|
if (match) {
|
|
1720
|
-
const from = match[1]
|
|
1721
|
-
const to = match[2]
|
|
1828
|
+
const from = normalizeTimePlace(match[1]);
|
|
1829
|
+
const to = normalizeTimePlace(match[2]);
|
|
1722
1830
|
const fromTz = resolveTimezone(from);
|
|
1723
1831
|
const toTz = resolveTimezone(to);
|
|
1724
1832
|
if (fromTz || toTz) {
|
|
@@ -1734,11 +1842,11 @@ function tryTimeIntent(input) {
|
|
|
1734
1842
|
}
|
|
1735
1843
|
const placeSuffixMatch = input.match(/^(.+?)\s+(?:time|now|time now)$/i);
|
|
1736
1844
|
if (placeSuffixMatch) {
|
|
1737
|
-
const place = placeSuffixMatch[1]
|
|
1845
|
+
const place = normalizeTimePlace(placeSuffixMatch[1]);
|
|
1738
1846
|
const tz2 = resolveTimezone(place);
|
|
1739
1847
|
if (tz2) return { kind: "time", query: place, to: tz2 };
|
|
1740
1848
|
}
|
|
1741
|
-
const tz = resolveTimezone(input);
|
|
1849
|
+
const tz = resolveTimezone(normalizeTimePlace(input));
|
|
1742
1850
|
if (tz) {
|
|
1743
1851
|
return { kind: "time", query: input, to: tz };
|
|
1744
1852
|
}
|
|
@@ -1747,11 +1855,12 @@ function tryTimeIntent(input) {
|
|
|
1747
1855
|
var DATE_PATTERNS = [
|
|
1748
1856
|
// Relative dates
|
|
1749
1857
|
/^(today|now|tomorrow|yesterday)$/i,
|
|
1858
|
+
/^(date|day after tomorrow|day before yesterday)$/i,
|
|
1750
1859
|
/^(next|last|this)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday|week|month|year)$/i,
|
|
1751
1860
|
/^(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)\s+(from now|ago|from today|from tomorrow)$/i,
|
|
1752
1861
|
/^in\s+(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)$/i,
|
|
1753
1862
|
// Unix timestamp
|
|
1754
|
-
/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/i,
|
|
1863
|
+
/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/i,
|
|
1755
1864
|
/^(?:unix|timestamp|epoch)\s+(\d{10,13})$/i,
|
|
1756
1865
|
// "to unix" / "to timestamp"
|
|
1757
1866
|
/^.+\s+(?:to|in)\s+(?:unix|timestamp|epoch)$/i,
|
|
@@ -1759,6 +1868,7 @@ var DATE_PATTERNS = [
|
|
|
1759
1868
|
/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})?/,
|
|
1760
1869
|
// "date" queries
|
|
1761
1870
|
/^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/i,
|
|
1871
|
+
/^(?:(?:what(?:'s|s| is))\s+today|(?:what(?:'s|s| is))\s+today'?s date|what day is it|what date is .+|todays date|today'?s date|date today|current date|what day is .+|when is .+)$/i,
|
|
1762
1872
|
// Days between dates
|
|
1763
1873
|
/^(?:days?\s+)?(?:between|from)\s+.+\s+(?:to|and|until)\s+.+$/i
|
|
1764
1874
|
];
|
|
@@ -1772,7 +1882,8 @@ function tryDateIntent(input) {
|
|
|
1772
1882
|
}
|
|
1773
1883
|
var CONVERSION_PATTERN = /^([\d.,]+)?\s*([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)\s+(?:to|in|into|as|=)\s+([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)$/i;
|
|
1774
1884
|
function tryCurrencyOrCryptoIntent(input) {
|
|
1775
|
-
const
|
|
1885
|
+
const normalized = normalizeConversionInput(input);
|
|
1886
|
+
const match = normalized.match(CONVERSION_PATTERN);
|
|
1776
1887
|
if (!match) return null;
|
|
1777
1888
|
const amount = match[1] ? parseFloat(match[1].replace(/,/g, "")) : 1;
|
|
1778
1889
|
const fromToken = match[2].trim();
|
|
@@ -1795,13 +1906,12 @@ function tryCurrencyOrCryptoIntent(input) {
|
|
|
1795
1906
|
var UNIT_PATTERN = /^(-?[\d.,]+)\s*([a-zA-Z°/µμ'"²³]+(?:\s+[a-zA-Z]+(?:\s+[a-zA-Z]+)?)?)\s+(?:to|in|into|as|=)\s+([a-zA-Z°/µμ'"²³]+(?:\s+[a-zA-Z]+(?:\s+[a-zA-Z]+)?)?)$/i;
|
|
1796
1907
|
var UNIT_PATTERN_NO_SPACE = /^(-?[\d.,]+)([a-zA-Z°]+)\s+(?:to|in|into|as)\s+([a-zA-Z°]+(?:\s+[a-zA-Z]+)?)$/i;
|
|
1797
1908
|
function tryUnitIntent(input) {
|
|
1798
|
-
const
|
|
1909
|
+
const normalized = normalizeConversionInput(input);
|
|
1910
|
+
const match = normalized.match(UNIT_PATTERN) || normalized.match(UNIT_PATTERN_NO_SPACE);
|
|
1799
1911
|
if (!match) return null;
|
|
1800
1912
|
const amount = parseFloat(match[1].replace(/,/g, ""));
|
|
1801
1913
|
const fromToken = match[2].trim().toLowerCase();
|
|
1802
1914
|
const toToken = match[3].trim().toLowerCase();
|
|
1803
|
-
if (resolveFiat(fromToken) || resolveCrypto(fromToken)) return null;
|
|
1804
|
-
if (resolveFiat(toToken) || resolveCrypto(toToken)) return null;
|
|
1805
1915
|
const fromUnit = lookupUnit(fromToken);
|
|
1806
1916
|
const toUnit = lookupUnit(toToken);
|
|
1807
1917
|
if (fromUnit && toUnit && fromUnit.category === toUnit.category) {
|
|
@@ -1813,8 +1923,22 @@ function tryUnitIntent(input) {
|
|
|
1813
1923
|
category: fromUnit.category.name
|
|
1814
1924
|
};
|
|
1815
1925
|
}
|
|
1926
|
+
if (resolveFiat(fromToken) || resolveCrypto(fromToken)) return null;
|
|
1927
|
+
if (resolveFiat(toToken) || resolveCrypto(toToken)) return null;
|
|
1816
1928
|
return null;
|
|
1817
1929
|
}
|
|
1930
|
+
function normalizeWhitespace(input) {
|
|
1931
|
+
return input.trim().replace(/\s+/g, " ");
|
|
1932
|
+
}
|
|
1933
|
+
function normalizeConversionInput(input) {
|
|
1934
|
+
let normalized = normalizeWhitespace(input);
|
|
1935
|
+
normalized = normalized.replace(/^convert\s+/i, "").replace(/^how much is\s+/i, "").replace(/^what(?:'s|s| is)\s+/i, "");
|
|
1936
|
+
normalized = normalized.replace(/^([$€£¥₹₩₽₺₦₵₪฿])\s*([\d.,]+)/, "$2 $1");
|
|
1937
|
+
return normalized;
|
|
1938
|
+
}
|
|
1939
|
+
function normalizeTimePlace(value) {
|
|
1940
|
+
return normalizeWhitespace(value).replace(/^(?:time|current time|time now|now)\s+(?:in|at)\s+/i, "").replace(/^(?:in|at)\s+/i, "").replace(/\s+(?:time|now|time now)$/i, "").trim();
|
|
1941
|
+
}
|
|
1818
1942
|
|
|
1819
1943
|
// src/evaluators/math.ts
|
|
1820
1944
|
var CONSTANTS = {
|
|
@@ -1965,19 +2089,10 @@ var Parser = class {
|
|
|
1965
2089
|
if (right === 0) throw new Error("Division by zero");
|
|
1966
2090
|
left /= right;
|
|
1967
2091
|
} else if (op === "%") {
|
|
1968
|
-
|
|
1969
|
-
this.
|
|
1970
|
-
|
|
1971
|
-
this.
|
|
1972
|
-
const next = this.expr[this.pos];
|
|
1973
|
-
if (next === void 0 || /[+\-*/^)|&<>]/.test(next)) {
|
|
1974
|
-
this.pos = savedPos;
|
|
1975
|
-
break;
|
|
1976
|
-
} else {
|
|
1977
|
-
this.pos = savedPos;
|
|
1978
|
-
this.consume();
|
|
1979
|
-
left = left % this.parseExponentiation();
|
|
1980
|
-
}
|
|
2092
|
+
this.consume();
|
|
2093
|
+
left = left % this.parseExponentiation();
|
|
2094
|
+
} else if (op && /[([a-zA-Zπτφ_]/.test(op)) {
|
|
2095
|
+
left *= this.parseExponentiation();
|
|
1981
2096
|
} else {
|
|
1982
2097
|
break;
|
|
1983
2098
|
}
|
|
@@ -2019,7 +2134,7 @@ var Parser = class {
|
|
|
2019
2134
|
if (this.expr[this.pos] === "!") {
|
|
2020
2135
|
this.pos++;
|
|
2021
2136
|
value = factorial(value);
|
|
2022
|
-
} else if (this.expr[this.pos] === "%") {
|
|
2137
|
+
} else if (this.expr[this.pos] === "%" && this.shouldTreatAsPercentage()) {
|
|
2023
2138
|
this.pos++;
|
|
2024
2139
|
value = value / 100;
|
|
2025
2140
|
} else {
|
|
@@ -2045,6 +2160,12 @@ var Parser = class {
|
|
|
2045
2160
|
}
|
|
2046
2161
|
throw new Error(`Unexpected character: '${this.expr[this.pos]}' at position ${this.pos}`);
|
|
2047
2162
|
}
|
|
2163
|
+
shouldTreatAsPercentage() {
|
|
2164
|
+
let i = this.pos + 1;
|
|
2165
|
+
while (i < this.expr.length && /\s/.test(this.expr[i])) i++;
|
|
2166
|
+
const next = this.expr[i];
|
|
2167
|
+
return next === void 0 || /[+\-*/^)%|&<>]/.test(next);
|
|
2168
|
+
}
|
|
2048
2169
|
charAt(i) {
|
|
2049
2170
|
return i < this.expr.length ? this.expr[i] : "";
|
|
2050
2171
|
}
|
|
@@ -2088,13 +2209,15 @@ var Parser = class {
|
|
|
2088
2209
|
const name = this.expr.slice(start, this.pos).toLowerCase();
|
|
2089
2210
|
this.skipWhitespace();
|
|
2090
2211
|
if (this.expr[this.pos] === "(") {
|
|
2212
|
+
if (name === "max" || name === "min") {
|
|
2213
|
+
const args = this.parseArguments();
|
|
2214
|
+
if (args.length === 0) throw new Error(`Function '${name}' requires at least one argument`);
|
|
2215
|
+
return name === "max" ? Math.max(...args) : Math.min(...args);
|
|
2216
|
+
}
|
|
2091
2217
|
if (FUNCTIONS2[name]) {
|
|
2092
|
-
this.
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
const b = this.parseBitwiseOr();
|
|
2096
|
-
this.consume(")");
|
|
2097
|
-
return FUNCTIONS2[name](a, b);
|
|
2218
|
+
const args = this.parseArguments();
|
|
2219
|
+
if (args.length !== 2) throw new Error(`Function '${name}' expects 2 arguments`);
|
|
2220
|
+
return FUNCTIONS2[name](args[0], args[1]);
|
|
2098
2221
|
}
|
|
2099
2222
|
if (FUNCTIONS[name]) {
|
|
2100
2223
|
this.consume("(");
|
|
@@ -2107,6 +2230,26 @@ var Parser = class {
|
|
|
2107
2230
|
if (CONSTANTS[name] !== void 0) return CONSTANTS[name];
|
|
2108
2231
|
throw new Error(`Unknown identifier: '${name}'`);
|
|
2109
2232
|
}
|
|
2233
|
+
parseArguments() {
|
|
2234
|
+
const args = [];
|
|
2235
|
+
this.consume("(");
|
|
2236
|
+
this.skipWhitespace();
|
|
2237
|
+
if (this.expr[this.pos] === ")") {
|
|
2238
|
+
this.consume(")");
|
|
2239
|
+
return args;
|
|
2240
|
+
}
|
|
2241
|
+
while (true) {
|
|
2242
|
+
args.push(this.parseBitwiseOr());
|
|
2243
|
+
this.skipWhitespace();
|
|
2244
|
+
if (this.expr[this.pos] === ",") {
|
|
2245
|
+
this.pos++;
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
this.consume(")");
|
|
2251
|
+
return args;
|
|
2252
|
+
}
|
|
2110
2253
|
};
|
|
2111
2254
|
function factorial(n) {
|
|
2112
2255
|
if (n < 0) throw new Error("Factorial of negative number");
|
|
@@ -2118,7 +2261,8 @@ function factorial(n) {
|
|
|
2118
2261
|
}
|
|
2119
2262
|
function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
2120
2263
|
try {
|
|
2121
|
-
const
|
|
2264
|
+
const normalizedExpression = normalizeMathExpression(expression);
|
|
2265
|
+
const parser = new Parser(normalizedExpression);
|
|
2122
2266
|
const result = parser.parse();
|
|
2123
2267
|
let formatted;
|
|
2124
2268
|
if (Number.isInteger(result) && Math.abs(result) < Number.MAX_SAFE_INTEGER) {
|
|
@@ -2143,7 +2287,10 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2143
2287
|
input: expression,
|
|
2144
2288
|
result,
|
|
2145
2289
|
formatted,
|
|
2146
|
-
metadata
|
|
2290
|
+
metadata: {
|
|
2291
|
+
...metadata,
|
|
2292
|
+
normalizedExpression
|
|
2293
|
+
}
|
|
2147
2294
|
};
|
|
2148
2295
|
} catch (err) {
|
|
2149
2296
|
return {
|
|
@@ -2153,6 +2300,41 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2153
2300
|
};
|
|
2154
2301
|
}
|
|
2155
2302
|
}
|
|
2303
|
+
function normalizeMathExpression(expression) {
|
|
2304
|
+
let normalized = expression.trim().replace(/\s+/g, " ");
|
|
2305
|
+
normalized = normalized.replace(/^(?:(?:what(?:'s|s| is))\s+the\s+|(?:what(?:'s|s| is))\s+|calculate\s+)/i, "");
|
|
2306
|
+
normalized = normalized.replace(/^the\s+/i, "");
|
|
2307
|
+
const wrappers = [
|
|
2308
|
+
[/^square root of (.+)$/i, "sqrt($1)"],
|
|
2309
|
+
[/^cube root of (.+)$/i, "cbrt($1)"],
|
|
2310
|
+
[/^log of (.+)$/i, "log($1)"],
|
|
2311
|
+
[/^sine of (.+)$/i, "sin($1)"],
|
|
2312
|
+
[/^cosine of (.+)$/i, "cos($1)"],
|
|
2313
|
+
[/^tangent of (.+)$/i, "tan($1)"],
|
|
2314
|
+
[/^absolute value of (.+)$/i, "abs($1)"],
|
|
2315
|
+
[/^factorial of (.+)$/i, "($1)!"],
|
|
2316
|
+
[/^half of (.+)$/i, "($1) / 2"],
|
|
2317
|
+
[/^double (.+)$/i, "2 * ($1)"],
|
|
2318
|
+
[/^triple (.+)$/i, "3 * ($1)"],
|
|
2319
|
+
[/^one third of (.+)$/i, "($1) / 3"],
|
|
2320
|
+
[/^one quarter of (.+)$/i, "($1) / 4"],
|
|
2321
|
+
[/^remainder of (.+?) divided by (.+)$/i, "($1) % ($2)"],
|
|
2322
|
+
[/^(.+?) to the power of (.+)$/i, "($1) ^ ($2)"],
|
|
2323
|
+
[/^(.+?) raised to (.+)$/i, "($1) ^ ($2)"],
|
|
2324
|
+
[/^(.+?) squared$/i, "($1) ^ 2"],
|
|
2325
|
+
[/^(.+?) cubed$/i, "($1) ^ 3"],
|
|
2326
|
+
[/^(\d+(?:\.\d+)?) percent of (.+)$/i, "($1%) * ($2)"],
|
|
2327
|
+
[/^(.+?%) of (.+)$/i, "($1) * ($2)"]
|
|
2328
|
+
];
|
|
2329
|
+
for (const [pattern, replacement] of wrappers) {
|
|
2330
|
+
if (pattern.test(normalized)) {
|
|
2331
|
+
normalized = normalized.replace(pattern, replacement);
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
normalized = normalized.replace(/\bdivided by\b/gi, "/").replace(/\btimes\b/gi, "*").replace(/\bplus\b/gi, "+").replace(/\bminus\b/gi, "-");
|
|
2336
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
2337
|
+
}
|
|
2156
2338
|
|
|
2157
2339
|
// src/evaluators/unit.ts
|
|
2158
2340
|
function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision = 10) {
|
|
@@ -2190,7 +2372,7 @@ function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision =
|
|
|
2190
2372
|
}
|
|
2191
2373
|
|
|
2192
2374
|
// src/evaluators/time.ts
|
|
2193
|
-
function evaluateTime(query, fromTz, toTz, localTz) {
|
|
2375
|
+
function evaluateTime(query, fromTz, toTz, explicitTime, localTz) {
|
|
2194
2376
|
const now = /* @__PURE__ */ new Date();
|
|
2195
2377
|
const userTz = localTz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2196
2378
|
try {
|
|
@@ -2223,18 +2405,20 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2223
2405
|
const resolvedFrom = resolveTimezone(fromTz) ?? fromTz;
|
|
2224
2406
|
const resolvedTo = resolveTimezone(toTz) ?? toTz;
|
|
2225
2407
|
try {
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2408
|
+
const sourceDate = explicitTime ? buildDateInZone(explicitTime, resolvedFrom, now) : now;
|
|
2409
|
+
const fromTime = explicitTime ? `${explicitTime} (${getShortTzName(resolvedFrom)})` : formatTimeInZone(sourceDate, resolvedFrom);
|
|
2410
|
+
const toTime = formatTimeInZone(sourceDate, resolvedTo);
|
|
2411
|
+
const formatted = toTime;
|
|
2229
2412
|
return {
|
|
2230
2413
|
type: "time",
|
|
2231
2414
|
input: query,
|
|
2232
2415
|
result: formatted,
|
|
2233
2416
|
formatted,
|
|
2234
2417
|
metadata: {
|
|
2418
|
+
explicitTime,
|
|
2235
2419
|
from: { timezone: resolvedFrom, time: fromTime },
|
|
2236
2420
|
to: { timezone: resolvedTo, time: toTime },
|
|
2237
|
-
iso:
|
|
2421
|
+
iso: sourceDate.toISOString()
|
|
2238
2422
|
}
|
|
2239
2423
|
};
|
|
2240
2424
|
} catch {
|
|
@@ -2254,6 +2438,55 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2254
2438
|
};
|
|
2255
2439
|
}
|
|
2256
2440
|
}
|
|
2441
|
+
function buildDateInZone(timeExpression, timezone, anchor) {
|
|
2442
|
+
const { hour, minute } = parseTimeExpression(timeExpression);
|
|
2443
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2444
|
+
timeZone: timezone,
|
|
2445
|
+
year: "numeric",
|
|
2446
|
+
month: "2-digit",
|
|
2447
|
+
day: "2-digit"
|
|
2448
|
+
}).formatToParts(anchor);
|
|
2449
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2450
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2451
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2452
|
+
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0));
|
|
2453
|
+
const offset = getTimeZoneOffsetMs(utcGuess, timezone);
|
|
2454
|
+
return new Date(utcGuess.getTime() - offset);
|
|
2455
|
+
}
|
|
2456
|
+
function parseTimeExpression(value) {
|
|
2457
|
+
const trimmed = value.trim().toLowerCase();
|
|
2458
|
+
if (trimmed === "midnight") return { hour: 0, minute: 0 };
|
|
2459
|
+
if (trimmed === "noon") return { hour: 12, minute: 0 };
|
|
2460
|
+
const match = trimmed.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/);
|
|
2461
|
+
if (!match) throw new Error(`Invalid time: '${value}'`);
|
|
2462
|
+
let hour = parseInt(match[1], 10);
|
|
2463
|
+
const minute = parseInt(match[2] ?? "0", 10);
|
|
2464
|
+
const meridiem = match[3];
|
|
2465
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
2466
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
2467
|
+
if (hour > 23 || minute > 59) throw new Error(`Invalid time: '${value}'`);
|
|
2468
|
+
return { hour, minute };
|
|
2469
|
+
}
|
|
2470
|
+
function getTimeZoneOffsetMs(date, timezone) {
|
|
2471
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2472
|
+
timeZone: timezone,
|
|
2473
|
+
hour12: false,
|
|
2474
|
+
year: "numeric",
|
|
2475
|
+
month: "2-digit",
|
|
2476
|
+
day: "2-digit",
|
|
2477
|
+
hour: "2-digit",
|
|
2478
|
+
minute: "2-digit",
|
|
2479
|
+
second: "2-digit"
|
|
2480
|
+
}).formatToParts(date);
|
|
2481
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2482
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2483
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2484
|
+
const rawHour = Number(parts.find((p) => p.type === "hour")?.value);
|
|
2485
|
+
const hour = rawHour % 24;
|
|
2486
|
+
const minute = Number(parts.find((p) => p.type === "minute")?.value);
|
|
2487
|
+
const second = Number(parts.find((p) => p.type === "second")?.value);
|
|
2488
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - date.getTime();
|
|
2489
|
+
}
|
|
2257
2490
|
function formatTimeInZone(date, timezone) {
|
|
2258
2491
|
const timePart = new Intl.DateTimeFormat("en-US", {
|
|
2259
2492
|
timeZone: timezone,
|
|
@@ -2290,7 +2523,7 @@ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday
|
|
|
2290
2523
|
function evaluateDate(query) {
|
|
2291
2524
|
const lower = query.toLowerCase().trim();
|
|
2292
2525
|
try {
|
|
2293
|
-
if (lower === "today" || lower === "now" || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2526
|
+
if (lower === "today" || lower === "now" || lower === "date" || /^(?:(?:what(?:'s|s| is))\s+today|(?:what(?:'s|s| is))\s+today'?s date|what day is it|todays date|today'?s date|date today|current date|what is the date today)$/.test(lower) || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2294
2527
|
return formatDateResult(query, /* @__PURE__ */ new Date());
|
|
2295
2528
|
}
|
|
2296
2529
|
if (lower === "tomorrow") {
|
|
@@ -2303,6 +2536,26 @@ function evaluateDate(query) {
|
|
|
2303
2536
|
d.setDate(d.getDate() - 1);
|
|
2304
2537
|
return formatDateResult(query, d);
|
|
2305
2538
|
}
|
|
2539
|
+
if (lower === "day after tomorrow") {
|
|
2540
|
+
const d = /* @__PURE__ */ new Date();
|
|
2541
|
+
d.setDate(d.getDate() + 2);
|
|
2542
|
+
return formatDateResult(query, d);
|
|
2543
|
+
}
|
|
2544
|
+
if (lower === "day before yesterday") {
|
|
2545
|
+
const d = /* @__PURE__ */ new Date();
|
|
2546
|
+
d.setDate(d.getDate() - 2);
|
|
2547
|
+
return formatDateResult(query, d);
|
|
2548
|
+
}
|
|
2549
|
+
const whatDateMatch = lower.match(/^what date is (.+)$/);
|
|
2550
|
+
if (whatDateMatch) {
|
|
2551
|
+
const d = parseFlexibleDate(whatDateMatch[1]);
|
|
2552
|
+
if (d) return formatDateResult(query, d);
|
|
2553
|
+
}
|
|
2554
|
+
const whenIsMatch = lower.match(/^(?:what day is|when is)\s+(.+)$/);
|
|
2555
|
+
if (whenIsMatch) {
|
|
2556
|
+
const d = parseFlexibleDate(whenIsMatch[1]);
|
|
2557
|
+
if (d) return formatDateResult(query, d);
|
|
2558
|
+
}
|
|
2306
2559
|
const nextLastMatch = lower.match(/^(next|last|this)\s+(.+)$/);
|
|
2307
2560
|
if (nextLastMatch) {
|
|
2308
2561
|
const direction = nextLastMatch[1];
|
|
@@ -2350,7 +2603,7 @@ function evaluateDate(query) {
|
|
|
2350
2603
|
const d = applyOffset(/* @__PURE__ */ new Date(), amount, unit);
|
|
2351
2604
|
return formatDateResult(query, d);
|
|
2352
2605
|
}
|
|
2353
|
-
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/);
|
|
2606
|
+
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/);
|
|
2354
2607
|
if (unixMatch) {
|
|
2355
2608
|
const ts = parseInt(unixMatch[1]);
|
|
2356
2609
|
const ms = ts > 9999999999 ? ts : ts * 1e3;
|
|
@@ -2370,14 +2623,11 @@ function evaluateDate(query) {
|
|
|
2370
2623
|
if (toUnixMatch) {
|
|
2371
2624
|
const dateStr = toUnixMatch[1].trim();
|
|
2372
2625
|
let d;
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
} else {
|
|
2376
|
-
d = new Date(dateStr);
|
|
2377
|
-
}
|
|
2378
|
-
if (isNaN(d.getTime())) {
|
|
2626
|
+
const parsed = parseFlexibleDate(dateStr);
|
|
2627
|
+
if (!parsed) {
|
|
2379
2628
|
return { type: "error", input: query, error: `Cannot parse date: '${dateStr}'` };
|
|
2380
2629
|
}
|
|
2630
|
+
d = parsed;
|
|
2381
2631
|
const unix = Math.floor(d.getTime() / 1e3);
|
|
2382
2632
|
return {
|
|
2383
2633
|
type: "date",
|
|
@@ -2486,7 +2736,7 @@ function formatDateResult(input, d, extraMeta) {
|
|
|
2486
2736
|
};
|
|
2487
2737
|
}
|
|
2488
2738
|
function parseFlexibleDate(str) {
|
|
2489
|
-
if (str === "today" || str === "now") return /* @__PURE__ */ new Date();
|
|
2739
|
+
if (str === "today" || str === "now" || str === "date") return /* @__PURE__ */ new Date();
|
|
2490
2740
|
if (str === "tomorrow") {
|
|
2491
2741
|
const d2 = /* @__PURE__ */ new Date();
|
|
2492
2742
|
d2.setDate(d2.getDate() + 1);
|
|
@@ -2497,6 +2747,22 @@ function parseFlexibleDate(str) {
|
|
|
2497
2747
|
d2.setDate(d2.getDate() - 1);
|
|
2498
2748
|
return d2;
|
|
2499
2749
|
}
|
|
2750
|
+
if (str === "day after tomorrow") {
|
|
2751
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2752
|
+
d2.setDate(d2.getDate() + 2);
|
|
2753
|
+
return d2;
|
|
2754
|
+
}
|
|
2755
|
+
if (str === "day before yesterday") {
|
|
2756
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2757
|
+
d2.setDate(d2.getDate() - 2);
|
|
2758
|
+
return d2;
|
|
2759
|
+
}
|
|
2760
|
+
const relativeDay = str.match(/^(next|last|this)\s+(.+)$/);
|
|
2761
|
+
if (relativeDay) {
|
|
2762
|
+
const direction = relativeDay[1];
|
|
2763
|
+
const dayIndex = DAY_NAMES.indexOf(relativeDay[2]);
|
|
2764
|
+
if (dayIndex !== -1) return getRelativeDay(dayIndex, direction);
|
|
2765
|
+
}
|
|
2500
2766
|
const d = new Date(str);
|
|
2501
2767
|
return isNaN(d.getTime()) ? null : d;
|
|
2502
2768
|
}
|
|
@@ -2614,6 +2880,7 @@ async function calculate(input, options) {
|
|
|
2614
2880
|
intent.query,
|
|
2615
2881
|
intent.from,
|
|
2616
2882
|
intent.to,
|
|
2883
|
+
intent.time,
|
|
2617
2884
|
options?.timezone
|
|
2618
2885
|
);
|
|
2619
2886
|
case "date":
|
package/dist/index.mjs
CHANGED
|
@@ -894,7 +894,7 @@ for (const cat of UNIT_CATEGORIES) {
|
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
function lookupUnit(token) {
|
|
897
|
-
return _unitIndex.get(token
|
|
897
|
+
return _unitIndex.get(normalizeUnitToken(token));
|
|
898
898
|
}
|
|
899
899
|
function convertUnit(value, from, to) {
|
|
900
900
|
if (from.category !== to.category) {
|
|
@@ -909,6 +909,9 @@ function convertUnit(value, from, to) {
|
|
|
909
909
|
const baseValue = value * from.def.factor;
|
|
910
910
|
return baseValue / to.def.factor;
|
|
911
911
|
}
|
|
912
|
+
function normalizeUnitToken(token) {
|
|
913
|
+
return token.toLowerCase().trim().replace(/\s+/g, " ").replace(/²/g, "2").replace(/³/g, "3");
|
|
914
|
+
}
|
|
912
915
|
|
|
913
916
|
// src/data/timezones.ts
|
|
914
917
|
var TIMEZONE_MAP = {
|
|
@@ -981,8 +984,11 @@ var TIMEZONE_MAP = {
|
|
|
981
984
|
"buenos aires": "America/Argentina/Buenos_Aires",
|
|
982
985
|
argentina: "America/Argentina/Buenos_Aires",
|
|
983
986
|
lima: "America/Lima",
|
|
987
|
+
peru: "America/Lima",
|
|
984
988
|
bogota: "America/Bogota",
|
|
989
|
+
colombia: "America/Bogota",
|
|
985
990
|
santiago: "America/Santiago",
|
|
991
|
+
chile: "America/Santiago",
|
|
986
992
|
caracas: "America/Caracas",
|
|
987
993
|
quito: "America/Guayaquil",
|
|
988
994
|
// ─── Europe ───────────────────────────────────────────
|
|
@@ -1266,6 +1272,9 @@ var COUNTRY_TZ = {
|
|
|
1266
1272
|
canada: "America/Toronto",
|
|
1267
1273
|
mexico: "America/Mexico_City",
|
|
1268
1274
|
brazil: "America/Sao_Paulo",
|
|
1275
|
+
colombia: "America/Bogota",
|
|
1276
|
+
peru: "America/Lima",
|
|
1277
|
+
chile: "America/Santiago",
|
|
1269
1278
|
uk: "Europe/London",
|
|
1270
1279
|
"united kingdom": "Europe/London",
|
|
1271
1280
|
britain: "Europe/London",
|
|
@@ -1336,6 +1345,58 @@ function resolveTimezone(name) {
|
|
|
1336
1345
|
}
|
|
1337
1346
|
|
|
1338
1347
|
// src/providers/default.ts
|
|
1348
|
+
var STATIC_FIAT_PER_USD = {
|
|
1349
|
+
USD: 1,
|
|
1350
|
+
EUR: 0.92,
|
|
1351
|
+
GBP: 0.79,
|
|
1352
|
+
INR: 83.1,
|
|
1353
|
+
JPY: 150,
|
|
1354
|
+
CNY: 7.2,
|
|
1355
|
+
AUD: 1.52,
|
|
1356
|
+
CAD: 1.35,
|
|
1357
|
+
CHF: 0.88,
|
|
1358
|
+
KRW: 1330,
|
|
1359
|
+
SGD: 1.34,
|
|
1360
|
+
HKD: 7.8,
|
|
1361
|
+
NZD: 1.64,
|
|
1362
|
+
SEK: 10.5,
|
|
1363
|
+
NOK: 10.7,
|
|
1364
|
+
DKK: 6.85,
|
|
1365
|
+
MXN: 17.1,
|
|
1366
|
+
BRL: 5,
|
|
1367
|
+
THB: 35.8,
|
|
1368
|
+
PHP: 56.2,
|
|
1369
|
+
IDR: 15700,
|
|
1370
|
+
MYR: 4.48,
|
|
1371
|
+
ZAR: 18.4,
|
|
1372
|
+
AED: 3.67,
|
|
1373
|
+
SAR: 3.75,
|
|
1374
|
+
TRY: 32.1,
|
|
1375
|
+
PLN: 3.95,
|
|
1376
|
+
CZK: 23.2,
|
|
1377
|
+
RUB: 92
|
|
1378
|
+
};
|
|
1379
|
+
var STATIC_CRYPTO_USD = {
|
|
1380
|
+
BTC: 92e3,
|
|
1381
|
+
ETH: 3500,
|
|
1382
|
+
SOL: 180,
|
|
1383
|
+
XRP: 2.3,
|
|
1384
|
+
DOGE: 0.18,
|
|
1385
|
+
ADA: 0.8,
|
|
1386
|
+
MATIC: 0.95,
|
|
1387
|
+
DOT: 7.5,
|
|
1388
|
+
LTC: 95,
|
|
1389
|
+
AVAX: 42,
|
|
1390
|
+
LINK: 20,
|
|
1391
|
+
UNI: 12,
|
|
1392
|
+
ATOM: 10,
|
|
1393
|
+
XLM: 0.12,
|
|
1394
|
+
TON: 6,
|
|
1395
|
+
SHIB: 25e-6,
|
|
1396
|
+
BNB: 650,
|
|
1397
|
+
USDT: 1,
|
|
1398
|
+
USDC: 1
|
|
1399
|
+
};
|
|
1339
1400
|
async function fetchJSON(url, timeoutMs = 8e3) {
|
|
1340
1401
|
const controller = new AbortController();
|
|
1341
1402
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -1358,6 +1419,30 @@ async function withFallback(fns) {
|
|
|
1358
1419
|
}
|
|
1359
1420
|
throw lastError ?? new Error("All providers failed");
|
|
1360
1421
|
}
|
|
1422
|
+
async function staticFiatRate(base, target) {
|
|
1423
|
+
const from = STATIC_FIAT_PER_USD[base];
|
|
1424
|
+
const to = STATIC_FIAT_PER_USD[target];
|
|
1425
|
+
if (from === void 0 || to === void 0) {
|
|
1426
|
+
throw new Error(`Static fiat rate unavailable for ${base}/${target}`);
|
|
1427
|
+
}
|
|
1428
|
+
return to / from;
|
|
1429
|
+
}
|
|
1430
|
+
async function staticCryptoRate(from, to) {
|
|
1431
|
+
const fromCrypto = STATIC_CRYPTO_USD[from];
|
|
1432
|
+
const toCrypto = STATIC_CRYPTO_USD[to];
|
|
1433
|
+
if (fromCrypto !== void 0 && toCrypto !== void 0) {
|
|
1434
|
+
return fromCrypto / toCrypto;
|
|
1435
|
+
}
|
|
1436
|
+
if (fromCrypto !== void 0) {
|
|
1437
|
+
const usdToTarget = await staticFiatRate("USD", to);
|
|
1438
|
+
return fromCrypto * usdToTarget;
|
|
1439
|
+
}
|
|
1440
|
+
if (toCrypto !== void 0) {
|
|
1441
|
+
const fromToUsd = await staticFiatRate(from, "USD");
|
|
1442
|
+
return fromToUsd / toCrypto;
|
|
1443
|
+
}
|
|
1444
|
+
throw new Error(`Static crypto rate unavailable for ${from}/${to}`);
|
|
1445
|
+
}
|
|
1361
1446
|
async function frankfurterRate(base, target) {
|
|
1362
1447
|
const data = await fetchJSON(
|
|
1363
1448
|
`https://api.frankfurter.dev/v1/latest?base=${base}&symbols=${target}`
|
|
@@ -1607,7 +1692,8 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1607
1692
|
return await withFallback([
|
|
1608
1693
|
() => coingeckoRate(from, to),
|
|
1609
1694
|
() => binanceRate(from, to),
|
|
1610
|
-
() => coincapRate(from, to)
|
|
1695
|
+
() => coincapRate(from, to),
|
|
1696
|
+
() => staticCryptoRate(from, to)
|
|
1611
1697
|
]);
|
|
1612
1698
|
} catch {
|
|
1613
1699
|
}
|
|
@@ -1615,7 +1701,8 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1615
1701
|
const cryptoToUsd = await withFallback([
|
|
1616
1702
|
() => coingeckoRate(from, "USD"),
|
|
1617
1703
|
() => binanceRate(from, "USDT"),
|
|
1618
|
-
() => coincapRate(from, "USD")
|
|
1704
|
+
() => coincapRate(from, "USD"),
|
|
1705
|
+
() => staticCryptoRate(from, "USD")
|
|
1619
1706
|
]);
|
|
1620
1707
|
if (to === "USD" || to === "USDT" || to === "USDC") return cryptoToUsd;
|
|
1621
1708
|
const usdToFiat = await getFiat("USD", to);
|
|
@@ -1632,11 +1719,12 @@ async function getCryptoRateWithFiatFallback(from, to, getFiat) {
|
|
|
1632
1719
|
async () => {
|
|
1633
1720
|
const p = await coincapRate(to, "USD");
|
|
1634
1721
|
return p === 0 ? Promise.reject("zero") : 1 / p;
|
|
1635
|
-
}
|
|
1722
|
+
},
|
|
1723
|
+
() => staticCryptoRate("USD", to)
|
|
1636
1724
|
]);
|
|
1637
1725
|
return fiatToUsd * usdToCrypto;
|
|
1638
1726
|
}
|
|
1639
|
-
|
|
1727
|
+
return staticCryptoRate(from, to);
|
|
1640
1728
|
}
|
|
1641
1729
|
function createDefaultProvider() {
|
|
1642
1730
|
return {
|
|
@@ -1644,14 +1732,16 @@ function createDefaultProvider() {
|
|
|
1644
1732
|
return withFallback([
|
|
1645
1733
|
() => frankfurterRate(base, target),
|
|
1646
1734
|
() => exchangeRateApiRate(base, target),
|
|
1647
|
-
() => fawazCurrencyRate(base, target)
|
|
1735
|
+
() => fawazCurrencyRate(base, target),
|
|
1736
|
+
() => staticFiatRate(base, target)
|
|
1648
1737
|
]);
|
|
1649
1738
|
},
|
|
1650
1739
|
async getCryptoRate(base, target) {
|
|
1651
1740
|
const getFiat = (b, t) => withFallback([
|
|
1652
1741
|
() => frankfurterRate(b, t),
|
|
1653
1742
|
() => exchangeRateApiRate(b, t),
|
|
1654
|
-
() => fawazCurrencyRate(b, t)
|
|
1743
|
+
() => fawazCurrencyRate(b, t),
|
|
1744
|
+
() => staticFiatRate(b, t)
|
|
1655
1745
|
]);
|
|
1656
1746
|
return getCryptoRateWithFiatFallback(base, target, getFiat);
|
|
1657
1747
|
}
|
|
@@ -1660,28 +1750,46 @@ function createDefaultProvider() {
|
|
|
1660
1750
|
|
|
1661
1751
|
// src/parser/intent.ts
|
|
1662
1752
|
function detectIntent(input) {
|
|
1663
|
-
const trimmed = input
|
|
1753
|
+
const trimmed = normalizeWhitespace(input);
|
|
1664
1754
|
if (!trimmed) {
|
|
1665
1755
|
return { kind: "math", expression: "0" };
|
|
1666
1756
|
}
|
|
1667
1757
|
return tryTimeIntent(trimmed) ?? tryDateIntent(trimmed) ?? tryCurrencyOrCryptoIntent(trimmed) ?? tryUnitIntent(trimmed) ?? { kind: "math", expression: trimmed };
|
|
1668
1758
|
}
|
|
1669
|
-
var
|
|
1670
|
-
|
|
1759
|
+
var TIME_IN_PATTERNS = [
|
|
1760
|
+
/^(?:what(?:'s| is) )?(?:the )?(?:current )?time (?:in|at) (.+)$/i,
|
|
1761
|
+
/^(?:what time is it|whats the time|what is the time|current time|time now) (?:in|at) (.+)$/i,
|
|
1762
|
+
/^(?:now|current time) in (.+)$/i
|
|
1763
|
+
];
|
|
1764
|
+
var TIME_EXPLICIT_CONVERT_PATTERN = /^(midnight|noon|\d{1,2}(?::\d{2})?(?:\s*(?:am|pm))?)\s+(?:in\s+)?(.+?)\s+to\s+(.+?)(?:\s+time)?$/i;
|
|
1765
|
+
var TIME_CONVERT_PATTERN = /^(.+?) to (.+?)(?: time)?$/i;
|
|
1671
1766
|
var TIME_NOW_PATTERN = /^(?:what(?:'s| is) )?(?:the )?(?:current )?time(?: now)?$/i;
|
|
1672
1767
|
function tryTimeIntent(input) {
|
|
1673
1768
|
let match;
|
|
1674
|
-
|
|
1769
|
+
for (const pattern of TIME_IN_PATTERNS) {
|
|
1770
|
+
match = input.match(pattern);
|
|
1771
|
+
if (match) {
|
|
1772
|
+
const place = normalizeTimePlace(match[1]);
|
|
1773
|
+
const tz2 = resolveTimezone(place);
|
|
1774
|
+
if (tz2) return { kind: "time", query: input, to: tz2 };
|
|
1775
|
+
return { kind: "time", query: input, to: place };
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
match = input.match(TIME_EXPLICIT_CONVERT_PATTERN);
|
|
1675
1779
|
if (match) {
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1780
|
+
const time = match[1].trim();
|
|
1781
|
+
const from = normalizeTimePlace(match[2]);
|
|
1782
|
+
const to = normalizeTimePlace(match[3]);
|
|
1783
|
+
const fromTz = resolveTimezone(from);
|
|
1784
|
+
const toTz = resolveTimezone(to);
|
|
1785
|
+
if (fromTz && toTz) {
|
|
1786
|
+
return { kind: "time", query: input, from: fromTz, to: toTz, time };
|
|
1787
|
+
}
|
|
1680
1788
|
}
|
|
1681
1789
|
match = input.match(TIME_CONVERT_PATTERN);
|
|
1682
1790
|
if (match) {
|
|
1683
|
-
const from = match[1]
|
|
1684
|
-
const to = match[2]
|
|
1791
|
+
const from = normalizeTimePlace(match[1]);
|
|
1792
|
+
const to = normalizeTimePlace(match[2]);
|
|
1685
1793
|
const fromTz = resolveTimezone(from);
|
|
1686
1794
|
const toTz = resolveTimezone(to);
|
|
1687
1795
|
if (fromTz || toTz) {
|
|
@@ -1697,11 +1805,11 @@ function tryTimeIntent(input) {
|
|
|
1697
1805
|
}
|
|
1698
1806
|
const placeSuffixMatch = input.match(/^(.+?)\s+(?:time|now|time now)$/i);
|
|
1699
1807
|
if (placeSuffixMatch) {
|
|
1700
|
-
const place = placeSuffixMatch[1]
|
|
1808
|
+
const place = normalizeTimePlace(placeSuffixMatch[1]);
|
|
1701
1809
|
const tz2 = resolveTimezone(place);
|
|
1702
1810
|
if (tz2) return { kind: "time", query: place, to: tz2 };
|
|
1703
1811
|
}
|
|
1704
|
-
const tz = resolveTimezone(input);
|
|
1812
|
+
const tz = resolveTimezone(normalizeTimePlace(input));
|
|
1705
1813
|
if (tz) {
|
|
1706
1814
|
return { kind: "time", query: input, to: tz };
|
|
1707
1815
|
}
|
|
@@ -1710,11 +1818,12 @@ function tryTimeIntent(input) {
|
|
|
1710
1818
|
var DATE_PATTERNS = [
|
|
1711
1819
|
// Relative dates
|
|
1712
1820
|
/^(today|now|tomorrow|yesterday)$/i,
|
|
1821
|
+
/^(date|day after tomorrow|day before yesterday)$/i,
|
|
1713
1822
|
/^(next|last|this)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday|week|month|year)$/i,
|
|
1714
1823
|
/^(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)\s+(from now|ago|from today|from tomorrow)$/i,
|
|
1715
1824
|
/^in\s+(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?|seconds?)$/i,
|
|
1716
1825
|
// Unix timestamp
|
|
1717
|
-
/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/i,
|
|
1826
|
+
/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/i,
|
|
1718
1827
|
/^(?:unix|timestamp|epoch)\s+(\d{10,13})$/i,
|
|
1719
1828
|
// "to unix" / "to timestamp"
|
|
1720
1829
|
/^.+\s+(?:to|in)\s+(?:unix|timestamp|epoch)$/i,
|
|
@@ -1722,6 +1831,7 @@ var DATE_PATTERNS = [
|
|
|
1722
1831
|
/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})?/,
|
|
1723
1832
|
// "date" queries
|
|
1724
1833
|
/^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/i,
|
|
1834
|
+
/^(?:(?:what(?:'s|s| is))\s+today|(?:what(?:'s|s| is))\s+today'?s date|what day is it|what date is .+|todays date|today'?s date|date today|current date|what day is .+|when is .+)$/i,
|
|
1725
1835
|
// Days between dates
|
|
1726
1836
|
/^(?:days?\s+)?(?:between|from)\s+.+\s+(?:to|and|until)\s+.+$/i
|
|
1727
1837
|
];
|
|
@@ -1735,7 +1845,8 @@ function tryDateIntent(input) {
|
|
|
1735
1845
|
}
|
|
1736
1846
|
var CONVERSION_PATTERN = /^([\d.,]+)?\s*([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)\s+(?:to|in|into|as|=)\s+([a-zA-Z$€£¥₹₩₽₺₦₵₪฿]+(?:\s+[a-zA-Z]+)?)$/i;
|
|
1737
1847
|
function tryCurrencyOrCryptoIntent(input) {
|
|
1738
|
-
const
|
|
1848
|
+
const normalized = normalizeConversionInput(input);
|
|
1849
|
+
const match = normalized.match(CONVERSION_PATTERN);
|
|
1739
1850
|
if (!match) return null;
|
|
1740
1851
|
const amount = match[1] ? parseFloat(match[1].replace(/,/g, "")) : 1;
|
|
1741
1852
|
const fromToken = match[2].trim();
|
|
@@ -1758,13 +1869,12 @@ function tryCurrencyOrCryptoIntent(input) {
|
|
|
1758
1869
|
var UNIT_PATTERN = /^(-?[\d.,]+)\s*([a-zA-Z°/µμ'"²³]+(?:\s+[a-zA-Z]+(?:\s+[a-zA-Z]+)?)?)\s+(?:to|in|into|as|=)\s+([a-zA-Z°/µμ'"²³]+(?:\s+[a-zA-Z]+(?:\s+[a-zA-Z]+)?)?)$/i;
|
|
1759
1870
|
var UNIT_PATTERN_NO_SPACE = /^(-?[\d.,]+)([a-zA-Z°]+)\s+(?:to|in|into|as)\s+([a-zA-Z°]+(?:\s+[a-zA-Z]+)?)$/i;
|
|
1760
1871
|
function tryUnitIntent(input) {
|
|
1761
|
-
const
|
|
1872
|
+
const normalized = normalizeConversionInput(input);
|
|
1873
|
+
const match = normalized.match(UNIT_PATTERN) || normalized.match(UNIT_PATTERN_NO_SPACE);
|
|
1762
1874
|
if (!match) return null;
|
|
1763
1875
|
const amount = parseFloat(match[1].replace(/,/g, ""));
|
|
1764
1876
|
const fromToken = match[2].trim().toLowerCase();
|
|
1765
1877
|
const toToken = match[3].trim().toLowerCase();
|
|
1766
|
-
if (resolveFiat(fromToken) || resolveCrypto(fromToken)) return null;
|
|
1767
|
-
if (resolveFiat(toToken) || resolveCrypto(toToken)) return null;
|
|
1768
1878
|
const fromUnit = lookupUnit(fromToken);
|
|
1769
1879
|
const toUnit = lookupUnit(toToken);
|
|
1770
1880
|
if (fromUnit && toUnit && fromUnit.category === toUnit.category) {
|
|
@@ -1776,8 +1886,22 @@ function tryUnitIntent(input) {
|
|
|
1776
1886
|
category: fromUnit.category.name
|
|
1777
1887
|
};
|
|
1778
1888
|
}
|
|
1889
|
+
if (resolveFiat(fromToken) || resolveCrypto(fromToken)) return null;
|
|
1890
|
+
if (resolveFiat(toToken) || resolveCrypto(toToken)) return null;
|
|
1779
1891
|
return null;
|
|
1780
1892
|
}
|
|
1893
|
+
function normalizeWhitespace(input) {
|
|
1894
|
+
return input.trim().replace(/\s+/g, " ");
|
|
1895
|
+
}
|
|
1896
|
+
function normalizeConversionInput(input) {
|
|
1897
|
+
let normalized = normalizeWhitespace(input);
|
|
1898
|
+
normalized = normalized.replace(/^convert\s+/i, "").replace(/^how much is\s+/i, "").replace(/^what(?:'s|s| is)\s+/i, "");
|
|
1899
|
+
normalized = normalized.replace(/^([$€£¥₹₩₽₺₦₵₪฿])\s*([\d.,]+)/, "$2 $1");
|
|
1900
|
+
return normalized;
|
|
1901
|
+
}
|
|
1902
|
+
function normalizeTimePlace(value) {
|
|
1903
|
+
return normalizeWhitespace(value).replace(/^(?:time|current time|time now|now)\s+(?:in|at)\s+/i, "").replace(/^(?:in|at)\s+/i, "").replace(/\s+(?:time|now|time now)$/i, "").trim();
|
|
1904
|
+
}
|
|
1781
1905
|
|
|
1782
1906
|
// src/evaluators/math.ts
|
|
1783
1907
|
var CONSTANTS = {
|
|
@@ -1928,19 +2052,10 @@ var Parser = class {
|
|
|
1928
2052
|
if (right === 0) throw new Error("Division by zero");
|
|
1929
2053
|
left /= right;
|
|
1930
2054
|
} else if (op === "%") {
|
|
1931
|
-
|
|
1932
|
-
this.
|
|
1933
|
-
|
|
1934
|
-
this.
|
|
1935
|
-
const next = this.expr[this.pos];
|
|
1936
|
-
if (next === void 0 || /[+\-*/^)|&<>]/.test(next)) {
|
|
1937
|
-
this.pos = savedPos;
|
|
1938
|
-
break;
|
|
1939
|
-
} else {
|
|
1940
|
-
this.pos = savedPos;
|
|
1941
|
-
this.consume();
|
|
1942
|
-
left = left % this.parseExponentiation();
|
|
1943
|
-
}
|
|
2055
|
+
this.consume();
|
|
2056
|
+
left = left % this.parseExponentiation();
|
|
2057
|
+
} else if (op && /[([a-zA-Zπτφ_]/.test(op)) {
|
|
2058
|
+
left *= this.parseExponentiation();
|
|
1944
2059
|
} else {
|
|
1945
2060
|
break;
|
|
1946
2061
|
}
|
|
@@ -1982,7 +2097,7 @@ var Parser = class {
|
|
|
1982
2097
|
if (this.expr[this.pos] === "!") {
|
|
1983
2098
|
this.pos++;
|
|
1984
2099
|
value = factorial(value);
|
|
1985
|
-
} else if (this.expr[this.pos] === "%") {
|
|
2100
|
+
} else if (this.expr[this.pos] === "%" && this.shouldTreatAsPercentage()) {
|
|
1986
2101
|
this.pos++;
|
|
1987
2102
|
value = value / 100;
|
|
1988
2103
|
} else {
|
|
@@ -2008,6 +2123,12 @@ var Parser = class {
|
|
|
2008
2123
|
}
|
|
2009
2124
|
throw new Error(`Unexpected character: '${this.expr[this.pos]}' at position ${this.pos}`);
|
|
2010
2125
|
}
|
|
2126
|
+
shouldTreatAsPercentage() {
|
|
2127
|
+
let i = this.pos + 1;
|
|
2128
|
+
while (i < this.expr.length && /\s/.test(this.expr[i])) i++;
|
|
2129
|
+
const next = this.expr[i];
|
|
2130
|
+
return next === void 0 || /[+\-*/^)%|&<>]/.test(next);
|
|
2131
|
+
}
|
|
2011
2132
|
charAt(i) {
|
|
2012
2133
|
return i < this.expr.length ? this.expr[i] : "";
|
|
2013
2134
|
}
|
|
@@ -2051,13 +2172,15 @@ var Parser = class {
|
|
|
2051
2172
|
const name = this.expr.slice(start, this.pos).toLowerCase();
|
|
2052
2173
|
this.skipWhitespace();
|
|
2053
2174
|
if (this.expr[this.pos] === "(") {
|
|
2175
|
+
if (name === "max" || name === "min") {
|
|
2176
|
+
const args = this.parseArguments();
|
|
2177
|
+
if (args.length === 0) throw new Error(`Function '${name}' requires at least one argument`);
|
|
2178
|
+
return name === "max" ? Math.max(...args) : Math.min(...args);
|
|
2179
|
+
}
|
|
2054
2180
|
if (FUNCTIONS2[name]) {
|
|
2055
|
-
this.
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const b = this.parseBitwiseOr();
|
|
2059
|
-
this.consume(")");
|
|
2060
|
-
return FUNCTIONS2[name](a, b);
|
|
2181
|
+
const args = this.parseArguments();
|
|
2182
|
+
if (args.length !== 2) throw new Error(`Function '${name}' expects 2 arguments`);
|
|
2183
|
+
return FUNCTIONS2[name](args[0], args[1]);
|
|
2061
2184
|
}
|
|
2062
2185
|
if (FUNCTIONS[name]) {
|
|
2063
2186
|
this.consume("(");
|
|
@@ -2070,6 +2193,26 @@ var Parser = class {
|
|
|
2070
2193
|
if (CONSTANTS[name] !== void 0) return CONSTANTS[name];
|
|
2071
2194
|
throw new Error(`Unknown identifier: '${name}'`);
|
|
2072
2195
|
}
|
|
2196
|
+
parseArguments() {
|
|
2197
|
+
const args = [];
|
|
2198
|
+
this.consume("(");
|
|
2199
|
+
this.skipWhitespace();
|
|
2200
|
+
if (this.expr[this.pos] === ")") {
|
|
2201
|
+
this.consume(")");
|
|
2202
|
+
return args;
|
|
2203
|
+
}
|
|
2204
|
+
while (true) {
|
|
2205
|
+
args.push(this.parseBitwiseOr());
|
|
2206
|
+
this.skipWhitespace();
|
|
2207
|
+
if (this.expr[this.pos] === ",") {
|
|
2208
|
+
this.pos++;
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
this.consume(")");
|
|
2214
|
+
return args;
|
|
2215
|
+
}
|
|
2073
2216
|
};
|
|
2074
2217
|
function factorial(n) {
|
|
2075
2218
|
if (n < 0) throw new Error("Factorial of negative number");
|
|
@@ -2081,7 +2224,8 @@ function factorial(n) {
|
|
|
2081
2224
|
}
|
|
2082
2225
|
function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
2083
2226
|
try {
|
|
2084
|
-
const
|
|
2227
|
+
const normalizedExpression = normalizeMathExpression(expression);
|
|
2228
|
+
const parser = new Parser(normalizedExpression);
|
|
2085
2229
|
const result = parser.parse();
|
|
2086
2230
|
let formatted;
|
|
2087
2231
|
if (Number.isInteger(result) && Math.abs(result) < Number.MAX_SAFE_INTEGER) {
|
|
@@ -2106,7 +2250,10 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2106
2250
|
input: expression,
|
|
2107
2251
|
result,
|
|
2108
2252
|
formatted,
|
|
2109
|
-
metadata
|
|
2253
|
+
metadata: {
|
|
2254
|
+
...metadata,
|
|
2255
|
+
normalizedExpression
|
|
2256
|
+
}
|
|
2110
2257
|
};
|
|
2111
2258
|
} catch (err) {
|
|
2112
2259
|
return {
|
|
@@ -2116,6 +2263,41 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2116
2263
|
};
|
|
2117
2264
|
}
|
|
2118
2265
|
}
|
|
2266
|
+
function normalizeMathExpression(expression) {
|
|
2267
|
+
let normalized = expression.trim().replace(/\s+/g, " ");
|
|
2268
|
+
normalized = normalized.replace(/^(?:(?:what(?:'s|s| is))\s+the\s+|(?:what(?:'s|s| is))\s+|calculate\s+)/i, "");
|
|
2269
|
+
normalized = normalized.replace(/^the\s+/i, "");
|
|
2270
|
+
const wrappers = [
|
|
2271
|
+
[/^square root of (.+)$/i, "sqrt($1)"],
|
|
2272
|
+
[/^cube root of (.+)$/i, "cbrt($1)"],
|
|
2273
|
+
[/^log of (.+)$/i, "log($1)"],
|
|
2274
|
+
[/^sine of (.+)$/i, "sin($1)"],
|
|
2275
|
+
[/^cosine of (.+)$/i, "cos($1)"],
|
|
2276
|
+
[/^tangent of (.+)$/i, "tan($1)"],
|
|
2277
|
+
[/^absolute value of (.+)$/i, "abs($1)"],
|
|
2278
|
+
[/^factorial of (.+)$/i, "($1)!"],
|
|
2279
|
+
[/^half of (.+)$/i, "($1) / 2"],
|
|
2280
|
+
[/^double (.+)$/i, "2 * ($1)"],
|
|
2281
|
+
[/^triple (.+)$/i, "3 * ($1)"],
|
|
2282
|
+
[/^one third of (.+)$/i, "($1) / 3"],
|
|
2283
|
+
[/^one quarter of (.+)$/i, "($1) / 4"],
|
|
2284
|
+
[/^remainder of (.+?) divided by (.+)$/i, "($1) % ($2)"],
|
|
2285
|
+
[/^(.+?) to the power of (.+)$/i, "($1) ^ ($2)"],
|
|
2286
|
+
[/^(.+?) raised to (.+)$/i, "($1) ^ ($2)"],
|
|
2287
|
+
[/^(.+?) squared$/i, "($1) ^ 2"],
|
|
2288
|
+
[/^(.+?) cubed$/i, "($1) ^ 3"],
|
|
2289
|
+
[/^(\d+(?:\.\d+)?) percent of (.+)$/i, "($1%) * ($2)"],
|
|
2290
|
+
[/^(.+?%) of (.+)$/i, "($1) * ($2)"]
|
|
2291
|
+
];
|
|
2292
|
+
for (const [pattern, replacement] of wrappers) {
|
|
2293
|
+
if (pattern.test(normalized)) {
|
|
2294
|
+
normalized = normalized.replace(pattern, replacement);
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
normalized = normalized.replace(/\bdivided by\b/gi, "/").replace(/\btimes\b/gi, "*").replace(/\bplus\b/gi, "+").replace(/\bminus\b/gi, "-");
|
|
2299
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
2300
|
+
}
|
|
2119
2301
|
|
|
2120
2302
|
// src/evaluators/unit.ts
|
|
2121
2303
|
function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision = 10) {
|
|
@@ -2153,7 +2335,7 @@ function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision =
|
|
|
2153
2335
|
}
|
|
2154
2336
|
|
|
2155
2337
|
// src/evaluators/time.ts
|
|
2156
|
-
function evaluateTime(query, fromTz, toTz, localTz) {
|
|
2338
|
+
function evaluateTime(query, fromTz, toTz, explicitTime, localTz) {
|
|
2157
2339
|
const now = /* @__PURE__ */ new Date();
|
|
2158
2340
|
const userTz = localTz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2159
2341
|
try {
|
|
@@ -2186,18 +2368,20 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2186
2368
|
const resolvedFrom = resolveTimezone(fromTz) ?? fromTz;
|
|
2187
2369
|
const resolvedTo = resolveTimezone(toTz) ?? toTz;
|
|
2188
2370
|
try {
|
|
2189
|
-
const
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2371
|
+
const sourceDate = explicitTime ? buildDateInZone(explicitTime, resolvedFrom, now) : now;
|
|
2372
|
+
const fromTime = explicitTime ? `${explicitTime} (${getShortTzName(resolvedFrom)})` : formatTimeInZone(sourceDate, resolvedFrom);
|
|
2373
|
+
const toTime = formatTimeInZone(sourceDate, resolvedTo);
|
|
2374
|
+
const formatted = toTime;
|
|
2192
2375
|
return {
|
|
2193
2376
|
type: "time",
|
|
2194
2377
|
input: query,
|
|
2195
2378
|
result: formatted,
|
|
2196
2379
|
formatted,
|
|
2197
2380
|
metadata: {
|
|
2381
|
+
explicitTime,
|
|
2198
2382
|
from: { timezone: resolvedFrom, time: fromTime },
|
|
2199
2383
|
to: { timezone: resolvedTo, time: toTime },
|
|
2200
|
-
iso:
|
|
2384
|
+
iso: sourceDate.toISOString()
|
|
2201
2385
|
}
|
|
2202
2386
|
};
|
|
2203
2387
|
} catch {
|
|
@@ -2217,6 +2401,55 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2217
2401
|
};
|
|
2218
2402
|
}
|
|
2219
2403
|
}
|
|
2404
|
+
function buildDateInZone(timeExpression, timezone, anchor) {
|
|
2405
|
+
const { hour, minute } = parseTimeExpression(timeExpression);
|
|
2406
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2407
|
+
timeZone: timezone,
|
|
2408
|
+
year: "numeric",
|
|
2409
|
+
month: "2-digit",
|
|
2410
|
+
day: "2-digit"
|
|
2411
|
+
}).formatToParts(anchor);
|
|
2412
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2413
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2414
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2415
|
+
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0));
|
|
2416
|
+
const offset = getTimeZoneOffsetMs(utcGuess, timezone);
|
|
2417
|
+
return new Date(utcGuess.getTime() - offset);
|
|
2418
|
+
}
|
|
2419
|
+
function parseTimeExpression(value) {
|
|
2420
|
+
const trimmed = value.trim().toLowerCase();
|
|
2421
|
+
if (trimmed === "midnight") return { hour: 0, minute: 0 };
|
|
2422
|
+
if (trimmed === "noon") return { hour: 12, minute: 0 };
|
|
2423
|
+
const match = trimmed.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/);
|
|
2424
|
+
if (!match) throw new Error(`Invalid time: '${value}'`);
|
|
2425
|
+
let hour = parseInt(match[1], 10);
|
|
2426
|
+
const minute = parseInt(match[2] ?? "0", 10);
|
|
2427
|
+
const meridiem = match[3];
|
|
2428
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
2429
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
2430
|
+
if (hour > 23 || minute > 59) throw new Error(`Invalid time: '${value}'`);
|
|
2431
|
+
return { hour, minute };
|
|
2432
|
+
}
|
|
2433
|
+
function getTimeZoneOffsetMs(date, timezone) {
|
|
2434
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2435
|
+
timeZone: timezone,
|
|
2436
|
+
hour12: false,
|
|
2437
|
+
year: "numeric",
|
|
2438
|
+
month: "2-digit",
|
|
2439
|
+
day: "2-digit",
|
|
2440
|
+
hour: "2-digit",
|
|
2441
|
+
minute: "2-digit",
|
|
2442
|
+
second: "2-digit"
|
|
2443
|
+
}).formatToParts(date);
|
|
2444
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2445
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2446
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2447
|
+
const rawHour = Number(parts.find((p) => p.type === "hour")?.value);
|
|
2448
|
+
const hour = rawHour % 24;
|
|
2449
|
+
const minute = Number(parts.find((p) => p.type === "minute")?.value);
|
|
2450
|
+
const second = Number(parts.find((p) => p.type === "second")?.value);
|
|
2451
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - date.getTime();
|
|
2452
|
+
}
|
|
2220
2453
|
function formatTimeInZone(date, timezone) {
|
|
2221
2454
|
const timePart = new Intl.DateTimeFormat("en-US", {
|
|
2222
2455
|
timeZone: timezone,
|
|
@@ -2253,7 +2486,7 @@ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday
|
|
|
2253
2486
|
function evaluateDate(query) {
|
|
2254
2487
|
const lower = query.toLowerCase().trim();
|
|
2255
2488
|
try {
|
|
2256
|
-
if (lower === "today" || lower === "now" || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2489
|
+
if (lower === "today" || lower === "now" || lower === "date" || /^(?:(?:what(?:'s|s| is))\s+today|(?:what(?:'s|s| is))\s+today'?s date|what day is it|todays date|today'?s date|date today|current date|what is the date today)$/.test(lower) || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2257
2490
|
return formatDateResult(query, /* @__PURE__ */ new Date());
|
|
2258
2491
|
}
|
|
2259
2492
|
if (lower === "tomorrow") {
|
|
@@ -2266,6 +2499,26 @@ function evaluateDate(query) {
|
|
|
2266
2499
|
d.setDate(d.getDate() - 1);
|
|
2267
2500
|
return formatDateResult(query, d);
|
|
2268
2501
|
}
|
|
2502
|
+
if (lower === "day after tomorrow") {
|
|
2503
|
+
const d = /* @__PURE__ */ new Date();
|
|
2504
|
+
d.setDate(d.getDate() + 2);
|
|
2505
|
+
return formatDateResult(query, d);
|
|
2506
|
+
}
|
|
2507
|
+
if (lower === "day before yesterday") {
|
|
2508
|
+
const d = /* @__PURE__ */ new Date();
|
|
2509
|
+
d.setDate(d.getDate() - 2);
|
|
2510
|
+
return formatDateResult(query, d);
|
|
2511
|
+
}
|
|
2512
|
+
const whatDateMatch = lower.match(/^what date is (.+)$/);
|
|
2513
|
+
if (whatDateMatch) {
|
|
2514
|
+
const d = parseFlexibleDate(whatDateMatch[1]);
|
|
2515
|
+
if (d) return formatDateResult(query, d);
|
|
2516
|
+
}
|
|
2517
|
+
const whenIsMatch = lower.match(/^(?:what day is|when is)\s+(.+)$/);
|
|
2518
|
+
if (whenIsMatch) {
|
|
2519
|
+
const d = parseFlexibleDate(whenIsMatch[1]);
|
|
2520
|
+
if (d) return formatDateResult(query, d);
|
|
2521
|
+
}
|
|
2269
2522
|
const nextLastMatch = lower.match(/^(next|last|this)\s+(.+)$/);
|
|
2270
2523
|
if (nextLastMatch) {
|
|
2271
2524
|
const direction = nextLastMatch[1];
|
|
@@ -2313,7 +2566,7 @@ function evaluateDate(query) {
|
|
|
2313
2566
|
const d = applyOffset(/* @__PURE__ */ new Date(), amount, unit);
|
|
2314
2567
|
return formatDateResult(query, d);
|
|
2315
2568
|
}
|
|
2316
|
-
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/);
|
|
2569
|
+
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/);
|
|
2317
2570
|
if (unixMatch) {
|
|
2318
2571
|
const ts = parseInt(unixMatch[1]);
|
|
2319
2572
|
const ms = ts > 9999999999 ? ts : ts * 1e3;
|
|
@@ -2333,14 +2586,11 @@ function evaluateDate(query) {
|
|
|
2333
2586
|
if (toUnixMatch) {
|
|
2334
2587
|
const dateStr = toUnixMatch[1].trim();
|
|
2335
2588
|
let d;
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
} else {
|
|
2339
|
-
d = new Date(dateStr);
|
|
2340
|
-
}
|
|
2341
|
-
if (isNaN(d.getTime())) {
|
|
2589
|
+
const parsed = parseFlexibleDate(dateStr);
|
|
2590
|
+
if (!parsed) {
|
|
2342
2591
|
return { type: "error", input: query, error: `Cannot parse date: '${dateStr}'` };
|
|
2343
2592
|
}
|
|
2593
|
+
d = parsed;
|
|
2344
2594
|
const unix = Math.floor(d.getTime() / 1e3);
|
|
2345
2595
|
return {
|
|
2346
2596
|
type: "date",
|
|
@@ -2449,7 +2699,7 @@ function formatDateResult(input, d, extraMeta) {
|
|
|
2449
2699
|
};
|
|
2450
2700
|
}
|
|
2451
2701
|
function parseFlexibleDate(str) {
|
|
2452
|
-
if (str === "today" || str === "now") return /* @__PURE__ */ new Date();
|
|
2702
|
+
if (str === "today" || str === "now" || str === "date") return /* @__PURE__ */ new Date();
|
|
2453
2703
|
if (str === "tomorrow") {
|
|
2454
2704
|
const d2 = /* @__PURE__ */ new Date();
|
|
2455
2705
|
d2.setDate(d2.getDate() + 1);
|
|
@@ -2460,6 +2710,22 @@ function parseFlexibleDate(str) {
|
|
|
2460
2710
|
d2.setDate(d2.getDate() - 1);
|
|
2461
2711
|
return d2;
|
|
2462
2712
|
}
|
|
2713
|
+
if (str === "day after tomorrow") {
|
|
2714
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2715
|
+
d2.setDate(d2.getDate() + 2);
|
|
2716
|
+
return d2;
|
|
2717
|
+
}
|
|
2718
|
+
if (str === "day before yesterday") {
|
|
2719
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2720
|
+
d2.setDate(d2.getDate() - 2);
|
|
2721
|
+
return d2;
|
|
2722
|
+
}
|
|
2723
|
+
const relativeDay = str.match(/^(next|last|this)\s+(.+)$/);
|
|
2724
|
+
if (relativeDay) {
|
|
2725
|
+
const direction = relativeDay[1];
|
|
2726
|
+
const dayIndex = DAY_NAMES.indexOf(relativeDay[2]);
|
|
2727
|
+
if (dayIndex !== -1) return getRelativeDay(dayIndex, direction);
|
|
2728
|
+
}
|
|
2463
2729
|
const d = new Date(str);
|
|
2464
2730
|
return isNaN(d.getTime()) ? null : d;
|
|
2465
2731
|
}
|
|
@@ -2577,6 +2843,7 @@ async function calculate(input, options) {
|
|
|
2577
2843
|
intent.query,
|
|
2578
2844
|
intent.from,
|
|
2579
2845
|
intent.to,
|
|
2846
|
+
intent.time,
|
|
2580
2847
|
options?.timezone
|
|
2581
2848
|
);
|
|
2582
2849
|
case "date":
|
package/package.json
CHANGED