diff --git a/__tests__/humanFileSize.spec.ts b/__tests__/humanFileSize.spec.ts index ae15da6b..c33f8fc7 100644 --- a/__tests__/humanFileSize.spec.ts +++ b/__tests__/humanFileSize.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { formatFileSize } from '../lib/humanfilesize' +import { formatFileSize, parseFileSize } from '../lib/humanfilesize' describe('humanFileSize', () => { describe('formatFileSize', () => { @@ -101,3 +101,97 @@ describe('humanFileSize', () => { }) }) }) + +describe('parseFileSize', () => { + it('should return null on error', () => { + const invalid = [ + '', + 'a', + '.', + 'kb', + '1.1.2', + '10ob', + '1z', + ] + + for (const line of invalid) { + expect(parseFileSize(line)).toBeNull() + } + }) + + it('can parse base 2', () => { + const values = { + '2kib': 2048, + '2mib': 2 * (1024 ** 2), + '2gib': 2 * (1024 ** 3), + '2tib': 2 * (1024 ** 4), + '2pib': 2 * (1024 ** 5), + } + + for (const [text, value] of Object.entries(values)) { + expect(parseFileSize(text)).toBe(value) + } + }) + + it('can parse base 10', () => { + const values = { + '2kb': 2000, + '2mb': 2 * (1000 ** 2), + '2gb': 2 * (1000 ** 3), + '2tb': 2 * (1000 ** 4), + '2pb': 2 * (1000 ** 5), + } + + for (const [text, value] of Object.entries(values)) { + expect(parseFileSize(text)).toBe(value) + } + }) + + it('parses missing binary prefixes if force is set true', () => { + const values = { + '2kb': 2048, + '2mb': 2 * (1024 ** 2), + '2gb': 2 * (1024 ** 3), + '2tb': 2 * (1024 ** 4), + '2pb': 2 * (1024 ** 5), + } + + for (const [text, value] of Object.entries(values)) { + expect(parseFileSize(text, true)).toBe(value) + } + }) + + it('can parse with spaces', () => { + const values = { + '2kb': 2000, + '2 kb': 2000, + ' 2kb': 2000, + '2kb ': 2000, + ' 2 k b ': 2000, + '2kib': 2048, + '2 kIb': 2048, + ' 2kib': 2048, + '2Kib ': 2048, + ' 2 k i b ': 2048, + } + + for (const [text, value] of Object.entries(values)) { + expect(parseFileSize(text)).toBe(value) + } + }) + + it('can parse decimals', () => { + const values = { + '2kb': 2000, + '2.2kb': 2200, + '2.kb': 2000, + '.2kb ': 200, + ',2kb': 200, + '2,2kb': 2200, + } + + for (const [text, value] of Object.entries(values)) { + expect(parseFileSize(text)).toBe(value) + } + }) +}) diff --git a/lib/humanfilesize.ts b/lib/humanfilesize.ts index 37b793c9..bf25cf6d 100644 --- a/lib/humanfilesize.ts +++ b/lib/humanfilesize.ts @@ -64,3 +64,40 @@ export function formatFileSize(size: number|string, skipSmallSizes = false, bina return relativeSize + ' ' + readableFormat } + +/** + * Returns a file size in bytes from a humanly readable string + * Note: `b` and `B` are both parsed as bytes and not as bit or byte. + * + * @param {string} value file size in human-readable format + * @param {boolean} forceBinary for backwards compatibility this allows values to be base 2 (so 2KB means 2048 bytes instead of 2000 bytes) + * @return {number} or null if string could not be parsed + */ +export function parseFileSize(value: string, forceBinary = false) { + try { + value = `${value}`.toLocaleLowerCase().replaceAll(/\s+/g, '').replaceAll(',', '.') + } catch (e) { + return null + } + + const match = value.match(/^([0-9]*(\.[0-9]*)?)([kmgtp]?)(i?)b?$/) + // ignore not found, missing pre- and decimal, empty number + if (match === null || match[1] === '.' || match[1] === '') { + return null + } + + const bytesArray = { + '': 0, + k: 1, + m: 2, + g: 3, + t: 4, + p: 5, + e: 6, + } + + const decimalString = `${match[1]}` + const base = (match[4] === 'i' || forceBinary) ? 1024 : 1000 + + return Math.round(Number.parseFloat(decimalString) * (base ** bytesArray[match[3]])) +}