Skip to content

Commit 88e0e8b

Browse files
committed
feat(new tool): SI Prefixes Converter
Fix #295
1 parent 6f6eccd commit 88e0e8b

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { MathSymbols } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'SI Prefixes Converter',
6+
path: '/si-prefixes-converter',
7+
description: 'Convert between Metric Prefixes',
8+
keywords: ['si', 'international', 'metric', 'units', 'converter'],
9+
component: () => import('./si-prefixes-converter.vue'),
10+
icon: MathSymbols,
11+
createdAt: new Date('2026-01-11'),
12+
category: 'Physics',
13+
});
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<script setup lang="ts">
2+
import Big from 'big.js';
3+
import { useQueryParamOrStorage } from '@/composable/queryParams';
4+
5+
interface Prefix {
6+
label: string
7+
symbol: string
8+
exponent: number
9+
};
10+
11+
const prefixes: Prefix[] = [
12+
{ label: 'quetta (Q)', symbol: 'Q', exponent: 30 },
13+
{ label: 'ronna (R)', symbol: 'R', exponent: 27 },
14+
{ label: 'yotta (Y)', symbol: 'Y', exponent: 24 },
15+
{ label: 'zetta (Z)', symbol: 'Z', exponent: 21 },
16+
{ label: 'exa (E)', symbol: 'E', exponent: 18 },
17+
{ label: 'peta (P)', symbol: 'P', exponent: 15 },
18+
{ label: 'tera (T)', symbol: 'T', exponent: 12 },
19+
{ label: 'giga (G)', symbol: 'G', exponent: 9 },
20+
{ label: 'mega (M)', symbol: 'M', exponent: 6 },
21+
{ label: 'kilo (k)', symbol: 'k', exponent: 3 },
22+
{ label: 'hecto (h)', symbol: 'h', exponent: 2 },
23+
{ label: 'deca (da)', symbol: 'da', exponent: 1 },
24+
{ label: 'unit', symbol: '0', exponent: 0 },
25+
{ label: 'deci (d)', symbol: 'd', exponent: -1 },
26+
{ label: 'centi (c)', symbol: 'c', exponent: -2 },
27+
{ label: 'milli (m)', symbol: 'm', exponent: -3 },
28+
{ label: 'micro (µ)', symbol: 'µ', exponent: -6 },
29+
{ label: 'nano (n)', symbol: 'n', exponent: -9 },
30+
{ label: 'pico (p)', symbol: 'p', exponent: -12 },
31+
{ label: 'femto (f)', symbol: 'f', exponent: -15 },
32+
{ label: 'atto (a)', symbol: 'a', exponent: -18 },
33+
{ label: 'zepto (z)', symbol: 'z', exponent: -21 },
34+
{ label: 'yocto (y)', symbol: 'y', exponent: -24 },
35+
{ label: 'ronto (r)', symbol: 'r', exponent: -27 },
36+
{ label: 'quecto (q)', symbol: 'q', exponent: -30 },
37+
];
38+
39+
const prefixOptions = prefixes.map(p => ({
40+
label: p.label,
41+
value: p.symbol,
42+
}));
43+
44+
const fromSymbol = useQueryParamOrStorage({ storageName: 'si-pref-conv:f', name: 'from', defaultValue: 'q' });
45+
const toSymbol = useQueryParamOrStorage({ storageName: 'si-pref-conv:t', name: 'to', defaultValue: 'Q' });
46+
const formatMode = useQueryParamOrStorage<'auto' | 'fixed' | 'exp'>({ storageName: 'si-pref-conv:m', name: 'mode', defaultValue: 'auto' });
47+
const decimals = useQueryParamOrStorage({ storageName: 'si-pref-conv:d', name: 'dec', defaultValue: 6 });
48+
const thousandSep = useQueryParamOrStorage({ storageName: 'si-pref-conv:s', name: 'sep', defaultValue: true });
49+
50+
const inputValue = ref<number | null>(1);
51+
52+
const fromPrefix = computed(() => prefixes.find(p => p.symbol === fromSymbol.value)!);
53+
const toPrefix = computed(() => prefixes.find(p => p.symbol === toSymbol.value)!);
54+
55+
// Conversion
56+
const converted = computed(() => {
57+
if (inputValue.value === null) {
58+
return null;
59+
}
60+
const delta = fromPrefix.value.exponent - toPrefix.value.exponent;
61+
return new Big(inputValue.value).times(new Big(10).pow(delta));
62+
});
63+
64+
// Formatting pipeline
65+
function formatValue(v: Big): string {
66+
if (formatMode.value === 'exp') {
67+
return v.toExponential(decimals.value);
68+
}
69+
70+
if (formatMode.value === 'fixed') {
71+
const fixed = v.toFixed(decimals.value);
72+
return thousandSep.value ? addThousandSep(fixed) : fixed;
73+
}
74+
75+
// AUTO mode
76+
const abs = v.abs();
77+
if (abs.gte('1e6') || abs.lte('1e-4')) {
78+
return v.toExponential(decimals.value);
79+
}
80+
81+
const normal = v.toFixed(decimals.value);
82+
return thousandSep.value ? addThousandSep(normal) : normal;
83+
}
84+
85+
function addThousandSep(str: string): string {
86+
const [int, dec] = str.split('.');
87+
const withSep = int.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
88+
return dec ? `${withSep}.${dec}` : withSep;
89+
}
90+
91+
const displayValue = computed(() => {
92+
if (!converted.value) {
93+
return '';
94+
}
95+
return formatValue(converted.value);
96+
});
97+
</script>
98+
99+
<template>
100+
<div>
101+
<NSpace justify="center" wrap>
102+
<NFormItem label="Convert:" label-placement="left">
103+
<n-input-number-i18n v-model:value="inputValue" mr-1 />
104+
<NSelect
105+
v-model:value="fromSymbol"
106+
:options="prefixOptions"
107+
style="width: 200px"
108+
filterable
109+
/>
110+
</NFormItem>
111+
</NSpace>
112+
113+
<NSpace justify="center">
114+
<NFormItem label="Mode:" label-placement="left">
115+
<NSelect
116+
v-model:value="formatMode"
117+
:options="[
118+
{ label: 'Auto', value: 'auto' },
119+
{ label: 'Fixed', value: 'fixed' },
120+
{ label: 'Exponential', value: 'exp' },
121+
]"
122+
style="width: 200px"
123+
filterable
124+
/>
125+
</NFormItem>
126+
<NFormItem label="Decimals:" label-placement="left">
127+
<NInputNumber v-model:value="decimals" :min="0" :max="60" style="width: 200px" />
128+
</NFormItem>
129+
<NFormItem label="Thousand separators:" label-placement="left">
130+
<NSwitch v-model:value="thousandSep" />
131+
</NFormItem>
132+
</NSpace>
133+
134+
<NSpace justify="center" align="center" wrap>
135+
<NFormItem label="To:" label-placement="left">
136+
<NSelect v-model:value="toSymbol" :options="prefixOptions" style="width: 200px" mr-1 />
137+
<NText mr-1>
138+
=
139+
</NText>
140+
<input-copyable :value="displayValue" autosize style="width: 400px" />
141+
</NFormItem>
142+
</NSpace>
143+
</div>
144+
</template>

0 commit comments

Comments
 (0)