Skip to content

Commit

Permalink
feat: add parse() and format() exports (#213)
Browse files Browse the repository at this point in the history
This PR exports `parse()` and `format()` functions which makes it much easier to work with arbitrary input from users.

For example, `onChange(e => ms.parse(e.target.value))` no longer needs to type cast since `parse()` accepts a string.

I also added `parseStrict()` for the case when you want the type safety and you're using a literal.
  • Loading branch information
styfle committed Mar 19, 2024
1 parent dfd1803 commit f150c1c
Show file tree
Hide file tree
Showing 4 changed files with 598 additions and 7 deletions.
174 changes: 174 additions & 0 deletions 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();
});
});
41 changes: 34 additions & 7 deletions src/index.ts
Expand Up @@ -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)}`
Expand All @@ -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 =
/^(?<value>-?(?:\d+)?\.?\d+) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*/
Expand Down

0 comments on commit f150c1c

Please sign in to comment.