This commit is contained in:
Hardianto 2026-10-20 07:42:55 +07:00 • committed by GitHub
commit 4ac502a42e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 247 additions and 1 deletions

View file

@ -75,6 +75,10 @@ tools:
title: Password strength analyser
description: Discover the strength of your password with this client-side-only password strength analyser and crack time estimation tool.
data-storage-converter:
title: Data storage converter
description: Convert data storage units (bytes, kilobytes, megabytes, gigabytes, terabytes, petabytes, and their binary IEC counterparts) in real time.
chronometer:
title: Chronometer
description: Monitor the duration of a thing. Basically a chronometer with simple chronometer features.

View file

@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { bitsToUnit, formatPrecision, unitToBits } from './data-storage-converter.models';
describe('data-storage-converter models', () => {
it('converts bits and bytes', () => {
expect(unitToBits(1, 'byte')).toBe(8);
expect(bitsToUnit(8, 'byte')).toBe(1);
expect(unitToBits(1, 'bit')).toBe(1);
});
it('converts decimal (SI) units', () => {
expect(unitToBits(1, 'kilobyte')).toBe(8000);
expect(bitsToUnit(8e9, 'gigabyte')).toBe(1);
expect(bitsToUnit(unitToBits(1, 'petabyte'), 'gigabyte')).toBe(1e6);
expect(bitsToUnit(unitToBits(1, 'megabit'), 'kilobit')).toBe(1000);
});
it('converts binary (IEC) units', () => {
expect(unitToBits(1, 'kibibyte')).toBe(1024 * 8);
expect(bitsToUnit(unitToBits(1, 'gibibyte'), 'megabyte')).toBeCloseTo(1073.741824, 6);
expect(bitsToUnit(unitToBits(1, 'pebibyte'), 'terabyte')).toBeCloseTo(1125.899906842624, 6);
});
it('round-trips without loss of precision', () => {
for (const unit of ['bit', 'byte', 'megabyte', 'gibibyte', 'terabit', 'pebibyte'] as const) {
const bits = unitToBits(123.456, unit);
expect(bitsToUnit(bits, unit)).toBeCloseTo(123.456, 6);
}
});
it('formats precision by trimming trailing zeros', () => {
expect(formatPrecision(0)).toBe('0');
expect(formatPrecision(1024)).toBe('1024');
expect(formatPrecision(1073.741824)).toBe('1073.741824');
});
});

View file

@ -0,0 +1,109 @@
export type StorageUnit
= | 'bit'
| 'byte'
| 'kilobit'
| 'kilobyte'
| 'megabit'
| 'megabyte'
| 'gigabit'
| 'gigabyte'
| 'terabit'
| 'terabyte'
| 'petabit'
| 'petabyte'
| 'kibibit'
| 'kibibyte'
| 'mebibit'
| 'mebibyte'
| 'gibibit'
| 'gibibyte'
| 'tebibit'
| 'tebibyte'
| 'pebibit'
| 'pebibyte';
const BITS_PER_BYTE = 8;
// Decimal (SI) prefixes: powers of 1000
const SI_PREFIXES: Partial<Record<StorageUnit, number>> = {
kilobit: 1e3,
kilobyte: 1e3,
megabit: 1e6,
megabyte: 1e6,
gigabit: 1e9,
gigabyte: 1e9,
terabit: 1e12,
terabyte: 1e12,
petabit: 1e15,
petabyte: 1e15,
};
// Binary (IEC) prefixes: powers of 1024
const IEC_PREFIXES: Partial<Record<StorageUnit, number>> = {
kibibit: 1024,
kibibyte: 1024,
mebibit: 1024 ** 2,
mebibyte: 1024 ** 2,
gibibit: 1024 ** 3,
gibibyte: 1024 ** 3,
tebibit: 1024 ** 4,
tebibyte: 1024 ** 4,
pebibit: 1024 ** 5,
pebibyte: 1024 ** 5,
};
export function unitToBits(value: number, unit: StorageUnit): number {
let bits: number;
switch (unit) {
case 'bit':
bits = value;
break;
case 'byte':
bits = value * BITS_PER_BYTE;
break;
default: {
const factor = SI_PREFIXES[unit] ?? IEC_PREFIXES[unit] ?? 1;
bits = unit.endsWith('bit') ? value * factor : value * factor * BITS_PER_BYTE;
break;
}
}
return bits;
}
export function bitsToUnit(bits: number, unit: StorageUnit): number {
let value: number;
switch (unit) {
case 'bit':
value = bits;
break;
case 'byte':
value = bits / BITS_PER_BYTE;
break;
default: {
const factor = SI_PREFIXES[unit] ?? IEC_PREFIXES[unit] ?? 1;
value = unit.endsWith('bit') ? bits / factor : bits / factor / BITS_PER_BYTE;
break;
}
}
return value;
}
export function formatPrecision(value: number): string {
if (Number.isNaN(value) || !Number.isFinite(value)) {
return String(value);
}
if (value === 0) {
return '0';
}
const abs = Math.abs(value);
// Use full double precision (up to 12 decimal places), trimming trailing zeros
const digits = Math.max(0, 12 - Math.max(0, Math.floor(Math.log10(abs))) - 1);
return String(Number(value.toFixed(Math.min(digits, 12))));
}

