@lacspace/humanize 1.0.0 → 1.0.2

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 CHANGED
@@ -52,18 +52,24 @@ Options let you switch to IEC units, long durations, more precision, Oxford comm
52
52
 
53
53
  ## Licensing
54
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.
55
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
56
56
 
57
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
58
 
59
+ <!-- LACSPACE-DEV-PLATFORM -->
60
+
59
61
  ---
60
62
 
61
- <div align="center">
63
+ ## The Lacspace Developer Platform
62
64
 
63
- **Part of the Lacspace ecosystem zero-dependency, isomorphic TypeScript packages.**
65
+ `@lacspace/humanize` is part of **63 zero-dependency, isomorphic TypeScript packages** — one standard library for the modern web. Explore the ecosystem:
64
66
 
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)
67
+ - 📦 **This package, documented** https://developer.lacspace.com/packages/humanize
68
+ - 🗂️ **All 63 packages** — https://developer.lacspace.com/packages
69
+ - 🧭 **Developer handbook** — guides & runnable recipes — https://developer.lacspace.com/handbook
70
+ - 🧪 **Live playground** — run any package in your browser — https://developer.lacspace.com/playground
71
+ - 🖥️ **Finished app templates** — https://templates.lacspace.com
72
+ - 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
66
73
 
67
- </div>
74
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
68
75
 
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 CHANGED
@@ -95,7 +95,14 @@ function compact(n, decimals = 1) {
95
95
  function number(n, opts = {}) {
96
96
  const sep = opts.separator ?? ",";
97
97
  const dec = opts.decimal ?? ".";
98
- const [int, frac] = String(n).split(".");
98
+ if (!Number.isFinite(n)) return String(n);
99
+ const str = String(n);
100
+ if (str.includes("e") || str.includes("E")) {
101
+ const grouped = n.toLocaleString("en-US", { maximumFractionDigits: 20, useGrouping: true });
102
+ const [g, f] = grouped.split(".");
103
+ return g.split(",").join(sep) + (f ? dec + f : "");
104
+ }
105
+ const [int, frac] = str.split(".");
99
106
  const sign = int.startsWith("-") ? "-" : "";
100
107
  const digits = int.replace("-", "").replace(/\B(?=(\d{3})+(?!\d))/g, sep);
101
108
  return sign + digits + (frac ? dec + frac : "");
@@ -1 +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"]}
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 if (!Number.isFinite(n)) return String(n);\n const str = String(n);\n // Exponential notation (e.g. 1e21, 1e-7) mangles a naive split — expand it via\n // locale grouping (en-US: \",\" groups, \".\" decimal) then apply the custom separators.\n if (str.includes(\"e\") || str.includes(\"E\")) {\n const grouped = n.toLocaleString(\"en-US\", { maximumFractionDigits: 20, useGrouping: true });\n const [g, f] = grouped.split(\".\");\n return g!.split(\",\").join(sep) + (f ? dec + f : \"\");\n }\n const [int, frac] = str.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/dist/index.js CHANGED
@@ -93,7 +93,14 @@ function compact(n, decimals = 1) {
93
93
  function number(n, opts = {}) {
94
94
  const sep = opts.separator ?? ",";
95
95
  const dec = opts.decimal ?? ".";
96
- const [int, frac] = String(n).split(".");
96
+ if (!Number.isFinite(n)) return String(n);
97
+ const str = String(n);
98
+ if (str.includes("e") || str.includes("E")) {
99
+ const grouped = n.toLocaleString("en-US", { maximumFractionDigits: 20, useGrouping: true });
100
+ const [g, f] = grouped.split(".");
101
+ return g.split(",").join(sep) + (f ? dec + f : "");
102
+ }
103
+ const [int, frac] = str.split(".");
97
104
  const sign = int.startsWith("-") ? "-" : "";
98
105
  const digits = int.replace("-", "").replace(/\B(?=(\d{3})+(?!\d))/g, sep);
99
106
  return sign + digits + (frac ? dec + frac : "");
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAUA,IAAM,UAAA,GAAa,CAAC,GAAA,EAAK,IAAA,EAAM,MAAM,IAAA,EAAM,IAAA,EAAM,MAAM,IAAI,CAAA;AAC3D,IAAM,cAAA,GAAiB,CAAC,GAAA,EAAK,KAAA,EAAO,OAAO,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK,CAAA;AAU9D,SAAS,KAAA,CAAM,CAAA,EAAW,IAAA,GAAqB,EAAC,EAAW;AAChE,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,GAAS,IAAA,GAAO,GAAA;AAClC,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,GAAS,cAAA,GAAiB,UAAA;AAC7C,EAAA,MAAM,GAAA,GAAM,CAAA,GAAI,CAAA,GAAI,GAAA,GAAM,EAAA;AAC1B,EAAA,IAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AAClB,EAAA,IAAI,IAAI,IAAA,EAAM,OAAO,CAAA,EAAG,GAAG,GAAG,CAAC,CAAA,EAAA,CAAA;AAC/B,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAO,CAAA,IAAK,IAAA,IAAQ,CAAA,GAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AAAE,IAAA,CAAA,IAAK,IAAA;AAAM,IAAA,CAAA,EAAA;AAAA,EAAK;AAC5D,EAAA,MAAM,EAAA,GAAK,KAAK,QAAA,IAAY,CAAA;AAC5B,EAAA,OAAO,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AAC3C;AAGO,SAAS,WAAW,KAAA,EAAuB;AAChD,EAAA,MAAM,CAAA,GAAI,iCAAA,CAAkC,IAAA,CAAK,KAAK,CAAA;AACtD,EAAA,IAAI,CAAC,GAAG,OAAO,GAAA;AACf,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,CAAA,CAAE,CAAC,CAAE,CAAA;AAC9B,EAAA,MAAM,IAAA,GAAA,CAAQ,CAAA,CAAE,CAAC,CAAA,IAAK,KAAK,WAAA,EAAY;AACvC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AAChC,EAAA,MAAM,GAAA,GAAA,CAAO,MAAA,GAAS,cAAA,GAAiB,UAAA,EAAY,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,CAAE,WAAA,EAAY,KAAM,IAAI,CAAA;AAC5F,EAAA,IAAI,GAAA,KAAQ,EAAA,EAAI,OAAO,IAAA,KAAS,MAAM,KAAA,GAAQ,GAAA;AAC9C,EAAA,OAAO,KAAA,GAAA,CAAS,MAAA,GAAS,IAAA,GAAO,GAAA,KAAS,GAAA;AAC3C;AAIA,IAAM,GAAA,GAAM;AAAA,EACV,EAAE,CAAA,EAAG,GAAA,EAAK,EAAA,EAAI,OAAA,EAAa,MAAM,MAAA,EAAO;AAAA,EACxC,EAAE,CAAA,EAAG,IAAA,EAAM,EAAA,EAAI,MAAA,EAAY,MAAM,OAAA,EAAQ;AAAA,EACzC,EAAE,CAAA,EAAG,GAAA,EAAK,EAAA,EAAI,KAAA,EAAU,MAAM,KAAA,EAAM;AAAA,EACpC,EAAE,CAAA,EAAG,GAAA,EAAK,EAAA,EAAI,IAAA,EAAS,MAAM,MAAA,EAAO;AAAA,EACpC,EAAE,CAAA,EAAG,GAAA,EAAK,EAAA,EAAI,GAAA,EAAO,MAAM,QAAA,EAAS;AAAA,EACpC,EAAE,CAAA,EAAG,GAAA,EAAK,EAAA,EAAI,GAAA,EAAM,MAAM,QAAA,EAAS;AAAA,EACnC,EAAE,CAAA,EAAG,IAAA,EAAM,EAAA,EAAI,CAAA,EAAG,MAAM,aAAA;AAC1B,CAAA;AAUO,SAAS,QAAA,CAAS,EAAA,EAAY,IAAA,GAAwB,EAAC,EAAW;AACvE,EAAA,MAAM,GAAA,GAAM,KAAK,KAAA,IAAS,CAAA;AAC1B,EAAA,IAAI,MAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,EAAE,CAAC,CAAA;AACjC,EAAA,IAAI,GAAA,KAAQ,CAAA,EAAG,OAAO,IAAA,CAAK,OAAO,WAAA,GAAc,IAAA;AAChD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,KAAK,GAAA,EAAK;AACnB,IAAA,IAAI,GAAA,GAAM,EAAE,EAAA,EAAI;AAChB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,EAAE,EAAE,CAAA;AACnC,IAAA,GAAA,IAAO,QAAQ,CAAA,CAAE,EAAA;AACjB,IAAA,KAAA,CAAM,KAAK,IAAA,CAAK,IAAA,GAAO,CAAA,EAAG,KAAK,IAAI,MAAA,CAAO,CAAA,CAAE,IAAA,EAAM,KAAK,CAAC,CAAA,CAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAA;AAC7E,IAAA,IAAI,KAAA,CAAM,UAAU,GAAA,EAAK;AAAA,EAC3B;AACA,EAAA,OAAA,CAAQ,EAAA,GAAK,IAAI,GAAA,GAAM,EAAA,IAAM,MAAM,IAAA,CAAK,IAAA,CAAK,IAAA,GAAO,GAAA,GAAM,GAAG,CAAA;AAC/D;AAIA,IAAM,EAAA,GAAyB;AAAA,EAC7B,CAAC,SAAU,MAAM,CAAA;AAAA,EAAG,CAAC,QAAS,OAAO,CAAA;AAAA,EAAG,CAAC,QAAQ,MAAM,CAAA;AAAA,EACvD,CAAC,OAAO,KAAK,CAAA;AAAA,EAAG,CAAC,MAAM,MAAM,CAAA;AAAA,EAAG,CAAC,IAAI,QAAQ,CAAA;AAAA,EAAG,CAAC,GAAG,QAAQ;AAC9D,CAAA;AAGO,SAAS,YAAA,CAAa,IAAA,EAA8B,IAAA,GAAsB,IAAA,CAAK,KAAI,EAAW;AACnG,EAAA,MAAM,CAAA,GAAI,IAAA,YAAgB,IAAA,GAAO,IAAA,CAAK,SAAQ,GAAI,OAAO,IAAA,KAAS,QAAA,GAAW,IAAI,IAAA,CAAK,IAAI,CAAA,CAAE,SAAQ,GAAI,IAAA;AACxG,EAAA,MAAM,CAAA,GAAI,IAAA,YAAgB,IAAA,GAAO,IAAA,CAAK,SAAQ,GAAI,IAAA;AAClD,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAA,CAAO,CAAA,GAAI,KAAK,GAAI,CAAA;AACtC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AACzB,EAAA,IAAI,GAAA,GAAM,GAAG,OAAO,UAAA;AACpB,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,IAAI,CAAA,IAAK,EAAA,EAAI;AAC7B,IAAA,IAAI,OAAO,IAAA,EAAM;AACf,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,IAAI,CAAA;AACnC,MAAA,MAAM,QAAQ,CAAA,EAAG,KAAK,IAAI,MAAA,CAAO,IAAA,EAAM,KAAK,CAAC,CAAA,CAAA;AAC7C,MAAA,OAAO,QAAQ,CAAA,GAAI,CAAA,EAAG,KAAK,CAAA,IAAA,CAAA,GAAS,MAAM,KAAK,CAAA,CAAA;AAAA,IACjD;AAAA,EACF;AACA,EAAA,OAAO,UAAA;AACT;AAKO,SAAS,QAAQ,CAAA,EAAmB;AACzC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,GAAI,GAAA;AAC1B,EAAA,MAAM,OAAO,GAAA,GAAM,EAAA;AACnB,EAAA,MAAM,MAAA,GAAS,GAAA,IAAO,EAAA,IAAM,GAAA,IAAO,KAAK,IAAA,GAAO,IAAA,KAAS,CAAA,GAAI,IAAA,GAAO,IAAA,KAAS,CAAA,GAAI,IAAA,GAAO,IAAA,KAAS,IAAI,IAAA,GAAO,IAAA;AAC3G,EAAA,OAAO,CAAA,EAAG,CAAC,CAAA,EAAG,MAAM,CAAA,CAAA;AACtB;AAGO,SAAS,OAAA,CAAQ,CAAA,EAAW,QAAA,GAAW,CAAA,EAAW;AACvD,EAAA,MAAM,GAAA,GAAM,CAAA,GAAI,CAAA,GAAI,GAAA,GAAM,EAAA;AAC1B,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AACpB,EAAA,MAAM,QAA4B,CAAC,CAAC,IAAA,EAAM,GAAG,GAAG,CAAC,GAAA,EAAK,GAAG,CAAA,EAAG,CAAC,GAAA,EAAK,GAAG,GAAG,CAAC,GAAA,EAAK,GAAG,CAAC,CAAA;AAClF,EAAA,KAAA,MAAW,CAAC,SAAA,EAAW,CAAC,CAAA,IAAK,KAAA,EAAO;AAClC,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,MAAM,MAAM,CAAA,GAAI,SAAA;AAChB,MAAA,OAAO,CAAA,EAAG,GAAG,CAAA,EAAI,GAAA,GAAM,MAAM,CAAA,GAAI,GAAA,CAAI,OAAA,CAAQ,CAAC,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAE,GAAG,CAAC,CAAA,CAAA;AAAA,IAC9E;AAAA,EACF;AACA,EAAA,OAAO,CAAA,EAAG,GAAG,CAAA,EAAG,CAAC,CAAA,CAAA;AACnB;AAGO,SAAS,MAAA,CAAO,CAAA,EAAW,IAAA,GAAiD,EAAC,EAAW;AAC7F,EAAA,MAAM,GAAA,GAAM,KAAK,SAAA,IAAa,GAAA;AAC9B,EAAA,MAAM,GAAA,GAAM,KAAK,OAAA,IAAW,GAAA;AAC5B,EAAA,MAAM,CAAC,KAAK,IAAI,CAAA,GAAI,OAAO,CAAC,CAAA,CAAE,MAAM,GAAG,CAAA;AACvC,EAAA,MAAM,IAAA,GAAO,GAAA,CAAK,UAAA,CAAW,GAAG,IAAI,GAAA,GAAM,EAAA;AAC1C,EAAA,MAAM,MAAA,GAAS,IAAK,OAAA,CAAQ,GAAA,EAAK,EAAE,CAAA,CAAE,OAAA,CAAQ,yBAAyB,GAAG,CAAA;AACzE,EAAA,OAAO,IAAA,GAAO,MAAA,IAAU,IAAA,GAAO,GAAA,GAAM,IAAA,GAAO,EAAA,CAAA;AAC9C;AAKO,SAAS,MAAA,CAAO,IAAA,EAAc,KAAA,EAAe,UAAA,EAA6B;AAC/E,EAAA,IAAI,KAAA,KAAU,GAAG,OAAO,IAAA;AACxB,EAAA,IAAI,YAAY,OAAO,UAAA;AACvB,EAAA,IAAI,kBAAkB,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,GAAG,IAAI,CAAA,EAAA,CAAA;AAChD,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,GAAG,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA,GAAA,CAAA;AACzD,EAAA,OAAO,GAAG,IAAI,CAAA,CAAA,CAAA;AAChB;AAGO,SAAS,SAAA,CAAU,KAAA,EAAe,IAAA,EAAc,UAAA,EAA6B;AAClF,EAAA,OAAO,GAAG,KAAK,CAAA,CAAA,EAAI,OAAO,IAAA,EAAM,KAAA,EAAO,UAAU,CAAC,CAAA,CAAA;AACpD;AAGO,SAAS,IAAA,CAAK,KAAA,EAAiB,IAAA,GAAmD,EAAC,EAAW;AACnG,EAAA,MAAM,IAAA,GAAO,KAAK,WAAA,IAAe,KAAA;AACjC,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,EAAA;AAC/B,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,MAAM,CAAC,CAAA;AACtC,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA,EAAG,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,EAAI,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AAC9D,EAAA,MAAM,OAAO,KAAA,CAAM,KAAA,CAAM,GAAG,EAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AACzC,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,IAAA,CAAK,SAAS,GAAA,GAAM,EAAE,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,EAAI,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAC,CAAA,CAAA;AAC5E;AAGO,SAAS,QAAA,CAAS,IAAA,EAAc,SAAA,EAAmB,QAAA,GAAW,QAAA,EAAa;AAChF,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,SAAA,EAAW,OAAO,IAAA;AACrC,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,SAAA,GAAY,SAAS,MAAM,CAAA;AACrD,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AACrC,EAAA,OAAA,CAAQ,SAAA,GAAY,SAAA,GAAY,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,GAAI,GAAA,EAAK,OAAA,CAAQ,eAAA,EAAiB,EAAE,CAAA,GAAI,QAAA;AACtG;AAGO,SAAS,UAAU,IAAA,EAAsB;AAC9C,EAAA,OAAO,IAAA,CAAK,aAAY,CAAE,OAAA,CAAQ,SAAS,CAAC,CAAA,KAAM,CAAA,CAAE,WAAA,EAAa,CAAA;AACnE","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"]}
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 if (!Number.isFinite(n)) return String(n);\n const str = String(n);\n // Exponential notation (e.g. 1e21, 1e-7) mangles a naive split — expand it via\n // locale grouping (en-US: \",\" groups, \".\" decimal) then apply the custom separators.\n if (str.includes(\"e\") || str.includes(\"E\")) {\n const grouped = n.toLocaleString(\"en-US\", { maximumFractionDigits: 20, useGrouping: true });\n const [g, f] = grouped.split(\".\");\n return g!.split(\",\").join(sep) + (f ? dec + f : \"\");\n }\n const [int, frac] = str.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 CHANGED
@@ -1,21 +1,61 @@
1
1
  {
2
2
  "name": "@lacspace/humanize",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Turn machine values into human-readable text — bytes, durations, relative time, ordinals, plurals, compact numbers and grammatical lists. Zero-dependency, isomorphic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
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"],
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
11
24
  "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"],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "humanize",
31
+ "pretty-bytes",
32
+ "format-bytes",
33
+ "duration",
34
+ "relative-time",
35
+ "time-ago",
36
+ "ordinal",
37
+ "pluralize",
38
+ "compact-number",
39
+ "format-number",
40
+ "zero-dependency",
41
+ "isomorphic",
42
+ "typescript"
43
+ ],
14
44
  "author": "Lacspace <contact@lacspace.com>",
15
45
  "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" }
46
+ "homepage": "https://developer.lacspace.com/packages/humanize",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/lacspace/npm-packages.git",
50
+ "directory": "humanize"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/lacspace/npm-packages/issues"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
21
61
  }