1(function (root, factory) {
2 if (typeof module === 'object' && module.exports) {
3 module.exports = factory();
4 } else {
5 root.UnitConverter = factory();
6 }
7})(typeof self !== 'undefined' ? self : this, function () {
8 'use strict';
9
10 const CATEGORIES = {
11 length: {
12 label: 'Length',
13 base: 'm',
14 units: {
15 m: { label: 'Meter', plural: 'Meters', factor: 1 },
16 km: { label: 'Kilometer', plural: 'Kilometers', factor: 1000 },
17 cm: { label: 'Centimeter', plural: 'Centimeters', factor: 0.01 },
18 mm: { label: 'Millimeter', plural: 'Millimeters', factor: 0.001 },
19 in: { label: 'Inch', plural: 'Inches', factor: 0.0254 },
20 ft: { label: 'Foot', plural: 'Feet', factor: 0.3048 },
21 yd: { label: 'Yard', plural: 'Yards', factor: 0.9144 },
22 mi: { label: 'Mile', plural: 'Miles', factor: 1609.344 },
23 nmi: { label: 'Nautical mile', plural: 'Nautical miles', factor: 1852 }
24 }
25 },
26 mass: {
27 label: 'Mass',
28 base: 'kg',
29 units: {
30 kg: { label: 'Kilogram', plural: 'Kilograms', factor: 1 },
31 g: { label: 'Gram', plural: 'Grams', factor: 0.001 },
32 mg: { label: 'Milligram', plural: 'Milligrams', factor: 0.000001 },
33 lb: { label: 'Pound', plural: 'Pounds', factor: 0.45359237 },
34 oz: { label: 'Ounce', plural: 'Ounces', factor: 0.028349523125 },
35 st: { label: 'Stone', plural: 'Stone', factor: 6.35029318 },
36 t: { label: 'Metric ton', plural: 'Metric tons', factor: 1000 }
37 }
38 },
39 temperature: {
40 label: 'Temperature',
41 base: 'C',
42 units: {
43 C: {
44 label: 'Celsius', plural: 'Celsius',
45 toBase: function (v) { return v; },
46 fromBase: function (v) { return v; }
47 },
48 F: {
49 label: 'Fahrenheit', plural: 'Fahrenheit',
50 toBase: function (v) { return (v - 32) * 5 / 9; },
51 fromBase: function (v) { return v * 9 / 5 + 32; }
52 },
53 K: {
54 label: 'Kelvin', plural: 'Kelvin',
55 toBase: function (v) { return v - 273.15; },
56 fromBase: function (v) { return v + 273.15; }
57 }
58 }
59 },
60 data: {
61 label: 'Data size',
62 base: 'B',
63 units: {
64 b: { label: 'Bit', plural: 'Bits', factor: 0.125 },
65 B: { label: 'Byte', plural: 'Bytes', factor: 1 },
66 KB: { label: 'Kilobyte', plural: 'Kilobytes', factor: 1000 },
67 MB: { label: 'Megabyte', plural: 'Megabytes', factor: 1000 ** 2 },
68 GB: { label: 'Gigabyte', plural: 'Gigabytes', factor: 1000 ** 3 },
69 TB: { label: 'Terabyte', plural: 'Terabytes', factor: 1000 ** 4 },
70 KiB: { label: 'Kibibyte', plural: 'Kibibytes', factor: 1024 },
71 MiB: { label: 'Mebibyte', plural: 'Mebibytes', factor: 1024 ** 2 },
72 GiB: { label: 'Gibibyte', plural: 'Gibibytes', factor: 1024 ** 3 },
73 TiB: { label: 'Tebibyte', plural: 'Tebibytes', factor: 1024 ** 4 }
74 }
75 },
76 time: {
77 label: 'Time',
78 base: 's',
79 units: {
80 ns: { label: 'Nanosecond', plural: 'Nanoseconds', factor: 1e-9 },
81 us: { label: 'Microsecond', plural: 'Microseconds', factor: 1e-6 },
82 ms: { label: 'Millisecond', plural: 'Milliseconds', factor: 0.001 },
83 s: { label: 'Second', plural: 'Seconds', factor: 1 },
84 min: { label: 'Minute', plural: 'Minutes', factor: 60 },
85 h: { label: 'Hour', plural: 'Hours', factor: 3600 },
86 d: { label: 'Day', plural: 'Days', factor: 86400 },
87 wk: { label: 'Week', plural: 'Weeks', factor: 604800 },
88 yr: { label: 'Julian year', plural: 'Julian years', factor: 31557600 }
89 }
90 }
91 };
92
93 const ABSOLUTE_ZERO_C = -273.15;
94 const EPSILON = 1e-12;
95
96 function assertCategory(category) {
97 if (!CATEGORIES[category]) throw new Error('Unknown category: ' + category);
98 return CATEGORIES[category];
99 }
100
101 function assertUnit(categoryInfo, unit) {
102 if (!categoryInfo.units[unit]) throw new Error('Unknown unit: ' + unit);
103 return categoryInfo.units[unit];
104 }
105
106 function assertFiniteNumber(value) {
107 const number = typeof value === 'number' ? value : Number(value);
108 if (!Number.isFinite(number)) throw new TypeError('Value must be a finite number');
109 return number;
110 }
111
112 function validateTemperatureC(celsius) {
113 if (celsius < ABSOLUTE_ZERO_C - EPSILON) {
114 throw new RangeError('Temperature is below absolute zero');
115 }
116 }
117
118 function toBase(category, unit, value) {
119 const categoryInfo = assertCategory(category);
120 const unitInfo = assertUnit(categoryInfo, unit);
121 const number = assertFiniteNumber(value);
122
123 if (category === 'temperature') {
124 const celsius = unitInfo.toBase(number);
125 validateTemperatureC(celsius);
126 return celsius;
127 }
128
129 return number * unitInfo.factor;
130 }
131
132 function fromBase(category, unit, baseValue) {
133 const categoryInfo = assertCategory(category);
134 const unitInfo = assertUnit(categoryInfo, unit);
135 const number = assertFiniteNumber(baseValue);
136
137 if (category === 'temperature') {
138 validateTemperatureC(number);
139 return unitInfo.fromBase(number);
140 }
141
142 return number / unitInfo.factor;
143 }
144
145 function convertValue(value, fromUnit, toUnit, category) {
146 const baseValue = toBase(category, fromUnit, value);
147 return fromBase(category, toUnit, baseValue);
148 }
149
150 function unitOptions(category) {
151 const categoryInfo = assertCategory(category);
152 return Object.keys(categoryInfo.units).map(function (key) {
153 const unit = categoryInfo.units[key];
154 return { key: key, label: unit.label, plural: unit.plural };
155 });
156 }
157
158 function formatNumber(value, precision) {
159 const number = assertFiniteNumber(value);
160 const places = Math.max(0, Math.min(12, Number.parseInt(precision, 10)));
161 if (Object.is(number, -0)) return '0';
162 const fixed = number.toFixed(places);
163 if (places === 0) return fixed;
164 return fixed.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
165 }
166
167 function defaultPair(category) {
168 const options = unitOptions(category);
169 return [options[0].key, options[1] ? options[1].key : options[0].key];
170 }
171
172 function bootstrapUI() {
173 if (typeof document === 'undefined') return;
174
175 const $ = function (id) { return document.getElementById(id); };
176 const categorySelect = $('category');
177 const precisionSelect = $('precision');
178 const fromValue = $('fromValue');
179 const toValue = $('toValue');
180 const fromUnit = $('fromUnit');
181 const toUnit = $('toUnit');
182 const swapButton = $('swap');
183 const status = $('status');
184 const recentList = $('recentList');
185 const clearRecent = $('clearRecent');
186 const STORAGE_KEY = 'unit-converter-recents-v1';
187 let activeSide = 'from';
188 let saveTimer = null;
189
190 if (!categorySelect) return;
191
192 function getRecents() {
193 try {
194 return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
195 } catch (_error) {
196 return [];
197 }
198 }
199
200 function setRecents(items) {
201 localStorage.setItem(STORAGE_KEY, JSON.stringify(items.slice(0, 10)));
202 renderRecents();
203 }
204
205 function labelFor(category, unit) {
206 return CATEGORIES[category].units[unit].label;
207 }
208
209 function populateCategories() {
210 categorySelect.innerHTML = '';
211 Object.keys(CATEGORIES).forEach(function (key) {
212 const option = document.createElement('option');
213 option.value = key;
214 option.textContent = CATEGORIES[key].label;
215 categorySelect.appendChild(option);
216 });
217 }
218
219 function populateUnits(category, preferredFrom, preferredTo) {
220 const options = unitOptions(category);
221 const pair = defaultPair(category);
222 fromUnit.innerHTML = '';
223 toUnit.innerHTML = '';
224 options.forEach(function (unit) {
225 const fromOption = document.createElement('option');
226 fromOption.value = unit.key;
227 fromOption.textContent = unit.label + ' (' + unit.key + ')';
228 fromUnit.appendChild(fromOption);
229
230 const toOption = fromOption.cloneNode(true);
231 toUnit.appendChild(toOption);
232 });
233 fromUnit.value = preferredFrom && CATEGORIES[category].units[preferredFrom] ? preferredFrom : pair[0];
234 toUnit.value = preferredTo && CATEGORIES[category].units[preferredTo] ? preferredTo : pair[1];
235 }
236
237 function renderRecents() {
238 const recents = getRecents();
239 recentList.innerHTML = '';
240 if (!recents.length) {
241 const empty = document.createElement('li');
242 empty.className = 'empty';
243 empty.textContent = 'No recent conversions yet.';
244 recentList.appendChild(empty);
245 return;
246 }
247 recents.forEach(function (item) {
248 const li = document.createElement('li');
249 const button = document.createElement('button');
250 button.type = 'button';
251 button.textContent = item.text;
252 button.title = 'Load conversion';
253 button.addEventListener('click', function () {
254 categorySelect.value = item.category;
255 populateUnits(item.category, item.fromUnit, item.toUnit);
256 fromValue.value = item.fromValue;
257 activeSide = 'from';
258 update();
259 fromValue.focus();
260 });
261 li.appendChild(button);
262 recentList.appendChild(li);
263 });
264 }
265
266 function scheduleRecent(item) {
267 clearTimeout(saveTimer);
268 saveTimer = setTimeout(function () {
269 const recents = getRecents().filter(function (recent) {
270 return recent.key !== item.key;
271 });
272 recents.unshift(item);
273 setRecents(recents);
274 }, 250);
275 }
276
277 function setStatus(message, isError) {
278 status.textContent = message;
279 status.classList.toggle('error', Boolean(isError));
280 }
281
282 function makeRecentText(category, leftValue, leftUnit, rightValue, rightUnit) {
283 return formatNumber(leftValue, precisionSelect.value) + ' ' + leftUnit + ' = ' +
284 formatNumber(rightValue, precisionSelect.value) + ' ' + rightUnit +
285 ' · ' + CATEGORIES[category].label;
286 }
287
288 function update() {
289 const category = categorySelect.value;
290 const sourceInput = activeSide === 'from' ? fromValue : toValue;
291 const targetInput = activeSide === 'from' ? toValue : fromValue;
292 const sourceUnit = activeSide === 'from' ? fromUnit.value : toUnit.value;
293 const targetUnit = activeSide === 'from' ? toUnit.value : fromUnit.value;
294 const raw = sourceInput.value.trim();
295
296 if (raw === '' || raw === '-' || raw === '.' || raw === '-.') {
297 targetInput.value = '';
298 setStatus('Type a value to convert.', false);
299 return;
300 }
301
302 try {
303 const inputNumber = assertFiniteNumber(raw);
304 const result = convertValue(inputNumber, sourceUnit, targetUnit, category);
305 targetInput.value = formatNumber(result, precisionSelect.value);
306 const fromNumber = activeSide === 'from' ? inputNumber : result;
307 const toNumber = activeSide === 'from' ? result : inputNumber;
308 setStatus(
309 formatNumber(fromNumber, precisionSelect.value) + ' ' + labelFor(category, fromUnit.value) +
310 ' equals ' + formatNumber(toNumber, precisionSelect.value) + ' ' + labelFor(category, toUnit.value) + '.',
311 false
312 );
313 scheduleRecent({
314 key: [category, fromUnit.value, toUnit.value, formatNumber(fromNumber, precisionSelect.value), formatNumber(toNumber, precisionSelect.value)].join('|'),
315 category: category,
316 fromUnit: fromUnit.value,
317 toUnit: toUnit.value,
318 fromValue: String(formatNumber(fromNumber, precisionSelect.value)),
319 text: makeRecentText(category, fromNumber, fromUnit.value, toNumber, toUnit.value)
320 });
321 } catch (error) {
322 targetInput.value = '';
323 setStatus(error.message, true);
324 }
325 }
326
327 function swap() {
328 const oldFromUnit = fromUnit.value;
329 fromUnit.value = toUnit.value;
330 toUnit.value = oldFromUnit;
331 const oldFromValue = fromValue.value;
332 fromValue.value = toValue.value;
333 toValue.value = oldFromValue;
334 activeSide = activeSide === 'from' ? 'to' : 'from';
335 update();
336 }
337
338 populateCategories();
339 populateUnits('length');
340 fromValue.value = '1';
341 precisionSelect.value = '4';
342 renderRecents();
343 update();
344
345 categorySelect.addEventListener('change', function () {
346 populateUnits(categorySelect.value);
347 update();
348 });
349 precisionSelect.addEventListener('change', update);
350 fromUnit.addEventListener('change', update);
351 toUnit.addEventListener('change', update);
352 fromValue.addEventListener('input', function () { activeSide = 'from'; update(); });
353 toValue.addEventListener('input', function () { activeSide = 'to'; update(); });
354 fromValue.addEventListener('focus', function () { activeSide = 'from'; });
355 toValue.addEventListener('focus', function () { activeSide = 'to'; });
356 swapButton.addEventListener('click', swap);
357 clearRecent.addEventListener('click', function () { setRecents([]); fromValue.focus(); });
358
359 document.addEventListener('keydown', function (event) {
360 if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
361 event.preventDefault();
362 fromValue.select();
363 }
364 if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
365 event.preventDefault();
366 swap();
367 }
368 if (event.key === 'Escape') {
369 fromValue.value = '';
370 toValue.value = '';
371 setStatus('Cleared.', false);
372 fromValue.focus();
373 }
374 });
375 }
376
377 if (typeof document !== 'undefined') {
378 if (document.readyState === 'loading') {
379 document.addEventListener('DOMContentLoaded', bootstrapUI);
380 } else {
381 bootstrapUI();
382 }
383 }
384
385 return {
386 CATEGORIES: CATEGORIES,
387 ABSOLUTE_ZERO_C: ABSOLUTE_ZERO_C,
388 convertValue: convertValue,
389 toBase: toBase,
390 fromBase: fromBase,
391 formatNumber: formatNumber,
392 unitOptions: unitOptions
393 };
394});
395
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.