View file

@ -0,0 +1,61 @@
<script setup lang="ts">
import _ from 'lodash';
import { unitToBits, bitsToUnit, formatPrecision, type StorageUnit } from './data-storage-converter.models';
const units = reactive<Record<StorageUnit, { title: string; ref: number }>>({
bit: { title: 'Bits', ref: 0 },
kilobit: { title: 'Kilobits', ref: 0 },
kilobyte: { title: 'Kilobytes', ref: 0 },
kibibit: { title: 'Kibibits', ref: 0 },
kibibyte: { title: 'Kibibytes', ref: 0 },
megabit: { title: 'Megabits', ref: 0 },
megabyte: { title: 'Megabytes', ref: 0 },
mebibit: { title: 'Mebibits', ref: 0 },
mebibyte: { title: 'Mebibytes', ref: 0 },
gigabit: { title: 'Gigabits', ref: 0 },
gigabyte: { title: 'Gigabytes', ref: 0 },
gibibit: { title: 'Gibibits', ref: 0 },
gibibyte: { title: 'Gibibytes', ref: 0 },
terabit: { title: 'Terabits', ref: 0 },
terabyte: { title: 'Terabytes', ref: 0 },
tebibit: { title: 'Tebibits', ref: 0 },
tebibyte: { title: 'Tebibytes', ref: 0 },
petabit: { title: 'Petabits', ref: 0 },
petabyte: { title: 'Petabytes', ref: 0 },
pebibit: { title: 'Pebibits', ref: 0 },
pebibyte: { title: 'Pebibytes', ref: 0 },
byte: { title: 'Bytes', ref: 0 },
});
function update(key: StorageUnit) {
const { ref: value } = units[key];
const bits = unitToBits(value ?? 0, key);
_.chain(units)
.omit(key)
.forEach((_, index) => {
units[index as StorageUnit].ref = bitsToUnit(bits, index as StorageUnit);
})
.value();
}
update('megabyte');
</script>
<template>
<div>
<n-input-group v-for="[key, { title }] in Object.entries(units) as [StorageUnit, { title: string }][]" :key="key" mb-3 w-full>
<n-input-group-label style="width: 120px">
{{ title }}
</n-input-group-label>
<n-input-number
v-model:value="units[key].ref"
style="flex: 1"
:placeholder="`Value in ${title.toLowerCase()}`"
@update:value="() => update(key)"
/>
</n-input-group>
</div>
</template>

View file

@ -0,0 +1,35 @@
import { Database } from '@vicons/tabler';
import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({
name: translate('tools.data-storage-converter.title'),
path: '/data-storage-converter',
description: translate('tools.data-storage-converter.description'),
keywords: [
'data',
'storage',
'converter',
'bytes',
'bits',
'kilobyte',
'megabyte',
'gigabyte',
'terabyte',
'petabyte',
'kibibyte',
'mebibyte',
'gibibyte',
'tebibyte',
'pebibyte',
'bandwidth',
'transfer',
'speed',
'bps',
'kbps',
'mbps',
'gbps',
],
component: () => import('./data-storage-converter.vue'),
icon: Database,
});

View file

@ -78,6 +78,7 @@ import { tool as romanNumeralConverter } from './roman-numeral-converter';
import { tool as sqlPrettify } from './sql-prettify';
import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator';
import { tool as temperatureConverter } from './temperature-converter';
import { tool as dataStorageConverter } from './data-storage-converter';
import { tool as textStatistics } from './text-statistics';
import { tool as tokenGenerator } from './token-generator';
import type { ToolCategory } from './tools.types';
@ -172,7 +173,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Measurement',
components: [chronometer, temperatureConverter, benchmarkBuilder],
components: [chronometer, temperatureConverter, benchmarkBuilder, dataStorageConverter],
},
{
name: 'Text',