1(function (root, factory) {
2 const api = factory();
3 if (typeof module === "object" && module.exports) module.exports = api;
4 root.UnitConverter = api;
5})(typeof globalThis !== "undefined" ? globalThis : this, function () {
6 "use strict";
7
8 const CATEGORIES = {
9 length: {
10 label: "Length",
11 base: "m",
12 units: {
13 mm: { label: "Millimeter", factor: 0.001 },
14 cm: { label: "Centimeter", factor: 0.01 },
15 m: { label: "Meter", factor: 1 },
16 km: { label: "Kilometer", factor: 1000 },
17 in: { label: "Inch", factor: 0.0254 },
18 ft: { label: "Foot", factor: 0.3048 },
19 yd: { label: "Yard", factor: 0.9144 },
20 mi: { label: "Mile", factor: 1609.344 }
21 }
22 },
23 mass: {
24 label: "Mass",
25 base: "kg",
26 units: {
27 mg: { label: "Milligram", factor: 0.000001 },
28 g: { label: "Gram", factor: 0.001 },
29 kg: { label: "Kilogram", factor: 1 },
30 oz: { label: "Ounce", factor: 0.028349523125 },
31 lb: { label: "Pound", factor: 0.45359237 },
32 st: { label: "Stone", factor: 6.35029318 },
33 ton: { label: "Metric ton", factor: 1000 }
34 }
35 },
36 temperature: {
37 label: "Temperature",
38 base: "K",
39 units: {
40 C: { label: "Celsius", toBase: (v) => v + 273.15, fromBase: (v) => v - 273.15 },
41 F: { label: "Fahrenheit", toBase: (v) => (v - 32) * 5 / 9 + 273.15, fromBase: (v) => (v - 273.15) * 9 / 5 + 32 },
42 K: { label: "Kelvin", toBase: (v) => v, fromBase: (v) => v },
43 R: { label: "Rankine", toBase: (v) => v * 5 / 9, fromBase: (v) => v * 9 / 5 }
44 }
45 },
46 data: {
47 label: "Data size",
48 base: "B",
49 units: {
50 bit: { label: "Bit", factor: 0.125 },
51 B: { label: "Byte", factor: 1 },
52 KB: { label: "Kilobyte", factor: 1000 },
53 MB: { label: "Megabyte", factor: 1000 ** 2 },
54 GB: { label: "Gigabyte", factor: 1000 ** 3 },
55 TB: { label: "Terabyte", factor: 1000 ** 4 },
56 KiB: { label: "Kibibyte", factor: 1024 },
57 MiB: { label: "Mebibyte", factor: 1024 ** 2 },
58 GiB: { label: "Gibibyte", factor: 1024 ** 3 },
59 TiB: { label: "Tebibyte", factor: 1024 ** 4 }
60 }
61 },
62 time: {
63 label: "Time",
64 base: "s",
65 units: {
66 ns: { label: "Nanosecond", factor: 1e-9 },
67 ms: { label: "Millisecond", factor: 0.001 },
68 s: { label: "Second", factor: 1 },
69 min: { label: "Minute", factor: 60 },
70 h: { label: "Hour", factor: 3600 },
71 day: { label: "Day", factor: 86400 },
72 week: { label: "Week", factor: 604800 },
73 year: { label: "Year (365 d)", factor: 31536000 }
74 }
75 }
76 };
77
78 function assertUnit(category, unit) {
79 if (!CATEGORIES[category]) throw new Error(`Unknown category: ${category}`);
80 if (!CATEGORIES[category].units[unit]) throw new Error(`Unknown unit '${unit}' for ${category}`);
81 }
82
83 function toBase(value, category, unit) {
84 assertUnit(category, unit);
85 const u = CATEGORIES[category].units[unit];
86 return typeof u.toBase === "function" ? u.toBase(value) : value * u.factor;
87 }
88
89 function fromBase(value, category, unit) {
90 assertUnit(category, unit);
91 const u = CATEGORIES[category].units[unit];
92 return typeof u.fromBase === "function" ? u.fromBase(value) : value / u.factor;
93 }
94
95 function convert(value, category, fromUnit, toUnit) {
96 const n = Number(value);
97 if (!Number.isFinite(n)) return NaN;
98 return fromBase(toBase(n, category, fromUnit), category, toUnit);
99 }
100
101 function formatNumber(value, precision) {
102 if (!Number.isFinite(value)) return "";
103 const digits = Number(precision);
104 if (digits < 0) return String(value);
105 return Number(value.toFixed(digits)).toLocaleString(undefined, { maximumFractionDigits: digits });
106 }
107
108 function parseInput(text) {
109 if (typeof text !== "string") return NaN;
110 const normalized = text.replace(/,/g, "").trim();
111 if (normalized === "") return NaN;
112 return Number(normalized);
113 }
114
115 const api = { CATEGORIES, convert, toBase, fromBase, formatNumber, parseInput };
116
117 if (typeof document !== "undefined") {
118 document.addEventListener("DOMContentLoaded", () => initApp(api));
119 }
120
121 function initApp({ CATEGORIES, convert, formatNumber, parseInput }) {
122 const $ = (id) => document.getElementById(id);
123 const category = $("category");
124 const fromValue = $("fromValue");
125 const toValue = $("toValue");
126 const fromUnit = $("fromUnit");
127 const toUnit = $("toUnit");
128 const precision = $("precision");
129 const swap = $("swap");
130 const recent = $("recent");
131 const clearRecent = $("clearRecent");
132 const status = $("status");
133 const RECENT_KEY = "unit-converter-recent";
134 let editing = "from";
135 let saveTimer;
136
137 function option(value, label) {
138 const o = document.createElement("option");
139 o.value = value;
140 o.textContent = label;
141 return o;
142 }
143
144 function populateCategories() {
145 category.innerHTML = "";
146 Object.entries(CATEGORIES).forEach(([key, cat]) => category.append(option(key, cat.label)));
147 }
148
149 function populateUnits() {
150 const cat = CATEGORIES[category.value];
151 const prevFrom = fromUnit.value;
152 const prevTo = toUnit.value;
153 fromUnit.innerHTML = "";
154 toUnit.innerHTML = "";
155 Object.entries(cat.units).forEach(([key, unit]) => {
156 const label = `${unit.label} (${key})`;
157 fromUnit.append(option(key, label));
158 toUnit.append(option(key, label));
159 });
160 const keys = Object.keys(cat.units);
161 fromUnit.value = keys.includes(prevFrom) ? prevFrom : keys[0];
162 toUnit.value = keys.includes(prevTo) ? prevTo : (keys[1] || keys[0]);
163 calculate();
164 }
165
166 function calculate() {
167 const input = editing === "from" ? fromValue : toValue;
168 const output = editing === "from" ? toValue : fromValue;
169 const from = editing === "from" ? fromUnit.value : toUnit.value;
170 const to = editing === "from" ? toUnit.value : fromUnit.value;
171 const n = parseInput(input.value);
172 if (!Number.isFinite(n)) {
173 output.value = "";
174 status.textContent = "Enter a number to convert.";
175 return;
176 }
177 const result = convert(n, category.value, from, to);
178 output.value = formatNumber(result, precision.value);
179 status.textContent = `${input.value} ${from} = ${output.value} ${to}`;
180 queueRecent(n, result, from, to);
181 }
182
183 function getRecent() {
184 try { return JSON.parse(localStorage.getItem(RECENT_KEY)) || []; } catch { return []; }
185 }
186
187 function setRecent(items) {
188 localStorage.setItem(RECENT_KEY, JSON.stringify(items.slice(0, 10)));
189 renderRecent();
190 }
191
192 function queueRecent(input, result, from, to) {
193 clearTimeout(saveTimer);
194 saveTimer = setTimeout(() => {
195 const item = {
196 category: category.value,
197 input,
198 result,
199 from,
200 to,
201 precision: Number(precision.value),
202 at: Date.now()
203 };
204 const deduped = getRecent().filter((x) => !(x.category === item.category && x.input === item.input && x.from === item.from && x.to === item.to));
205 setRecent([item, ...deduped]);
206 }, 350);
207 }
208
209 function renderRecent() {
210 const items = getRecent();
211 recent.innerHTML = "";
212 if (!items.length) {
213 const li = document.createElement("li");
214 li.textContent = "No recent conversions yet.";
215 li.className = "empty";
216 recent.append(li);
217 return;
218 }
219 items.forEach((item) => {
220 const li = document.createElement("li");
221 const btn = document.createElement("button");
222 btn.type = "button";
223 btn.textContent = `${formatNumber(item.input, item.precision)} ${item.from} → ${formatNumber(item.result, item.precision)} ${item.to}`;
224 btn.title = "Restore this conversion";
225 btn.addEventListener("click", () => {
226 category.value = item.category;
227 populateUnits();
228 fromUnit.value = item.from;
229 toUnit.value = item.to;
230 precision.value = item.precision;
231 fromValue.value = String(item.input);
232 editing = "from";
233 calculate();
234 fromValue.focus();
235 });
236 li.append(btn);
237 recent.append(li);
238 });
239 }
240
241 function doSwap() {
242 [fromUnit.value, toUnit.value] = [toUnit.value, fromUnit.value];
243 [fromValue.value, toValue.value] = [toValue.value, fromValue.value];
244 editing = editing === "from" ? "to" : "from";
245 calculate();
246 }
247
248 populateCategories();
249 category.value = "length";
250 populateUnits();
251 fromValue.value = "1";
252 calculate();
253 renderRecent();
254
255 category.addEventListener("change", populateUnits);
256 fromUnit.addEventListener("change", () => { editing = "from"; calculate(); });
257 toUnit.addEventListener("change", () => { editing = "from"; calculate(); });
258 precision.addEventListener("change", calculate);
259 fromValue.addEventListener("input", () => { editing = "from"; calculate(); });
260 toValue.addEventListener("input", () => { editing = "to"; calculate(); });
261 fromValue.addEventListener("focus", () => { editing = "from"; });
262 toValue.addEventListener("focus", () => { editing = "to"; });
263 swap.addEventListener("click", doSwap);
264 clearRecent.addEventListener("click", () => setRecent([]));
265 document.addEventListener("keydown", (e) => {
266 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
267 e.preventDefault(); fromValue.select();
268 }
269 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
270 e.preventDefault(); doSwap();
271 }
272 });
273 }
274
275 return api;
276});
277
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.