Moved to module structure to work in embeded qs processes
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
.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 }));
|
||||
}
|
||||
Reference in New Issue
Block a user