@acus9345/tinydur 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +71 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +167 -0
- package/package.json +54 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project uses [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `parse()` turns durations such as `"1h 30m"`, `"1.5h"` or `"2 days"` into milliseconds.
|
|
10
|
+
- `format()` turns milliseconds into `"1h 30m"` or `"1 hour 30 minutes"`, with a `units` option.
|
|
11
|
+
- `DurationError` with messages that explain how to fix bad input.
|
|
12
|
+
- `tinydur` CLI with `--long`, `--units`, `--help` and `--version`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 acus9345
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,117 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tinydur
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tiny, zero-dependency duration parser and formatter: `"1h 30m"` ⇄ `5400000`, with a CLI.
|
|
4
|
+
|
|
5
|
+
A small replacement for `ms`, `parse-duration` and `humanize-duration`. Fully typed, ESM, no dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @acus9345/tinydur
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 22 or newer.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Save this as `example.mjs`:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { parse, format } from "@acus9345/tinydur";
|
|
21
|
+
|
|
22
|
+
console.log(parse("1h 30m")); // 5400000
|
|
23
|
+
console.log(format(5400000)); // 1h 30m
|
|
24
|
+
console.log(format(5400000, { long: true })); // 1 hour 30 minutes
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run it:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node example.mjs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or use the CLI without installing anything:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @acus9345/tinydur 1h30m
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `parse(input: string | number): number`
|
|
42
|
+
|
|
43
|
+
Turns a duration into milliseconds.
|
|
44
|
+
|
|
45
|
+
| Input | Result |
|
|
46
|
+
| ------------------------- | ------------ |
|
|
47
|
+
| `"1h 30m"`, `"1h30m"` | `5400000` |
|
|
48
|
+
| `"1 hour and 30 minutes"` | `5400000` |
|
|
49
|
+
| `"1.5h"` | `5400000` |
|
|
50
|
+
| `"90s"` | `90000` |
|
|
51
|
+
| `"2 days"` | `172800000` |
|
|
52
|
+
| `"-5m"` | `-300000` |
|
|
53
|
+
| `"100"` or `100` | `100` |
|
|
54
|
+
|
|
55
|
+
Units: `ms`, `s`, `m`, `h`, `d`, `w`, `y`, plus long forms such as `sec`, `mins`, `hours`, `days`, `weeks`, `years`. Case and spacing don't matter. A year is 365.25 days.
|
|
56
|
+
|
|
57
|
+
### `format(ms: number, options?): string`
|
|
58
|
+
|
|
59
|
+
Turns milliseconds into a readable duration, using days, hours, minutes, seconds and milliseconds.
|
|
60
|
+
|
|
61
|
+
| Option | Default | Meaning |
|
|
62
|
+
| ------- | ------- | ---------------------------------------------------------------- |
|
|
63
|
+
| `long` | `false` | `"1 hour 30 minutes"` instead of `"1h 30m"` |
|
|
64
|
+
| `units` | `2` | Maximum number of units, counting from the largest; the rest is rounded |
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
format(90061001); // "1d 1h"
|
|
68
|
+
format(90061001, { units: 4 }); // "1d 1h 1m 1s"
|
|
69
|
+
format(59999, { units: 1 }); // "1m"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `DurationError`
|
|
73
|
+
|
|
74
|
+
Both functions throw a `DurationError` (a subclass of `Error`) for bad input. Its message says what was wrong and how to fix it:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { parse, DurationError } from "@acus9345/tinydur";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
parse("5 parsecs");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof DurationError) console.log(error.message);
|
|
83
|
+
// Unknown unit "parsecs" in "5 parsecs". Use one of: ms, s, m, h, d, w, y (long names like "minutes" work too).
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## CLI
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @acus9345/tinydur 1h30m
|
|
91
|
+
npx @acus9345/tinydur "2 days 4 hours"
|
|
92
|
+
npx @acus9345/tinydur 5400000 --long
|
|
93
|
+
npx @acus9345/tinydur 90061001 --units 4
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Prints `5400000`, `187200000`, `1 hour 30 minutes` and `1d 1h 1m 1s`. A plain number is formatted; anything else is parsed to milliseconds.
|
|
97
|
+
|
|
98
|
+
| Option | Meaning |
|
|
99
|
+
| ----------------- | -------------------------------------------- |
|
|
100
|
+
| `-l`, `--long` | Long unit names |
|
|
101
|
+
| `-u`, `--units N` | Maximum number of units when formatting |
|
|
102
|
+
| `-h`, `--help` | Show help |
|
|
103
|
+
| `-v`, `--version` | Show version |
|
|
104
|
+
|
|
105
|
+
Works the same in PowerShell, cmd and POSIX shells. Quote durations that contain spaces, or pass the parts as separate arguments (`npx @acus9345/tinydur 1h 30m`). After `npm install`, the command is also available as `tinydur` in npm scripts.
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
- **`ERR_REQUIRE_ESM` or `require() of ES Module`**: tinydur is an ES module. Use `import`, or upgrade to Node.js 22.12+ where `require("@acus9345/tinydur")` works.
|
|
110
|
+
- **`Missing unit after 30`**: every number in a multi-part duration needs a unit, e.g. `"1h 30m"` rather than `"1h 30"`. A single bare number is read as milliseconds.
|
|
111
|
+
- **`m` means minutes**: there is no month unit because months vary in length. Use days or weeks.
|
|
112
|
+
- **`format("5000")` throws**: `format` takes a number. Call `parse` first if you have a string.
|
|
113
|
+
- **PowerShell treats `-5m` oddly in scripts**: quote it: `npx @acus9345/tinydur "-5m"`.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { DurationError, format, parse } from "./index.js";
|
|
4
|
+
const HELP = `tinydur - convert between readable durations and milliseconds
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
tinydur <duration> Print the duration in milliseconds
|
|
8
|
+
tinydur <milliseconds> Print the milliseconds as a readable duration
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
-l, --long Use long unit names ("1 hour 30 minutes")
|
|
12
|
+
-u, --units <n> Maximum number of units when formatting (default: 2)
|
|
13
|
+
-h, --help Show this help
|
|
14
|
+
-v, --version Show the version number
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
tinydur 1h30m 5400000
|
|
18
|
+
tinydur "2 days 4 hours" 187200000
|
|
19
|
+
tinydur 5400000 1h 30m
|
|
20
|
+
tinydur 5400000 --long 1 hour 30 minutes
|
|
21
|
+
tinydur 90061001 --units 4 1d 1h 1m 1s
|
|
22
|
+
`;
|
|
23
|
+
class UsageError extends Error {
|
|
24
|
+
}
|
|
25
|
+
function main(argv) {
|
|
26
|
+
const values = [];
|
|
27
|
+
let long = false;
|
|
28
|
+
let units;
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (arg === "-h" || arg === "--help") {
|
|
32
|
+
process.stdout.write(HELP);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (arg === "-v" || arg === "--version") {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
37
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "-l" || arg === "--long") {
|
|
41
|
+
long = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "-u" || arg === "--units" || arg.startsWith("--units=")) {
|
|
44
|
+
const raw = arg.startsWith("--units=") ? arg.slice("--units=".length) : argv[++i];
|
|
45
|
+
if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1) {
|
|
46
|
+
throw new UsageError(`--units needs a whole number of at least 1, e.g. "--units 3"${raw === undefined ? "" : `, got "${raw}"`}.`);
|
|
47
|
+
}
|
|
48
|
+
units = Number(raw);
|
|
49
|
+
}
|
|
50
|
+
else if (/^--?[a-z]/i.test(arg)) {
|
|
51
|
+
throw new UsageError(`Unknown option "${arg}".`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
values.push(arg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (values.length === 0)
|
|
58
|
+
throw new UsageError(`Missing duration. Example: tinydur 1h30m`);
|
|
59
|
+
const input = values.join(" ").trim();
|
|
60
|
+
const output = /^[+-]?(\d+\.?\d*|\.\d+)$/.test(input) ? format(Number(input), { long, units }) : String(parse(input));
|
|
61
|
+
process.stdout.write(`${output}\n`);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
main(process.argv.slice(2));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof DurationError || error instanceof UsageError))
|
|
68
|
+
throw error;
|
|
69
|
+
process.stderr.write(`tinydur: ${error.message}\nRun "tinydur --help" for usage.\n`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tinydur: tiny, zero-dependency duration parsing and formatting.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* import { parse, format } from "tinydur";
|
|
6
|
+
* parse("1h 30m"); // 5400000
|
|
7
|
+
* format(5400000); // "1h 30m"
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/** Thrown for invalid durations or options. The message says what went wrong and how to fix it. */
|
|
12
|
+
export declare class DurationError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/** Options for {@link format}. */
|
|
16
|
+
export interface FormatOptions {
|
|
17
|
+
/** Use long unit names, e.g. `"1 hour 30 minutes"` instead of `"1h 30m"`. Default: `false`. */
|
|
18
|
+
long?: boolean;
|
|
19
|
+
/** Maximum number of units to show, counting from the largest; the rest is rounded. Default: `2`. */
|
|
20
|
+
units?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse a human-readable duration into milliseconds.
|
|
24
|
+
*
|
|
25
|
+
* Accepts units `ms`, `s`, `m`, `h`, `d`, `w`, `y` and their long forms ("minutes", "hrs", ...),
|
|
26
|
+
* combined freely: `"1h30m"`, `"1 hour and 30 minutes"`, `"1.5h"`, `"-5m"`.
|
|
27
|
+
* A bare number such as `"100"` is read as milliseconds, and numbers are returned unchanged.
|
|
28
|
+
* A year is 365.25 days.
|
|
29
|
+
*
|
|
30
|
+
* @param input - The duration, e.g. `"1h 30m"`.
|
|
31
|
+
* @returns The duration in milliseconds.
|
|
32
|
+
* @throws {@link DurationError} if the input is empty, malformed or uses an unknown unit.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parse(input: string | number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Format milliseconds as a human-readable duration.
|
|
37
|
+
*
|
|
38
|
+
* Uses days, hours, minutes, seconds and milliseconds. Shows at most `units` units
|
|
39
|
+
* (default 2), starting from the largest, and rounds the rest.
|
|
40
|
+
*
|
|
41
|
+
* @param ms - The duration in milliseconds. May be negative.
|
|
42
|
+
* @param options - See {@link FormatOptions}.
|
|
43
|
+
* @returns For example `"1h 30m"`, or `"1 hour 30 minutes"` with `{ long: true }`.
|
|
44
|
+
* @throws {@link DurationError} if `ms` is not a finite number or `units` is not a whole number of at least 1.
|
|
45
|
+
*/
|
|
46
|
+
export declare function format(ms: number, options?: FormatOptions): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tinydur: tiny, zero-dependency duration parsing and formatting.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* import { parse, format } from "tinydur";
|
|
6
|
+
* parse("1h 30m"); // 5400000
|
|
7
|
+
* format(5400000); // "1h 30m"
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/** Thrown for invalid durations or options. The message says what went wrong and how to fix it. */
|
|
12
|
+
export class DurationError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "DurationError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const SECOND = 1000;
|
|
19
|
+
const MINUTE = 60 * SECOND;
|
|
20
|
+
const HOUR = 60 * MINUTE;
|
|
21
|
+
const DAY = 24 * HOUR;
|
|
22
|
+
const WEEK = 7 * DAY;
|
|
23
|
+
const YEAR = 365.25 * DAY;
|
|
24
|
+
const UNIT_MS = new Map();
|
|
25
|
+
for (const [ms, names] of [
|
|
26
|
+
[1, "ms msec msecs millisecond milliseconds"],
|
|
27
|
+
[SECOND, "s sec secs second seconds"],
|
|
28
|
+
[MINUTE, "m min mins minute minutes"],
|
|
29
|
+
[HOUR, "h hr hrs hour hours"],
|
|
30
|
+
[DAY, "d day days"],
|
|
31
|
+
[WEEK, "w wk wks week weeks"],
|
|
32
|
+
[YEAR, "y yr yrs year years"],
|
|
33
|
+
]) {
|
|
34
|
+
for (const name of names.split(" "))
|
|
35
|
+
UNIT_MS.set(name, ms);
|
|
36
|
+
}
|
|
37
|
+
const FORMAT_UNITS = [
|
|
38
|
+
{ ms: DAY, short: "d", long: "day" },
|
|
39
|
+
{ ms: HOUR, short: "h", long: "hour" },
|
|
40
|
+
{ ms: MINUTE, short: "m", long: "minute" },
|
|
41
|
+
{ ms: SECOND, short: "s", long: "second" },
|
|
42
|
+
{ ms: 1, short: "ms", long: "millisecond" },
|
|
43
|
+
];
|
|
44
|
+
const LAST_UNIT = FORMAT_UNITS.length - 1;
|
|
45
|
+
// One "<number><unit>" pair plus any separator after it (spaces, a comma, "and").
|
|
46
|
+
const TOKEN = /(\d+(?:\.\d+)?|\.\d+)\s*([a-z]*)\s*(?:,\s*)?(?:and\s+)?/y;
|
|
47
|
+
const EXAMPLES = `Try something like "1h 30m", "90s" or "2 days".`;
|
|
48
|
+
/**
|
|
49
|
+
* Parse a human-readable duration into milliseconds.
|
|
50
|
+
*
|
|
51
|
+
* Accepts units `ms`, `s`, `m`, `h`, `d`, `w`, `y` and their long forms ("minutes", "hrs", ...),
|
|
52
|
+
* combined freely: `"1h30m"`, `"1 hour and 30 minutes"`, `"1.5h"`, `"-5m"`.
|
|
53
|
+
* A bare number such as `"100"` is read as milliseconds, and numbers are returned unchanged.
|
|
54
|
+
* A year is 365.25 days.
|
|
55
|
+
*
|
|
56
|
+
* @param input - The duration, e.g. `"1h 30m"`.
|
|
57
|
+
* @returns The duration in milliseconds.
|
|
58
|
+
* @throws {@link DurationError} if the input is empty, malformed or uses an unknown unit.
|
|
59
|
+
*/
|
|
60
|
+
export function parse(input) {
|
|
61
|
+
if (typeof input === "number") {
|
|
62
|
+
if (!Number.isFinite(input))
|
|
63
|
+
throw new DurationError(`Expected a finite number of milliseconds, got ${input}.`);
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
if (typeof input !== "string") {
|
|
67
|
+
throw new DurationError(`Expected a duration string like "1h 30m" or a number of milliseconds, got ${describe(input)}.`);
|
|
68
|
+
}
|
|
69
|
+
let text = input.trim().toLowerCase();
|
|
70
|
+
if (text === "")
|
|
71
|
+
throw new DurationError(`Cannot parse an empty string as a duration. ${EXAMPLES}`);
|
|
72
|
+
let sign = 1;
|
|
73
|
+
if (text[0] === "-" || text[0] === "+") {
|
|
74
|
+
if (text[0] === "-")
|
|
75
|
+
sign = -1;
|
|
76
|
+
text = text.slice(1).trimStart();
|
|
77
|
+
}
|
|
78
|
+
let total = 0;
|
|
79
|
+
TOKEN.lastIndex = 0;
|
|
80
|
+
while (TOKEN.lastIndex < text.length) {
|
|
81
|
+
const start = TOKEN.lastIndex;
|
|
82
|
+
const match = TOKEN.exec(text);
|
|
83
|
+
if (!match) {
|
|
84
|
+
throw new DurationError(`Cannot parse ${quote(input)} as a duration: unexpected ${quote(text.slice(start))}. ${EXAMPLES}`);
|
|
85
|
+
}
|
|
86
|
+
const value = Number(match[1]);
|
|
87
|
+
const unit = match[2] ?? "";
|
|
88
|
+
if (unit === "") {
|
|
89
|
+
// A lone number means milliseconds; inside a longer duration it is a missing unit.
|
|
90
|
+
if (start !== 0 || TOKEN.lastIndex !== text.length) {
|
|
91
|
+
throw new DurationError(`Missing unit after ${match[1]} in ${quote(input)}. Add one, e.g. "${match[1]}m" or "${match[1]}s".`);
|
|
92
|
+
}
|
|
93
|
+
total += value;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const ms = UNIT_MS.get(unit);
|
|
97
|
+
if (ms === undefined) {
|
|
98
|
+
throw new DurationError(`Unknown unit ${quote(unit)} in ${quote(input)}. Use one of: ms, s, m, h, d, w, y (long names like "minutes" work too).`);
|
|
99
|
+
}
|
|
100
|
+
total += value * ms;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
total = Math.round(total * 1000) / 1000; // drop float noise such as 1.1 * 1000 = 1100.0000000000002
|
|
104
|
+
if (!Number.isFinite(total))
|
|
105
|
+
throw new DurationError(`${quote(input)} is too large to represent in milliseconds.`);
|
|
106
|
+
return total === 0 ? 0 : sign * total;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format milliseconds as a human-readable duration.
|
|
110
|
+
*
|
|
111
|
+
* Uses days, hours, minutes, seconds and milliseconds. Shows at most `units` units
|
|
112
|
+
* (default 2), starting from the largest, and rounds the rest.
|
|
113
|
+
*
|
|
114
|
+
* @param ms - The duration in milliseconds. May be negative.
|
|
115
|
+
* @param options - See {@link FormatOptions}.
|
|
116
|
+
* @returns For example `"1h 30m"`, or `"1 hour 30 minutes"` with `{ long: true }`.
|
|
117
|
+
* @throws {@link DurationError} if `ms` is not a finite number or `units` is not a whole number of at least 1.
|
|
118
|
+
*/
|
|
119
|
+
export function format(ms, options = {}) {
|
|
120
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) {
|
|
121
|
+
const got = typeof ms === "number" ? String(ms) : describe(ms);
|
|
122
|
+
throw new DurationError(`format() expects a finite number of milliseconds, got ${got}. Use parse() first if you have a string.`);
|
|
123
|
+
}
|
|
124
|
+
const { long = false, units = 2 } = options;
|
|
125
|
+
if (typeof units !== "number" || !Number.isInteger(units) || units < 1) {
|
|
126
|
+
throw new DurationError(`Option "units" must be a whole number of at least 1, got ${String(units)}.`);
|
|
127
|
+
}
|
|
128
|
+
const sign = ms < 0 ? "-" : "";
|
|
129
|
+
let abs = Math.round(Math.abs(ms));
|
|
130
|
+
if (abs === 0)
|
|
131
|
+
return long ? "0 milliseconds" : "0ms";
|
|
132
|
+
// Round to the smallest shown unit; repeat if rounding carried into a bigger unit (59.9s -> 1m).
|
|
133
|
+
let first = largestUnit(abs);
|
|
134
|
+
for (;;) {
|
|
135
|
+
const step = FORMAT_UNITS[Math.min(first + units - 1, LAST_UNIT)].ms;
|
|
136
|
+
abs = Math.round(abs / step) * step;
|
|
137
|
+
const next = largestUnit(abs);
|
|
138
|
+
if (next === first)
|
|
139
|
+
break;
|
|
140
|
+
first = next;
|
|
141
|
+
}
|
|
142
|
+
const last = Math.min(first + units - 1, LAST_UNIT);
|
|
143
|
+
const parts = [];
|
|
144
|
+
let rest = abs;
|
|
145
|
+
for (let i = first; i <= last; i++) {
|
|
146
|
+
const unit = FORMAT_UNITS[i];
|
|
147
|
+
const count = i === last ? Math.round(rest / unit.ms) : Math.floor(rest / unit.ms);
|
|
148
|
+
rest -= count * unit.ms;
|
|
149
|
+
if (count > 0)
|
|
150
|
+
parts.push(long ? `${count} ${unit.long}${count === 1 ? "" : "s"}` : `${count}${unit.short}`);
|
|
151
|
+
}
|
|
152
|
+
return sign + parts.join(" ");
|
|
153
|
+
}
|
|
154
|
+
function largestUnit(ms) {
|
|
155
|
+
const index = FORMAT_UNITS.findIndex((unit) => ms >= unit.ms);
|
|
156
|
+
return index === -1 ? LAST_UNIT : index;
|
|
157
|
+
}
|
|
158
|
+
function quote(text) {
|
|
159
|
+
return JSON.stringify(text.length > 40 ? `${text.slice(0, 40)}...` : text);
|
|
160
|
+
}
|
|
161
|
+
function describe(value) {
|
|
162
|
+
if (value === null)
|
|
163
|
+
return "null";
|
|
164
|
+
if (Array.isArray(value))
|
|
165
|
+
return "an array";
|
|
166
|
+
return typeof value;
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acus9345/tinydur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny, zero-dependency duration parser and formatter (\"1h 30m\" <-> milliseconds) with a CLI. A small replacement for ms, parse-duration and humanize-duration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"duration",
|
|
7
|
+
"time",
|
|
8
|
+
"ms",
|
|
9
|
+
"milliseconds",
|
|
10
|
+
"parse",
|
|
11
|
+
"format",
|
|
12
|
+
"humanize",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/acus9345/tinydur#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/acus9345/tinydur/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/acus9345/tinydur.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "acus9345 <acus9345@gmail.com>",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"tinydur": "dist/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"test": "npm run build && node --test",
|
|
50
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.6.5",
|
|
54
|
+
"typescript": "^7.0.2"
|
|
55
|
+
}
|
|
56
|
+
}
|