diff --git a/src/format.test.ts b/src/format.test.ts new file mode 100644 index 0000000..ab83c1b --- /dev/null +++ b/src/format.test.ts @@ -0,0 +1,174 @@ +import { format } from './index'; + +// numbers + +describe('format(number, { long: true })', () => { + it('should not throw an error', () => { + expect(() => { + format(500, { long: true }); + }).not.toThrowError(); + }); + + it('should support milliseconds', () => { + expect(format(500, { long: true })).toBe('500 ms'); + + expect(format(-500, { long: true })).toBe('-500 ms'); + }); + + it('should support seconds', () => { + expect(format(1000, { long: true })).toBe('1 second'); + expect(format(1200, { long: true })).toBe('1 second'); + expect(format(10000, { long: true })).toBe('10 seconds'); + + expect(format(-1000, { long: true })).toBe('-1 second'); + expect(format(-1200, { long: true })).toBe('-1 second'); + expect(format(-10000, { long: true })).toBe('-10 seconds'); + }); + + it('should support minutes', () => { + expect(format(60 * 1000, { long: true })).toBe('1 minute'); + expect(format(60 * 1200, { long: true })).toBe('1 minute'); + expect(format(60 * 10000, { long: true })).toBe('10 minutes'); + + expect(format(-1 * 60 * 1000, { long: true })).toBe('-1 minute'); + expect(format(-1 * 60 * 1200, { long: true })).toBe('-1 minute'); + expect(format(-1 * 60 * 10000, { long: true })).toBe('-10 minutes'); + }); + + it('should support hours', () => { + expect(format(60 * 60 * 1000, { long: true })).toBe('1 hour'); + expect(format(60 * 60 * 1200, { long: true })).toBe('1 hour'); + expect(format(60 * 60 * 10000, { long: true })).toBe('10 hours'); + + expect(format(-1 * 60 * 60 * 1000, { long: true })).toBe('-1 hour'); + expect(format(-1 * 60 * 60 * 1200, { long: true })).toBe('-1 hour'); + expect(format(-1 * 60 * 60 * 10000, { long: true })).toBe('-10 hours'); + }); + + it('should support days', () => { + expect(format(24 * 60 * 60 * 1000, { long: true })).toBe('1 day'); + expect(format(24 * 60 * 60 * 1200, { long: true })).toBe('1 day'); + expect(format(24 * 60 * 60 * 10000, { long: true })).toBe('10 days'); + + expect(format(-1 * 24 * 60 * 60 * 1000, { long: true })).toBe('-1 day'); + expect(format(-1 * 24 * 60 * 60 * 1200, { long: true })).toBe('-1 day'); + expect(format(-1 * 24 * 60 * 60 * 10000, { long: true })).toBe('-10 days'); + }); + + it('should round', () => { + expect(format(234234234, { long: true })).toBe('3 days'); + + expect(format(-234234234, { long: true })).toBe('-3 days'); + }); +}); + +// numbers + +describe('format(number)', () => { + it('should not throw an error', () => { + expect(() => { + format(500); + }).not.toThrowError(); + }); + + it('should support milliseconds', () => { + expect(format(500)).toBe('500ms'); + + expect(format(-500)).toBe('-500ms'); + }); + + it('should support seconds', () => { + expect(format(1000)).toBe('1s'); + expect(format(10000)).toBe('10s'); + + expect(format(-1000)).toBe('-1s'); + expect(format(-10000)).toBe('-10s'); + }); + + it('should support minutes', () => { + expect(format(60 * 1000)).toBe('1m'); + expect(format(60 * 10000)).toBe('10m'); + + expect(format(-1 * 60 * 1000)).toBe('-1m'); + expect(format(-1 * 60 * 10000)).toBe('-10m'); + }); + + it('should support hours', () => { + expect(format(60 * 60 * 1000)).toBe('1h'); + expect(format(60 * 60 * 10000)).toBe('10h'); + + expect(format(-1 * 60 * 60 * 1000)).toBe('-1h'); + expect(format(-1 * 60 * 60 * 10000)).toBe('-10h'); + }); + + it('should support days', () => { + expect(format(24 * 60 * 60 * 1000)).toBe('1d'); + expect(format(24 * 60 * 60 * 10000)).toBe('10d'); + + expect(format(-1 * 24 * 60 * 60 * 1000)).toBe('-1d'); + expect(format(-1 * 24 * 60 * 60 * 10000)).toBe('-10d'); + }); + + it('should round', () => { + expect(format(234234234)).toBe('3d'); + + expect(format(-234234234)).toBe('-3d'); + }); +}); + +// invalid inputs + +describe('format(invalid inputs)', () => { + it('should throw an error, when format("")', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + format(''); + }).toThrowError(); + }); + + it('should throw an error, when format(undefined)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + format(undefined); + }).toThrowError(); + }); + + it('should throw an error, when format(null)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + format(null); + }).toThrowError(); + }); + + it('should throw an error, when format([])', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + format([]); + }).toThrowError(); + }); + + it('should throw an error, when format({})', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + format({}); + }).toThrowError(); + }); + + it('should throw an error, when format(NaN)', () => { + expect(() => { + format(NaN); + }).toThrowError(); + }); + + it('should throw an error, when format(Infinity)', () => { + expect(() => { + format(Infinity); + }).toThrowError(); + }); + + it('should throw an error, when format(-Infinity)', () => { + expect(() => { + format(-Infinity); + }).toThrowError(); + }); +}); diff --git a/src/index.ts b/src/index.ts index bfaa84f..668644f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,12 +64,12 @@ function msFn(value: StringValue, options?: Options): number; function msFn(value: number, options?: Options): string; function msFn(value: StringValue | number, options?: Options): number | string { try { - if (typeof value === 'string' && value.length > 0) { + if (typeof value === 'string') { return parse(value); - } else if (typeof value === 'number' && isFinite(value)) { - return options?.long ? fmtLong(value) : fmtShort(value); + } else if (typeof value === 'number') { + return format(value, options); } - throw new Error('Value is not a string or number.'); + throw new Error('Value provided to ms() must be a string or number.'); } catch (error) { const message = isError(error) ? `${error.message}. value=${JSON.stringify(value)}` @@ -85,9 +85,11 @@ function msFn(value: StringValue | number, options?: Options): number | string { * @returns The parsed value in milliseconds, or `NaN` if the string can't be * parsed */ -function parse(str: string): number { - if (str.length > 100) { - throw new Error('Value exceeds the maximum length of 100 characters.'); +export function parse(str: string): number { + if (typeof str !== 'string' || str.length === 0 || str.length > 100) { + throw new Error( + 'Value provided to ms.parse() must be a string with length between 1 and 99.', + ); } const match = /^(?-?(?:\d+)?\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( @@ -148,6 +150,17 @@ function parse(str: string): number { } } +/** + * Parse the given StringValue and return milliseconds. + * + * @param value - A typesafe StringValue to parse to milliseconds + * @returns The parsed value in milliseconds, or `NaN` if the string can't be + * parsed + */ +export function parseStrict(value: StringValue): number { + return parse(value); +} + // eslint-disable-next-line import/no-default-export export default msFn; @@ -191,6 +204,20 @@ function fmtLong(ms: number): StringValue { return `${ms} ms`; } +/** + * Format the given integer as a string. + * + * @param ms - milliseconds + * @param options - Options for the conversion + * @returns The formatted string + */ +export function format(ms: number, options?: Options): string { + if (typeof ms !== 'number' || !isFinite(ms)) { + throw new Error('Value provided to ms.format() must be of type number.'); + } + return options?.long ? fmtLong(ms) : fmtShort(ms); +} + /** * Pluralization helper. */ diff --git a/src/parse-strict.test.ts b/src/parse-strict.test.ts new file mode 100644 index 0000000..5707b87 --- /dev/null +++ b/src/parse-strict.test.ts @@ -0,0 +1,197 @@ +import { parseStrict } from './index'; + +describe('parseStrict(string)', () => { + it('should not throw an error', () => { + expect(() => { + parseStrict('1m'); + }).not.toThrowError(); + }); + + it('should preserve ms', () => { + expect(parseStrict('100')).toBe(100); + }); + + it('should convert from m to ms', () => { + expect(parseStrict('1m')).toBe(60000); + }); + + it('should convert from h to ms', () => { + expect(parseStrict('1h')).toBe(3600000); + }); + + it('should convert d to ms', () => { + expect(parseStrict('2d')).toBe(172800000); + }); + + it('should convert w to ms', () => { + expect(parseStrict('3w')).toBe(1814400000); + }); + + it('should convert s to ms', () => { + expect(parseStrict('1s')).toBe(1000); + }); + + it('should convert ms to ms', () => { + expect(parseStrict('100ms')).toBe(100); + }); + + it('should convert y to ms', () => { + expect(parseStrict('1y')).toBe(31557600000); + }); + + it('should work with ms', () => { + expect(parseStrict('1.5h')).toBe(5400000); + }); + + it('should work with multiple spaces', () => { + expect(parseStrict('1 s')).toBe(1000); + }); + + it('should return NaN if invalid', () => { + // @ts-expect-error - We expect this to fail. + expect(isNaN(parseStrict('☃'))).toBe(true); + // @ts-expect-error - We expect this to fail. + expect(isNaN(parseStrict('10-.5'))).toBe(true); + // @ts-expect-error - We expect this to fail. + expect(isNaN(parseStrict('foo'))).toBe(true); + }); + + it('should be case-insensitive', () => { + expect(parseStrict('1.5H')).toBe(5400000); + }); + + it('should work with numbers starting with .', () => { + expect(parseStrict('.5ms')).toBe(0.5); + }); + + it('should work with negative integers', () => { + expect(parseStrict('-100ms')).toBe(-100); + }); + + it('should work with negative decimals', () => { + expect(parseStrict('-1.5h')).toBe(-5400000); + expect(parseStrict('-10.5h')).toBe(-37800000); + }); + + it('should work with negative decimals starting with "."', () => { + expect(parseStrict('-.5h')).toBe(-1800000); + }); +}); + +// long strings + +describe('parseStrict(long string)', () => { + it('should not throw an error', () => { + expect(() => { + parseStrict('53 milliseconds'); + }).not.toThrowError(); + }); + + it('should convert milliseconds to ms', () => { + expect(parseStrict('53 milliseconds')).toBe(53); + }); + + it('should convert msecs to ms', () => { + expect(parseStrict('17 msecs')).toBe(17); + }); + + it('should convert sec to ms', () => { + expect(parseStrict('1 sec')).toBe(1000); + }); + + it('should convert from min to ms', () => { + expect(parseStrict('1 min')).toBe(60000); + }); + + it('should convert from hr to ms', () => { + expect(parseStrict('1 hr')).toBe(3600000); + }); + + it('should convert days to ms', () => { + expect(parseStrict('2 days')).toBe(172800000); + }); + + it('should convert weeks to ms', () => { + expect(parseStrict('1 week')).toBe(604800000); + }); + + it('should convert years to ms', () => { + expect(parseStrict('1 year')).toBe(31557600000); + }); + + it('should work with decimals', () => { + expect(parseStrict('1.5 hours')).toBe(5400000); + }); + + it('should work with negative integers', () => { + expect(parseStrict('-100 milliseconds')).toBe(-100); + }); + + it('should work with negative decimals', () => { + expect(parseStrict('-1.5 hours')).toBe(-5400000); + }); + + it('should work with negative decimals starting with "."', () => { + expect(parseStrict('-.5 hr')).toBe(-1800000); + }); +}); + +// invalid inputs + +describe('parseStrict(invalid inputs)', () => { + it('should throw an error, when parseStrict("")', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(''); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict(undefined)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(undefined); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict(null)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(null); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict([])', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict([]); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict({})', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict({}); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict(NaN)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(NaN); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict(Infinity)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(Infinity); + }).toThrowError(); + }); + + it('should throw an error, when parseStrict(-Infinity)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parseStrict(-Infinity); + }).toThrowError(); + }); +}); diff --git a/src/parse.test.ts b/src/parse.test.ts new file mode 100644 index 0000000..25dea3d --- /dev/null +++ b/src/parse.test.ts @@ -0,0 +1,193 @@ +import { parse } from './index'; + +describe('parse(string)', () => { + it('should not throw an error', () => { + expect(() => { + parse('1m'); + }).not.toThrowError(); + }); + + it('should preserve ms', () => { + expect(parse('100')).toBe(100); + }); + + it('should convert from m to ms', () => { + expect(parse('1m')).toBe(60000); + }); + + it('should convert from h to ms', () => { + expect(parse('1h')).toBe(3600000); + }); + + it('should convert d to ms', () => { + expect(parse('2d')).toBe(172800000); + }); + + it('should convert w to ms', () => { + expect(parse('3w')).toBe(1814400000); + }); + + it('should convert s to ms', () => { + expect(parse('1s')).toBe(1000); + }); + + it('should convert ms to ms', () => { + expect(parse('100ms')).toBe(100); + }); + + it('should convert y to ms', () => { + expect(parse('1y')).toBe(31557600000); + }); + + it('should work with ms', () => { + expect(parse('1.5h')).toBe(5400000); + }); + + it('should work with multiple spaces', () => { + expect(parse('1 s')).toBe(1000); + }); + + it('should return NaN if invalid', () => { + expect(isNaN(parse('☃'))).toBe(true); + expect(isNaN(parse('10-.5'))).toBe(true); + expect(isNaN(parse('foo'))).toBe(true); + }); + + it('should be case-insensitive', () => { + expect(parse('1.5H')).toBe(5400000); + }); + + it('should work with numbers starting with .', () => { + expect(parse('.5ms')).toBe(0.5); + }); + + it('should work with negative integers', () => { + expect(parse('-100ms')).toBe(-100); + }); + + it('should work with negative decimals', () => { + expect(parse('-1.5h')).toBe(-5400000); + expect(parse('-10.5h')).toBe(-37800000); + }); + + it('should work with negative decimals starting with "."', () => { + expect(parse('-.5h')).toBe(-1800000); + }); +}); + +// long strings + +describe('parse(long string)', () => { + it('should not throw an error', () => { + expect(() => { + parse('53 milliseconds'); + }).not.toThrowError(); + }); + + it('should convert milliseconds to ms', () => { + expect(parse('53 milliseconds')).toBe(53); + }); + + it('should convert msecs to ms', () => { + expect(parse('17 msecs')).toBe(17); + }); + + it('should convert sec to ms', () => { + expect(parse('1 sec')).toBe(1000); + }); + + it('should convert from min to ms', () => { + expect(parse('1 min')).toBe(60000); + }); + + it('should convert from hr to ms', () => { + expect(parse('1 hr')).toBe(3600000); + }); + + it('should convert days to ms', () => { + expect(parse('2 days')).toBe(172800000); + }); + + it('should convert weeks to ms', () => { + expect(parse('1 week')).toBe(604800000); + }); + + it('should convert years to ms', () => { + expect(parse('1 year')).toBe(31557600000); + }); + + it('should work with decimals', () => { + expect(parse('1.5 hours')).toBe(5400000); + }); + + it('should work with negative integers', () => { + expect(parse('-100 milliseconds')).toBe(-100); + }); + + it('should work with negative decimals', () => { + expect(parse('-1.5 hours')).toBe(-5400000); + }); + + it('should work with negative decimals starting with "."', () => { + expect(parse('-.5 hr')).toBe(-1800000); + }); +}); + +// invalid inputs + +describe('parse(invalid inputs)', () => { + it('should throw an error, when parse("")', () => { + expect(() => { + parse(''); + }).toThrowError(); + }); + + it('should throw an error, when parse(undefined)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse(undefined); + }).toThrowError(); + }); + + it('should throw an error, when parse(null)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse(null); + }).toThrowError(); + }); + + it('should throw an error, when parse([])', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse([]); + }).toThrowError(); + }); + + it('should throw an error, when parse({})', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse({}); + }).toThrowError(); + }); + + it('should throw an error, when parse(NaN)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse(NaN); + }).toThrowError(); + }); + + it('should throw an error, when parse(Infinity)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse(Infinity); + }).toThrowError(); + }); + + it('should throw an error, when parse(-Infinity)', () => { + expect(() => { + // @ts-expect-error - We expect this to throw. + parse(-Infinity); + }).toThrowError(); + }); +});