@lacspace/humanize 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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence โ€” a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/humanize
4
+
5
+ **Turn machine values into human-readable text โ€” bytes, durations, relative time, ordinals, plurals, compact numbers & lists.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/humanize?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/humanize)
8
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/humanize?label=minzip)](https://bundlephobia.com/package/@lacspace/humanize)
9
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/humanize)
10
+ [![license](https://img.shields.io/npm/l/@lacspace/humanize?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
11
+
12
+ </div>
13
+
14
+ > All the little "make it readable" helpers you reach for on every UI โ€” instead of installing `pretty-bytes` + `ms` + `humanize-duration` + a pluralize lib, get them as one tiny, typed, dependency-free package.
15
+
16
+ - ๐Ÿ’พ `bytes` / `parseBytes` โ€” `1536 โ†’ "1.5 KB"` (and IEC KiB)
17
+ - โฑ๏ธ `duration` โ€” `90061000 โ†’ "1d 1h"` (or long form)
18
+ - ๐Ÿ•’ `relativeTime` โ€” `"3 hours ago"` / `"in 2 days"`
19
+ - ๐Ÿ”ข `ordinal`, `compact` (`1.2M`), `number` (grouping)
20
+ - โœ๏ธ `pluralize`, `list` (`"a, b and c"`), `truncate`, `titleCase`
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @lacspace/humanize # or pnpm add / yarn add / bun add
26
+ ```
27
+
28
+ ## Use
29
+
30
+ ```ts
31
+ import { bytes, duration, relativeTime, compact, ordinal, pluralize, list } from "@lacspace/humanize";
32
+
33
+ bytes(1536); // "1.5 KB"
34
+ duration(90061000); // "1d 1h"
35
+ relativeTime(Date.now() - 3.6e6); // "1 hour ago"
36
+ compact(1234567); // "1.2M"
37
+ ordinal(21); // "21st"
38
+ pluralize(3, "city"); // "3 cities"
39
+ list(["red", "green", "blue"]); // "red, green and blue"
40
+ ```
41
+
42
+ ## API
43
+
44
+ | Group | Functions |
45
+ | --- | --- |
46
+ | Size | `bytes`, `parseBytes` |
47
+ | Time | `duration`, `relativeTime` |
48
+ | Numbers | `ordinal`, `compact`, `number` |
49
+ | Words | `pluralize`, `plural`, `list`, `truncate`, `titleCase` |
50
+
51
+ Options let you switch to IEC units, long durations, more precision, Oxford commas and custom separators.
52
+
53
+ ## Licensing
54
+
55
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** โ€” MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
56
+
57
+ Not every Lacspace package is free. We also offer **Commercial**, **Client-specific** and **Private** packages under separate terms โ€” see the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
58
+
59
+ ---
60
+
61
+ <div align="center">
62
+
63
+ **Part of the Lacspace ecosystem โ€” zero-dependency, isomorphic TypeScript packages.**
64
+
65
+ [All packages โ†—](https://lacspace.com/packages) ยท [npm org โ†—](https://www.npmjs.com/org/lacspace) ยท [Licence Centre โ†—](https://lacspace.com/licenses) ยท [GitHub โ†—](https://github.com/lacspace/npm-packages)
66
+
67
+ </div>
68
+
69
+ <div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> ยท Lacspace Free Licence ยท <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
package/dist/index.cjs ADDED
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
5
+ var BYTE_UNITS_IEC = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
6
+ function bytes(n, opts = {}) {
7
+ const base = opts.binary ? 1024 : 1e3;
8
+ const units = opts.binary ? BYTE_UNITS_IEC : BYTE_UNITS;
9
+ const neg = n < 0 ? "-" : "";
10
+ let v = Math.abs(n);
11
+ if (v < base) return `${neg}${v} B`;
12
+ let i = 0;
13
+ while (v >= base && i < units.length - 1) {
14
+ v /= base;
15
+ i++;
16
+ }
17
+ const dp = opts.decimals ?? 1;
18
+ return `${neg}${v.toFixed(dp)} ${units[i]}`;
19
+ }
20
+ function parseBytes(input) {
21
+ const m = /^\s*(-?[\d.]+)\s*([a-z]+)?\s*$/i.exec(input);
22
+ if (!m) return NaN;
23
+ const value = parseFloat(m[1]);
24
+ const unit = (m[2] ?? "B").toUpperCase();
25
+ const binary = unit.includes("I");
26
+ const idx = (binary ? BYTE_UNITS_IEC : BYTE_UNITS).findIndex((u) => u.toUpperCase() === unit);
27
+ if (idx === -1) return unit === "B" ? value : NaN;
28
+ return value * (binary ? 1024 : 1e3) ** idx;
29
+ }
30
+ var DUR = [
31
+ { u: "y", ms: 31536e6, long: "year" },
32
+ { u: "mo", ms: 2592e6, long: "month" },
33
+ { u: "d", ms: 864e5, long: "day" },
34
+ { u: "h", ms: 36e5, long: "hour" },
35
+ { u: "m", ms: 6e4, long: "minute" },
36
+ { u: "s", ms: 1e3, long: "second" },
37
+ { u: "ms", ms: 1, long: "millisecond" }
38
+ ];
39
+ function duration(ms, opts = {}) {
40
+ const max = opts.units ?? 2;
41
+ let rem = Math.abs(Math.round(ms));
42
+ if (rem === 0) return opts.long ? "0 seconds" : "0s";
43
+ const parts = [];
44
+ for (const d of DUR) {
45
+ if (rem < d.ms) continue;
46
+ const count = Math.floor(rem / d.ms);
47
+ rem -= count * d.ms;
48
+ parts.push(opts.long ? `${count} ${plural(d.long, count)}` : `${count}${d.u}`);
49
+ if (parts.length >= max) break;
50
+ }
51
+ return (ms < 0 ? "-" : "") + parts.join(opts.long ? " " : " ");
52
+ }
53
+ var RT = [
54
+ [31536e3, "year"],
55
+ [2592e3, "month"],
56
+ [604800, "week"],
57
+ [86400, "day"],
58
+ [3600, "hour"],
59
+ [60, "minute"],
60
+ [1, "second"]
61
+ ];
62
+ function relativeTime(date, base = Date.now()) {
63
+ const t = date instanceof Date ? date.getTime() : typeof date === "string" ? new Date(date).getTime() : date;
64
+ const b = base instanceof Date ? base.getTime() : base;
65
+ const diff = Math.round((b - t) / 1e3);
66
+ const abs = Math.abs(diff);
67
+ if (abs < 5) return "just now";
68
+ for (const [secs, unit] of RT) {
69
+ if (abs >= secs) {
70
+ const count = Math.floor(abs / secs);
71
+ const label = `${count} ${plural(unit, count)}`;
72
+ return diff >= 0 ? `${label} ago` : `in ${label}`;
73
+ }
74
+ }
75
+ return "just now";
76
+ }
77
+ function ordinal(n) {
78
+ const abs = Math.abs(n) % 100;
79
+ const last = abs % 10;
80
+ const suffix = abs >= 11 && abs <= 13 ? "th" : last === 1 ? "st" : last === 2 ? "nd" : last === 3 ? "rd" : "th";
81
+ return `${n}${suffix}`;
82
+ }
83
+ function compact(n, decimals = 1) {
84
+ const neg = n < 0 ? "-" : "";
85
+ const v = Math.abs(n);
86
+ const units = [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]];
87
+ for (const [threshold, u] of units) {
88
+ if (v >= threshold) {
89
+ const num = v / threshold;
90
+ return `${neg}${num % 1 === 0 ? num.toFixed(0) : num.toFixed(decimals)}${u}`;
91
+ }
92
+ }
93
+ return `${neg}${v}`;
94
+ }
95
+ function number(n, opts = {}) {
96
+ const sep = opts.separator ?? ",";
97
+ const dec = opts.decimal ?? ".";
98
+ const [int, frac] = String(n).split(".");
99
+ const sign = int.startsWith("-") ? "-" : "";
100
+ const digits = int.replace("-", "").replace(/\B(?=(\d{3})+(?!\d))/g, sep);
101
+ return sign + digits + (frac ? dec + frac : "");
102
+ }
103
+ function plural(word, count, pluralForm) {
104
+ if (count === 1) return word;
105
+ if (pluralForm) return pluralForm;
106
+ if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
107
+ if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
108
+ return `${word}s`;
109
+ }
110
+ function pluralize(count, word, pluralForm) {
111
+ return `${count} ${plural(word, count, pluralForm)}`;
112
+ }
113
+ function list(items, opts = {}) {
114
+ const conj = opts.conjunction ?? "and";
115
+ if (items.length === 0) return "";
116
+ if (items.length === 1) return items[0];
117
+ if (items.length === 2) return `${items[0]} ${conj} ${items[1]}`;
118
+ const head = items.slice(0, -1).join(", ");
119
+ return `${head}${opts.oxford ? "," : ""} ${conj} ${items[items.length - 1]}`;
120
+ }
121
+ function truncate(text, maxLength, ellipsis = "\u2026") {
122
+ if (text.length <= maxLength) return text;
123
+ const cut = text.slice(0, maxLength - ellipsis.length);
124
+ const lastSpace = cut.lastIndexOf(" ");
125
+ return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[\s.,;:!?-]+$/, "") + ellipsis;
126
+ }
127
+ function titleCase(text) {
128
+ return text.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
129
+ }
130
+
131
+ exports.bytes = bytes;
132
+ exports.compact = compact;
133
+ exports.duration = duration;
134
+ exports.list = list;
135
+ exports.number = number;
136
+ exports.ordinal = ordinal;
137
+ exports.parseBytes = parseBytes;
138
+ exports.plural = plural;
139
+ exports.pluralize = pluralize;
140
+ exports.relativeTime = relativeTime;
141
+ exports.titleCase = titleCase;
142
+ exports.truncate = truncate;
143
+ //# sourceMappingURL=index.cjs.map
144
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/humanize\n *\n * Turn machine values into human-readable text โ€” bytes, durations, relative\n * time, ordinals, plurals, compact numbers and grammatical lists.\n * Zero dependencies, isomorphic.\n */\n\n/* ------------------------------ bytes ------------------------------ */\n\nconst BYTE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\"];\nconst BYTE_UNITS_IEC = [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\"];\n\nexport interface BytesOptions {\n /** Use 1024-based IEC units (KiB, MiBโ€ฆ). Default false (1000-based). */\n binary?: boolean;\n /** Decimal places. Default 1 (0 for bytes). */\n decimals?: number;\n}\n\n/** Human-readable file size. `bytes(1536)` โ†’ \"1.5 KB\". */\nexport function bytes(n: number, opts: BytesOptions = {}): string {\n const base = opts.binary ? 1024 : 1000;\n const units = opts.binary ? BYTE_UNITS_IEC : BYTE_UNITS;\n const neg = n < 0 ? \"-\" : \"\";\n let v = Math.abs(n);\n if (v < base) return `${neg}${v} B`;\n let i = 0;\n while (v >= base && i < units.length - 1) { v /= base; i++; }\n const dp = opts.decimals ?? 1;\n return `${neg}${v.toFixed(dp)} ${units[i]}`;\n}\n\n/** Parse a size string back to bytes. `parseBytes(\"1.5 KB\")` โ†’ 1500. */\nexport function parseBytes(input: string): number {\n const m = /^\\s*(-?[\\d.]+)\\s*([a-z]+)?\\s*$/i.exec(input);\n if (!m) return NaN;\n const value = parseFloat(m[1]!);\n const unit = (m[2] ?? \"B\").toUpperCase();\n const binary = unit.includes(\"I\");\n const idx = (binary ? BYTE_UNITS_IEC : BYTE_UNITS).findIndex((u) => u.toUpperCase() === unit);\n if (idx === -1) return unit === \"B\" ? value : NaN;\n return value * (binary ? 1024 : 1000) ** idx;\n}\n\n/* ------------------------------ duration ------------------------------ */\n\nconst DUR = [\n { u: \"y\", ms: 31536000000, long: \"year\" },\n { u: \"mo\", ms: 2592000000, long: \"month\" },\n { u: \"d\", ms: 86400000, long: \"day\" },\n { u: \"h\", ms: 3600000, long: \"hour\" },\n { u: \"m\", ms: 60000, long: \"minute\" },\n { u: \"s\", ms: 1000, long: \"second\" },\n { u: \"ms\", ms: 1, long: \"millisecond\" },\n];\n\nexport interface DurationOptions {\n /** Max number of units to show. Default 2. */\n units?: number;\n /** Use full words (\"2 hours 3 minutes\"). Default false (\"2h 3m\"). */\n long?: boolean;\n}\n\n/** Human-readable duration. `duration(90061000)` โ†’ \"1d 1h\". */\nexport function duration(ms: number, opts: DurationOptions = {}): string {\n const max = opts.units ?? 2;\n let rem = Math.abs(Math.round(ms));\n if (rem === 0) return opts.long ? \"0 seconds\" : \"0s\";\n const parts: string[] = [];\n for (const d of DUR) {\n if (rem < d.ms) continue;\n const count = Math.floor(rem / d.ms);\n rem -= count * d.ms;\n parts.push(opts.long ? `${count} ${plural(d.long, count)}` : `${count}${d.u}`);\n if (parts.length >= max) break;\n }\n return (ms < 0 ? \"-\" : \"\") + parts.join(opts.long ? \" \" : \" \");\n}\n\n/* ------------------------------ relative time ------------------------------ */\n\nconst RT: [number, string][] = [\n [31536000, \"year\"], [2592000, \"month\"], [604800, \"week\"],\n [86400, \"day\"], [3600, \"hour\"], [60, \"minute\"], [1, \"second\"],\n];\n\n/** Relative time. `relativeTime(Date.now() - 3600e3)` โ†’ \"1 hour ago\". */\nexport function relativeTime(date: Date | number | string, base: Date | number = Date.now()): string {\n const t = date instanceof Date ? date.getTime() : typeof date === \"string\" ? new Date(date).getTime() : date;\n const b = base instanceof Date ? base.getTime() : base;\n const diff = Math.round((b - t) / 1000);\n const abs = Math.abs(diff);\n if (abs < 5) return \"just now\";\n for (const [secs, unit] of RT) {\n if (abs >= secs) {\n const count = Math.floor(abs / secs);\n const label = `${count} ${plural(unit, count)}`;\n return diff >= 0 ? `${label} ago` : `in ${label}`;\n }\n }\n return \"just now\";\n}\n\n/* ------------------------------ numbers ------------------------------ */\n\n/** Ordinal suffix. `ordinal(21)` โ†’ \"21st\". */\nexport function ordinal(n: number): string {\n const abs = Math.abs(n) % 100;\n const last = abs % 10;\n const suffix = abs >= 11 && abs <= 13 ? \"th\" : last === 1 ? \"st\" : last === 2 ? \"nd\" : last === 3 ? \"rd\" : \"th\";\n return `${n}${suffix}`;\n}\n\n/** Compact number. `compact(1234567)` โ†’ \"1.2M\". */\nexport function compact(n: number, decimals = 1): string {\n const neg = n < 0 ? \"-\" : \"\";\n const v = Math.abs(n);\n const units: [number, string][] = [[1e12, \"T\"], [1e9, \"B\"], [1e6, \"M\"], [1e3, \"K\"]];\n for (const [threshold, u] of units) {\n if (v >= threshold) {\n const num = v / threshold;\n return `${neg}${(num % 1 === 0 ? num.toFixed(0) : num.toFixed(decimals))}${u}`;\n }\n }\n return `${neg}${v}`;\n}\n\n/** Group digits with separators. `number(1234567.5)` โ†’ \"1,234,567.5\". */\nexport function number(n: number, opts: { separator?: string; decimal?: string } = {}): string {\n const sep = opts.separator ?? \",\";\n const dec = opts.decimal ?? \".\";\n const [int, frac] = String(n).split(\".\");\n const sign = int!.startsWith(\"-\") ? \"-\" : \"\";\n const digits = int!.replace(\"-\", \"\").replace(/\\B(?=(\\d{3})+(?!\\d))/g, sep);\n return sign + digits + (frac ? dec + frac : \"\");\n}\n\n/* ------------------------------ words ------------------------------ */\n\n/** Pluralize a word for a count (naive English + irregulars). */\nexport function plural(word: string, count: number, pluralForm?: string): string {\n if (count === 1) return word;\n if (pluralForm) return pluralForm;\n if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;\n if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;\n return `${word}s`;\n}\n\n/** \"5 items\" / \"1 item\". */\nexport function pluralize(count: number, word: string, pluralForm?: string): string {\n return `${count} ${plural(word, count, pluralForm)}`;\n}\n\n/** Grammatical list. `list([\"a\",\"b\",\"c\"])` โ†’ \"a, b and c\". */\nexport function list(items: string[], opts: { conjunction?: string; oxford?: boolean } = {}): string {\n const conj = opts.conjunction ?? \"and\";\n if (items.length === 0) return \"\";\n if (items.length === 1) return items[0]!;\n if (items.length === 2) return `${items[0]} ${conj} ${items[1]}`;\n const head = items.slice(0, -1).join(\", \");\n return `${head}${opts.oxford ? \",\" : \"\"} ${conj} ${items[items.length - 1]}`;\n}\n\n/** Truncate to a max length with an ellipsis on a word boundary. */\nexport function truncate(text: string, maxLength: number, ellipsis = \"โ€ฆ\"): string {\n if (text.length <= maxLength) return text;\n const cut = text.slice(0, maxLength - ellipsis.length);\n const lastSpace = cut.lastIndexOf(\" \");\n return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[\\s.,;:!?-]+$/, \"\") + ellipsis;\n}\n\n/** Title Case a string. */\nexport function titleCase(text: string): string {\n return text.toLowerCase().replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @lacspace/humanize
3
+ *
4
+ * Turn machine values into human-readable text โ€” bytes, durations, relative
5
+ * time, ordinals, plurals, compact numbers and grammatical lists.
6
+ * Zero dependencies, isomorphic.
7
+ */
8
+ interface BytesOptions {
9
+ /** Use 1024-based IEC units (KiB, MiBโ€ฆ). Default false (1000-based). */
10
+ binary?: boolean;
11
+ /** Decimal places. Default 1 (0 for bytes). */
12
+ decimals?: number;
13
+ }
14
+ /** Human-readable file size. `bytes(1536)` โ†’ "1.5 KB". */
15
+ declare function bytes(n: number, opts?: BytesOptions): string;
16
+ /** Parse a size string back to bytes. `parseBytes("1.5 KB")` โ†’ 1500. */
17
+ declare function parseBytes(input: string): number;
18
+ interface DurationOptions {
19
+ /** Max number of units to show. Default 2. */
20
+ units?: number;
21
+ /** Use full words ("2 hours 3 minutes"). Default false ("2h 3m"). */
22
+ long?: boolean;
23
+ }
24
+ /** Human-readable duration. `duration(90061000)` โ†’ "1d 1h". */
25
+ declare function duration(ms: number, opts?: DurationOptions): string;
26
+ /** Relative time. `relativeTime(Date.now() - 3600e3)` โ†’ "1 hour ago". */
27
+ declare function relativeTime(date: Date | number | string, base?: Date | number): string;
28
+ /** Ordinal suffix. `ordinal(21)` โ†’ "21st". */
29
+ declare function ordinal(n: number): string;
30
+ /** Compact number. `compact(1234567)` โ†’ "1.2M". */
31
+ declare function compact(n: number, decimals?: number): string;
32
+ /** Group digits with separators. `number(1234567.5)` โ†’ "1,234,567.5". */
33
+ declare function number(n: number, opts?: {
34
+ separator?: string;
35
+ decimal?: string;
36
+ }): string;
37
+ /** Pluralize a word for a count (naive English + irregulars). */
38
+ declare function plural(word: string, count: number, pluralForm?: string): string;
39
+ /** "5 items" / "1 item". */
40
+ declare function pluralize(count: number, word: string, pluralForm?: string): string;
41
+ /** Grammatical list. `list(["a","b","c"])` โ†’ "a, b and c". */
42
+ declare function list(items: string[], opts?: {
43
+ conjunction?: string;
44
+ oxford?: boolean;
45
+ }): string;
46
+ /** Truncate to a max length with an ellipsis on a word boundary. */
47
+ declare function truncate(text: string, maxLength: number, ellipsis?: string): string;
48
+ /** Title Case a string. */
49
+ declare function titleCase(text: string): string;
50
+
51
+ export { type BytesOptions, type DurationOptions, bytes, compact, duration, list, number, ordinal, parseBytes, plural, pluralize, relativeTime, titleCase, truncate };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @lacspace/humanize
3
+ *
4
+ * Turn machine values into human-readable text โ€” bytes, durations, relative
5
+ * time, ordinals, plurals, compact numbers and grammatical lists.
6
+ * Zero dependencies, isomorphic.
7
+ */
8
+ interface BytesOptions {
9
+ /** Use 1024-based IEC units (KiB, MiBโ€ฆ). Default false (1000-based). */
10
+ binary?: boolean;
11
+ /** Decimal places. Default 1 (0 for bytes). */
12
+ decimals?: number;
13
+ }
14
+ /** Human-readable file size. `bytes(1536)` โ†’ "1.5 KB". */
15
+ declare function bytes(n: number, opts?: BytesOptions): string;
16
+ /** Parse a size string back to bytes. `parseBytes("1.5 KB")` โ†’ 1500. */
17
+ declare function parseBytes(input: string): number;
18
+ interface DurationOptions {
19
+ /** Max number of units to show. Default 2. */
20
+ units?: number;
21
+ /** Use full words ("2 hours 3 minutes"). Default false ("2h 3m"). */
22
+ long?: boolean;
23
+ }
24
+ /** Human-readable duration. `duration(90061000)` โ†’ "1d 1h". */
25
+ declare function duration(ms: number, opts?: DurationOptions): string;
26
+ /** Relative time. `relativeTime(Date.now() - 3600e3)` โ†’ "1 hour ago". */
27
+ declare function relativeTime(date: Date | number | string, base?: Date | number): string;
28
+ /** Ordinal suffix. `ordinal(21)` โ†’ "21st". */
29
+ declare function ordinal(n: number): string;
30
+ /** Compact number. `compact(1234567)` โ†’ "1.2M". */
31
+ declare function compact(n: number, decimals?: number): string;
32
+ /** Group digits with separators. `number(1234567.5)` โ†’ "1,234,567.5". */
33
+ declare function number(n: number, opts?: {
34
+ separator?: string;
35
+ decimal?: string;
36
+ }): string;
37
+ /** Pluralize a word for a count (naive English + irregulars). */
38
+ declare function plural(word: string, count: number, pluralForm?: string): string;
39
+ /** "5 items" / "1 item". */
40
+ declare function pluralize(count: number, word: string, pluralForm?: string): string;
41
+ /** Grammatical list. `list(["a","b","c"])` โ†’ "a, b and c". */
42
+ declare function list(items: string[], opts?: {
43
+ conjunction?: string;
44
+ oxford?: boolean;
45
+ }): string;
46
+ /** Truncate to a max length with an ellipsis on a word boundary. */
47
+ declare function truncate(text: string, maxLength: number, ellipsis?: string): string;
48
+ /** Title Case a string. */
49
+ declare function titleCase(text: string): string;
50
+
51
+ export { type BytesOptions, type DurationOptions, bytes, compact, duration, list, number, ordinal, parseBytes, plural, pluralize, relativeTime, titleCase, truncate };
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ // src/index.ts
2
+ var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
3
+ var BYTE_UNITS_IEC = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
4
+ function bytes(n, opts = {}) {
5
+ const base = opts.binary ? 1024 : 1e3;
6
+ const units = opts.binary ? BYTE_UNITS_IEC : BYTE_UNITS;
7
+ const neg = n < 0 ? "-" : "";
8
+ let v = Math.abs(n);
9
+ if (v < base) return `${neg}${v} B`;
10
+ let i = 0;
11
+ while (v >= base && i < units.length - 1) {
12
+ v /= base;
13
+ i++;
14
+ }
15
+ const dp = opts.decimals ?? 1;
16
+ return `${neg}${v.toFixed(dp)} ${units[i]}`;
17
+ }
18
+ function parseBytes(input) {
19
+ const m = /^\s*(-?[\d.]+)\s*([a-z]+)?\s*$/i.exec(input);
20
+ if (!m) return NaN;
21
+ const value = parseFloat(m[1]);
22
+ const unit = (m[2] ?? "B").toUpperCase();
23
+ const binary = unit.includes("I");
24
+ const idx = (binary ? BYTE_UNITS_IEC : BYTE_UNITS).findIndex((u) => u.toUpperCase() === unit);
25
+ if (idx === -1) return unit === "B" ? value : NaN;
26
+ return value * (binary ? 1024 : 1e3) ** idx;
27
+ }
28
+ var DUR = [
29
+ { u: "y", ms: 31536e6, long: "year" },
30
+ { u: "mo", ms: 2592e6, long: "month" },
31
+ { u: "d", ms: 864e5, long: "day" },
32
+ { u: "h", ms: 36e5, long: "hour" },
33
+ { u: "m", ms: 6e4, long: "minute" },
34
+ { u: "s", ms: 1e3, long: "second" },
35
+ { u: "ms", ms: 1, long: "millisecond" }
36
+ ];
37
+ function duration(ms, opts = {}) {
38
+ const max = opts.units ?? 2;
39
+ let rem = Math.abs(Math.round(ms));
40
+ if (rem === 0) return opts.long ? "0 seconds" : "0s";
41
+ const parts = [];
42
+ for (const d of DUR) {
43
+ if (rem < d.ms) continue;
44
+ const count = Math.floor(rem / d.ms);
45
+ rem -= count * d.ms;
46
+ parts.push(opts.long ? `${count} ${plural(d.long, count)}` : `${count}${d.u}`);
47
+ if (parts.length >= max) break;
48
+ }
49
+ return (ms < 0 ? "-" : "") + parts.join(opts.long ? " " : " ");
50
+ }
51
+ var RT = [
52
+ [31536e3, "year"],
53
+ [2592e3, "month"],
54
+ [604800, "week"],
55
+ [86400, "day"],
56
+ [3600, "hour"],
57
+ [60, "minute"],
58
+ [1, "second"]
59
+ ];
60
+ function relativeTime(date, base = Date.now()) {
61
+ const t = date instanceof Date ? date.getTime() : typeof date === "string" ? new Date(date).getTime() : date;
62
+ const b = base instanceof Date ? base.getTime() : base;
63
+ const diff = Math.round((b - t) / 1e3);
64
+ const abs = Math.abs(diff);
65
+ if (abs < 5) return "just now";
66
+ for (const [secs, unit] of RT) {
67
+ if (abs >= secs) {
68
+ const count = Math.floor(abs / secs);
69
+ const label = `${count} ${plural(unit, count)}`;
70
+ return diff >= 0 ? `${label} ago` : `in ${label}`;
71
+ }
72
+ }
73
+ return "just now";
74
+ }
75
+ function ordinal(n) {
76
+ const abs = Math.abs(n) % 100;
77
+ const last = abs % 10;
78
+ const suffix = abs >= 11 && abs <= 13 ? "th" : last === 1 ? "st" : last === 2 ? "nd" : last === 3 ? "rd" : "th";
79
+ return `${n}${suffix}`;
80
+ }
81
+ function compact(n, decimals = 1) {
82
+ const neg = n < 0 ? "-" : "";
83
+ const v = Math.abs(n);
84
+ const units = [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]];
85
+ for (const [threshold, u] of units) {
86
+ if (v >= threshold) {
87
+ const num = v / threshold;
88
+ return `${neg}${num % 1 === 0 ? num.toFixed(0) : num.toFixed(decimals)}${u}`;
89
+ }
90
+ }
91
+ return `${neg}${v}`;
92
+ }
93
+ function number(n, opts = {}) {
94
+ const sep = opts.separator ?? ",";
95
+ const dec = opts.decimal ?? ".";
96
+ const [int, frac] = String(n).split(".");
97
+ const sign = int.startsWith("-") ? "-" : "";
98
+ const digits = int.replace("-", "").replace(/\B(?=(\d{3})+(?!\d))/g, sep);
99
+ return sign + digits + (frac ? dec + frac : "");
100
+ }
101
+ function plural(word, count, pluralForm) {
102
+ if (count === 1) return word;
103
+ if (pluralForm) return pluralForm;
104
+ if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
105
+ if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
106
+ return `${word}s`;
107
+ }
108
+ function pluralize(count, word, pluralForm) {
109
+ return `${count} ${plural(word, count, pluralForm)}`;
110
+ }
111
+ function list(items, opts = {}) {
112
+ const conj = opts.conjunction ?? "and";
113
+ if (items.length === 0) return "";
114
+ if (items.length === 1) return items[0];
115
+ if (items.length === 2) return `${items[0]} ${conj} ${items[1]}`;
116
+ const head = items.slice(0, -1).join(", ");
117
+ return `${head}${opts.oxford ? "," : ""} ${conj} ${items[items.length - 1]}`;
118
+ }
119
+ function truncate(text, maxLength, ellipsis = "\u2026") {
120
+ if (text.length <= maxLength) return text;
121
+ const cut = text.slice(0, maxLength - ellipsis.length);
122
+ const lastSpace = cut.lastIndexOf(" ");
123
+ return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[\s.,;:!?-]+$/, "") + ellipsis;
124
+ }
125
+ function titleCase(text) {
126
+ return text.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
127
+ }
128
+
129
+ export { bytes, compact, duration, list, number, ordinal, parseBytes, plural, pluralize, relativeTime, titleCase, truncate };
130
+ //# sourceMappingURL=index.js.map
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/humanize\n *\n * Turn machine values into human-readable text โ€” bytes, durations, relative\n * time, ordinals, plurals, compact numbers and grammatical lists.\n * Zero dependencies, isomorphic.\n */\n\n/* ------------------------------ bytes ------------------------------ */\n\nconst BYTE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\"];\nconst BYTE_UNITS_IEC = [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\"];\n\nexport interface BytesOptions {\n /** Use 1024-based IEC units (KiB, MiBโ€ฆ). Default false (1000-based). */\n binary?: boolean;\n /** Decimal places. Default 1 (0 for bytes). */\n decimals?: number;\n}\n\n/** Human-readable file size. `bytes(1536)` โ†’ \"1.5 KB\". */\nexport function bytes(n: number, opts: BytesOptions = {}): string {\n const base = opts.binary ? 1024 : 1000;\n const units = opts.binary ? BYTE_UNITS_IEC : BYTE_UNITS;\n const neg = n < 0 ? \"-\" : \"\";\n let v = Math.abs(n);\n if (v < base) return `${neg}${v} B`;\n let i = 0;\n while (v >= base && i < units.length - 1) { v /= base; i++; }\n const dp = opts.decimals ?? 1;\n return `${neg}${v.toFixed(dp)} ${units[i]}`;\n}\n\n/** Parse a size string back to bytes. `parseBytes(\"1.5 KB\")` โ†’ 1500. */\nexport function parseBytes(input: string): number {\n const m = /^\\s*(-?[\\d.]+)\\s*([a-z]+)?\\s*$/i.exec(input);\n if (!m) return NaN;\n const value = parseFloat(m[1]!);\n const unit = (m[2] ?? \"B\").toUpperCase();\n const binary = unit.includes(\"I\");\n const idx = (binary ? BYTE_UNITS_IEC : BYTE_UNITS).findIndex((u) => u.toUpperCase() === unit);\n if (idx === -1) return unit === \"B\" ? value : NaN;\n return value * (binary ? 1024 : 1000) ** idx;\n}\n\n/* ------------------------------ duration ------------------------------ */\n\nconst DUR = [\n { u: \"y\", ms: 31536000000, long: \"year\" },\n { u: \"mo\", ms: 2592000000, long: \"month\" },\n { u: \"d\", ms: 86400000, long: \"day\" },\n { u: \"h\", ms: 3600000, long: \"hour\" },\n { u: \"m\", ms: 60000, long: \"minute\" },\n { u: \"s\", ms: 1000, long: \"second\" },\n { u: \"ms\", ms: 1, long: \"millisecond\" },\n];\n\nexport interface DurationOptions {\n /** Max number of units to show. Default 2. */\n units?: number;\n /** Use full words (\"2 hours 3 minutes\"). Default false (\"2h 3m\"). */\n long?: boolean;\n}\n\n/** Human-readable duration. `duration(90061000)` โ†’ \"1d 1h\". */\nexport function duration(ms: number, opts: DurationOptions = {}): string {\n const max = opts.units ?? 2;\n let rem = Math.abs(Math.round(ms));\n if (rem === 0) return opts.long ? \"0 seconds\" : \"0s\";\n const parts: string[] = [];\n for (const d of DUR) {\n if (rem < d.ms) continue;\n const count = Math.floor(rem / d.ms);\n rem -= count * d.ms;\n parts.push(opts.long ? `${count} ${plural(d.long, count)}` : `${count}${d.u}`);\n if (parts.length >= max) break;\n }\n return (ms < 0 ? \"-\" : \"\") + parts.join(opts.long ? \" \" : \" \");\n}\n\n/* ------------------------------ relative time ------------------------------ */\n\nconst RT: [number, string][] = [\n [31536000, \"year\"], [2592000, \"month\"], [604800, \"week\"],\n [86400, \"day\"], [3600, \"hour\"], [60, \"minute\"], [1, \"second\"],\n];\n\n/** Relative time. `relativeTime(Date.now() - 3600e3)` โ†’ \"1 hour ago\". */\nexport function relativeTime(date: Date | number | string, base: Date | number = Date.now()): string {\n const t = date instanceof Date ? date.getTime() : typeof date === \"string\" ? new Date(date).getTime() : date;\n const b = base instanceof Date ? base.getTime() : base;\n const diff = Math.round((b - t) / 1000);\n const abs = Math.abs(diff);\n if (abs < 5) return \"just now\";\n for (const [secs, unit] of RT) {\n if (abs >= secs) {\n const count = Math.floor(abs / secs);\n const label = `${count} ${plural(unit, count)}`;\n return diff >= 0 ? `${label} ago` : `in ${label}`;\n }\n }\n return \"just now\";\n}\n\n/* ------------------------------ numbers ------------------------------ */\n\n/** Ordinal suffix. `ordinal(21)` โ†’ \"21st\". */\nexport function ordinal(n: number): string {\n const abs = Math.abs(n) % 100;\n const last = abs % 10;\n const suffix = abs >= 11 && abs <= 13 ? \"th\" : last === 1 ? \"st\" : last === 2 ? \"nd\" : last === 3 ? \"rd\" : \"th\";\n return `${n}${suffix}`;\n}\n\n/** Compact number. `compact(1234567)` โ†’ \"1.2M\". */\nexport function compact(n: number, decimals = 1): string {\n const neg = n < 0 ? \"-\" : \"\";\n const v = Math.abs(n);\n const units: [number, string][] = [[1e12, \"T\"], [1e9, \"B\"], [1e6, \"M\"], [1e3, \"K\"]];\n for (const [threshold, u] of units) {\n if (v >= threshold) {\n const num = v / threshold;\n return `${neg}${(num % 1 === 0 ? num.toFixed(0) : num.toFixed(decimals))}${u}`;\n }\n }\n return `${neg}${v}`;\n}\n\n/** Group digits with separators. `number(1234567.5)` โ†’ \"1,234,567.5\". */\nexport function number(n: number, opts: { separator?: string; decimal?: string } = {}): string {\n const sep = opts.separator ?? \",\";\n const dec = opts.decimal ?? \".\";\n const [int, frac] = String(n).split(\".\");\n const sign = int!.startsWith(\"-\") ? \"-\" : \"\";\n const digits = int!.replace(\"-\", \"\").replace(/\\B(?=(\\d{3})+(?!\\d))/g, sep);\n return sign + digits + (frac ? dec + frac : \"\");\n}\n\n/* ------------------------------ words ------------------------------ */\n\n/** Pluralize a word for a count (naive English + irregulars). */\nexport function plural(word: string, count: number, pluralForm?: string): string {\n if (count === 1) return word;\n if (pluralForm) return pluralForm;\n if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;\n if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;\n return `${word}s`;\n}\n\n/** \"5 items\" / \"1 item\". */\nexport function pluralize(count: number, word: string, pluralForm?: string): string {\n return `${count} ${plural(word, count, pluralForm)}`;\n}\n\n/** Grammatical list. `list([\"a\",\"b\",\"c\"])` โ†’ \"a, b and c\". */\nexport function list(items: string[], opts: { conjunction?: string; oxford?: boolean } = {}): string {\n const conj = opts.conjunction ?? \"and\";\n if (items.length === 0) return \"\";\n if (items.length === 1) return items[0]!;\n if (items.length === 2) return `${items[0]} ${conj} ${items[1]}`;\n const head = items.slice(0, -1).join(\", \");\n return `${head}${opts.oxford ? \",\" : \"\"} ${conj} ${items[items.length - 1]}`;\n}\n\n/** Truncate to a max length with an ellipsis on a word boundary. */\nexport function truncate(text: string, maxLength: number, ellipsis = \"โ€ฆ\"): string {\n if (text.length <= maxLength) return text;\n const cut = text.slice(0, maxLength - ellipsis.length);\n const lastSpace = cut.lastIndexOf(\" \");\n return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[\\s.,;:!?-]+$/, \"\") + ellipsis;\n}\n\n/** Title Case a string. */\nexport function titleCase(text: string): string {\n return text.toLowerCase().replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@lacspace/humanize",
3
+ "version": "1.0.0",
4
+ "description": "Turn machine values into human-readable text โ€” bytes, durations, relative time, ordinals, plurals, compact numbers and grammatical lists. Zero-dependency, isomorphic.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": { ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } } },
10
+ "files": ["dist"],
11
+ "sideEffects": false,
12
+ "scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
13
+ "keywords": ["humanize", "pretty-bytes", "format-bytes", "duration", "relative-time", "time-ago", "ordinal", "pluralize", "compact-number", "format-number", "zero-dependency", "isomorphic", "typescript"],
14
+ "author": "Lacspace <contact@lacspace.com>",
15
+ "license": "SEE LICENSE IN LICENSE",
16
+ "homepage": "https://lacspace.com/packages",
17
+ "repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "humanize" },
18
+ "bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
19
+ "engines": { "node": ">=18" },
20
+ "publishConfig": { "access": "public" }
21
+ }