├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ └── quality.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── license.md ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── scripts └── build.js ├── src ├── format.test.ts ├── index.test.ts ├── index.ts ├── parse-strict.test.ts └── parse.test.ts └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@vercel/style-guide/eslint/node'), 5 | require.resolve('@vercel/style-guide/eslint/typescript'), 6 | ], 7 | parserOptions: { 8 | project: 'tsconfig.json', 9 | }, 10 | ignorePatterns: ['dist/**'], 11 | overrides: [ 12 | { 13 | files: ['**/*.test.ts'], 14 | extends: [require.resolve('@vercel/style-guide/eslint/jest')], 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | name: 'Test' 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x] 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2.2.2 22 | with: 23 | version: 7 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Run tests 35 | run: pnpm test 36 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | prettier: 11 | name: 'Prettier' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup pnpm 18 | uses: pnpm/action-setup@v2.2.2 19 | with: 20 | version: 7 21 | 22 | - name: Use Node.js 16 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: '16' 26 | cache: 'pnpm' 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Run Prettier check 32 | run: pnpm run prettier-check 33 | 34 | eslint: 35 | name: 'ESLint' 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v2 40 | 41 | - name: Setup pnpm 42 | uses: pnpm/action-setup@v2.2.2 43 | with: 44 | version: 7 45 | 46 | - name: Use Node.js 16 47 | uses: actions/setup-node@v2 48 | with: 49 | node-version: '16' 50 | cache: 'pnpm' 51 | 52 | - name: Install dependencies 53 | run: pnpm install 54 | 55 | - name: Run ESLint check 56 | run: pnpm run eslint-check 57 | 58 | types: 59 | name: 'TypeScript' 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v2 64 | 65 | - name: Setup pnpm 66 | uses: pnpm/action-setup@v2.2.2 67 | with: 68 | version: 7 69 | 70 | - name: Use Node.js 16 71 | uses: actions/setup-node@v2 72 | with: 73 | node-version: '16' 74 | cache: 'pnpm' 75 | 76 | - name: Install dependencies 77 | run: pnpm install 78 | 79 | - name: Run TypeScript type check 80 | run: pnpm run type-check 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms", 3 | "version": "3.0.0-canary.1", 4 | "description": "Tiny millisecond conversion utility", 5 | "repository": "vercel/ms", 6 | "main": "./dist/index.cjs", 7 | "type": "module", 8 | "exports": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "module": "./dist/index.mjs", 14 | "types": "./dist/index.d.ts", 15 | "sideEffects": false, 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">=12.13" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "test": "npm run test-nodejs && npm run test-edge", 25 | "test-nodejs": "jest --env node", 26 | "test-edge": "jest --env @edge-runtime/jest-environment", 27 | "build": "scripts/build.js", 28 | "prepublishOnly": "npm run build", 29 | "eslint-check": "eslint --max-warnings=0 .", 30 | "prettier-check": "prettier --check .", 31 | "type-check": "tsc --noEmit", 32 | "precommit": "lint-staged", 33 | "prepare": "husky install" 34 | }, 35 | "jest": { 36 | "preset": "ts-jest", 37 | "testEnvironment": "node" 38 | }, 39 | "prettier": "@vercel/style-guide/prettier", 40 | "lint-staged": { 41 | "*": [ 42 | "prettier --ignore-unknown --write" 43 | ], 44 | "*.{js,jsx,ts,tsx}": [ 45 | "eslint --max-warnings=0 --fix" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@edge-runtime/jest-environment": "1.1.0-beta.6", 50 | "@types/jest": "27.0.1", 51 | "@vercel/style-guide": "3.0.0", 52 | "eslint": "8.12.0", 53 | "husky": "7.0.2", 54 | "jest": "27.1.1", 55 | "lint-staged": "11.1.2", 56 | "prettier": "2.6.2", 57 | "ts-jest": "27.0.5", 58 | "typescript": "4.6.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ms 2 | 3 | ![CI](https://github.com/vercel/ms/workflows/CI/badge.svg) 4 | ![Edge Runtime Compatible](https://img.shields.io/badge/edge--runtime-%E2%9C%94%20compatible-black) 5 | 6 | Use this package to easily convert various time formats to milliseconds. 7 | 8 | ## Examples 9 | 10 | 11 | ```js 12 | ms('2 days') // 172800000 13 | ms('1d') // 86400000 14 | ms('10h') // 36000000 15 | ms('2.5 hrs') // 9000000 16 | ms('2h') // 7200000 17 | ms('1m') // 60000 18 | ms('5s') // 5000 19 | ms('1y') // 31557600000 20 | ms('100') // 100 21 | ms('-3 days') // -259200000 22 | ms('-1h') // -3600000 23 | ms('-200') // -200 24 | ``` 25 | 26 | ### Convert from Milliseconds 27 | 28 | 29 | ```js 30 | ms(60000) // "1m" 31 | ms(2 * 60000) // "2m" 32 | ms(-3 * 60000) // "-3m" 33 | ms(ms('10 hours')) // "10h" 34 | ``` 35 | 36 | ### Time Format Written-Out 37 | 38 | 39 | ```js 40 | ms(60000, { long: true }) // "1 minute" 41 | ms(2 * 60000, { long: true }) // "2 minutes" 42 | ms(-3 * 60000, { long: true }) // "-3 minutes" 43 | ms(ms('10 hours'), { long: true }) // "10 hours" 44 | ``` 45 | 46 | ## Features 47 | 48 | - Works both in [Node.js](https://nodejs.org) and in the browser 49 | - If a number is supplied to `ms`, a string with a unit is returned 50 | - If a string that contains the number is supplied, it returns it as a number (e.g.: it returns `100` for `'100'`) 51 | - If you pass a string with a number and a valid unit, the number of equivalent milliseconds is returned 52 | 53 | ## TypeScript support 54 | 55 | As of `v3.0`, this package includes TypeScript definitions. 56 | 57 | For added safety, we're using [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) (added in [TypeScript 4.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html)). This ensures that you don't accidentally pass `ms` values that it can't process. 58 | 59 | This won't require you to do anything special in most situations, but you can also import the `StringValue` type from `ms` if you need to use it. 60 | 61 | ```ts 62 | import ms, { StringValue } from 'ms'; 63 | 64 | // Using the exported type. 65 | function example(value: StringValue) { 66 | ms(value); 67 | } 68 | 69 | // This function will only accept a string compatible with `ms`. 70 | example('1 h'); 71 | ``` 72 | 73 | In this example, we use a [Type Assertion](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) to coerce a `string`. 74 | 75 | ```ts 76 | import ms, { StringValue } from 'ms'; 77 | 78 | // Type assertion with the exported type. 79 | function example(value: string) { 80 | try { 81 | // A string could be "wider" than the values accepted by `ms`, so we assert 82 | // that our `value` is a `StringValue`. 83 | // 84 | // It's important to note that this can be dangerous (see below). 85 | ms(value as StringValue); 86 | } catch (error: Error) { 87 | // Handle any errors from invalid values. 88 | console.error(error); 89 | } 90 | } 91 | 92 | // This function will accept any string, which may result in a bug. 93 | example('any value'); 94 | ``` 95 | 96 | You may also create a custom Template Literal Type. 97 | 98 | ```ts 99 | import ms from 'ms'; 100 | 101 | type OnlyDaysAndWeeks = `${number} ${'days' | 'weeks'}`; 102 | 103 | // Using a custom Template Literal Type. 104 | function example(value: OnlyDaysAndWeeks) { 105 | // The type of `value` is narrower than the values `ms` accepts, which is 106 | // safe to use without coercion. 107 | ms(value); 108 | } 109 | 110 | // This function will accept "# days" or "# weeks" only. 111 | example('5.2 days'); 112 | ``` 113 | 114 | ## Advanced Usage 115 | 116 | As of `v3.0`, you can import `parse` and `format` separately. 117 | 118 | ```ts 119 | import { parse, format } from 'ms'; 120 | 121 | parse('1h'); // 3600000 122 | 123 | format(2000); // "2s" 124 | ``` 125 | 126 | If you want strict type checking for the input value, you can use `parseStrict`. 127 | 128 | ```ts 129 | import { parseStrict } from 'ms'; 130 | 131 | parseStrict('1h'); // 3600000 132 | 133 | function example(s: string) { 134 | return parseStrict(str); // tsc error 135 | } 136 | ``` 137 | 138 | ## Edge Runtime Support 139 | 140 | `ms` is compatible with the [Edge Runtime](https://edge-runtime.vercel.app/). It can be used inside environments like [Vercel Edge Functions](https://vercel.com/edge) as follows: 141 | 142 | ```js 143 | // Next.js (pages/api/edge.js) (npm i next@canary) 144 | // Other frameworks (api/edge.js) (npm i -g vercel@canary) 145 | 146 | import ms from 'ms'; 147 | const start = Date.now(); 148 | 149 | export default (req) => { 150 | return new Response(`Alive since ${ms(Date.now() - start)}`); 151 | }; 152 | 153 | export const config = { 154 | runtime: 'experimental-edge', 155 | }; 156 | ``` 157 | 158 | ## Related Packages 159 | 160 | - [ms.macro](https://github.com/knpwrs/ms.macro) - Run `ms` as a macro at build-time. 161 | 162 | ## Caught a Bug? 163 | 164 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 165 | 2. Link the package to the global module directory: `npm link` 166 | 3. Within the module you want to test your local development instance of ms, just link it to the dependencies: `npm link ms`. Instead of the default one from npm, Node.js will now use your clone of ms! 167 | 168 | As always, you can run the tests using: `npm test` 169 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | import { mkdirSync, readFileSync, rmdirSync } from 'fs'; 4 | import { writeFile } from 'fs/promises'; 5 | import { join, sep } from 'path'; 6 | import ts from 'typescript'; 7 | 8 | const DIR = './dist'; 9 | 10 | // Delete and recreate the output directory. 11 | try { 12 | rmdirSync(DIR, { recursive: true }); 13 | } catch (error) { 14 | if (error.code !== 'ENOENT') throw error; 15 | } 16 | mkdirSync(DIR); 17 | 18 | // Read the TypeScript config file. 19 | const { config } = ts.readConfigFile('tsconfig.json', (fileName) => 20 | readFileSync(fileName).toString(), 21 | ); 22 | 23 | const sourceFile = join('src', 'index.ts'); 24 | // Build CommonJS module. 25 | compile([sourceFile], { module: ts.ModuleKind.CommonJS }); 26 | // Build an ES2015 module and type declarations. 27 | compile([sourceFile], { 28 | module: ts.ModuleKind.ES2020, 29 | declaration: true, 30 | }); 31 | 32 | /** 33 | * Compiles files to JavaScript. 34 | * 35 | * @param {string[]} files 36 | * @param {ts.CompilerOptions} options 37 | */ 38 | function compile(files, options) { 39 | const compilerOptions = { ...config.compilerOptions, ...options }; 40 | const host = ts.createCompilerHost(compilerOptions); 41 | 42 | host.writeFile = (fileName, contents) => { 43 | const isDts = fileName.endsWith('.d.ts'); 44 | let path = join(DIR, fileName.split(sep)[1]); 45 | 46 | if (!isDts) { 47 | switch (compilerOptions.module) { 48 | case ts.ModuleKind.CommonJS: { 49 | // Adds backwards-compatibility for Node.js. 50 | // eslint-disable-next-line no-param-reassign 51 | contents += `module.exports = exports.default;\nmodule.exports.default = exports.default;\n`; 52 | // Use the .cjs file extension. 53 | path = path.replace(/\.js$/, '.cjs'); 54 | break; 55 | } 56 | case ts.ModuleKind.ES2020: { 57 | // Use the .mjs file extension. 58 | path = path.replace(/\.js$/, '.mjs'); 59 | break; 60 | } 61 | default: 62 | throw Error('Unhandled module type'); 63 | } 64 | } 65 | 66 | writeFile(path, contents) 67 | .then(() => { 68 | // eslint-disable-next-line no-console 69 | console.log('Built', path); 70 | }) 71 | .catch((error) => { 72 | // eslint-disable-next-line no-console 73 | console.error(error); 74 | }); 75 | }; 76 | 77 | const program = ts.createProgram(files, compilerOptions, host); 78 | 79 | program.emit(); 80 | } 81 | -------------------------------------------------------------------------------- /src/format.test.ts: -------------------------------------------------------------------------------- 1 | import { format } from './index'; 2 | 3 | // numbers 4 | 5 | describe('format(number, { long: true })', () => { 6 | it('should not throw an error', () => { 7 | expect(() => { 8 | format(500, { long: true }); 9 | }).not.toThrowError(); 10 | }); 11 | 12 | it('should support milliseconds', () => { 13 | expect(format(500, { long: true })).toBe('500 ms'); 14 | 15 | expect(format(-500, { long: true })).toBe('-500 ms'); 16 | }); 17 | 18 | it('should support seconds', () => { 19 | expect(format(1000, { long: true })).toBe('1 second'); 20 | expect(format(1200, { long: true })).toBe('1 second'); 21 | expect(format(10000, { long: true })).toBe('10 seconds'); 22 | 23 | expect(format(-1000, { long: true })).toBe('-1 second'); 24 | expect(format(-1200, { long: true })).toBe('-1 second'); 25 | expect(format(-10000, { long: true })).toBe('-10 seconds'); 26 | }); 27 | 28 | it('should support minutes', () => { 29 | expect(format(60 * 1000, { long: true })).toBe('1 minute'); 30 | expect(format(60 * 1200, { long: true })).toBe('1 minute'); 31 | expect(format(60 * 10000, { long: true })).toBe('10 minutes'); 32 | 33 | expect(format(-1 * 60 * 1000, { long: true })).toBe('-1 minute'); 34 | expect(format(-1 * 60 * 1200, { long: true })).toBe('-1 minute'); 35 | expect(format(-1 * 60 * 10000, { long: true })).toBe('-10 minutes'); 36 | }); 37 | 38 | it('should support hours', () => { 39 | expect(format(60 * 60 * 1000, { long: true })).toBe('1 hour'); 40 | expect(format(60 * 60 * 1200, { long: true })).toBe('1 hour'); 41 | expect(format(60 * 60 * 10000, { long: true })).toBe('10 hours'); 42 | 43 | expect(format(-1 * 60 * 60 * 1000, { long: true })).toBe('-1 hour'); 44 | expect(format(-1 * 60 * 60 * 1200, { long: true })).toBe('-1 hour'); 45 | expect(format(-1 * 60 * 60 * 10000, { long: true })).toBe('-10 hours'); 46 | }); 47 | 48 | it('should support days', () => { 49 | expect(format(24 * 60 * 60 * 1000, { long: true })).toBe('1 day'); 50 | expect(format(24 * 60 * 60 * 1200, { long: true })).toBe('1 day'); 51 | expect(format(24 * 60 * 60 * 10000, { long: true })).toBe('10 days'); 52 | 53 | expect(format(-1 * 24 * 60 * 60 * 1000, { long: true })).toBe('-1 day'); 54 | expect(format(-1 * 24 * 60 * 60 * 1200, { long: true })).toBe('-1 day'); 55 | expect(format(-1 * 24 * 60 * 60 * 10000, { long: true })).toBe('-10 days'); 56 | }); 57 | 58 | it('should round', () => { 59 | expect(format(234234234, { long: true })).toBe('3 days'); 60 | 61 | expect(format(-234234234, { long: true })).toBe('-3 days'); 62 | }); 63 | }); 64 | 65 | // numbers 66 | 67 | describe('format(number)', () => { 68 | it('should not throw an error', () => { 69 | expect(() => { 70 | format(500); 71 | }).not.toThrowError(); 72 | }); 73 | 74 | it('should support milliseconds', () => { 75 | expect(format(500)).toBe('500ms'); 76 | 77 | expect(format(-500)).toBe('-500ms'); 78 | }); 79 | 80 | it('should support seconds', () => { 81 | expect(format(1000)).toBe('1s'); 82 | expect(format(10000)).toBe('10s'); 83 | 84 | expect(format(-1000)).toBe('-1s'); 85 | expect(format(-10000)).toBe('-10s'); 86 | }); 87 | 88 | it('should support minutes', () => { 89 | expect(format(60 * 1000)).toBe('1m'); 90 | expect(format(60 * 10000)).toBe('10m'); 91 | 92 | expect(format(-1 * 60 * 1000)).toBe('-1m'); 93 | expect(format(-1 * 60 * 10000)).toBe('-10m'); 94 | }); 95 | 96 | it('should support hours', () => { 97 | expect(format(60 * 60 * 1000)).toBe('1h'); 98 | expect(format(60 * 60 * 10000)).toBe('10h'); 99 | 100 | expect(format(-1 * 60 * 60 * 1000)).toBe('-1h'); 101 | expect(format(-1 * 60 * 60 * 10000)).toBe('-10h'); 102 | }); 103 | 104 | it('should support days', () => { 105 | expect(format(24 * 60 * 60 * 1000)).toBe('1d'); 106 | expect(format(24 * 60 * 60 * 10000)).toBe('10d'); 107 | 108 | expect(format(-1 * 24 * 60 * 60 * 1000)).toBe('-1d'); 109 | expect(format(-1 * 24 * 60 * 60 * 10000)).toBe('-10d'); 110 | }); 111 | 112 | it('should round', () => { 113 | expect(format(234234234)).toBe('3d'); 114 | 115 | expect(format(-234234234)).toBe('-3d'); 116 | }); 117 | }); 118 | 119 | // invalid inputs 120 | 121 | describe('format(invalid inputs)', () => { 122 | it('should throw an error, when format("")', () => { 123 | expect(() => { 124 | // @ts-expect-error - We expect this to throw. 125 | format(''); 126 | }).toThrowError(); 127 | }); 128 | 129 | it('should throw an error, when format(undefined)', () => { 130 | expect(() => { 131 | // @ts-expect-error - We expect this to throw. 132 | format(undefined); 133 | }).toThrowError(); 134 | }); 135 | 136 | it('should throw an error, when format(null)', () => { 137 | expect(() => { 138 | // @ts-expect-error - We expect this to throw. 139 | format(null); 140 | }).toThrowError(); 141 | }); 142 | 143 | it('should throw an error, when format([])', () => { 144 | expect(() => { 145 | // @ts-expect-error - We expect this to throw. 146 | format([]); 147 | }).toThrowError(); 148 | }); 149 | 150 | it('should throw an error, when format({})', () => { 151 | expect(() => { 152 | // @ts-expect-error - We expect this to throw. 153 | format({}); 154 | }).toThrowError(); 155 | }); 156 | 157 | it('should throw an error, when format(NaN)', () => { 158 | expect(() => { 159 | format(NaN); 160 | }).toThrowError(); 161 | }); 162 | 163 | it('should throw an error, when format(Infinity)', () => { 164 | expect(() => { 165 | format(Infinity); 166 | }).toThrowError(); 167 | }); 168 | 169 | it('should throw an error, when format(-Infinity)', () => { 170 | expect(() => { 171 | format(-Infinity); 172 | }).toThrowError(); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import ms from './index'; 2 | 3 | describe('ms(string)', () => { 4 | it('should not throw an error', () => { 5 | expect(() => { 6 | ms('1m'); 7 | }).not.toThrowError(); 8 | }); 9 | 10 | it('should preserve ms', () => { 11 | expect(ms('100')).toBe(100); 12 | }); 13 | 14 | it('should convert from m to ms', () => { 15 | expect(ms('1m')).toBe(60000); 16 | }); 17 | 18 | it('should convert from h to ms', () => { 19 | expect(ms('1h')).toBe(3600000); 20 | }); 21 | 22 | it('should convert d to ms', () => { 23 | expect(ms('2d')).toBe(172800000); 24 | }); 25 | 26 | it('should convert w to ms', () => { 27 | expect(ms('3w')).toBe(1814400000); 28 | }); 29 | 30 | it('should convert s to ms', () => { 31 | expect(ms('1s')).toBe(1000); 32 | }); 33 | 34 | it('should convert ms to ms', () => { 35 | expect(ms('100ms')).toBe(100); 36 | }); 37 | 38 | it('should convert y to ms', () => { 39 | expect(ms('1y')).toBe(31557600000); 40 | }); 41 | 42 | it('should work with decimals', () => { 43 | expect(ms('1.5h')).toBe(5400000); 44 | }); 45 | 46 | it('should work with multiple spaces', () => { 47 | expect(ms('1 s')).toBe(1000); 48 | }); 49 | 50 | it('should return NaN if invalid', () => { 51 | // @ts-expect-error - We expect this to fail. 52 | expect(isNaN(ms('☃'))).toBe(true); 53 | // @ts-expect-error - We expect this to fail. 54 | expect(isNaN(ms('10-.5'))).toBe(true); 55 | // @ts-expect-error - We expect this to fail. 56 | expect(isNaN(ms('ms'))).toBe(true); 57 | }); 58 | 59 | it('should be case-insensitive', () => { 60 | expect(ms('1.5H')).toBe(5400000); 61 | }); 62 | 63 | it('should work with numbers starting with .', () => { 64 | expect(ms('.5ms')).toBe(0.5); 65 | }); 66 | 67 | it('should work with negative integers', () => { 68 | expect(ms('-100ms')).toBe(-100); 69 | }); 70 | 71 | it('should work with negative decimals', () => { 72 | expect(ms('-1.5h')).toBe(-5400000); 73 | expect(ms('-10.5h')).toBe(-37800000); 74 | }); 75 | 76 | it('should work with negative decimals starting with "."', () => { 77 | expect(ms('-.5h')).toBe(-1800000); 78 | }); 79 | }); 80 | 81 | // long strings 82 | 83 | describe('ms(long string)', () => { 84 | it('should not throw an error', () => { 85 | expect(() => { 86 | ms('53 milliseconds'); 87 | }).not.toThrowError(); 88 | }); 89 | 90 | it('should convert milliseconds to ms', () => { 91 | expect(ms('53 milliseconds')).toBe(53); 92 | }); 93 | 94 | it('should convert msecs to ms', () => { 95 | expect(ms('17 msecs')).toBe(17); 96 | }); 97 | 98 | it('should convert sec to ms', () => { 99 | expect(ms('1 sec')).toBe(1000); 100 | }); 101 | 102 | it('should convert from min to ms', () => { 103 | expect(ms('1 min')).toBe(60000); 104 | }); 105 | 106 | it('should convert from hr to ms', () => { 107 | expect(ms('1 hr')).toBe(3600000); 108 | }); 109 | 110 | it('should convert days to ms', () => { 111 | expect(ms('2 days')).toBe(172800000); 112 | }); 113 | 114 | it('should convert weeks to ms', () => { 115 | expect(ms('1 week')).toBe(604800000); 116 | }); 117 | 118 | it('should convert years to ms', () => { 119 | expect(ms('1 year')).toBe(31557600000); 120 | }); 121 | 122 | it('should work with decimals', () => { 123 | expect(ms('1.5 hours')).toBe(5400000); 124 | }); 125 | 126 | it('should work with negative integers', () => { 127 | expect(ms('-100 milliseconds')).toBe(-100); 128 | }); 129 | 130 | it('should work with negative decimals', () => { 131 | expect(ms('-1.5 hours')).toBe(-5400000); 132 | }); 133 | 134 | it('should work with negative decimals starting with "."', () => { 135 | expect(ms('-.5 hr')).toBe(-1800000); 136 | }); 137 | }); 138 | 139 | // numbers 140 | 141 | describe('ms(number, { long: true })', () => { 142 | it('should not throw an error', () => { 143 | expect(() => { 144 | ms(500, { long: true }); 145 | }).not.toThrowError(); 146 | }); 147 | 148 | it('should support milliseconds', () => { 149 | expect(ms(500, { long: true })).toBe('500 ms'); 150 | 151 | expect(ms(-500, { long: true })).toBe('-500 ms'); 152 | }); 153 | 154 | it('should support seconds', () => { 155 | expect(ms(1000, { long: true })).toBe('1 second'); 156 | expect(ms(1200, { long: true })).toBe('1 second'); 157 | expect(ms(10000, { long: true })).toBe('10 seconds'); 158 | 159 | expect(ms(-1000, { long: true })).toBe('-1 second'); 160 | expect(ms(-1200, { long: true })).toBe('-1 second'); 161 | expect(ms(-10000, { long: true })).toBe('-10 seconds'); 162 | }); 163 | 164 | it('should support minutes', () => { 165 | expect(ms(60 * 1000, { long: true })).toBe('1 minute'); 166 | expect(ms(60 * 1200, { long: true })).toBe('1 minute'); 167 | expect(ms(60 * 10000, { long: true })).toBe('10 minutes'); 168 | 169 | expect(ms(-1 * 60 * 1000, { long: true })).toBe('-1 minute'); 170 | expect(ms(-1 * 60 * 1200, { long: true })).toBe('-1 minute'); 171 | expect(ms(-1 * 60 * 10000, { long: true })).toBe('-10 minutes'); 172 | }); 173 | 174 | it('should support hours', () => { 175 | expect(ms(60 * 60 * 1000, { long: true })).toBe('1 hour'); 176 | expect(ms(60 * 60 * 1200, { long: true })).toBe('1 hour'); 177 | expect(ms(60 * 60 * 10000, { long: true })).toBe('10 hours'); 178 | 179 | expect(ms(-1 * 60 * 60 * 1000, { long: true })).toBe('-1 hour'); 180 | expect(ms(-1 * 60 * 60 * 1200, { long: true })).toBe('-1 hour'); 181 | expect(ms(-1 * 60 * 60 * 10000, { long: true })).toBe('-10 hours'); 182 | }); 183 | 184 | it('should support days', () => { 185 | expect(ms(24 * 60 * 60 * 1000, { long: true })).toBe('1 day'); 186 | expect(ms(24 * 60 * 60 * 1200, { long: true })).toBe('1 day'); 187 | expect(ms(24 * 60 * 60 * 10000, { long: true })).toBe('10 days'); 188 | 189 | expect(ms(-1 * 24 * 60 * 60 * 1000, { long: true })).toBe('-1 day'); 190 | expect(ms(-1 * 24 * 60 * 60 * 1200, { long: true })).toBe('-1 day'); 191 | expect(ms(-1 * 24 * 60 * 60 * 10000, { long: true })).toBe('-10 days'); 192 | }); 193 | 194 | it('should round', () => { 195 | expect(ms(234234234, { long: true })).toBe('3 days'); 196 | 197 | expect(ms(-234234234, { long: true })).toBe('-3 days'); 198 | }); 199 | }); 200 | 201 | // numbers 202 | 203 | describe('ms(number)', () => { 204 | it('should not throw an error', () => { 205 | expect(() => { 206 | ms(500); 207 | }).not.toThrowError(); 208 | }); 209 | 210 | it('should support milliseconds', () => { 211 | expect(ms(500)).toBe('500ms'); 212 | 213 | expect(ms(-500)).toBe('-500ms'); 214 | }); 215 | 216 | it('should support seconds', () => { 217 | expect(ms(1000)).toBe('1s'); 218 | expect(ms(10000)).toBe('10s'); 219 | 220 | expect(ms(-1000)).toBe('-1s'); 221 | expect(ms(-10000)).toBe('-10s'); 222 | }); 223 | 224 | it('should support minutes', () => { 225 | expect(ms(60 * 1000)).toBe('1m'); 226 | expect(ms(60 * 10000)).toBe('10m'); 227 | 228 | expect(ms(-1 * 60 * 1000)).toBe('-1m'); 229 | expect(ms(-1 * 60 * 10000)).toBe('-10m'); 230 | }); 231 | 232 | it('should support hours', () => { 233 | expect(ms(60 * 60 * 1000)).toBe('1h'); 234 | expect(ms(60 * 60 * 10000)).toBe('10h'); 235 | 236 | expect(ms(-1 * 60 * 60 * 1000)).toBe('-1h'); 237 | expect(ms(-1 * 60 * 60 * 10000)).toBe('-10h'); 238 | }); 239 | 240 | it('should support days', () => { 241 | expect(ms(24 * 60 * 60 * 1000)).toBe('1d'); 242 | expect(ms(24 * 60 * 60 * 10000)).toBe('10d'); 243 | 244 | expect(ms(-1 * 24 * 60 * 60 * 1000)).toBe('-1d'); 245 | expect(ms(-1 * 24 * 60 * 60 * 10000)).toBe('-10d'); 246 | }); 247 | 248 | it('should round', () => { 249 | expect(ms(234234234)).toBe('3d'); 250 | 251 | expect(ms(-234234234)).toBe('-3d'); 252 | }); 253 | }); 254 | 255 | // invalid inputs 256 | 257 | describe('ms(invalid inputs)', () => { 258 | it('should throw an error, when ms("")', () => { 259 | expect(() => { 260 | // @ts-expect-error - We expect this to throw. 261 | ms(''); 262 | }).toThrowError(); 263 | }); 264 | 265 | it('should throw an error, when ms(undefined)', () => { 266 | expect(() => { 267 | // @ts-expect-error - We expect this to throw. 268 | ms(undefined); 269 | }).toThrowError(); 270 | }); 271 | 272 | it('should throw an error, when ms(null)', () => { 273 | expect(() => { 274 | // @ts-expect-error - We expect this to throw. 275 | ms(null); 276 | }).toThrowError(); 277 | }); 278 | 279 | it('should throw an error, when ms([])', () => { 280 | expect(() => { 281 | // @ts-expect-error - We expect this to throw. 282 | ms([]); 283 | }).toThrowError(); 284 | }); 285 | 286 | it('should throw an error, when ms({})', () => { 287 | expect(() => { 288 | // @ts-expect-error - We expect this to throw. 289 | ms({}); 290 | }).toThrowError(); 291 | }); 292 | 293 | it('should throw an error, when ms(NaN)', () => { 294 | expect(() => { 295 | ms(NaN); 296 | }).toThrowError(); 297 | }); 298 | 299 | it('should throw an error, when ms(Infinity)', () => { 300 | expect(() => { 301 | ms(Infinity); 302 | }).toThrowError(); 303 | }); 304 | 305 | it('should throw an error, when ms(-Infinity)', () => { 306 | expect(() => { 307 | ms(-Infinity); 308 | }).toThrowError(); 309 | }); 310 | }); 311 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Helpers. 2 | const s = 1000; 3 | const m = s * 60; 4 | const h = m * 60; 5 | const d = h * 24; 6 | const w = d * 7; 7 | const y = d * 365.25; 8 | 9 | type Unit = 10 | | 'Years' 11 | | 'Year' 12 | | 'Yrs' 13 | | 'Yr' 14 | | 'Y' 15 | | 'Weeks' 16 | | 'Week' 17 | | 'W' 18 | | 'Days' 19 | | 'Day' 20 | | 'D' 21 | | 'Hours' 22 | | 'Hour' 23 | | 'Hrs' 24 | | 'Hr' 25 | | 'H' 26 | | 'Minutes' 27 | | 'Minute' 28 | | 'Mins' 29 | | 'Min' 30 | | 'M' 31 | | 'Seconds' 32 | | 'Second' 33 | | 'Secs' 34 | | 'Sec' 35 | | 's' 36 | | 'Milliseconds' 37 | | 'Millisecond' 38 | | 'Msecs' 39 | | 'Msec' 40 | | 'Ms'; 41 | 42 | type UnitAnyCase = Unit | Uppercase | Lowercase; 43 | 44 | export type StringValue = 45 | | `${number}` 46 | | `${number}${UnitAnyCase}` 47 | | `${number} ${UnitAnyCase}`; 48 | 49 | interface Options { 50 | /** 51 | * Set to `true` to use verbose formatting. Defaults to `false`. 52 | */ 53 | long?: boolean; 54 | } 55 | 56 | /** 57 | * Parse or format the given value. 58 | * 59 | * @param value - The string or number to convert 60 | * @param options - Options for the conversion 61 | * @throws Error if `value` is not a non-empty string or a number 62 | */ 63 | function msFn(value: StringValue, options?: Options): number; 64 | function msFn(value: number, options?: Options): string; 65 | function msFn(value: StringValue | number, options?: Options): number | string { 66 | try { 67 | if (typeof value === 'string') { 68 | return parse(value); 69 | } else if (typeof value === 'number') { 70 | return format(value, options); 71 | } 72 | throw new Error('Value provided to ms() must be a string or number.'); 73 | } catch (error) { 74 | const message = isError(error) 75 | ? `${error.message}. value=${JSON.stringify(value)}` 76 | : 'An unknown error has occurred.'; 77 | throw new Error(message); 78 | } 79 | } 80 | 81 | /** 82 | * Parse the given string and return milliseconds. 83 | * 84 | * @param str - A string to parse to milliseconds 85 | * @returns The parsed value in milliseconds, or `NaN` if the string can't be 86 | * parsed 87 | */ 88 | export function parse(str: string): number { 89 | if (typeof str !== 'string' || str.length === 0 || str.length > 100) { 90 | throw new Error( 91 | 'Value provided to ms.parse() must be a string with length between 1 and 99.', 92 | ); 93 | } 94 | const match = 95 | /^(?-?(?:\d+)?\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( 96 | str, 97 | ); 98 | // Named capture groups need to be manually typed today. 99 | // https://github.com/microsoft/TypeScript/issues/32098 100 | const groups = match?.groups as { value: string; type?: string } | undefined; 101 | if (!groups) { 102 | return NaN; 103 | } 104 | const n = parseFloat(groups.value); 105 | const type = (groups.type || 'ms').toLowerCase() as Lowercase; 106 | switch (type) { 107 | case 'years': 108 | case 'year': 109 | case 'yrs': 110 | case 'yr': 111 | case 'y': 112 | return n * y; 113 | case 'weeks': 114 | case 'week': 115 | case 'w': 116 | return n * w; 117 | case 'days': 118 | case 'day': 119 | case 'd': 120 | return n * d; 121 | case 'hours': 122 | case 'hour': 123 | case 'hrs': 124 | case 'hr': 125 | case 'h': 126 | return n * h; 127 | case 'minutes': 128 | case 'minute': 129 | case 'mins': 130 | case 'min': 131 | case 'm': 132 | return n * m; 133 | case 'seconds': 134 | case 'second': 135 | case 'secs': 136 | case 'sec': 137 | case 's': 138 | return n * s; 139 | case 'milliseconds': 140 | case 'millisecond': 141 | case 'msecs': 142 | case 'msec': 143 | case 'ms': 144 | return n; 145 | default: 146 | // This should never occur. 147 | throw new Error( 148 | `The unit ${type as string} was matched, but no matching case exists.`, 149 | ); 150 | } 151 | } 152 | 153 | /** 154 | * Parse the given StringValue and return milliseconds. 155 | * 156 | * @param value - A typesafe StringValue to parse to milliseconds 157 | * @returns The parsed value in milliseconds, or `NaN` if the string can't be 158 | * parsed 159 | */ 160 | export function parseStrict(value: StringValue): number { 161 | return parse(value); 162 | } 163 | 164 | // eslint-disable-next-line import/no-default-export 165 | export default msFn; 166 | 167 | /** 168 | * Short format for `ms`. 169 | */ 170 | function fmtShort(ms: number): StringValue { 171 | const msAbs = Math.abs(ms); 172 | if (msAbs >= d) { 173 | return `${Math.round(ms / d)}d`; 174 | } 175 | if (msAbs >= h) { 176 | return `${Math.round(ms / h)}h`; 177 | } 178 | if (msAbs >= m) { 179 | return `${Math.round(ms / m)}m`; 180 | } 181 | if (msAbs >= s) { 182 | return `${Math.round(ms / s)}s`; 183 | } 184 | return `${ms}ms`; 185 | } 186 | 187 | /** 188 | * Long format for `ms`. 189 | */ 190 | function fmtLong(ms: number): StringValue { 191 | const msAbs = Math.abs(ms); 192 | if (msAbs >= d) { 193 | return plural(ms, msAbs, d, 'day'); 194 | } 195 | if (msAbs >= h) { 196 | return plural(ms, msAbs, h, 'hour'); 197 | } 198 | if (msAbs >= m) { 199 | return plural(ms, msAbs, m, 'minute'); 200 | } 201 | if (msAbs >= s) { 202 | return plural(ms, msAbs, s, 'second'); 203 | } 204 | return `${ms} ms`; 205 | } 206 | 207 | /** 208 | * Format the given integer as a string. 209 | * 210 | * @param ms - milliseconds 211 | * @param options - Options for the conversion 212 | * @returns The formatted string 213 | */ 214 | export function format(ms: number, options?: Options): string { 215 | if (typeof ms !== 'number' || !isFinite(ms)) { 216 | throw new Error('Value provided to ms.format() must be of type number.'); 217 | } 218 | return options?.long ? fmtLong(ms) : fmtShort(ms); 219 | } 220 | 221 | /** 222 | * Pluralization helper. 223 | */ 224 | function plural( 225 | ms: number, 226 | msAbs: number, 227 | n: number, 228 | name: string, 229 | ): StringValue { 230 | const isPlural = msAbs >= n * 1.5; 231 | return `${Math.round(ms / n)} ${name}${isPlural ? 's' : ''}` as StringValue; 232 | } 233 | 234 | /** 235 | * A type guard for errors. 236 | * 237 | * @param value - The value to test 238 | * @returns A boolean `true` if the provided value is an Error-like object 239 | */ 240 | function isError(value: unknown): value is Error { 241 | return typeof value === 'object' && value !== null && 'message' in value; 242 | } 243 | -------------------------------------------------------------------------------- /src/parse-strict.test.ts: -------------------------------------------------------------------------------- 1 | import { parseStrict } from './index'; 2 | 3 | describe('parseStrict(string)', () => { 4 | it('should not throw an error', () => { 5 | expect(() => { 6 | parseStrict('1m'); 7 | }).not.toThrowError(); 8 | }); 9 | 10 | it('should preserve ms', () => { 11 | expect(parseStrict('100')).toBe(100); 12 | }); 13 | 14 | it('should convert from m to ms', () => { 15 | expect(parseStrict('1m')).toBe(60000); 16 | }); 17 | 18 | it('should convert from h to ms', () => { 19 | expect(parseStrict('1h')).toBe(3600000); 20 | }); 21 | 22 | it('should convert d to ms', () => { 23 | expect(parseStrict('2d')).toBe(172800000); 24 | }); 25 | 26 | it('should convert w to ms', () => { 27 | expect(parseStrict('3w')).toBe(1814400000); 28 | }); 29 | 30 | it('should convert s to ms', () => { 31 | expect(parseStrict('1s')).toBe(1000); 32 | }); 33 | 34 | it('should convert ms to ms', () => { 35 | expect(parseStrict('100ms')).toBe(100); 36 | }); 37 | 38 | it('should convert y to ms', () => { 39 | expect(parseStrict('1y')).toBe(31557600000); 40 | }); 41 | 42 | it('should work with ms', () => { 43 | expect(parseStrict('1.5h')).toBe(5400000); 44 | }); 45 | 46 | it('should work with multiple spaces', () => { 47 | expect(parseStrict('1 s')).toBe(1000); 48 | }); 49 | 50 | it('should return NaN if invalid', () => { 51 | // @ts-expect-error - We expect this to fail. 52 | expect(isNaN(parseStrict('☃'))).toBe(true); 53 | // @ts-expect-error - We expect this to fail. 54 | expect(isNaN(parseStrict('10-.5'))).toBe(true); 55 | // @ts-expect-error - We expect this to fail. 56 | expect(isNaN(parseStrict('foo'))).toBe(true); 57 | }); 58 | 59 | it('should be case-insensitive', () => { 60 | expect(parseStrict('1.5H')).toBe(5400000); 61 | }); 62 | 63 | it('should work with numbers starting with .', () => { 64 | expect(parseStrict('.5ms')).toBe(0.5); 65 | }); 66 | 67 | it('should work with negative integers', () => { 68 | expect(parseStrict('-100ms')).toBe(-100); 69 | }); 70 | 71 | it('should work with negative decimals', () => { 72 | expect(parseStrict('-1.5h')).toBe(-5400000); 73 | expect(parseStrict('-10.5h')).toBe(-37800000); 74 | }); 75 | 76 | it('should work with negative decimals starting with "."', () => { 77 | expect(parseStrict('-.5h')).toBe(-1800000); 78 | }); 79 | }); 80 | 81 | // long strings 82 | 83 | describe('parseStrict(long string)', () => { 84 | it('should not throw an error', () => { 85 | expect(() => { 86 | parseStrict('53 milliseconds'); 87 | }).not.toThrowError(); 88 | }); 89 | 90 | it('should convert milliseconds to ms', () => { 91 | expect(parseStrict('53 milliseconds')).toBe(53); 92 | }); 93 | 94 | it('should convert msecs to ms', () => { 95 | expect(parseStrict('17 msecs')).toBe(17); 96 | }); 97 | 98 | it('should convert sec to ms', () => { 99 | expect(parseStrict('1 sec')).toBe(1000); 100 | }); 101 | 102 | it('should convert from min to ms', () => { 103 | expect(parseStrict('1 min')).toBe(60000); 104 | }); 105 | 106 | it('should convert from hr to ms', () => { 107 | expect(parseStrict('1 hr')).toBe(3600000); 108 | }); 109 | 110 | it('should convert days to ms', () => { 111 | expect(parseStrict('2 days')).toBe(172800000); 112 | }); 113 | 114 | it('should convert weeks to ms', () => { 115 | expect(parseStrict('1 week')).toBe(604800000); 116 | }); 117 | 118 | it('should convert years to ms', () => { 119 | expect(parseStrict('1 year')).toBe(31557600000); 120 | }); 121 | 122 | it('should work with decimals', () => { 123 | expect(parseStrict('1.5 hours')).toBe(5400000); 124 | }); 125 | 126 | it('should work with negative integers', () => { 127 | expect(parseStrict('-100 milliseconds')).toBe(-100); 128 | }); 129 | 130 | it('should work with negative decimals', () => { 131 | expect(parseStrict('-1.5 hours')).toBe(-5400000); 132 | }); 133 | 134 | it('should work with negative decimals starting with "."', () => { 135 | expect(parseStrict('-.5 hr')).toBe(-1800000); 136 | }); 137 | }); 138 | 139 | // invalid inputs 140 | 141 | describe('parseStrict(invalid inputs)', () => { 142 | it('should throw an error, when parseStrict("")', () => { 143 | expect(() => { 144 | // @ts-expect-error - We expect this to throw. 145 | parseStrict(''); 146 | }).toThrowError(); 147 | }); 148 | 149 | it('should throw an error, when parseStrict(undefined)', () => { 150 | expect(() => { 151 | // @ts-expect-error - We expect this to throw. 152 | parseStrict(undefined); 153 | }).toThrowError(); 154 | }); 155 | 156 | it('should throw an error, when parseStrict(null)', () => { 157 | expect(() => { 158 | // @ts-expect-error - We expect this to throw. 159 | parseStrict(null); 160 | }).toThrowError(); 161 | }); 162 | 163 | it('should throw an error, when parseStrict([])', () => { 164 | expect(() => { 165 | // @ts-expect-error - We expect this to throw. 166 | parseStrict([]); 167 | }).toThrowError(); 168 | }); 169 | 170 | it('should throw an error, when parseStrict({})', () => { 171 | expect(() => { 172 | // @ts-expect-error - We expect this to throw. 173 | parseStrict({}); 174 | }).toThrowError(); 175 | }); 176 | 177 | it('should throw an error, when parseStrict(NaN)', () => { 178 | expect(() => { 179 | // @ts-expect-error - We expect this to throw. 180 | parseStrict(NaN); 181 | }).toThrowError(); 182 | }); 183 | 184 | it('should throw an error, when parseStrict(Infinity)', () => { 185 | expect(() => { 186 | // @ts-expect-error - We expect this to throw. 187 | parseStrict(Infinity); 188 | }).toThrowError(); 189 | }); 190 | 191 | it('should throw an error, when parseStrict(-Infinity)', () => { 192 | expect(() => { 193 | // @ts-expect-error - We expect this to throw. 194 | parseStrict(-Infinity); 195 | }).toThrowError(); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /src/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { parse } from './index'; 2 | 3 | describe('parse(string)', () => { 4 | it('should not throw an error', () => { 5 | expect(() => { 6 | parse('1m'); 7 | }).not.toThrowError(); 8 | }); 9 | 10 | it('should preserve ms', () => { 11 | expect(parse('100')).toBe(100); 12 | }); 13 | 14 | it('should convert from m to ms', () => { 15 | expect(parse('1m')).toBe(60000); 16 | }); 17 | 18 | it('should convert from h to ms', () => { 19 | expect(parse('1h')).toBe(3600000); 20 | }); 21 | 22 | it('should convert d to ms', () => { 23 | expect(parse('2d')).toBe(172800000); 24 | }); 25 | 26 | it('should convert w to ms', () => { 27 | expect(parse('3w')).toBe(1814400000); 28 | }); 29 | 30 | it('should convert s to ms', () => { 31 | expect(parse('1s')).toBe(1000); 32 | }); 33 | 34 | it('should convert ms to ms', () => { 35 | expect(parse('100ms')).toBe(100); 36 | }); 37 | 38 | it('should convert y to ms', () => { 39 | expect(parse('1y')).toBe(31557600000); 40 | }); 41 | 42 | it('should work with ms', () => { 43 | expect(parse('1.5h')).toBe(5400000); 44 | }); 45 | 46 | it('should work with multiple spaces', () => { 47 | expect(parse('1 s')).toBe(1000); 48 | }); 49 | 50 | it('should return NaN if invalid', () => { 51 | expect(isNaN(parse('☃'))).toBe(true); 52 | expect(isNaN(parse('10-.5'))).toBe(true); 53 | expect(isNaN(parse('foo'))).toBe(true); 54 | }); 55 | 56 | it('should be case-insensitive', () => { 57 | expect(parse('1.5H')).toBe(5400000); 58 | }); 59 | 60 | it('should work with numbers starting with .', () => { 61 | expect(parse('.5ms')).toBe(0.5); 62 | }); 63 | 64 | it('should work with negative integers', () => { 65 | expect(parse('-100ms')).toBe(-100); 66 | }); 67 | 68 | it('should work with negative decimals', () => { 69 | expect(parse('-1.5h')).toBe(-5400000); 70 | expect(parse('-10.5h')).toBe(-37800000); 71 | }); 72 | 73 | it('should work with negative decimals starting with "."', () => { 74 | expect(parse('-.5h')).toBe(-1800000); 75 | }); 76 | }); 77 | 78 | // long strings 79 | 80 | describe('parse(long string)', () => { 81 | it('should not throw an error', () => { 82 | expect(() => { 83 | parse('53 milliseconds'); 84 | }).not.toThrowError(); 85 | }); 86 | 87 | it('should convert milliseconds to ms', () => { 88 | expect(parse('53 milliseconds')).toBe(53); 89 | }); 90 | 91 | it('should convert msecs to ms', () => { 92 | expect(parse('17 msecs')).toBe(17); 93 | }); 94 | 95 | it('should convert sec to ms', () => { 96 | expect(parse('1 sec')).toBe(1000); 97 | }); 98 | 99 | it('should convert from min to ms', () => { 100 | expect(parse('1 min')).toBe(60000); 101 | }); 102 | 103 | it('should convert from hr to ms', () => { 104 | expect(parse('1 hr')).toBe(3600000); 105 | }); 106 | 107 | it('should convert days to ms', () => { 108 | expect(parse('2 days')).toBe(172800000); 109 | }); 110 | 111 | it('should convert weeks to ms', () => { 112 | expect(parse('1 week')).toBe(604800000); 113 | }); 114 | 115 | it('should convert years to ms', () => { 116 | expect(parse('1 year')).toBe(31557600000); 117 | }); 118 | 119 | it('should work with decimals', () => { 120 | expect(parse('1.5 hours')).toBe(5400000); 121 | }); 122 | 123 | it('should work with negative integers', () => { 124 | expect(parse('-100 milliseconds')).toBe(-100); 125 | }); 126 | 127 | it('should work with negative decimals', () => { 128 | expect(parse('-1.5 hours')).toBe(-5400000); 129 | }); 130 | 131 | it('should work with negative decimals starting with "."', () => { 132 | expect(parse('-.5 hr')).toBe(-1800000); 133 | }); 134 | }); 135 | 136 | // invalid inputs 137 | 138 | describe('parse(invalid inputs)', () => { 139 | it('should throw an error, when parse("")', () => { 140 | expect(() => { 141 | parse(''); 142 | }).toThrowError(); 143 | }); 144 | 145 | it('should throw an error, when parse(undefined)', () => { 146 | expect(() => { 147 | // @ts-expect-error - We expect this to throw. 148 | parse(undefined); 149 | }).toThrowError(); 150 | }); 151 | 152 | it('should throw an error, when parse(null)', () => { 153 | expect(() => { 154 | // @ts-expect-error - We expect this to throw. 155 | parse(null); 156 | }).toThrowError(); 157 | }); 158 | 159 | it('should throw an error, when parse([])', () => { 160 | expect(() => { 161 | // @ts-expect-error - We expect this to throw. 162 | parse([]); 163 | }).toThrowError(); 164 | }); 165 | 166 | it('should throw an error, when parse({})', () => { 167 | expect(() => { 168 | // @ts-expect-error - We expect this to throw. 169 | parse({}); 170 | }).toThrowError(); 171 | }); 172 | 173 | it('should throw an error, when parse(NaN)', () => { 174 | expect(() => { 175 | // @ts-expect-error - We expect this to throw. 176 | parse(NaN); 177 | }).toThrowError(); 178 | }); 179 | 180 | it('should throw an error, when parse(Infinity)', () => { 181 | expect(() => { 182 | // @ts-expect-error - We expect this to throw. 183 | parse(Infinity); 184 | }).toThrowError(); 185 | }); 186 | 187 | it('should throw an error, when parse(-Infinity)', () => { 188 | expect(() => { 189 | // @ts-expect-error - We expect this to throw. 190 | parse(-Infinity); 191 | }).toThrowError(); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vercel/style-guide/typescript", 3 | "compilerOptions": { 4 | "target": "ES2019" 5 | }, 6 | "include": ["src/"] 7 | } 8 | --------------------------------------------------------------------------------