├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── format-duration.d.ts ├── format-duration.js ├── format-duration.test.js ├── package.json └── stopwatch.png /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "npm" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: "daily" 13 | # Enable updates to github actions 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | node: [14, 16, 18] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | - run: npm install 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | stopwatch.png 2 | CONTRIBUTING.md 3 | *.test.js 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # format-duration change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## 3.0.2 - 2023-01-17 7 | 8 | ### Fixes 9 | - Move FormatDurationOptions under namespace formatDuration (#20) - thanks @sebinemeth 10 | 11 | ## 3.0.1 - 2022-12-18 12 | 13 | ### Fixes 14 | - Fix typo to load types properly (#18) - thanks @petrbela 15 | 16 | ### Misc 17 | - Fix repo URL in package.json 18 | 19 | ## 3.0.0 - 2022-12-12 20 | 21 | ### Breaking 22 | - Drop support for Node 12 23 | 24 | ### Features 25 | - New option: `ms` (show milliseconds) (#17) 26 | - TypeScript: Export FormatDurationOptions (#15) 27 | 28 | ### Misc 29 | - CI: add Node 18 to test matrix 30 | 31 | ## 2.0.0 - 2022-03-16 32 | 33 | ### Breaking Changes 34 | - Drop support for node 8 & 10 (now only supporting node LTS: 12, 14, 16). 35 | 36 | ### Misc 37 | - No dependencies! Code from parse-ms and add-zero has been consolidated for easier maintenance and smaller module footprint. 38 | - Repo ownership transfer (hypermodules -> ungoldman). Same maintainers, new URL. 39 | 40 | ## 1.4.0 - 2021-08-04 41 | 42 | ### Features 43 | - add type definitions (#5) - thanks @guytepper 44 | 45 | ## 1.3.1 - 2018-10-11 46 | 47 | ### Fixes 48 | - update URLs to reflect ownership transfer to hypermodules 49 | 50 | ## 1.3.0 - 2018-10-11 51 | 52 | ### Features 53 | - add option for leading zeros (hours, minutes, seconds) (#3) - thanks @Deseteral 54 | 55 | ## 1.2.0 - 2018-07-24 56 | 57 | ### Features 58 | - convert to es5 (#2) 59 | 60 | ## 1.1.0 - 2018-02-23 61 | 62 | ### Features 63 | - add support for negative durations (#1) - thanks @lrn2prgrm 64 | 65 | ## 1.0.1 - 2018-02-23 66 | 67 | ### Chores 68 | - update license 69 | - update contributing guidelines 70 | - update author field 71 | - add downloads badge to readme 72 | 73 | ## 1.0.0 - 2016-08-26 74 | - initial release 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Code of Conduct 4 | 5 | This project is intended to be a safe, welcoming space for collaboration. 6 | 7 | All contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 8 | 9 | Thank you for being kind to each other! 10 | 11 | ## Contributions welcome! 12 | 13 | **Before spending lots of time on something, ask for feedback on your idea first!** 14 | 15 | Please search [issues](../../issues/) and [pull requests](../../pulls/) before adding something new! This helps avoid duplicating efforts and conversations. 16 | 17 | This project welcomes any kind of contribution! Here are a few suggestions: 18 | 19 | - **Ideas**: participate in an issue thread or start your own to have your voice heard. 20 | - **Writing**: contribute your expertise in an area by helping expand the included content. 21 | - **Copy editing**: fix typos, clarify language, and generally improve the quality of the content. 22 | - **Formatting**: help keep content easy to read with consistent formatting. 23 | - **Code**: help maintain and improve the project codebase. 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [ISC License](https://spdx.org/licenses/ISC) 2 | 3 | Copyright (c) 2018, Nate Goldman 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | stopwatch 4 | 5 | # format-duration 6 | 7 | Convert a number in milliseconds to a standard duration string. 8 | 9 | [![npm][npm-image]][npm-url] 10 | [![build][build-image]][build-url] 11 | [![downloads][downloads-image]][npm-url] 12 | 13 | [npm-image]: https://img.shields.io/npm/v/format-duration.svg 14 | [npm-url]: https://www.npmjs.com/package/format-duration 15 | [build-image]: https://github.com/ungoldman/format-duration/actions/workflows/tests.yml/badge.svg 16 | [build-url]: https://github.com/ungoldman/format-duration/actions/workflows/tests.yml 17 | [downloads-image]: https://img.shields.io/npm/dm/format-duration.svg 18 | 19 |
20 | 21 | ## Install 22 | 23 | ``` 24 | npm install format-duration 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | const format = require('format-duration') 31 | 32 | // anything under a second is rounded down to zero 33 | format(999) // '0:00' 34 | 35 | // 1000 milliseconds is a second 36 | format(1000) // '0:01' 37 | 38 | // 1999 rounds down to 0:01 39 | format(1000 * 2 - 1) // '0:01' 40 | 41 | // 60 seconds is a minute 42 | format(1000 * 60) // '1:00' 43 | 44 | // 59 seconds looks like this 45 | format(1000 * 60 - 1) // '0:59' 46 | 47 | // 60 minutes is an hour 48 | format(1000 * 60 * 60) // '1:00:00' 49 | 50 | // 59 minutes and 59 seconds looks like this 51 | format(1000 * 60 * 60 - 1) // '59:59' 52 | 53 | // 24 hours is a day 54 | format(1000 * 60 * 60 * 24) // '1:00:00:00' 55 | 56 | // 23 hours, 59 minutes, and 59 seconds looks like this 57 | format(1000 * 60 * 60 * 24 - 1) // '23:59:59' 58 | 59 | // 365 days looks like this (not bothering with years) 60 | format(1000 * 60 * 60 * 24 * 365) // '365:00:00:00' 61 | 62 | // anything under a second is rounded down to zero 63 | format(-999) // '0:00' 64 | 65 | // 1000 milliseconds is a second 66 | format(-1000) // '-0:01' 67 | 68 | // 365 days looks like this (not bothering with years) 69 | format(-1000 * 60 * 60 * 24 * 365) // '-365:00:00:00' 70 | 71 | // with `leading` option, formatting looks like this 72 | format(1000 * 60, { leading: true }) // '01:00' 73 | format(1000 * 60 - 1, { leading: true }) // '00:59' 74 | format(1000 * 60 * 60, { leading: true }) // '01:00:00' 75 | 76 | // with `ms` option, formatting looks like this 77 | format(999, { ms: true }) // '0:00.999' 78 | format(1000 * 60, { ms: true }) // '1:00.000' 79 | format(1000 * 60 * 60 * 24 - 1, { ms: true }) // '23:59:59.999' 80 | ``` 81 | 82 | ## Contributing 83 | 84 | Contributions welcome! Please read the [contributing guidelines](CONTRIBUTING.md) first. 85 | 86 | ## License 87 | 88 | [ISC](LICENSE.md) 89 | 90 | Stopwatch image is from [emojidex](https://emojidex.com/emoji/stopwatch). 91 | -------------------------------------------------------------------------------- /format-duration.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace formatDuration { 2 | interface FormatDurationOptions { 3 | /** 4 | * Adds leading zero to the formatted string. 5 | */ 6 | leading?: boolean 7 | ms?: boolean 8 | } 9 | } 10 | 11 | /** 12 | * Convert a number in milliseconds to a standard duration string. 13 | * 14 | * @param {number} ms The number to format. 15 | * @param {object} options - Formatting options 16 | * @returns {string} The formatted duration string. 17 | */ 18 | declare function formatDuration( 19 | ms: number, 20 | options?: formatDuration.FormatDurationOptions 21 | ): string 22 | export = formatDuration 23 | -------------------------------------------------------------------------------- /format-duration.js: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/sindresorhus/parse-ms. 2 | // moved to internal function because parse-ms is now pure ESM. 3 | function parseMs (ms) { 4 | if (typeof ms !== 'number') { 5 | throw new TypeError('Expected a number') 6 | } 7 | 8 | return { 9 | days: Math.trunc(ms / 86400000), 10 | hours: Math.trunc(ms / 3600000) % 24, 11 | minutes: Math.trunc(ms / 60000) % 60, 12 | seconds: Math.trunc(ms / 1000) % 60, 13 | ms: Math.trunc(ms) % 1000 14 | } 15 | } 16 | 17 | // adapted from https://github.com/rafaelrinaldi/add-zero. 18 | // moved to internal function b/c addZero is unmaintained (7+ years). 19 | // stripped out negative sign logic since we're already doing it elsewhere. 20 | function addZero (value, digits) { 21 | digits = digits || 2 22 | 23 | let str = value.toString() 24 | let size = 0 25 | 26 | size = digits - str.length + 1 27 | str = new Array(size).join('0').concat(str) 28 | 29 | return str 30 | } 31 | 32 | function getSign (duration, showMs) { 33 | if (showMs) return duration < 0 ? '-' : '' 34 | return duration <= -1000 ? '-' : '' 35 | } 36 | 37 | /** 38 | * Convert a number in milliseconds to a standard duration string. 39 | * @param {number} ms - duration in milliseconds 40 | * @param {object} options - formatDuration options object 41 | * @param {boolean} [options.leading=false] - add leading zero 42 | * @param {boolean} [options.milliseconds=false] - add milliseconds 43 | * @returns string - formatted duration string 44 | */ 45 | function formatDuration (ms, options) { 46 | const leading = options && options.leading 47 | const showMs = options && options.ms 48 | const unsignedMs = ms < 0 ? -ms : ms 49 | const sign = getSign(ms, showMs) 50 | const t = parseMs(unsignedMs) 51 | const seconds = addZero(t.seconds) 52 | let output = '' 53 | 54 | if (t.days && !output) output = sign + t.days + ':' + addZero(t.hours) + ':' + addZero(t.minutes) + ':' + seconds 55 | if (t.hours && !output) output = sign + (leading ? addZero(t.hours) : t.hours) + ':' + addZero(t.minutes) + ':' + seconds 56 | if (!output) output = sign + (leading ? addZero(t.minutes) : t.minutes) + ':' + seconds 57 | 58 | if (showMs) output += '.' + addZero(t.ms, 3) 59 | return output 60 | } 61 | 62 | module.exports = formatDuration 63 | -------------------------------------------------------------------------------- /format-duration.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const f = require('./format-duration') 3 | 4 | test('it works', function (t) { 5 | t.equal(f(999), '0:00', 'anything under a second is 0:00') 6 | t.equal(f(1000), '0:01', 'check 1000 milliseconds is a second') 7 | t.equal(f(1000 * 2 - 1), '0:01', 'rounds 1999 down to 0:01') 8 | t.equal(f(1000 * 60), '1:00', 'check 60 seconds is a minute') 9 | t.equal(f(1000 * 60 - 1), '0:59', 'check 59 seconds looks ok') 10 | t.equal(f(1000 * 60 * 60), '1:00:00', 'check 60 minutes is an hour') 11 | t.equal(f(1000 * 60 * 60 - 1), '59:59', 'check 59 minutes looks ok') 12 | t.equal(f(1000 * 60 * 60 * 24), '1:00:00:00', 'check 24 hours is a day') 13 | t.equal(f(1000 * 60 * 60 * 24 - 1), '23:59:59', 'check 23 hours looks okay') 14 | t.equal(f(1000 * 60 * 60 * 24 * 365), '365:00:00:00', 'check 365 days is too long to care') 15 | t.end() 16 | }) 17 | 18 | test('it works with negative durations', function (t) { 19 | t.equal(f(-999), '0:00', 'anything under a second is 0:00') 20 | t.equal(f(-1000), '-0:01', 'check -1000 milliseconds is a second') 21 | t.equal(f(-1000 * 2 + 1), '-0:01', 'rounds -1999 up to -0:01') 22 | t.equal(f(-1000 * 60), '-1:00', 'check -60 seconds is a minute') 23 | t.equal(f(-1000 * 60 + 1), '-0:59', 'check -59 seconds looks ok') 24 | t.equal(f(-1000 * 60 * 60), '-1:00:00', 'check -60 minutes is an hour') 25 | t.equal(f(-1000 * 60 * 60 + 1), '-59:59', 'check -59 minutes looks ok') 26 | t.equal(f(-1000 * 60 * 60 * 24), '-1:00:00:00', 'check -24 hours is a day') 27 | t.equal(f(-1000 * 60 * 60 * 24 + 1), '-23:59:59', 'check -23 hours looks okay') 28 | t.equal(f(-1000 * 60 * 60 * 24 * 365), '-365:00:00:00', 'check -365 days is too long to care') 29 | t.end() 30 | }) 31 | 32 | test('it works with leading zeros', function (t) { 33 | t.equal(f(999, { leading: true }), '00:00', 'anything under a second is 00:00') 34 | t.equal(f(1000, { leading: true }), '00:01', 'check 1000 milliseconds is a second') 35 | t.equal(f(1000 * 2 - 1, { leading: true }), '00:01', 'rounds 1999 down to 00:01') 36 | t.equal(f(1000 * 60, { leading: true }), '01:00', 'check 60 seconds is a minute') 37 | t.equal(f(1000 * 60 - 1, { leading: true }), '00:59', 'check 59 seconds looks ok') 38 | t.equal(f(1000 * 60 * 60, { leading: true }), '01:00:00', 'check 60 minutes is an hour') 39 | t.equal(f(1000 * 60 * 60 - 1, { leading: true }), '59:59', 'check 59 minutes looks ok') 40 | t.equal(f(1000 * 60 * 60 * 24, { leading: true }), '1:00:00:00', 'check 24 hours is a day') 41 | t.equal(f(1000 * 60 * 60 * 24 - 1, { leading: true }), '23:59:59', 'check 23 hours looks okay') 42 | t.equal(f(1000 * 60 * 60 * 24 * 365, { leading: true }), '365:00:00:00', 'check 365 days is too long to care') 43 | t.end() 44 | }) 45 | 46 | test('it works with leading zeros and milliseconds', function (t) { 47 | t.equal(f(999, { leading: true, ms: true }), '00:00.999', 'anything under a second is correctly displayed') 48 | t.equal(f(1000, { leading: true, ms: true }), '00:01.000', 'check 1000 milliseconds is a second') 49 | t.equal(f(1000 * 2 - 1, { leading: true, ms: true }), '00:01.999', 'displays 1999 as 00:01.999') 50 | t.equal(f(1000 * 60, { leading: true, ms: true }), '01:00.000', 'check 60 seconds is a minute') 51 | t.equal(f(1000 * 60 - 1, { leading: true, ms: true }), '00:59.999', 'check 59.999 seconds looks ok') 52 | t.equal(f(1000 * 60 * 60, { leading: true, ms: true }), '01:00:00.000', 'check 60 minutes is an hour') 53 | t.equal(f(1000 * 60 * 60 - 1, { leading: true, ms: true }), '59:59.999', 'check 59 minutes with 999 milliseconds looks ok') 54 | t.equal(f(1000 * 60 * 60 * 24, { leading: true, ms: true }), '1:00:00:00.000', 'check 24 hours is a day') 55 | t.equal(f(1000 * 60 * 60 * 24 - 1, { leading: true, ms: true }), '23:59:59.999', 'check 23 hours with 999 milliseconds looks okay') 56 | t.equal(f(1000 * 60 * 60 * 24 * 365, { leading: true, ms: true }), '365:00:00:00.000', 'check 365 days is too long to care') 57 | t.end() 58 | }) 59 | 60 | test('it works with negative durations and milliseconds', function (t) { 61 | t.equal(f(-999, { ms: true }), '-0:00.999', 'anything under a second is correctly displayed with negative sign') 62 | t.equal(f(-1000, { ms: true }), '-0:01.000', 'check -1000 milliseconds is a second') 63 | t.equal(f(-1000 * 2 + 1, { ms: true }), '-0:01.999', 'displays -1999 as -00:01.999') 64 | t.equal(f(-1000 * 60, { ms: true }), '-1:00.000', 'check -60 seconds is a minute') 65 | t.equal(f(-1000 * 60 + 1, { ms: true }), '-0:59.999', 'check -59.999 seconds looks ok') 66 | t.equal(f(-1000 * 60 * 60, { ms: true }), '-1:00:00.000', 'check -60 minutes is an hour') 67 | t.equal(f(-1000 * 60 * 60 + 1, { ms: true }), '-59:59.999', 'check -59 minutes with 999 milliseconds looks ok') 68 | t.equal(f(-1000 * 60 * 60 * 24, { ms: true }), '-1:00:00:00.000', 'check -24 hours is a day') 69 | t.equal(f(-1000 * 60 * 60 * 24 + 1, { ms: true }), '-23:59:59.999', 'check -23 hours with 999 milliseconds looks okay') 70 | t.equal(f(-1000 * 60 * 60 * 24 * 365, { ms: true }), '-365:00:00:00.000', 'check -365 days is too long to care') 71 | t.end() 72 | }) 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "format-duration", 3 | "description": "Convert a number in milliseconds to a standard duration string.", 4 | "version": "3.0.2", 5 | "author": "Nate Goldman (https://ungoldman.com/)", 6 | "bugs": { 7 | "url": "https://github.com/ungoldman/format-duration/issues" 8 | }, 9 | "devDependencies": { 10 | "standard": "^17.0.0", 11 | "tape": "^5.5.2" 12 | }, 13 | "homepage": "https://github.com/ungoldman/format-duration", 14 | "keywords": [ 15 | "display", 16 | "duration", 17 | "format", 18 | "hh:mm:ss", 19 | "hours", 20 | "milliseconds", 21 | "mm:ss", 22 | "seconds", 23 | "time" 24 | ], 25 | "license": "ISC", 26 | "main": "format-duration.js", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/ungoldman/format-duration.git" 30 | }, 31 | "scripts": { 32 | "test": "standard && tape *.test.js" 33 | }, 34 | "types": "format-duration.d.ts" 35 | } 36 | -------------------------------------------------------------------------------- /stopwatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungoldman/format-duration/227770fb7c2cb2f7f95370a3e9ee57079a9af433/stopwatch.png --------------------------------------------------------------------------------