@acus9345/tinydur 0.1.0 → 0.2.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 +21 -12
- package/README.md +164 -117
- package/dist/bytes.d.ts +41 -0
- package/dist/bytes.js +126 -0
- package/dist/cli.js +37 -9
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -4
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
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.
|
|
6
|
-
|
|
7
|
-
### Added
|
|
8
|
-
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
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.2.0] - 2026-10-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `parseBytes()` turns sizes such as `"20MB"`, `"1.5 GiB"` or `"512kb"` into bytes. `KB`, `MB`... are powers of 1000 (or 1024 with `{ binary: true }`); `KiB`, `MiB`... are always powers of 1024.
|
|
10
|
+
- `formatBytes()` turns bytes into `"20 MB"`, `"19.07 MiB"` or `"1.5 megabytes"`, with `binary`, `decimals` and `long` options.
|
|
11
|
+
- `ByteSizeError` with messages that explain how to fix bad input.
|
|
12
|
+
- The CLI converts sizes too: `tinydur 20MB`, `tinydur bytes 20000000`, and `--binary`.
|
|
13
|
+
|
|
14
|
+
## [0.1.0] - 2026-10-16
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `parse()` turns durations such as `"1h 30m"`, `"1.5h"` or `"2 days"` into milliseconds.
|
|
19
|
+
- `format()` turns milliseconds into `"1h 30m"` or `"1 hour 30 minutes"`, with a `units` option.
|
|
20
|
+
- `DurationError` with messages that explain how to fix bad input.
|
|
21
|
+
- `tinydur` CLI with `--long`, `--units`, `--help` and `--version`.
|
package/README.md
CHANGED
|
@@ -1,117 +1,164 @@
|
|
|
1
|
-
# tinydur
|
|
2
|
-
|
|
3
|
-
Tiny, zero-dependency duration parser and formatter: `"1h 30m"` ⇄ `5400000`, with a CLI.
|
|
4
|
-
|
|
5
|
-
A small replacement for `ms`, `parse-duration` and `
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
| `"
|
|
50
|
-
| `"
|
|
51
|
-
| `"
|
|
52
|
-
| `"
|
|
53
|
-
| `"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
format(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1
|
+
# tinydur
|
|
2
|
+
|
|
3
|
+
Tiny, zero-dependency duration and byte-size parser and formatter: `"1h 30m"` ⇄ `5400000` and `"20MB"` ⇄ `20000000`, with a CLI.
|
|
4
|
+
|
|
5
|
+
A small replacement for `ms`, `parse-duration`, `humanize-duration`, `bytes` and `pretty-bytes`. 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, parseBytes, formatBytes } 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
|
+
console.log(parseBytes("20MB")); // 20000000
|
|
26
|
+
console.log(formatBytes(1610612736, { binary: true })); // 1.5 GiB
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run it:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node example.mjs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or use the CLI without installing anything:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @acus9345/tinydur 1h30m
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### `parse(input: string | number): number`
|
|
44
|
+
|
|
45
|
+
Turns a duration into milliseconds.
|
|
46
|
+
|
|
47
|
+
| Input | Result |
|
|
48
|
+
| ------------------------- | ------------ |
|
|
49
|
+
| `"1h 30m"`, `"1h30m"` | `5400000` |
|
|
50
|
+
| `"1 hour and 30 minutes"` | `5400000` |
|
|
51
|
+
| `"1.5h"` | `5400000` |
|
|
52
|
+
| `"90s"` | `90000` |
|
|
53
|
+
| `"2 days"` | `172800000` |
|
|
54
|
+
| `"-5m"` | `-300000` |
|
|
55
|
+
| `"100"` or `100` | `100` |
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
### `format(ms: number, options?): string`
|
|
60
|
+
|
|
61
|
+
Turns milliseconds into a readable duration, using days, hours, minutes, seconds and milliseconds.
|
|
62
|
+
|
|
63
|
+
| Option | Default | Meaning |
|
|
64
|
+
| ------- | ------- | ---------------------------------------------------------------- |
|
|
65
|
+
| `long` | `false` | `"1 hour 30 minutes"` instead of `"1h 30m"` |
|
|
66
|
+
| `units` | `2` | Maximum number of units, counting from the largest; the rest is rounded |
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
format(90061001); // "1d 1h"
|
|
70
|
+
format(90061001, { units: 4 }); // "1d 1h 1m 1s"
|
|
71
|
+
format(59999, { units: 1 }); // "1m"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `parseBytes(input: string | number, options?): number`
|
|
75
|
+
|
|
76
|
+
Turns a size into bytes.
|
|
77
|
+
|
|
78
|
+
| Input | Result |
|
|
79
|
+
| ------------------------ | --------------- |
|
|
80
|
+
| `"20MB"`, `"20 mb"` | `20000000` |
|
|
81
|
+
| `"1.5GB"` | `1500000000` |
|
|
82
|
+
| `"1.5 GiB"` | `1610612736` |
|
|
83
|
+
| `"512 KiB"` | `524288` |
|
|
84
|
+
| `"1GB 512MB"` | `1512000000` |
|
|
85
|
+
| `"2 megabytes"` | `2000000` |
|
|
86
|
+
| `"100"` or `100` | `100` |
|
|
87
|
+
|
|
88
|
+
`kB`, `MB`, `GB`, `TB`, `PB` and `EB` are powers of 1000, as in the SI standard and on macOS and Linux. Pass `{ binary: true }` to read them as powers of 1024, like Windows does. `KiB`, `MiB`, `GiB`... are always powers of 1024. Short forms (`k`, `m`, `g`) and long names (`kilobytes`, `gibibytes`) work too, case doesn't matter, and the result is rounded to whole bytes.
|
|
89
|
+
|
|
90
|
+
### `formatBytes(bytes: number, options?): string`
|
|
91
|
+
|
|
92
|
+
| Option | Default | Meaning |
|
|
93
|
+
| ---------- | ------- | ---------------------------------------------------------- |
|
|
94
|
+
| `binary` | `false` | Powers of 1024 with `KiB`, `MiB`... instead of `kB`, `MB` |
|
|
95
|
+
| `decimals` | `2` | Most digits after the decimal point; trailing zeros dropped |
|
|
96
|
+
| `long` | `false` | `"20 megabytes"` instead of `"20 MB"` |
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
formatBytes(20000000); // "20 MB"
|
|
100
|
+
formatBytes(20000000, { binary: true }); // "19.07 MiB"
|
|
101
|
+
formatBytes(1536); // "1.54 kB"
|
|
102
|
+
formatBytes(1500000, { long: true }); // "1.5 megabytes"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Bad input throws a `ByteSizeError` that explains the fix, just like `DurationError` below.
|
|
106
|
+
|
|
107
|
+
### `DurationError`
|
|
108
|
+
|
|
109
|
+
Both functions throw a `DurationError` (a subclass of `Error`) for bad input. Its message says what was wrong and how to fix it:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
import { parse, DurationError } from "@acus9345/tinydur";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
parse("5 parsecs");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof DurationError) console.log(error.message);
|
|
118
|
+
// Unknown unit "parsecs" in "5 parsecs". Use one of: ms, s, m, h, d, w, y (long names like "minutes" work too).
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## CLI
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx @acus9345/tinydur 1h30m
|
|
126
|
+
npx @acus9345/tinydur "2 days 4 hours"
|
|
127
|
+
npx @acus9345/tinydur 5400000 --long
|
|
128
|
+
npx @acus9345/tinydur 90061001 --units 4
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Prints `5400000`, `187200000`, `1 hour 30 minutes` and `1d 1h 1m 1s`. A plain number is formatted; anything else is parsed to milliseconds.
|
|
132
|
+
|
|
133
|
+
Sizes work the same way. Anything with a byte unit is converted to bytes, and `bytes` formats a number of bytes:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx @acus9345/tinydur 20MB # 20000000
|
|
137
|
+
npx @acus9345/tinydur 1.5GiB # 1610612736
|
|
138
|
+
npx @acus9345/tinydur bytes 20000000 # 20 MB
|
|
139
|
+
npx @acus9345/tinydur bytes 20000000 --binary # 19.07 MiB
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`20m` on its own still means 20 minutes; write `20MB`, or `tinydur bytes 20m`, for megabytes.
|
|
143
|
+
|
|
144
|
+
| Option | Meaning |
|
|
145
|
+
| ----------------- | -------------------------------------------- |
|
|
146
|
+
| `-l`, `--long` | Long unit names |
|
|
147
|
+
| `-u`, `--units N` | Maximum number of units when formatting |
|
|
148
|
+
| `-b`, `--binary` | Sizes in powers of 1024 (`KiB`, `MiB`...) |
|
|
149
|
+
| `-h`, `--help` | Show help |
|
|
150
|
+
| `-v`, `--version` | Show version |
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
## Troubleshooting
|
|
155
|
+
|
|
156
|
+
- **`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.
|
|
157
|
+
- **`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.
|
|
158
|
+
- **`m` means minutes**: there is no month unit because months vary in length. Use days or weeks.
|
|
159
|
+
- **`format("5000")` throws**: `format` takes a number. Call `parse` first if you have a string.
|
|
160
|
+
- **PowerShell treats `-5m` oddly in scripts**: quote it: `npx @acus9345/tinydur "-5m"`.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Thrown for invalid byte sizes or options. The message says what went wrong and how to fix it. */
|
|
2
|
+
export declare class ByteSizeError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Options for {@link parseBytes}. */
|
|
6
|
+
export interface ParseBytesOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Read `KB`, `MB`, `GB`... as powers of 1024, like Windows and many older tools.
|
|
9
|
+
* Default: `false`, so `1 MB` is 1,000,000 bytes (SI). `KiB`, `MiB`... are always powers of 1024.
|
|
10
|
+
*/
|
|
11
|
+
binary?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Options for {@link formatBytes}. */
|
|
14
|
+
export interface FormatBytesOptions {
|
|
15
|
+
/** Use powers of 1024 and `KiB`, `MiB`... Default: `false` (powers of 1000, `kB`, `MB`...). */
|
|
16
|
+
binary?: boolean;
|
|
17
|
+
/** Most digits after the decimal point; trailing zeros are dropped. Default: `2`. */
|
|
18
|
+
decimals?: number;
|
|
19
|
+
/** Long unit names: `"1.5 megabytes"`. Default: `false`. */
|
|
20
|
+
long?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Byte units that can't be mistaken for anything else (used by the CLI to tell sizes from durations). */
|
|
23
|
+
export declare const UNAMBIGUOUS_BYTE_UNIT: RegExp;
|
|
24
|
+
/**
|
|
25
|
+
* Parse a human-readable size into bytes.
|
|
26
|
+
*
|
|
27
|
+
* Units: `B`, `kB`/`KB`, `MB`, `GB`, `TB`, `PB`, `EB` (powers of 1000, or 1024 with
|
|
28
|
+
* `{ binary: true }`), `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB` (always 1024), the
|
|
29
|
+
* short forms `k`, `m`, `g`... and long names (`megabytes`, `gibibytes`). Case and
|
|
30
|
+
* spacing don't matter, decimals work (`"1.5GB"`), parts can be combined (`"1GB 512MB"`),
|
|
31
|
+
* and a bare number is bytes. The result is rounded to whole bytes.
|
|
32
|
+
*
|
|
33
|
+
* @throws {@link ByteSizeError} if the input is empty, malformed or uses an unknown unit.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseBytes(input: string | number, options?: ParseBytesOptions): number;
|
|
36
|
+
/**
|
|
37
|
+
* Format a number of bytes, like `"20 MB"`, `"19.07 MiB"` or `"1.5 kilobytes"`.
|
|
38
|
+
*
|
|
39
|
+
* @throws {@link ByteSizeError} if `bytes` is not a finite number or `decimals` is invalid.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatBytes(bytes: number, options?: FormatBytesOptions): string;
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Thrown for invalid byte sizes or options. The message says what went wrong and how to fix it. */
|
|
2
|
+
export class ByteSizeError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ByteSizeError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const PREFIXES = ["", "k", "m", "g", "t", "p", "e"];
|
|
9
|
+
const LONG_PREFIXES = ["", "kilo", "mega", "giga", "tera", "peta", "exa"];
|
|
10
|
+
const LONG_BINARY = ["", "kibi", "mebi", "gibi", "tebi", "pebi", "exbi"];
|
|
11
|
+
/** unit name → [power, binary?]; `undefined` binary means "decimal unless the binary option is set". */
|
|
12
|
+
const UNITS = new Map();
|
|
13
|
+
for (const [power, prefix] of PREFIXES.entries()) {
|
|
14
|
+
if (power === 0) {
|
|
15
|
+
for (const name of ["b", "byte", "bytes"])
|
|
16
|
+
UNITS.set(name, [0, false]);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
UNITS.set(`${prefix}b`, [power, undefined]);
|
|
20
|
+
UNITS.set(prefix, [power, undefined]);
|
|
21
|
+
UNITS.set(`${prefix}ib`, [power, true]);
|
|
22
|
+
UNITS.set(`${LONG_PREFIXES[power]}byte`, [power, undefined]);
|
|
23
|
+
UNITS.set(`${LONG_PREFIXES[power]}bytes`, [power, undefined]);
|
|
24
|
+
UNITS.set(`${LONG_BINARY[power]}byte`, [power, true]);
|
|
25
|
+
UNITS.set(`${LONG_BINARY[power]}bytes`, [power, true]);
|
|
26
|
+
}
|
|
27
|
+
const TOKEN = /(\d+(?:\.\d+)?|\.\d+)\s*([a-z]*)\s*(?:,\s*)?/y;
|
|
28
|
+
const EXAMPLES = `Try something like "20MB", "1.5 GiB" or "512kb".`;
|
|
29
|
+
/** Byte units that can't be mistaken for anything else (used by the CLI to tell sizes from durations). */
|
|
30
|
+
export const UNAMBIGUOUS_BYTE_UNIT = /^(?:b|bytes?|[kmgtpe]i?b|(?:kilo|mega|giga|tera|peta|exa|kibi|mebi|gibi|tebi|pebi|exbi)bytes?)$/i;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a human-readable size into bytes.
|
|
33
|
+
*
|
|
34
|
+
* Units: `B`, `kB`/`KB`, `MB`, `GB`, `TB`, `PB`, `EB` (powers of 1000, or 1024 with
|
|
35
|
+
* `{ binary: true }`), `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB` (always 1024), the
|
|
36
|
+
* short forms `k`, `m`, `g`... and long names (`megabytes`, `gibibytes`). Case and
|
|
37
|
+
* spacing don't matter, decimals work (`"1.5GB"`), parts can be combined (`"1GB 512MB"`),
|
|
38
|
+
* and a bare number is bytes. The result is rounded to whole bytes.
|
|
39
|
+
*
|
|
40
|
+
* @throws {@link ByteSizeError} if the input is empty, malformed or uses an unknown unit.
|
|
41
|
+
*/
|
|
42
|
+
export function parseBytes(input, options = {}) {
|
|
43
|
+
if (typeof input === "number") {
|
|
44
|
+
if (!Number.isFinite(input))
|
|
45
|
+
throw new ByteSizeError(`Expected a finite number of bytes, got ${input}.`);
|
|
46
|
+
return Math.round(input);
|
|
47
|
+
}
|
|
48
|
+
if (typeof input !== "string") {
|
|
49
|
+
throw new ByteSizeError(`Expected a size string like "20MB" or a number of bytes, got ${input === null ? "null" : typeof input}.`);
|
|
50
|
+
}
|
|
51
|
+
let text = input.trim().toLowerCase();
|
|
52
|
+
if (text === "")
|
|
53
|
+
throw new ByteSizeError(`Cannot parse an empty string as a size. ${EXAMPLES}`);
|
|
54
|
+
let sign = 1;
|
|
55
|
+
if (text[0] === "-" || text[0] === "+") {
|
|
56
|
+
if (text[0] === "-")
|
|
57
|
+
sign = -1;
|
|
58
|
+
text = text.slice(1).trimStart();
|
|
59
|
+
}
|
|
60
|
+
let total = 0;
|
|
61
|
+
TOKEN.lastIndex = 0;
|
|
62
|
+
while (TOKEN.lastIndex < text.length) {
|
|
63
|
+
const start = TOKEN.lastIndex;
|
|
64
|
+
const match = TOKEN.exec(text);
|
|
65
|
+
if (!match)
|
|
66
|
+
throw new ByteSizeError(`Cannot parse ${quote(input)} as a size: unexpected ${quote(text.slice(start))}. ${EXAMPLES}`);
|
|
67
|
+
const value = Number(match[1]);
|
|
68
|
+
const unit = match[2] ?? "";
|
|
69
|
+
if (unit === "") {
|
|
70
|
+
if (start !== 0 || TOKEN.lastIndex !== text.length) {
|
|
71
|
+
throw new ByteSizeError(`Missing unit after ${match[1]} in ${quote(input)}. Add one, e.g. "${match[1]}MB".`);
|
|
72
|
+
}
|
|
73
|
+
total += value;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const found = UNITS.get(unit);
|
|
77
|
+
if (found === undefined) {
|
|
78
|
+
throw new ByteSizeError(`Unknown unit ${quote(unit)} in ${quote(input)}. Use B, kB, MB, GB, TB, PB, or KiB, MiB, GiB, TiB, PiB.`);
|
|
79
|
+
}
|
|
80
|
+
const [power, binary] = found;
|
|
81
|
+
const base = (binary ?? options.binary === true) ? 1024 : 1000;
|
|
82
|
+
total += value * base ** power;
|
|
83
|
+
}
|
|
84
|
+
const bytes = Math.round(total);
|
|
85
|
+
if (!Number.isFinite(bytes))
|
|
86
|
+
throw new ByteSizeError(`${quote(input)} is too large to represent.`);
|
|
87
|
+
return bytes === 0 ? 0 : sign * bytes;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format a number of bytes, like `"20 MB"`, `"19.07 MiB"` or `"1.5 kilobytes"`.
|
|
91
|
+
*
|
|
92
|
+
* @throws {@link ByteSizeError} if `bytes` is not a finite number or `decimals` is invalid.
|
|
93
|
+
*/
|
|
94
|
+
export function formatBytes(bytes, options = {}) {
|
|
95
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes)) {
|
|
96
|
+
throw new ByteSizeError(`formatBytes() expects a finite number of bytes, got ${String(bytes)}. Use parseBytes() first if you have a string.`);
|
|
97
|
+
}
|
|
98
|
+
const { binary = false, decimals = 2, long = false } = options;
|
|
99
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 20) {
|
|
100
|
+
throw new ByteSizeError(`Option "decimals" must be a whole number from 0 to 20, got ${String(decimals)}.`);
|
|
101
|
+
}
|
|
102
|
+
const base = binary ? 1024 : 1000;
|
|
103
|
+
const sign = bytes < 0 ? "-" : "";
|
|
104
|
+
let value = Math.abs(bytes);
|
|
105
|
+
let power = 0;
|
|
106
|
+
while (value >= base && power < PREFIXES.length - 1) {
|
|
107
|
+
value /= base;
|
|
108
|
+
power++;
|
|
109
|
+
}
|
|
110
|
+
let rounded = Number(value.toFixed(power === 0 ? 0 : decimals));
|
|
111
|
+
// 999.999 kB rounds to 1000 kB: move up a unit.
|
|
112
|
+
if (rounded >= base && power < PREFIXES.length - 1) {
|
|
113
|
+
power++;
|
|
114
|
+
rounded = Number((rounded / base).toFixed(decimals));
|
|
115
|
+
}
|
|
116
|
+
const number = String(rounded);
|
|
117
|
+
if (long) {
|
|
118
|
+
const name = power === 0 ? "byte" : `${(binary ? LONG_BINARY : LONG_PREFIXES)[power]}byte`;
|
|
119
|
+
return `${sign}${number} ${name}${rounded === 1 ? "" : "s"}`;
|
|
120
|
+
}
|
|
121
|
+
const unit = power === 0 ? "B" : binary ? `${PREFIXES[power].toUpperCase()}iB` : `${power === 1 ? "k" : PREFIXES[power].toUpperCase()}B`;
|
|
122
|
+
return `${sign}${number} ${unit}`;
|
|
123
|
+
}
|
|
124
|
+
function quote(text) {
|
|
125
|
+
return JSON.stringify(text.length > 40 ? `${text.slice(0, 40)}...` : text);
|
|
126
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { DurationError, format, parse } from "./index.js";
|
|
4
|
-
|
|
3
|
+
import { ByteSizeError, DurationError, format, formatBytes, parse, parseBytes } from "./index.js";
|
|
4
|
+
import { UNAMBIGUOUS_BYTE_UNIT } from "./bytes.js";
|
|
5
|
+
const HELP = `tinydur - convert between readable durations or sizes and numbers
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
7
|
-
tinydur <duration>
|
|
8
|
-
tinydur <milliseconds>
|
|
8
|
+
tinydur <duration> Print the duration in milliseconds
|
|
9
|
+
tinydur <milliseconds> Print the milliseconds as a readable duration
|
|
10
|
+
tinydur <size> Print the size in bytes (when it has a byte unit, like 20MB)
|
|
11
|
+
tinydur bytes <size|bytes> Convert sizes either way
|
|
9
12
|
|
|
10
13
|
Options:
|
|
11
|
-
-l, --long Use long unit names ("1 hour 30 minutes")
|
|
12
|
-
-u, --units <n> Maximum number of units when formatting (default: 2)
|
|
14
|
+
-l, --long Use long unit names ("1 hour 30 minutes", "20 megabytes")
|
|
15
|
+
-u, --units <n> Maximum number of units when formatting durations (default: 2)
|
|
16
|
+
-b, --binary Sizes: KB, MB... mean powers of 1024, and output uses KiB, MiB...
|
|
13
17
|
-h, --help Show this help
|
|
14
18
|
-v, --version Show the version number
|
|
15
19
|
|
|
@@ -19,12 +23,18 @@ Examples:
|
|
|
19
23
|
tinydur 5400000 1h 30m
|
|
20
24
|
tinydur 5400000 --long 1 hour 30 minutes
|
|
21
25
|
tinydur 90061001 --units 4 1d 1h 1m 1s
|
|
26
|
+
tinydur 20MB 20000000
|
|
27
|
+
tinydur 1.5GiB 1610612736
|
|
28
|
+
tinydur bytes 20000000 20 MB
|
|
29
|
+
tinydur bytes 20000000 -b 19.07 MiB
|
|
22
30
|
`;
|
|
23
31
|
class UsageError extends Error {
|
|
24
32
|
}
|
|
33
|
+
const NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
|
|
25
34
|
function main(argv) {
|
|
26
35
|
const values = [];
|
|
27
36
|
let long = false;
|
|
37
|
+
let binary = false;
|
|
28
38
|
let units;
|
|
29
39
|
for (let i = 0; i < argv.length; i++) {
|
|
30
40
|
const arg = argv[i];
|
|
@@ -40,6 +50,9 @@ function main(argv) {
|
|
|
40
50
|
if (arg === "-l" || arg === "--long") {
|
|
41
51
|
long = true;
|
|
42
52
|
}
|
|
53
|
+
else if (arg === "-b" || arg === "--binary") {
|
|
54
|
+
binary = true;
|
|
55
|
+
}
|
|
43
56
|
else if (arg === "-u" || arg === "--units" || arg.startsWith("--units=")) {
|
|
44
57
|
const raw = arg.startsWith("--units=") ? arg.slice("--units=".length) : argv[++i];
|
|
45
58
|
if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1) {
|
|
@@ -54,17 +67,32 @@ function main(argv) {
|
|
|
54
67
|
values.push(arg);
|
|
55
68
|
}
|
|
56
69
|
}
|
|
70
|
+
let bytesMode = false;
|
|
71
|
+
if (values[0]?.toLowerCase() === "bytes" || values[0]?.toLowerCase() === "size") {
|
|
72
|
+
bytesMode = true;
|
|
73
|
+
values.shift();
|
|
74
|
+
}
|
|
57
75
|
if (values.length === 0)
|
|
58
|
-
throw new UsageError(`Missing duration. Example: tinydur 1h30m`);
|
|
76
|
+
throw new UsageError(bytesMode ? `Missing size. Example: tinydur bytes 20MB` : `Missing duration. Example: tinydur 1h30m`);
|
|
59
77
|
const input = values.join(" ").trim();
|
|
60
|
-
|
|
78
|
+
// A size with an unmistakable byte unit (20MB, 1.5 GiB) is converted to bytes even without "bytes".
|
|
79
|
+
const units_ = input.toLowerCase().match(/[a-z]+/g) ?? [];
|
|
80
|
+
if (!bytesMode && units_.length > 0 && units_.every((u) => UNAMBIGUOUS_BYTE_UNIT.test(u)))
|
|
81
|
+
bytesMode = true;
|
|
82
|
+
let output;
|
|
83
|
+
if (bytesMode) {
|
|
84
|
+
output = NUMBER.test(input) ? formatBytes(Number(input), { binary, long }) : String(parseBytes(input, { binary }));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
output = NUMBER.test(input) ? format(Number(input), { long, units }) : String(parse(input));
|
|
88
|
+
}
|
|
61
89
|
process.stdout.write(`${output}\n`);
|
|
62
90
|
}
|
|
63
91
|
try {
|
|
64
92
|
main(process.argv.slice(2));
|
|
65
93
|
}
|
|
66
94
|
catch (error) {
|
|
67
|
-
if (!(error instanceof DurationError || error instanceof UsageError))
|
|
95
|
+
if (!(error instanceof DurationError || error instanceof ByteSizeError || error instanceof UsageError))
|
|
68
96
|
throw error;
|
|
69
97
|
process.stderr.write(`tinydur: ${error.message}\nRun "tinydur --help" for usage.\n`);
|
|
70
98
|
process.exitCode = 1;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tinydur: tiny, zero-dependency duration parsing and formatting.
|
|
2
|
+
* tinydur: tiny, zero-dependency duration and byte-size parsing and formatting.
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
|
-
* import { parse, format } from "tinydur";
|
|
6
|
-
* parse("1h 30m");
|
|
7
|
-
* format(5400000);
|
|
5
|
+
* import { parse, format, parseBytes, formatBytes } from "@acus9345/tinydur";
|
|
6
|
+
* parse("1h 30m"); // 5400000
|
|
7
|
+
* format(5400000); // "1h 30m"
|
|
8
|
+
* parseBytes("20MB"); // 20000000
|
|
9
|
+
* formatBytes(20000000); // "20 MB"
|
|
8
10
|
*
|
|
9
11
|
* @packageDocumentation
|
|
10
12
|
*/
|
|
13
|
+
export { ByteSizeError, formatBytes, parseBytes } from "./bytes.js";
|
|
14
|
+
export type { FormatBytesOptions, ParseBytesOptions } from "./bytes.js";
|
|
11
15
|
/** Thrown for invalid durations or options. The message says what went wrong and how to fix it. */
|
|
12
16
|
export declare class DurationError extends Error {
|
|
13
17
|
constructor(message: string);
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tinydur: tiny, zero-dependency duration parsing and formatting.
|
|
2
|
+
* tinydur: tiny, zero-dependency duration and byte-size parsing and formatting.
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
|
-
* import { parse, format } from "tinydur";
|
|
6
|
-
* parse("1h 30m");
|
|
7
|
-
* format(5400000);
|
|
5
|
+
* import { parse, format, parseBytes, formatBytes } from "@acus9345/tinydur";
|
|
6
|
+
* parse("1h 30m"); // 5400000
|
|
7
|
+
* format(5400000); // "1h 30m"
|
|
8
|
+
* parseBytes("20MB"); // 20000000
|
|
9
|
+
* formatBytes(20000000); // "20 MB"
|
|
8
10
|
*
|
|
9
11
|
* @packageDocumentation
|
|
10
12
|
*/
|
|
13
|
+
export { ByteSizeError, formatBytes, parseBytes } from "./bytes.js";
|
|
11
14
|
/** Thrown for invalid durations or options. The message says what went wrong and how to fix it. */
|
|
12
15
|
export class DurationError extends Error {
|
|
13
16
|
constructor(message) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acus9345/tinydur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Tiny, zero-dependency duration parser and formatter (\"1h 30m\" <-> milliseconds) with a CLI. A small replacement for ms, parse-duration and
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Tiny, zero-dependency duration and byte-size parser and formatter (\"1h 30m\" <-> milliseconds, \"20MB\" <-> bytes) with a CLI. A small replacement for ms, parse-duration, humanize-duration, bytes and pretty-bytes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"duration",
|
|
7
7
|
"time",
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
"parse",
|
|
11
11
|
"format",
|
|
12
12
|
"humanize",
|
|
13
|
-
"cli"
|
|
13
|
+
"cli",
|
|
14
|
+
"bytes",
|
|
15
|
+
"filesize",
|
|
16
|
+
"size",
|
|
17
|
+
"pretty-bytes",
|
|
18
|
+
"kib",
|
|
19
|
+
"mib"
|
|
14
20
|
],
|
|
15
21
|
"homepage": "https://github.com/acus9345/tinydur#readme",
|
|
16
22
|
"bugs": {
|