@supercmd/calculator 1.0.0 → 1.0.1
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 +320 -56
- package/dist/index.mjs +320 -56
- 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,23 +1787,41 @@ 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+(.+?)\s+to\s+(.+?)$/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 = match[1].trim();
|
|
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 = match[2].trim();
|
|
1819
|
+
const to = match[3].trim();
|
|
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) {
|
|
@@ -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,19 @@ 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
|
+
}
|
|
1818
1939
|
|
|
1819
1940
|
// src/evaluators/math.ts
|
|
1820
1941
|
var CONSTANTS = {
|
|
@@ -1965,19 +2086,10 @@ var Parser = class {
|
|
|
1965
2086
|
if (right === 0) throw new Error("Division by zero");
|
|
1966
2087
|
left /= right;
|
|
1967
2088
|
} 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
|
-
}
|
|
2089
|
+
this.consume();
|
|
2090
|
+
left = left % this.parseExponentiation();
|
|
2091
|
+
} else if (op && /[([a-zA-Zπτφ_]/.test(op)) {
|
|
2092
|
+
left *= this.parseExponentiation();
|
|
1981
2093
|
} else {
|
|
1982
2094
|
break;
|
|
1983
2095
|
}
|
|
@@ -2019,7 +2131,7 @@ var Parser = class {
|
|
|
2019
2131
|
if (this.expr[this.pos] === "!") {
|
|
2020
2132
|
this.pos++;
|
|
2021
2133
|
value = factorial(value);
|
|
2022
|
-
} else if (this.expr[this.pos] === "%") {
|
|
2134
|
+
} else if (this.expr[this.pos] === "%" && this.shouldTreatAsPercentage()) {
|
|
2023
2135
|
this.pos++;
|
|
2024
2136
|
value = value / 100;
|
|
2025
2137
|
} else {
|
|
@@ -2045,6 +2157,12 @@ var Parser = class {
|
|
|
2045
2157
|
}
|
|
2046
2158
|
throw new Error(`Unexpected character: '${this.expr[this.pos]}' at position ${this.pos}`);
|
|
2047
2159
|
}
|
|
2160
|
+
shouldTreatAsPercentage() {
|
|
2161
|
+
let i = this.pos + 1;
|
|
2162
|
+
while (i < this.expr.length && /\s/.test(this.expr[i])) i++;
|
|
2163
|
+
const next = this.expr[i];
|
|
2164
|
+
return next === void 0 || /[+\-*/^)%|&<>]/.test(next);
|
|
2165
|
+
}
|
|
2048
2166
|
charAt(i) {
|
|
2049
2167
|
return i < this.expr.length ? this.expr[i] : "";
|
|
2050
2168
|
}
|
|
@@ -2088,13 +2206,15 @@ var Parser = class {
|
|
|
2088
2206
|
const name = this.expr.slice(start, this.pos).toLowerCase();
|
|
2089
2207
|
this.skipWhitespace();
|
|
2090
2208
|
if (this.expr[this.pos] === "(") {
|
|
2209
|
+
if (name === "max" || name === "min") {
|
|
2210
|
+
const args = this.parseArguments();
|
|
2211
|
+
if (args.length === 0) throw new Error(`Function '${name}' requires at least one argument`);
|
|
2212
|
+
return name === "max" ? Math.max(...args) : Math.min(...args);
|
|
2213
|
+
}
|
|
2091
2214
|
if (FUNCTIONS2[name]) {
|
|
2092
|
-
this.
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
const b = this.parseBitwiseOr();
|
|
2096
|
-
this.consume(")");
|
|
2097
|
-
return FUNCTIONS2[name](a, b);
|
|
2215
|
+
const args = this.parseArguments();
|
|
2216
|
+
if (args.length !== 2) throw new Error(`Function '${name}' expects 2 arguments`);
|
|
2217
|
+
return FUNCTIONS2[name](args[0], args[1]);
|
|
2098
2218
|
}
|
|
2099
2219
|
if (FUNCTIONS[name]) {
|
|
2100
2220
|
this.consume("(");
|
|
@@ -2107,6 +2227,26 @@ var Parser = class {
|
|
|
2107
2227
|
if (CONSTANTS[name] !== void 0) return CONSTANTS[name];
|
|
2108
2228
|
throw new Error(`Unknown identifier: '${name}'`);
|
|
2109
2229
|
}
|
|
2230
|
+
parseArguments() {
|
|
2231
|
+
const args = [];
|
|
2232
|
+
this.consume("(");
|
|
2233
|
+
this.skipWhitespace();
|
|
2234
|
+
if (this.expr[this.pos] === ")") {
|
|
2235
|
+
this.consume(")");
|
|
2236
|
+
return args;
|
|
2237
|
+
}
|
|
2238
|
+
while (true) {
|
|
2239
|
+
args.push(this.parseBitwiseOr());
|
|
2240
|
+
this.skipWhitespace();
|
|
2241
|
+
if (this.expr[this.pos] === ",") {
|
|
2242
|
+
this.pos++;
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
this.consume(")");
|
|
2248
|
+
return args;
|
|
2249
|
+
}
|
|
2110
2250
|
};
|
|
2111
2251
|
function factorial(n) {
|
|
2112
2252
|
if (n < 0) throw new Error("Factorial of negative number");
|
|
@@ -2118,7 +2258,8 @@ function factorial(n) {
|
|
|
2118
2258
|
}
|
|
2119
2259
|
function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
2120
2260
|
try {
|
|
2121
|
-
const
|
|
2261
|
+
const normalizedExpression = normalizeMathExpression(expression);
|
|
2262
|
+
const parser = new Parser(normalizedExpression);
|
|
2122
2263
|
const result = parser.parse();
|
|
2123
2264
|
let formatted;
|
|
2124
2265
|
if (Number.isInteger(result) && Math.abs(result) < Number.MAX_SAFE_INTEGER) {
|
|
@@ -2143,7 +2284,10 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2143
2284
|
input: expression,
|
|
2144
2285
|
result,
|
|
2145
2286
|
formatted,
|
|
2146
|
-
metadata
|
|
2287
|
+
metadata: {
|
|
2288
|
+
...metadata,
|
|
2289
|
+
normalizedExpression
|
|
2290
|
+
}
|
|
2147
2291
|
};
|
|
2148
2292
|
} catch (err) {
|
|
2149
2293
|
return {
|
|
@@ -2153,6 +2297,41 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2153
2297
|
};
|
|
2154
2298
|
}
|
|
2155
2299
|
}
|
|
2300
|
+
function normalizeMathExpression(expression) {
|
|
2301
|
+
let normalized = expression.trim().replace(/\s+/g, " ");
|
|
2302
|
+
normalized = normalized.replace(/^(?:(?:what(?:'s|s| is))\s+the\s+|(?:what(?:'s|s| is))\s+|calculate\s+)/i, "");
|
|
2303
|
+
normalized = normalized.replace(/^the\s+/i, "");
|
|
2304
|
+
const wrappers = [
|
|
2305
|
+
[/^square root of (.+)$/i, "sqrt($1)"],
|
|
2306
|
+
[/^cube root of (.+)$/i, "cbrt($1)"],
|
|
2307
|
+
[/^log of (.+)$/i, "log($1)"],
|
|
2308
|
+
[/^sine of (.+)$/i, "sin($1)"],
|
|
2309
|
+
[/^cosine of (.+)$/i, "cos($1)"],
|
|
2310
|
+
[/^tangent of (.+)$/i, "tan($1)"],
|
|
2311
|
+
[/^absolute value of (.+)$/i, "abs($1)"],
|
|
2312
|
+
[/^factorial of (.+)$/i, "($1)!"],
|
|
2313
|
+
[/^half of (.+)$/i, "($1) / 2"],
|
|
2314
|
+
[/^double (.+)$/i, "2 * ($1)"],
|
|
2315
|
+
[/^triple (.+)$/i, "3 * ($1)"],
|
|
2316
|
+
[/^one third of (.+)$/i, "($1) / 3"],
|
|
2317
|
+
[/^one quarter of (.+)$/i, "($1) / 4"],
|
|
2318
|
+
[/^remainder of (.+?) divided by (.+)$/i, "($1) % ($2)"],
|
|
2319
|
+
[/^(.+?) to the power of (.+)$/i, "($1) ^ ($2)"],
|
|
2320
|
+
[/^(.+?) raised to (.+)$/i, "($1) ^ ($2)"],
|
|
2321
|
+
[/^(.+?) squared$/i, "($1) ^ 2"],
|
|
2322
|
+
[/^(.+?) cubed$/i, "($1) ^ 3"],
|
|
2323
|
+
[/^(\d+(?:\.\d+)?) percent of (.+)$/i, "($1%) * ($2)"],
|
|
2324
|
+
[/^(.+?%) of (.+)$/i, "($1) * ($2)"]
|
|
2325
|
+
];
|
|
2326
|
+
for (const [pattern, replacement] of wrappers) {
|
|
2327
|
+
if (pattern.test(normalized)) {
|
|
2328
|
+
normalized = normalized.replace(pattern, replacement);
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
normalized = normalized.replace(/\bdivided by\b/gi, "/").replace(/\btimes\b/gi, "*").replace(/\bplus\b/gi, "+").replace(/\bminus\b/gi, "-");
|
|
2333
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
2334
|
+
}
|
|
2156
2335
|
|
|
2157
2336
|
// src/evaluators/unit.ts
|
|
2158
2337
|
function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision = 10) {
|
|
@@ -2190,7 +2369,7 @@ function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision =
|
|
|
2190
2369
|
}
|
|
2191
2370
|
|
|
2192
2371
|
// src/evaluators/time.ts
|
|
2193
|
-
function evaluateTime(query, fromTz, toTz, localTz) {
|
|
2372
|
+
function evaluateTime(query, fromTz, toTz, explicitTime, localTz) {
|
|
2194
2373
|
const now = /* @__PURE__ */ new Date();
|
|
2195
2374
|
const userTz = localTz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2196
2375
|
try {
|
|
@@ -2223,18 +2402,20 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2223
2402
|
const resolvedFrom = resolveTimezone(fromTz) ?? fromTz;
|
|
2224
2403
|
const resolvedTo = resolveTimezone(toTz) ?? toTz;
|
|
2225
2404
|
try {
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2405
|
+
const sourceDate = explicitTime ? buildDateInZone(explicitTime, resolvedFrom, now) : now;
|
|
2406
|
+
const fromTime = explicitTime ? `${explicitTime} (${getShortTzName(resolvedFrom)})` : formatTimeInZone(sourceDate, resolvedFrom);
|
|
2407
|
+
const toTime = formatTimeInZone(sourceDate, resolvedTo);
|
|
2408
|
+
const formatted = toTime;
|
|
2229
2409
|
return {
|
|
2230
2410
|
type: "time",
|
|
2231
2411
|
input: query,
|
|
2232
2412
|
result: formatted,
|
|
2233
2413
|
formatted,
|
|
2234
2414
|
metadata: {
|
|
2415
|
+
explicitTime,
|
|
2235
2416
|
from: { timezone: resolvedFrom, time: fromTime },
|
|
2236
2417
|
to: { timezone: resolvedTo, time: toTime },
|
|
2237
|
-
iso:
|
|
2418
|
+
iso: sourceDate.toISOString()
|
|
2238
2419
|
}
|
|
2239
2420
|
};
|
|
2240
2421
|
} catch {
|
|
@@ -2254,6 +2435,55 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2254
2435
|
};
|
|
2255
2436
|
}
|
|
2256
2437
|
}
|
|
2438
|
+
function buildDateInZone(timeExpression, timezone, anchor) {
|
|
2439
|
+
const { hour, minute } = parseTimeExpression(timeExpression);
|
|
2440
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2441
|
+
timeZone: timezone,
|
|
2442
|
+
year: "numeric",
|
|
2443
|
+
month: "2-digit",
|
|
2444
|
+
day: "2-digit"
|
|
2445
|
+
}).formatToParts(anchor);
|
|
2446
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2447
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2448
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2449
|
+
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0));
|
|
2450
|
+
const offset = getTimeZoneOffsetMs(utcGuess, timezone);
|
|
2451
|
+
return new Date(utcGuess.getTime() - offset);
|
|
2452
|
+
}
|
|
2453
|
+
function parseTimeExpression(value) {
|
|
2454
|
+
const trimmed = value.trim().toLowerCase();
|
|
2455
|
+
if (trimmed === "midnight") return { hour: 0, minute: 0 };
|
|
2456
|
+
if (trimmed === "noon") return { hour: 12, minute: 0 };
|
|
2457
|
+
const match = trimmed.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/);
|
|
2458
|
+
if (!match) throw new Error(`Invalid time: '${value}'`);
|
|
2459
|
+
let hour = parseInt(match[1], 10);
|
|
2460
|
+
const minute = parseInt(match[2] ?? "0", 10);
|
|
2461
|
+
const meridiem = match[3];
|
|
2462
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
2463
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
2464
|
+
if (hour > 23 || minute > 59) throw new Error(`Invalid time: '${value}'`);
|
|
2465
|
+
return { hour, minute };
|
|
2466
|
+
}
|
|
2467
|
+
function getTimeZoneOffsetMs(date, timezone) {
|
|
2468
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2469
|
+
timeZone: timezone,
|
|
2470
|
+
hour12: false,
|
|
2471
|
+
year: "numeric",
|
|
2472
|
+
month: "2-digit",
|
|
2473
|
+
day: "2-digit",
|
|
2474
|
+
hour: "2-digit",
|
|
2475
|
+
minute: "2-digit",
|
|
2476
|
+
second: "2-digit"
|
|
2477
|
+
}).formatToParts(date);
|
|
2478
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2479
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2480
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2481
|
+
const rawHour = Number(parts.find((p) => p.type === "hour")?.value);
|
|
2482
|
+
const hour = rawHour % 24;
|
|
2483
|
+
const minute = Number(parts.find((p) => p.type === "minute")?.value);
|
|
2484
|
+
const second = Number(parts.find((p) => p.type === "second")?.value);
|
|
2485
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - date.getTime();
|
|
2486
|
+
}
|
|
2257
2487
|
function formatTimeInZone(date, timezone) {
|
|
2258
2488
|
const timePart = new Intl.DateTimeFormat("en-US", {
|
|
2259
2489
|
timeZone: timezone,
|
|
@@ -2290,7 +2520,7 @@ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday
|
|
|
2290
2520
|
function evaluateDate(query) {
|
|
2291
2521
|
const lower = query.toLowerCase().trim();
|
|
2292
2522
|
try {
|
|
2293
|
-
if (lower === "today" || lower === "now" || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2523
|
+
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
2524
|
return formatDateResult(query, /* @__PURE__ */ new Date());
|
|
2295
2525
|
}
|
|
2296
2526
|
if (lower === "tomorrow") {
|
|
@@ -2303,6 +2533,26 @@ function evaluateDate(query) {
|
|
|
2303
2533
|
d.setDate(d.getDate() - 1);
|
|
2304
2534
|
return formatDateResult(query, d);
|
|
2305
2535
|
}
|
|
2536
|
+
if (lower === "day after tomorrow") {
|
|
2537
|
+
const d = /* @__PURE__ */ new Date();
|
|
2538
|
+
d.setDate(d.getDate() + 2);
|
|
2539
|
+
return formatDateResult(query, d);
|
|
2540
|
+
}
|
|
2541
|
+
if (lower === "day before yesterday") {
|
|
2542
|
+
const d = /* @__PURE__ */ new Date();
|
|
2543
|
+
d.setDate(d.getDate() - 2);
|
|
2544
|
+
return formatDateResult(query, d);
|
|
2545
|
+
}
|
|
2546
|
+
const whatDateMatch = lower.match(/^what date is (.+)$/);
|
|
2547
|
+
if (whatDateMatch) {
|
|
2548
|
+
const d = parseFlexibleDate(whatDateMatch[1]);
|
|
2549
|
+
if (d) return formatDateResult(query, d);
|
|
2550
|
+
}
|
|
2551
|
+
const whenIsMatch = lower.match(/^(?:what day is|when is)\s+(.+)$/);
|
|
2552
|
+
if (whenIsMatch) {
|
|
2553
|
+
const d = parseFlexibleDate(whenIsMatch[1]);
|
|
2554
|
+
if (d) return formatDateResult(query, d);
|
|
2555
|
+
}
|
|
2306
2556
|
const nextLastMatch = lower.match(/^(next|last|this)\s+(.+)$/);
|
|
2307
2557
|
if (nextLastMatch) {
|
|
2308
2558
|
const direction = nextLastMatch[1];
|
|
@@ -2350,7 +2600,7 @@ function evaluateDate(query) {
|
|
|
2350
2600
|
const d = applyOffset(/* @__PURE__ */ new Date(), amount, unit);
|
|
2351
2601
|
return formatDateResult(query, d);
|
|
2352
2602
|
}
|
|
2353
|
-
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/);
|
|
2603
|
+
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/);
|
|
2354
2604
|
if (unixMatch) {
|
|
2355
2605
|
const ts = parseInt(unixMatch[1]);
|
|
2356
2606
|
const ms = ts > 9999999999 ? ts : ts * 1e3;
|
|
@@ -2370,14 +2620,11 @@ function evaluateDate(query) {
|
|
|
2370
2620
|
if (toUnixMatch) {
|
|
2371
2621
|
const dateStr = toUnixMatch[1].trim();
|
|
2372
2622
|
let d;
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
} else {
|
|
2376
|
-
d = new Date(dateStr);
|
|
2377
|
-
}
|
|
2378
|
-
if (isNaN(d.getTime())) {
|
|
2623
|
+
const parsed = parseFlexibleDate(dateStr);
|
|
2624
|
+
if (!parsed) {
|
|
2379
2625
|
return { type: "error", input: query, error: `Cannot parse date: '${dateStr}'` };
|
|
2380
2626
|
}
|
|
2627
|
+
d = parsed;
|
|
2381
2628
|
const unix = Math.floor(d.getTime() / 1e3);
|
|
2382
2629
|
return {
|
|
2383
2630
|
type: "date",
|
|
@@ -2486,7 +2733,7 @@ function formatDateResult(input, d, extraMeta) {
|
|
|
2486
2733
|
};
|
|
2487
2734
|
}
|
|
2488
2735
|
function parseFlexibleDate(str) {
|
|
2489
|
-
if (str === "today" || str === "now") return /* @__PURE__ */ new Date();
|
|
2736
|
+
if (str === "today" || str === "now" || str === "date") return /* @__PURE__ */ new Date();
|
|
2490
2737
|
if (str === "tomorrow") {
|
|
2491
2738
|
const d2 = /* @__PURE__ */ new Date();
|
|
2492
2739
|
d2.setDate(d2.getDate() + 1);
|
|
@@ -2497,6 +2744,22 @@ function parseFlexibleDate(str) {
|
|
|
2497
2744
|
d2.setDate(d2.getDate() - 1);
|
|
2498
2745
|
return d2;
|
|
2499
2746
|
}
|
|
2747
|
+
if (str === "day after tomorrow") {
|
|
2748
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2749
|
+
d2.setDate(d2.getDate() + 2);
|
|
2750
|
+
return d2;
|
|
2751
|
+
}
|
|
2752
|
+
if (str === "day before yesterday") {
|
|
2753
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2754
|
+
d2.setDate(d2.getDate() - 2);
|
|
2755
|
+
return d2;
|
|
2756
|
+
}
|
|
2757
|
+
const relativeDay = str.match(/^(next|last|this)\s+(.+)$/);
|
|
2758
|
+
if (relativeDay) {
|
|
2759
|
+
const direction = relativeDay[1];
|
|
2760
|
+
const dayIndex = DAY_NAMES.indexOf(relativeDay[2]);
|
|
2761
|
+
if (dayIndex !== -1) return getRelativeDay(dayIndex, direction);
|
|
2762
|
+
}
|
|
2500
2763
|
const d = new Date(str);
|
|
2501
2764
|
return isNaN(d.getTime()) ? null : d;
|
|
2502
2765
|
}
|
|
@@ -2614,6 +2877,7 @@ async function calculate(input, options) {
|
|
|
2614
2877
|
intent.query,
|
|
2615
2878
|
intent.from,
|
|
2616
2879
|
intent.to,
|
|
2880
|
+
intent.time,
|
|
2617
2881
|
options?.timezone
|
|
2618
2882
|
);
|
|
2619
2883
|
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,23 +1750,41 @@ 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+(.+?)\s+to\s+(.+?)$/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 = match[1].trim();
|
|
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 = match[2].trim();
|
|
1782
|
+
const to = match[3].trim();
|
|
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) {
|
|
@@ -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,19 @@ 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
|
+
}
|
|
1781
1902
|
|
|
1782
1903
|
// src/evaluators/math.ts
|
|
1783
1904
|
var CONSTANTS = {
|
|
@@ -1928,19 +2049,10 @@ var Parser = class {
|
|
|
1928
2049
|
if (right === 0) throw new Error("Division by zero");
|
|
1929
2050
|
left /= right;
|
|
1930
2051
|
} 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
|
-
}
|
|
2052
|
+
this.consume();
|
|
2053
|
+
left = left % this.parseExponentiation();
|
|
2054
|
+
} else if (op && /[([a-zA-Zπτφ_]/.test(op)) {
|
|
2055
|
+
left *= this.parseExponentiation();
|
|
1944
2056
|
} else {
|
|
1945
2057
|
break;
|
|
1946
2058
|
}
|
|
@@ -1982,7 +2094,7 @@ var Parser = class {
|
|
|
1982
2094
|
if (this.expr[this.pos] === "!") {
|
|
1983
2095
|
this.pos++;
|
|
1984
2096
|
value = factorial(value);
|
|
1985
|
-
} else if (this.expr[this.pos] === "%") {
|
|
2097
|
+
} else if (this.expr[this.pos] === "%" && this.shouldTreatAsPercentage()) {
|
|
1986
2098
|
this.pos++;
|
|
1987
2099
|
value = value / 100;
|
|
1988
2100
|
} else {
|
|
@@ -2008,6 +2120,12 @@ var Parser = class {
|
|
|
2008
2120
|
}
|
|
2009
2121
|
throw new Error(`Unexpected character: '${this.expr[this.pos]}' at position ${this.pos}`);
|
|
2010
2122
|
}
|
|
2123
|
+
shouldTreatAsPercentage() {
|
|
2124
|
+
let i = this.pos + 1;
|
|
2125
|
+
while (i < this.expr.length && /\s/.test(this.expr[i])) i++;
|
|
2126
|
+
const next = this.expr[i];
|
|
2127
|
+
return next === void 0 || /[+\-*/^)%|&<>]/.test(next);
|
|
2128
|
+
}
|
|
2011
2129
|
charAt(i) {
|
|
2012
2130
|
return i < this.expr.length ? this.expr[i] : "";
|
|
2013
2131
|
}
|
|
@@ -2051,13 +2169,15 @@ var Parser = class {
|
|
|
2051
2169
|
const name = this.expr.slice(start, this.pos).toLowerCase();
|
|
2052
2170
|
this.skipWhitespace();
|
|
2053
2171
|
if (this.expr[this.pos] === "(") {
|
|
2172
|
+
if (name === "max" || name === "min") {
|
|
2173
|
+
const args = this.parseArguments();
|
|
2174
|
+
if (args.length === 0) throw new Error(`Function '${name}' requires at least one argument`);
|
|
2175
|
+
return name === "max" ? Math.max(...args) : Math.min(...args);
|
|
2176
|
+
}
|
|
2054
2177
|
if (FUNCTIONS2[name]) {
|
|
2055
|
-
this.
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const b = this.parseBitwiseOr();
|
|
2059
|
-
this.consume(")");
|
|
2060
|
-
return FUNCTIONS2[name](a, b);
|
|
2178
|
+
const args = this.parseArguments();
|
|
2179
|
+
if (args.length !== 2) throw new Error(`Function '${name}' expects 2 arguments`);
|
|
2180
|
+
return FUNCTIONS2[name](args[0], args[1]);
|
|
2061
2181
|
}
|
|
2062
2182
|
if (FUNCTIONS[name]) {
|
|
2063
2183
|
this.consume("(");
|
|
@@ -2070,6 +2190,26 @@ var Parser = class {
|
|
|
2070
2190
|
if (CONSTANTS[name] !== void 0) return CONSTANTS[name];
|
|
2071
2191
|
throw new Error(`Unknown identifier: '${name}'`);
|
|
2072
2192
|
}
|
|
2193
|
+
parseArguments() {
|
|
2194
|
+
const args = [];
|
|
2195
|
+
this.consume("(");
|
|
2196
|
+
this.skipWhitespace();
|
|
2197
|
+
if (this.expr[this.pos] === ")") {
|
|
2198
|
+
this.consume(")");
|
|
2199
|
+
return args;
|
|
2200
|
+
}
|
|
2201
|
+
while (true) {
|
|
2202
|
+
args.push(this.parseBitwiseOr());
|
|
2203
|
+
this.skipWhitespace();
|
|
2204
|
+
if (this.expr[this.pos] === ",") {
|
|
2205
|
+
this.pos++;
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
this.consume(")");
|
|
2211
|
+
return args;
|
|
2212
|
+
}
|
|
2073
2213
|
};
|
|
2074
2214
|
function factorial(n) {
|
|
2075
2215
|
if (n < 0) throw new Error("Factorial of negative number");
|
|
@@ -2081,7 +2221,8 @@ function factorial(n) {
|
|
|
2081
2221
|
}
|
|
2082
2222
|
function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
2083
2223
|
try {
|
|
2084
|
-
const
|
|
2224
|
+
const normalizedExpression = normalizeMathExpression(expression);
|
|
2225
|
+
const parser = new Parser(normalizedExpression);
|
|
2085
2226
|
const result = parser.parse();
|
|
2086
2227
|
let formatted;
|
|
2087
2228
|
if (Number.isInteger(result) && Math.abs(result) < Number.MAX_SAFE_INTEGER) {
|
|
@@ -2106,7 +2247,10 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2106
2247
|
input: expression,
|
|
2107
2248
|
result,
|
|
2108
2249
|
formatted,
|
|
2109
|
-
metadata
|
|
2250
|
+
metadata: {
|
|
2251
|
+
...metadata,
|
|
2252
|
+
normalizedExpression
|
|
2253
|
+
}
|
|
2110
2254
|
};
|
|
2111
2255
|
} catch (err) {
|
|
2112
2256
|
return {
|
|
@@ -2116,6 +2260,41 @@ function evaluateMath(expression, locale = "en-US", precision = 10) {
|
|
|
2116
2260
|
};
|
|
2117
2261
|
}
|
|
2118
2262
|
}
|
|
2263
|
+
function normalizeMathExpression(expression) {
|
|
2264
|
+
let normalized = expression.trim().replace(/\s+/g, " ");
|
|
2265
|
+
normalized = normalized.replace(/^(?:(?:what(?:'s|s| is))\s+the\s+|(?:what(?:'s|s| is))\s+|calculate\s+)/i, "");
|
|
2266
|
+
normalized = normalized.replace(/^the\s+/i, "");
|
|
2267
|
+
const wrappers = [
|
|
2268
|
+
[/^square root of (.+)$/i, "sqrt($1)"],
|
|
2269
|
+
[/^cube root of (.+)$/i, "cbrt($1)"],
|
|
2270
|
+
[/^log of (.+)$/i, "log($1)"],
|
|
2271
|
+
[/^sine of (.+)$/i, "sin($1)"],
|
|
2272
|
+
[/^cosine of (.+)$/i, "cos($1)"],
|
|
2273
|
+
[/^tangent of (.+)$/i, "tan($1)"],
|
|
2274
|
+
[/^absolute value of (.+)$/i, "abs($1)"],
|
|
2275
|
+
[/^factorial of (.+)$/i, "($1)!"],
|
|
2276
|
+
[/^half of (.+)$/i, "($1) / 2"],
|
|
2277
|
+
[/^double (.+)$/i, "2 * ($1)"],
|
|
2278
|
+
[/^triple (.+)$/i, "3 * ($1)"],
|
|
2279
|
+
[/^one third of (.+)$/i, "($1) / 3"],
|
|
2280
|
+
[/^one quarter of (.+)$/i, "($1) / 4"],
|
|
2281
|
+
[/^remainder of (.+?) divided by (.+)$/i, "($1) % ($2)"],
|
|
2282
|
+
[/^(.+?) to the power of (.+)$/i, "($1) ^ ($2)"],
|
|
2283
|
+
[/^(.+?) raised to (.+)$/i, "($1) ^ ($2)"],
|
|
2284
|
+
[/^(.+?) squared$/i, "($1) ^ 2"],
|
|
2285
|
+
[/^(.+?) cubed$/i, "($1) ^ 3"],
|
|
2286
|
+
[/^(\d+(?:\.\d+)?) percent of (.+)$/i, "($1%) * ($2)"],
|
|
2287
|
+
[/^(.+?%) of (.+)$/i, "($1) * ($2)"]
|
|
2288
|
+
];
|
|
2289
|
+
for (const [pattern, replacement] of wrappers) {
|
|
2290
|
+
if (pattern.test(normalized)) {
|
|
2291
|
+
normalized = normalized.replace(pattern, replacement);
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
normalized = normalized.replace(/\bdivided by\b/gi, "/").replace(/\btimes\b/gi, "*").replace(/\bplus\b/gi, "+").replace(/\bminus\b/gi, "-");
|
|
2296
|
+
return normalized.replace(/\s+/g, " ").trim();
|
|
2297
|
+
}
|
|
2119
2298
|
|
|
2120
2299
|
// src/evaluators/unit.ts
|
|
2121
2300
|
function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision = 10) {
|
|
@@ -2153,7 +2332,7 @@ function evaluateUnit(amount, fromToken, toToken, locale = "en-US", precision =
|
|
|
2153
2332
|
}
|
|
2154
2333
|
|
|
2155
2334
|
// src/evaluators/time.ts
|
|
2156
|
-
function evaluateTime(query, fromTz, toTz, localTz) {
|
|
2335
|
+
function evaluateTime(query, fromTz, toTz, explicitTime, localTz) {
|
|
2157
2336
|
const now = /* @__PURE__ */ new Date();
|
|
2158
2337
|
const userTz = localTz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2159
2338
|
try {
|
|
@@ -2186,18 +2365,20 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2186
2365
|
const resolvedFrom = resolveTimezone(fromTz) ?? fromTz;
|
|
2187
2366
|
const resolvedTo = resolveTimezone(toTz) ?? toTz;
|
|
2188
2367
|
try {
|
|
2189
|
-
const
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2368
|
+
const sourceDate = explicitTime ? buildDateInZone(explicitTime, resolvedFrom, now) : now;
|
|
2369
|
+
const fromTime = explicitTime ? `${explicitTime} (${getShortTzName(resolvedFrom)})` : formatTimeInZone(sourceDate, resolvedFrom);
|
|
2370
|
+
const toTime = formatTimeInZone(sourceDate, resolvedTo);
|
|
2371
|
+
const formatted = toTime;
|
|
2192
2372
|
return {
|
|
2193
2373
|
type: "time",
|
|
2194
2374
|
input: query,
|
|
2195
2375
|
result: formatted,
|
|
2196
2376
|
formatted,
|
|
2197
2377
|
metadata: {
|
|
2378
|
+
explicitTime,
|
|
2198
2379
|
from: { timezone: resolvedFrom, time: fromTime },
|
|
2199
2380
|
to: { timezone: resolvedTo, time: toTime },
|
|
2200
|
-
iso:
|
|
2381
|
+
iso: sourceDate.toISOString()
|
|
2201
2382
|
}
|
|
2202
2383
|
};
|
|
2203
2384
|
} catch {
|
|
@@ -2217,6 +2398,55 @@ function evaluateTime(query, fromTz, toTz, localTz) {
|
|
|
2217
2398
|
};
|
|
2218
2399
|
}
|
|
2219
2400
|
}
|
|
2401
|
+
function buildDateInZone(timeExpression, timezone, anchor) {
|
|
2402
|
+
const { hour, minute } = parseTimeExpression(timeExpression);
|
|
2403
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2404
|
+
timeZone: timezone,
|
|
2405
|
+
year: "numeric",
|
|
2406
|
+
month: "2-digit",
|
|
2407
|
+
day: "2-digit"
|
|
2408
|
+
}).formatToParts(anchor);
|
|
2409
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2410
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2411
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2412
|
+
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0));
|
|
2413
|
+
const offset = getTimeZoneOffsetMs(utcGuess, timezone);
|
|
2414
|
+
return new Date(utcGuess.getTime() - offset);
|
|
2415
|
+
}
|
|
2416
|
+
function parseTimeExpression(value) {
|
|
2417
|
+
const trimmed = value.trim().toLowerCase();
|
|
2418
|
+
if (trimmed === "midnight") return { hour: 0, minute: 0 };
|
|
2419
|
+
if (trimmed === "noon") return { hour: 12, minute: 0 };
|
|
2420
|
+
const match = trimmed.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/);
|
|
2421
|
+
if (!match) throw new Error(`Invalid time: '${value}'`);
|
|
2422
|
+
let hour = parseInt(match[1], 10);
|
|
2423
|
+
const minute = parseInt(match[2] ?? "0", 10);
|
|
2424
|
+
const meridiem = match[3];
|
|
2425
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
2426
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
2427
|
+
if (hour > 23 || minute > 59) throw new Error(`Invalid time: '${value}'`);
|
|
2428
|
+
return { hour, minute };
|
|
2429
|
+
}
|
|
2430
|
+
function getTimeZoneOffsetMs(date, timezone) {
|
|
2431
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2432
|
+
timeZone: timezone,
|
|
2433
|
+
hour12: false,
|
|
2434
|
+
year: "numeric",
|
|
2435
|
+
month: "2-digit",
|
|
2436
|
+
day: "2-digit",
|
|
2437
|
+
hour: "2-digit",
|
|
2438
|
+
minute: "2-digit",
|
|
2439
|
+
second: "2-digit"
|
|
2440
|
+
}).formatToParts(date);
|
|
2441
|
+
const year = Number(parts.find((p) => p.type === "year")?.value);
|
|
2442
|
+
const month = Number(parts.find((p) => p.type === "month")?.value);
|
|
2443
|
+
const day = Number(parts.find((p) => p.type === "day")?.value);
|
|
2444
|
+
const rawHour = Number(parts.find((p) => p.type === "hour")?.value);
|
|
2445
|
+
const hour = rawHour % 24;
|
|
2446
|
+
const minute = Number(parts.find((p) => p.type === "minute")?.value);
|
|
2447
|
+
const second = Number(parts.find((p) => p.type === "second")?.value);
|
|
2448
|
+
return Date.UTC(year, month - 1, day, hour, minute, second) - date.getTime();
|
|
2449
|
+
}
|
|
2220
2450
|
function formatTimeInZone(date, timezone) {
|
|
2221
2451
|
const timePart = new Intl.DateTimeFormat("en-US", {
|
|
2222
2452
|
timeZone: timezone,
|
|
@@ -2253,7 +2483,7 @@ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday
|
|
|
2253
2483
|
function evaluateDate(query) {
|
|
2254
2484
|
const lower = query.toLowerCase().trim();
|
|
2255
2485
|
try {
|
|
2256
|
-
if (lower === "today" || lower === "now" || /^(?:what(?:'s| is) )?(?:the )?(?:current )?date(?: today)?$/.test(lower)) {
|
|
2486
|
+
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
2487
|
return formatDateResult(query, /* @__PURE__ */ new Date());
|
|
2258
2488
|
}
|
|
2259
2489
|
if (lower === "tomorrow") {
|
|
@@ -2266,6 +2496,26 @@ function evaluateDate(query) {
|
|
|
2266
2496
|
d.setDate(d.getDate() - 1);
|
|
2267
2497
|
return formatDateResult(query, d);
|
|
2268
2498
|
}
|
|
2499
|
+
if (lower === "day after tomorrow") {
|
|
2500
|
+
const d = /* @__PURE__ */ new Date();
|
|
2501
|
+
d.setDate(d.getDate() + 2);
|
|
2502
|
+
return formatDateResult(query, d);
|
|
2503
|
+
}
|
|
2504
|
+
if (lower === "day before yesterday") {
|
|
2505
|
+
const d = /* @__PURE__ */ new Date();
|
|
2506
|
+
d.setDate(d.getDate() - 2);
|
|
2507
|
+
return formatDateResult(query, d);
|
|
2508
|
+
}
|
|
2509
|
+
const whatDateMatch = lower.match(/^what date is (.+)$/);
|
|
2510
|
+
if (whatDateMatch) {
|
|
2511
|
+
const d = parseFlexibleDate(whatDateMatch[1]);
|
|
2512
|
+
if (d) return formatDateResult(query, d);
|
|
2513
|
+
}
|
|
2514
|
+
const whenIsMatch = lower.match(/^(?:what day is|when is)\s+(.+)$/);
|
|
2515
|
+
if (whenIsMatch) {
|
|
2516
|
+
const d = parseFlexibleDate(whenIsMatch[1]);
|
|
2517
|
+
if (d) return formatDateResult(query, d);
|
|
2518
|
+
}
|
|
2269
2519
|
const nextLastMatch = lower.match(/^(next|last|this)\s+(.+)$/);
|
|
2270
2520
|
if (nextLastMatch) {
|
|
2271
2521
|
const direction = nextLastMatch[1];
|
|
@@ -2313,7 +2563,7 @@ function evaluateDate(query) {
|
|
|
2313
2563
|
const d = applyOffset(/* @__PURE__ */ new Date(), amount, unit);
|
|
2314
2564
|
return formatDateResult(query, d);
|
|
2315
2565
|
}
|
|
2316
|
-
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13})$/);
|
|
2566
|
+
const unixMatch = lower.match(/^(?:unix\s+)?(?:timestamp\s+)?(\d{10,13}|0)$/);
|
|
2317
2567
|
if (unixMatch) {
|
|
2318
2568
|
const ts = parseInt(unixMatch[1]);
|
|
2319
2569
|
const ms = ts > 9999999999 ? ts : ts * 1e3;
|
|
@@ -2333,14 +2583,11 @@ function evaluateDate(query) {
|
|
|
2333
2583
|
if (toUnixMatch) {
|
|
2334
2584
|
const dateStr = toUnixMatch[1].trim();
|
|
2335
2585
|
let d;
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
} else {
|
|
2339
|
-
d = new Date(dateStr);
|
|
2340
|
-
}
|
|
2341
|
-
if (isNaN(d.getTime())) {
|
|
2586
|
+
const parsed = parseFlexibleDate(dateStr);
|
|
2587
|
+
if (!parsed) {
|
|
2342
2588
|
return { type: "error", input: query, error: `Cannot parse date: '${dateStr}'` };
|
|
2343
2589
|
}
|
|
2590
|
+
d = parsed;
|
|
2344
2591
|
const unix = Math.floor(d.getTime() / 1e3);
|
|
2345
2592
|
return {
|
|
2346
2593
|
type: "date",
|
|
@@ -2449,7 +2696,7 @@ function formatDateResult(input, d, extraMeta) {
|
|
|
2449
2696
|
};
|
|
2450
2697
|
}
|
|
2451
2698
|
function parseFlexibleDate(str) {
|
|
2452
|
-
if (str === "today" || str === "now") return /* @__PURE__ */ new Date();
|
|
2699
|
+
if (str === "today" || str === "now" || str === "date") return /* @__PURE__ */ new Date();
|
|
2453
2700
|
if (str === "tomorrow") {
|
|
2454
2701
|
const d2 = /* @__PURE__ */ new Date();
|
|
2455
2702
|
d2.setDate(d2.getDate() + 1);
|
|
@@ -2460,6 +2707,22 @@ function parseFlexibleDate(str) {
|
|
|
2460
2707
|
d2.setDate(d2.getDate() - 1);
|
|
2461
2708
|
return d2;
|
|
2462
2709
|
}
|
|
2710
|
+
if (str === "day after tomorrow") {
|
|
2711
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2712
|
+
d2.setDate(d2.getDate() + 2);
|
|
2713
|
+
return d2;
|
|
2714
|
+
}
|
|
2715
|
+
if (str === "day before yesterday") {
|
|
2716
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
2717
|
+
d2.setDate(d2.getDate() - 2);
|
|
2718
|
+
return d2;
|
|
2719
|
+
}
|
|
2720
|
+
const relativeDay = str.match(/^(next|last|this)\s+(.+)$/);
|
|
2721
|
+
if (relativeDay) {
|
|
2722
|
+
const direction = relativeDay[1];
|
|
2723
|
+
const dayIndex = DAY_NAMES.indexOf(relativeDay[2]);
|
|
2724
|
+
if (dayIndex !== -1) return getRelativeDay(dayIndex, direction);
|
|
2725
|
+
}
|
|
2463
2726
|
const d = new Date(str);
|
|
2464
2727
|
return isNaN(d.getTime()) ? null : d;
|
|
2465
2728
|
}
|
|
@@ -2577,6 +2840,7 @@ async function calculate(input, options) {
|
|
|
2577
2840
|
intent.query,
|
|
2578
2841
|
intent.from,
|
|
2579
2842
|
intent.to,
|
|
2843
|
+
intent.time,
|
|
2580
2844
|
options?.timezone
|
|
2581
2845
|
);
|
|
2582
2846
|
case "date":
|
package/package.json
CHANGED