@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.
Files changed (3) hide show
  1. package/dist/index.js +320 -56
  2. package/dist/index.mjs +320 -56
  3. 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.toLowerCase());
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
- throw new Error(`Could not resolve rate for ${from} \u2192 ${to}`);
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.trim();
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 TIME_IN_PATTERN = /^(?:what(?:'s| is) )?(?:the )?(?:current )?time (?:in|at) (.+)$/i;
1707
- var TIME_CONVERT_PATTERN = /^(.+?) to (.+?) time$/i;
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
- match = input.match(TIME_IN_PATTERN);
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 place = match[1].trim();
1714
- const tz2 = resolveTimezone(place);
1715
- if (tz2) return { kind: "time", query: place, to: tz2 };
1716
- return { kind: "time", query: place, to: place };
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 match = input.match(CONVERSION_PATTERN);
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 match = input.match(UNIT_PATTERN) || input.match(UNIT_PATTERN_NO_SPACE);
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
- const savedPos = this.pos;
1969
- this.skipWhitespace();
1970
- this.pos++;
1971
- this.skipWhitespace();
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.consume("(");
2093
- const a = this.parseBitwiseOr();
2094
- this.consume(",");
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 parser = new Parser(expression);
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 fromTime = formatTimeInZone(now, resolvedFrom);
2227
- const toTime = formatTimeInZone(now, resolvedTo);
2228
- const formatted = `${fromTime} \u2192 ${toTime}`;
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: now.toISOString()
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
- if (dateStr === "now" || dateStr === "today") {
2374
- d = /* @__PURE__ */ new Date();
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.toLowerCase());
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
- throw new Error(`Could not resolve rate for ${from} \u2192 ${to}`);
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.trim();
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 TIME_IN_PATTERN = /^(?:what(?:'s| is) )?(?:the )?(?:current )?time (?:in|at) (.+)$/i;
1670
- var TIME_CONVERT_PATTERN = /^(.+?) to (.+?) time$/i;
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
- match = input.match(TIME_IN_PATTERN);
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 place = match[1].trim();
1677
- const tz2 = resolveTimezone(place);
1678
- if (tz2) return { kind: "time", query: place, to: tz2 };
1679
- return { kind: "time", query: place, to: place };
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 match = input.match(CONVERSION_PATTERN);
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 match = input.match(UNIT_PATTERN) || input.match(UNIT_PATTERN_NO_SPACE);
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
- const savedPos = this.pos;
1932
- this.skipWhitespace();
1933
- this.pos++;
1934
- this.skipWhitespace();
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.consume("(");
2056
- const a = this.parseBitwiseOr();
2057
- this.consume(",");
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 parser = new Parser(expression);
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 fromTime = formatTimeInZone(now, resolvedFrom);
2190
- const toTime = formatTimeInZone(now, resolvedTo);
2191
- const formatted = `${fromTime} \u2192 ${toTime}`;
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: now.toISOString()
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
- if (dateStr === "now" || dateStr === "today") {
2337
- d = /* @__PURE__ */ new Date();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercmd/calculator",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Powerful natural language calculator — math, units, currency, crypto, time zones, dates",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",