Files
Radu Macocian 58823cb24a
Tag and publish to AUR / tag (push) Successful in 1m20s
Tag and publish to AUR / publish (push) Has been skipped
Moved to module structure to work in embeded qs processes
2026-07-15 15:25:33 +02:00

191 lines
8.7 KiB
JavaScript

.pragma library
// Unit conversion. Linear units map to a base via { dim, f }; temperature
// uses explicit to/from-base helpers to handle the offset.
const TABLE = {
// length (base: meter)
"m": { dim: "len", f: 1 }, "meter": { dim: "len", f: 1 }, "meters": { dim: "len", f: 1 },
"km": { dim: "len", f: 1000 }, "cm": { dim: "len", f: 0.01 },
"mm": { dim: "len", f: 0.001 }, "um": { dim: "len", f: 1e-6 }, "nm": { dim: "len", f: 1e-9 },
"in": { dim: "len", f: 0.0254 }, "inch": { dim: "len", f: 0.0254 }, "inches": { dim: "len", f: 0.0254 },
"ft": { dim: "len", f: 0.3048 }, "foot": { dim: "len", f: 0.3048 }, "feet": { dim: "len", f: 0.3048 },
"yd": { dim: "len", f: 0.9144 }, "yard": { dim: "len", f: 0.9144 }, "yards": { dim: "len", f: 0.9144 },
"mi": { dim: "len", f: 1609.344 }, "mile": { dim: "len", f: 1609.344 }, "miles": { dim: "len", f: 1609.344 },
"nmi": { dim: "len", f: 1852 },
// mass (base: gram)
"g": { dim: "mass", f: 1 }, "gram": { dim: "mass", f: 1 }, "grams": { dim: "mass", f: 1 },
"kg": { dim: "mass", f: 1000 }, "mg": { dim: "mass", f: 0.001 },
"lb": { dim: "mass", f: 453.59237 }, "lbs": { dim: "mass", f: 453.59237 },
"oz": { dim: "mass", f: 28.349523125 },
"ton": { dim: "mass", f: 1e6 }, "tonne": { dim: "mass", f: 1e6 }, "t": { dim: "mass", f: 1e6 },
// volume (base: liter)
"l": { dim: "vol", f: 1 }, "liter": { dim: "vol", f: 1 }, "liters": { dim: "vol", f: 1 },
"ml": { dim: "vol", f: 0.001 }, "cl": { dim: "vol", f: 0.01 },
"gal": { dim: "vol", f: 3.785411784 }, "gallon": { dim: "vol", f: 3.785411784 },
"qt": { dim: "vol", f: 0.946352946 }, "pt": { dim: "vol", f: 0.473176473 },
"cup": { dim: "vol", f: 0.2365882365 }, "cups": { dim: "vol", f: 0.2365882365 },
"floz": { dim: "vol", f: 0.0295735295625 }, "tbsp": { dim: "vol", f: 0.01478676478125 },
"tsp": { dim: "vol", f: 0.00492892159375 },
// time (base: second)
"s": { dim: "time", f: 1 }, "sec": { dim: "time", f: 1 }, "secs": { dim: "time", f: 1 },
"ms": { dim: "time", f: 0.001 }, "us": { dim: "time", f: 1e-6 },
"min": { dim: "time", f: 60 }, "mins": { dim: "time", f: 60 },
"h": { dim: "time", f: 3600 }, "hr": { dim: "time", f: 3600 }, "hrs": { dim: "time", f: 3600 },
"hour": { dim: "time", f: 3600 }, "hours": { dim: "time", f: 3600 },
"day": { dim: "time", f: 86400 }, "days": { dim: "time", f: 86400 },
"week": { dim: "time", f: 604800 }, "weeks": { dim: "time", f: 604800 },
"year": { dim: "time", f: 31557600 }, "years": { dim: "time", f: 31557600 },
// data (base: byte; SI and binary)
"b": { dim: "data", f: 1 }, "byte": { dim: "data", f: 1 }, "bytes": { dim: "data", f: 1 },
"kb": { dim: "data", f: 1e3 }, "mb": { dim: "data", f: 1e6 },
"gb": { dim: "data", f: 1e9 }, "tb": { dim: "data", f: 1e12 }, "pb": { dim: "data", f: 1e15 },
"kib": { dim: "data", f: 1024 }, "mib": { dim: "data", f: 1048576 },
"gib": { dim: "data", f: 1073741824 }, "tib": { dim: "data", f: 1099511627776 },
"bit": { dim: "data", f: 0.125 }, "bits": { dim: "data", f: 0.125 },
// speed (base: m/s)
"mps": { dim: "speed", f: 1 },
"kph": { dim: "speed", f: 1 / 3.6 }, "kmh": { dim: "speed", f: 1 / 3.6 },
"mph": { dim: "speed", f: 0.44704 },
"knot": { dim: "speed", f: 0.514444 }, "knots": { dim: "speed", f: 0.514444 },
// angle (base: radian)
"rad": { dim: "angle", f: 1 }, "deg": { dim: "angle", f: Math.PI / 180 },
"turn": { dim: "angle", f: 2 * Math.PI }, "grad": { dim: "angle", f: Math.PI / 200 },
// temperature (offset; handled specially)
"c": { dim: "temp" }, "celsius": { dim: "temp" },
"f": { dim: "temp" }, "fahrenheit": { dim: "temp" },
"k": { dim: "temp" }, "kelvin": { dim: "temp" }
};
function tempToK(v, u) {
if (u === "c" || u === "celsius") return v + 273.15;
if (u === "f" || u === "fahrenheit") return (v - 32) * 5 / 9 + 273.15;
return v;
}
function tempFromK(v, u) {
if (u === "c" || u === "celsius") return v - 273.15;
if (u === "f" || u === "fahrenheit") return (v - 273.15) * 9 / 5 + 32;
return v;
}
// ISO codes Frankfurter supports. Used to detect currency queries before we
// have rates cached (rates are fetched lazily and cached per-day).
const CURRENCIES = ["AUD","BGN","BRL","CAD","CHF","CNY","CZK","DKK","EUR","GBP",
"HKD","HUF","IDR","ILS","INR","ISK","JPY","KRW","MXN","NOK",
"NZD","PHP","PLN","RON","SEK","SGD","THB","TRY","USD","ZAR"];
const CURRENCY_SET = (function() { const s = {}; for (const c of CURRENCIES) s[c] = 1; return s; })();
function isCurrency(code) {
return !!CURRENCY_SET[code.toUpperCase()];
}
// Parse the "<num> <from> to[ <partial-to>]" shape used by both unit and
// currency conversion. Returns null if it doesn't match.
function parseQuery(expr) {
const m = expr.match(/^(-?\d+(?:\.\d+)?(?:e-?\d+)?)\s*([a-zA-Z]+)\s+(?:to|in|->)(?:\s+([a-zA-Z]*))?$/i);
if (!m) return null;
return {
amount: parseFloat(m[1]),
from: m[2].toLowerCase(),
partial: (m[3] || "").toLowerCase()
};
}
function applyConversion(v, fu, tu) {
const fd = TABLE[fu], td = TABLE[tu];
if (fd.dim === "temp") return tempFromK(tempToK(v, fu), tu);
return v * fd.f / td.f;
}
// Parse "<number> <from> to|in|-> <to>" and return { value, from, to } or null.
function convert(expr) {
const m = expr.match(/^(-?\d+(?:\.\d+)?(?:e-?\d+)?)\s*([a-zA-Z]+)\s+(?:to|in|->)\s+([a-zA-Z]+)$/i);
if (!m) return null;
const v = parseFloat(m[1]);
const fu = m[2].toLowerCase();
const tu = m[3].toLowerCase();
const fd = TABLE[fu], td = TABLE[tu];
if (!fd || !td || fd.dim !== td.dim) return null;
return { value: applyConversion(v, fu, tu), from: fu, to: tu };
}
// Build, per dimension, the list of canonical unit names — deduped so e.g.
// "m"/"meter"/"meters" don't all appear; prefer the shortest alias.
const CANONICAL_BY_DIM = (function() {
const byDim = {};
const tempCanon = { "c": 1, "f": 1, "k": 1 };
for (const name in TABLE) {
const td = TABLE[name];
if (!byDim[td.dim]) byDim[td.dim] = {};
const bucket = byDim[td.dim];
const key = td.dim === "temp" ? name : String(td.f);
if (td.dim === "temp" && !tempCanon[name]) continue;
if (bucket[key] === undefined || name.length < bucket[key].length) {
bucket[key] = name;
}
}
const out = {};
for (const dim in byDim) out[dim] = Object.values(byDim[dim]);
return out;
})();
// Given a fetched-rate table { TGT: rate, ... } where amount-of-base * rate =
// amount-of-target, return ordered { value, from, to } entries filtered by
// `partial`. Mirrors suggest() for units.
function currencyConvert(amount, base, partial, rates) {
const baseU = base.toUpperCase();
const candidates = Object.keys(rates).filter(c => c !== baseU);
let ordered;
if (!partial) {
ordered = candidates.slice().sort();
} else {
const p = partial.toUpperCase();
const scored = [];
for (const c of candidates) {
const s = scoreFuzzy(p, c);
if (s > 0) scored.push({ c, s });
}
scored.sort((a, b) => b.s - a.s);
ordered = scored.map(x => x.c);
}
return ordered.map(c => ({ value: amount * rates[c], from: baseU, to: c }));
}
function scoreFuzzy(q, name) {
const idx = name.indexOf(q);
if (idx === 0) return 1000 - name.length;
if (idx > 0) return 500 - name.length;
let qi = 0;
for (let i = 0; i < name.length && qi < q.length; i++) {
if (name[i] === q[qi]) qi++;
}
return qi === q.length ? 100 - name.length : 0;
}
// Parse "<number> <from> to|in|->[ <partial>]" and return an array of
// { value, from, to } for every unit in <from>'s dimension that fuzzy-matches
// <partial> (or all of them if <partial> is empty). Returns null if the input
// doesn't fit the "awaiting target unit" shape.
function suggest(expr) {
const m = expr.match(/^(-?\d+(?:\.\d+)?(?:e-?\d+)?)\s*([a-zA-Z]+)\s+(?:to|in|->)(?:\s+([a-zA-Z]*))?$/i);
if (!m) return null;
const v = parseFloat(m[1]);
const fu = m[2].toLowerCase();
const partial = (m[3] || "").toLowerCase();
const fd = TABLE[fu];
if (!fd) return null;
const candidates = (CANONICAL_BY_DIM[fd.dim] || []).filter(u => u !== fu);
let ordered;
if (!partial) {
ordered = candidates.slice().sort();
} else {
const scored = [];
for (const u of candidates) {
const s = scoreFuzzy(partial, u);
if (s > 0) scored.push({ u, s });
}
scored.sort((a, b) => b.s - a.s);
ordered = scored.map(x => x.u);
}
return ordered.map(tu => ({ value: applyConversion(v, fu, tu), from: fu, to: tu }));
}