@padmaj/duration 1.0.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/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @padmaj/duration
2
+
3
+ Parse and format duration strings. Zero dependencies. TypeScript-first. Works in Node and browser.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @padmaj/duration
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Parse a duration string → milliseconds
14
+
15
+ ```ts
16
+ import { parseDuration } from '@padmaj/duration'
17
+
18
+ parseDuration('500ms') // → 500
19
+ parseDuration('30s') // → 30000
20
+ parseDuration('5m') // → 300000
21
+ parseDuration('2h') // → 7200000
22
+ parseDuration('1d') // → 86400000
23
+ parseDuration('1w') // → 604800000
24
+
25
+ // Compound durations
26
+ parseDuration('2h30m') // → 9000000
27
+ parseDuration('1d12h') // → 129600000
28
+ parseDuration('2h 30m') // → 9000000 (spaces OK)
29
+ ```
30
+
31
+ ### Format milliseconds → duration string
32
+
33
+ ```ts
34
+ import { formatDuration } from '@padmaj/duration'
35
+
36
+ formatDuration(500) // → '500ms'
37
+ formatDuration(30000) // → '30s'
38
+ formatDuration(9000000) // → '2h 30m'
39
+ formatDuration(129600000) // → '1d 12h'
40
+ formatDuration(0) // → '0ms'
41
+ ```
42
+
43
+ ## Supported units
44
+
45
+ | Unit | Meaning |
46
+ |---|---|
47
+ | `ms` | milliseconds |
48
+ | `s` | seconds |
49
+ | `m` | minutes |
50
+ | `h` | hours |
51
+ | `d` | days |
52
+ | `w` | weeks |
53
+
54
+ ## API
55
+
56
+ ### `parseDuration(input: string): number`
57
+
58
+ Parses a duration string and returns milliseconds. Throws `DurationError` if the input is empty or contains no recognizable units.
59
+
60
+ ### `formatDuration(ms: number): string`
61
+
62
+ Formats milliseconds into a human-readable duration string. Throws `RangeError` if the value is negative, `NaN`, or `Infinity`.
63
+
64
+ ### `DurationError`
65
+
66
+ Extends `Error`. Thrown by `parseDuration` on invalid input.
67
+
68
+ ```ts
69
+ import { parseDuration, DurationError } from '@padmaj/duration'
70
+
71
+ try {
72
+ parseDuration('abc')
73
+ } catch (e) {
74
+ if (e instanceof DurationError) {
75
+ console.error('Bad duration:', e.message)
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,7 @@
1
+ declare class DurationError extends Error {
2
+ constructor(input: string);
3
+ }
4
+ declare function parseDuration(input: string): number;
5
+ declare function formatDuration(ms: number): string;
6
+
7
+ export { DurationError, formatDuration, parseDuration };
@@ -0,0 +1,7 @@
1
+ declare class DurationError extends Error {
2
+ constructor(input: string);
3
+ }
4
+ declare function parseDuration(input: string): number;
5
+ declare function formatDuration(ms: number): string;
6
+
7
+ export { DurationError, formatDuration, parseDuration };
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DurationError: () => DurationError,
24
+ formatDuration: () => formatDuration,
25
+ parseDuration: () => parseDuration
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var UNITS = {
29
+ ms: 1,
30
+ s: 1e3,
31
+ m: 6e4,
32
+ h: 36e5,
33
+ d: 864e5,
34
+ w: 6048e5
35
+ };
36
+ var PARSE_RE = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/g;
37
+ var DurationError = class extends Error {
38
+ constructor(input) {
39
+ super(`Invalid duration string: "${input}"`);
40
+ this.name = "DurationError";
41
+ }
42
+ };
43
+ function parseDuration(input) {
44
+ const str = input.trim();
45
+ if (!str) throw new DurationError(input);
46
+ let total = 0;
47
+ let matched = false;
48
+ PARSE_RE.lastIndex = 0;
49
+ for (const match of str.matchAll(PARSE_RE)) {
50
+ total += parseFloat(match[1]) * UNITS[match[2]];
51
+ matched = true;
52
+ }
53
+ if (!matched) throw new DurationError(input);
54
+ return Math.round(total);
55
+ }
56
+ function formatDuration(ms) {
57
+ if (!Number.isFinite(ms) || ms < 0) {
58
+ throw new RangeError(`formatDuration expects a non-negative finite number, got ${ms}`);
59
+ }
60
+ if (ms === 0) return "0ms";
61
+ const units = [
62
+ ["w", UNITS.w],
63
+ ["d", UNITS.d],
64
+ ["h", UNITS.h],
65
+ ["m", UNITS.m],
66
+ ["s", UNITS.s],
67
+ ["ms", UNITS.ms]
68
+ ];
69
+ const parts = [];
70
+ let remaining = Math.round(ms);
71
+ for (const [unit, value] of units) {
72
+ if (remaining >= value) {
73
+ const count = Math.floor(remaining / value);
74
+ parts.push(`${count}${unit}`);
75
+ remaining %= value;
76
+ }
77
+ }
78
+ return parts.join(" ");
79
+ }
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ DurationError,
83
+ formatDuration,
84
+ parseDuration
85
+ });
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["const UNITS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n}\n\nconst PARSE_RE = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d|w)/g\n\nexport class DurationError extends Error {\n constructor(input: string) {\n super(`Invalid duration string: \"${input}\"`)\n this.name = 'DurationError'\n }\n}\n\nexport function parseDuration(input: string): number {\n const str = input.trim()\n if (!str) throw new DurationError(input)\n\n let total = 0\n let matched = false\n\n PARSE_RE.lastIndex = 0\n for (const match of str.matchAll(PARSE_RE)) {\n total += parseFloat(match[1]) * UNITS[match[2]]\n matched = true\n }\n\n if (!matched) throw new DurationError(input)\n return Math.round(total)\n}\n\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) {\n throw new RangeError(`formatDuration expects a non-negative finite number, got ${ms}`)\n }\n\n if (ms === 0) return '0ms'\n\n const units: [string, number][] = [\n ['w', UNITS.w],\n ['d', UNITS.d],\n ['h', UNITS.h],\n ['m', UNITS.m],\n ['s', UNITS.s],\n ['ms', UNITS.ms],\n ]\n\n const parts: string[] = []\n let remaining = Math.round(ms)\n\n for (const [unit, value] of units) {\n if (remaining >= value) {\n const count = Math.floor(remaining / value)\n parts.push(`${count}${unit}`)\n remaining %= value\n }\n }\n\n return parts.join(' ')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAM,QAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AACN;AAEA,IAAM,WAAW;AAEV,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YAAY,OAAe;AACzB,UAAM,6BAA6B,KAAK,GAAG;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,cAAc,OAAuB;AACnD,QAAM,MAAM,MAAM,KAAK;AACvB,MAAI,CAAC,IAAK,OAAM,IAAI,cAAc,KAAK;AAEvC,MAAI,QAAQ;AACZ,MAAI,UAAU;AAEd,WAAS,YAAY;AACrB,aAAW,SAAS,IAAI,SAAS,QAAQ,GAAG;AAC1C,aAAS,WAAW,MAAM,CAAC,CAAC,IAAI,MAAM,MAAM,CAAC,CAAC;AAC9C,cAAU;AAAA,EACZ;AAEA,MAAI,CAAC,QAAS,OAAM,IAAI,cAAc,KAAK;AAC3C,SAAO,KAAK,MAAM,KAAK;AACzB;AAEO,SAAS,eAAe,IAAoB;AACjD,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,KAAK,GAAG;AAClC,UAAM,IAAI,WAAW,4DAA4D,EAAE,EAAE;AAAA,EACvF;AAEA,MAAI,OAAO,EAAG,QAAO;AAErB,QAAM,QAA4B;AAAA,IAChC,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,MAAM,MAAM,EAAE;AAAA,EACjB;AAEA,QAAM,QAAkB,CAAC;AACzB,MAAI,YAAY,KAAK,MAAM,EAAE;AAE7B,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,QAAI,aAAa,OAAO;AACtB,YAAM,QAAQ,KAAK,MAAM,YAAY,KAAK;AAC1C,YAAM,KAAK,GAAG,KAAK,GAAG,IAAI,EAAE;AAC5B,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,GAAG;AACvB;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,59 @@
1
+ // src/index.ts
2
+ var UNITS = {
3
+ ms: 1,
4
+ s: 1e3,
5
+ m: 6e4,
6
+ h: 36e5,
7
+ d: 864e5,
8
+ w: 6048e5
9
+ };
10
+ var PARSE_RE = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/g;
11
+ var DurationError = class extends Error {
12
+ constructor(input) {
13
+ super(`Invalid duration string: "${input}"`);
14
+ this.name = "DurationError";
15
+ }
16
+ };
17
+ function parseDuration(input) {
18
+ const str = input.trim();
19
+ if (!str) throw new DurationError(input);
20
+ let total = 0;
21
+ let matched = false;
22
+ PARSE_RE.lastIndex = 0;
23
+ for (const match of str.matchAll(PARSE_RE)) {
24
+ total += parseFloat(match[1]) * UNITS[match[2]];
25
+ matched = true;
26
+ }
27
+ if (!matched) throw new DurationError(input);
28
+ return Math.round(total);
29
+ }
30
+ function formatDuration(ms) {
31
+ if (!Number.isFinite(ms) || ms < 0) {
32
+ throw new RangeError(`formatDuration expects a non-negative finite number, got ${ms}`);
33
+ }
34
+ if (ms === 0) return "0ms";
35
+ const units = [
36
+ ["w", UNITS.w],
37
+ ["d", UNITS.d],
38
+ ["h", UNITS.h],
39
+ ["m", UNITS.m],
40
+ ["s", UNITS.s],
41
+ ["ms", UNITS.ms]
42
+ ];
43
+ const parts = [];
44
+ let remaining = Math.round(ms);
45
+ for (const [unit, value] of units) {
46
+ if (remaining >= value) {
47
+ const count = Math.floor(remaining / value);
48
+ parts.push(`${count}${unit}`);
49
+ remaining %= value;
50
+ }
51
+ }
52
+ return parts.join(" ");
53
+ }
54
+ export {
55
+ DurationError,
56
+ formatDuration,
57
+ parseDuration
58
+ };
59
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["const UNITS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n}\n\nconst PARSE_RE = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d|w)/g\n\nexport class DurationError extends Error {\n constructor(input: string) {\n super(`Invalid duration string: \"${input}\"`)\n this.name = 'DurationError'\n }\n}\n\nexport function parseDuration(input: string): number {\n const str = input.trim()\n if (!str) throw new DurationError(input)\n\n let total = 0\n let matched = false\n\n PARSE_RE.lastIndex = 0\n for (const match of str.matchAll(PARSE_RE)) {\n total += parseFloat(match[1]) * UNITS[match[2]]\n matched = true\n }\n\n if (!matched) throw new DurationError(input)\n return Math.round(total)\n}\n\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) {\n throw new RangeError(`formatDuration expects a non-negative finite number, got ${ms}`)\n }\n\n if (ms === 0) return '0ms'\n\n const units: [string, number][] = [\n ['w', UNITS.w],\n ['d', UNITS.d],\n ['h', UNITS.h],\n ['m', UNITS.m],\n ['s', UNITS.s],\n ['ms', UNITS.ms],\n ]\n\n const parts: string[] = []\n let remaining = Math.round(ms)\n\n for (const [unit, value] of units) {\n if (remaining >= value) {\n const count = Math.floor(remaining / value)\n parts.push(`${count}${unit}`)\n remaining %= value\n }\n }\n\n return parts.join(' ')\n}\n"],"mappings":";AAAA,IAAM,QAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AAAA,EACJ,GAAI;AACN;AAEA,IAAM,WAAW;AAEV,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YAAY,OAAe;AACzB,UAAM,6BAA6B,KAAK,GAAG;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,cAAc,OAAuB;AACnD,QAAM,MAAM,MAAM,KAAK;AACvB,MAAI,CAAC,IAAK,OAAM,IAAI,cAAc,KAAK;AAEvC,MAAI,QAAQ;AACZ,MAAI,UAAU;AAEd,WAAS,YAAY;AACrB,aAAW,SAAS,IAAI,SAAS,QAAQ,GAAG;AAC1C,aAAS,WAAW,MAAM,CAAC,CAAC,IAAI,MAAM,MAAM,CAAC,CAAC;AAC9C,cAAU;AAAA,EACZ;AAEA,MAAI,CAAC,QAAS,OAAM,IAAI,cAAc,KAAK;AAC3C,SAAO,KAAK,MAAM,KAAK;AACzB;AAEO,SAAS,eAAe,IAAoB;AACjD,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,KAAK,GAAG;AAClC,UAAM,IAAI,WAAW,4DAA4D,EAAE,EAAE;AAAA,EACvF;AAEA,MAAI,OAAO,EAAG,QAAO;AAErB,QAAM,QAA4B;AAAA,IAChC,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,KAAK,MAAM,CAAC;AAAA,IACb,CAAC,MAAM,MAAM,EAAE;AAAA,EACjB;AAEA,QAAM,QAAkB,CAAC;AACzB,MAAI,YAAY,KAAK,MAAM,EAAE;AAE7B,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,QAAI,aAAa,OAAO;AACtB,YAAM,QAAQ,KAAK,MAAM,YAAY,KAAK;AAC1C,YAAM,KAAK,GAAG,KAAK,GAAG,IAAI,EAAE;AAC5B,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,GAAG;AACvB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@padmaj/duration",
3
+ "version": "1.0.0",
4
+ "description": "Parse and format duration strings. '2h30m' ↔ 9000000ms. Zero dependencies. TypeScript-first.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/padmajp4/duration.git"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "prepublishOnly": "npm run build && npm test"
25
+ },
26
+ "keywords": ["duration", "time", "parse", "format", "ms", "human-readable"],
27
+ "author": "Padmaj P Kumar",
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@types/node": "^26.0.1",
31
+ "tsup": "^8.5.1",
32
+ "typescript": "^6.0.3",
33
+ "vitest": "^4.1.9"
34
+ }
35
+ }