diff --git a/locales/en.yml b/locales/en.yml index d03d80d3..17011364 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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. diff --git a/src/tools/data-storage-converter/data-storage-converter.models.test.ts b/src/tools/data-storage-converter/data-storage-converter.models.test.ts new file mode 100644 index 00000000..a16d7510 --- /dev/null +++ b/src/tools/data-storage-converter/data-storage-converter.models.test.ts @@ -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'); + }); +}); diff --git a/src/tools/data-storage-converter/data-storage-converter.models.ts b/src/tools/data-storage-converter/data-storage-converter.models.ts new file mode 100644 index 00000000..87e132fe --- /dev/null +++ b/src/tools/data-storage-converter/data-storage-converter.models.ts @@ -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> = { + 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> = { + 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)))); +} diff --git a/src/tools/data-storage-converter/data-storage-converter.vue b/src/tools/data-storage-converter/data-storage-converter.vue new file mode 100644 index 00000000..f9d94892 --- /dev/null +++ b/src/tools/data-storage-converter/data-storage-converter.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/tools/data-storage-converter/index.ts b/src/tools/data-storage-converter/index.ts new file mode 100644 index 00000000..df5708af --- /dev/null +++ b/src/tools/data-storage-converter/index.ts @@ -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, +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..afc5290d 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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',