@forthtilliath/ts-kit 0.7.0 → 0.9.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 CHANGED
@@ -46,6 +46,10 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
46
46
  - `rankByNameMatch(items, query, getName)` — ranks `items` by relevance to
47
47
  `query`: earlier match position first, then shorter name — for a
48
48
  search-as-you-type list.
49
+ - `sortUpcomingFirst(items, getDates, { now? })` — upcoming items first
50
+ (nearest first), then past ones (most recent first), undated last — for a
51
+ list of events. An item may carry several dates: it stays upcoming while
52
+ one is ahead (ranked by its next date), then is ranked by its latest date.
49
53
  - `swapItems(items, i, j)` — copy of `items` with the elements at `i` and `j`
50
54
  swapped (e.g. moving an item up/down) — a plain copy if either position is
51
55
  out of bounds.
@@ -84,6 +88,16 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
84
88
  rarely needed since `FArray` is already assignable to `T[]` anywhere a
85
89
  plain array is expected, but useful when the runtime type matters too.
86
90
 
91
+ ### `csv`
92
+
93
+ - `parseCsvLine(line, delimiter?)` — splits one CSV line into trimmed cells;
94
+ quoted cells may contain the delimiter, `""` stands for a literal quote.
95
+ Delimiter defaults to `","`.
96
+ - `toCsv(rows, delimiter?)` — serializes rows to CSV, every cell quoted and
97
+ escaped, rows joined with `\n`.
98
+ - `downloadCsv(filename, content)` — browser download as `text/csv`, with a
99
+ UTF-8 BOM so Excel reads accented characters.
100
+
87
101
  ### `date`
88
102
 
89
103
  - `startOfDay(date?)` — returns a new `Date` at local midnight.
@@ -3,5 +3,6 @@ export * from "./flattenDeep.js";
3
3
  export * from "./getMostRecentIds.js";
4
4
  export * from "./nextInCycle.js";
5
5
  export * from "./rankByNameMatch.js";
6
+ export * from "./sortUpcomingFirst.js";
6
7
  export * from "./swapItems.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
@@ -3,4 +3,5 @@ export * from "./flattenDeep.js";
3
3
  export * from "./getMostRecentIds.js";
4
4
  export * from "./nextInCycle.js";
5
5
  export * from "./rankByNameMatch.js";
6
+ export * from "./sortUpcomingFirst.js";
6
7
  export * from "./swapItems.js";
@@ -0,0 +1,30 @@
1
+ export type DateInput = Date | string | number;
2
+ export interface SortUpcomingFirstOptions {
3
+ /** Instant separating upcoming from past items. Defaults to now. */
4
+ now?: DateInput;
5
+ }
6
+ /**
7
+ * Sorts items around a reference instant: upcoming items first, nearest
8
+ * first, then past items, most recent first — the usual order for a list of
9
+ * events (concerts, meetings, bookings…). Items without a valid date go last.
10
+ * Ties keep their original order.
11
+ *
12
+ * An item may carry several dates (e.g. an event with several sessions): it
13
+ * stays upcoming as long as one of its dates is still ahead and is ranked by
14
+ * its next date; once all are past, it is ranked by its latest date.
15
+ *
16
+ * @param items - The items to sort (left untouched).
17
+ * @param getDates - Reads the date(s) off an item (`Date`, ISO string or
18
+ * timestamp); `null`/`undefined` or invalid dates are ignored.
19
+ * @param options - `now`: the reference instant (a date equal to it counts as
20
+ * upcoming). Defaults to now.
21
+ * @returns A sorted copy of `items`.
22
+ * @example
23
+ * sortUpcomingFirst(
24
+ * [{ d: "2026-01-01" }, { d: "2026-12-01" }, { d: "2026-03-01" }, { d: "2026-10-01" }],
25
+ * (item) => item.d,
26
+ * { now: "2026-06-01" },
27
+ * ); // => 2026-10-01, 2026-12-01, 2026-03-01, 2026-01-01
28
+ */
29
+ export declare function sortUpcomingFirst<T>(items: readonly T[], getDates: (item: T) => DateInput | readonly DateInput[] | null | undefined, { now }?: SortUpcomingFirstOptions): T[];
30
+ //# sourceMappingURL=sortUpcomingFirst.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sortUpcomingFirst.d.ts","sourceRoot":"","sources":["../../src/array/sortUpcomingFirst.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE/C,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,GAAG,CAAC,EAAE,SAAS,CAAC;CACjB;AAiBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,GAAG,SAAS,SAAS,EAAE,GAAG,IAAI,GAAG,SAAS,EAC1E,EAAE,GAAgB,EAAE,GAAE,wBAA6B,GAClD,CAAC,EAAE,CAaL"}
@@ -0,0 +1,51 @@
1
+ // Upcoming items first, then past items, then items without a usable date.
2
+ const UPCOMING = 0;
3
+ const PAST = 1;
4
+ const UNDATED = 2;
5
+ function toTimes(dates) {
6
+ if (dates == null)
7
+ return [];
8
+ const list = Array.isArray(dates)
9
+ ? dates
10
+ : [dates];
11
+ return list
12
+ .map((date) => new Date(date).getTime())
13
+ .filter((time) => !Number.isNaN(time));
14
+ }
15
+ /**
16
+ * Sorts items around a reference instant: upcoming items first, nearest
17
+ * first, then past items, most recent first — the usual order for a list of
18
+ * events (concerts, meetings, bookings…). Items without a valid date go last.
19
+ * Ties keep their original order.
20
+ *
21
+ * An item may carry several dates (e.g. an event with several sessions): it
22
+ * stays upcoming as long as one of its dates is still ahead and is ranked by
23
+ * its next date; once all are past, it is ranked by its latest date.
24
+ *
25
+ * @param items - The items to sort (left untouched).
26
+ * @param getDates - Reads the date(s) off an item (`Date`, ISO string or
27
+ * timestamp); `null`/`undefined` or invalid dates are ignored.
28
+ * @param options - `now`: the reference instant (a date equal to it counts as
29
+ * upcoming). Defaults to now.
30
+ * @returns A sorted copy of `items`.
31
+ * @example
32
+ * sortUpcomingFirst(
33
+ * [{ d: "2026-01-01" }, { d: "2026-12-01" }, { d: "2026-03-01" }, { d: "2026-10-01" }],
34
+ * (item) => item.d,
35
+ * { now: "2026-06-01" },
36
+ * ); // => 2026-10-01, 2026-12-01, 2026-03-01, 2026-01-01
37
+ */
38
+ export function sortUpcomingFirst(items, getDates, { now = Date.now() } = {}) {
39
+ const nowTime = new Date(now).getTime();
40
+ const keyed = items.map((item) => {
41
+ const times = toTimes(getDates(item));
42
+ const upcoming = times.filter((time) => time >= nowTime);
43
+ if (upcoming.length > 0)
44
+ return { item, group: UPCOMING, key: Math.min(...upcoming) };
45
+ if (times.length > 0)
46
+ return { item, group: PAST, key: -Math.max(...times) };
47
+ return { item, group: UNDATED, key: 0 };
48
+ });
49
+ keyed.sort((a, b) => a.group - b.group || a.key - b.key);
50
+ return keyed.map((k) => k.item);
51
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Downloads CSV content as a file. Prepends a UTF-8 byte order mark so Excel
3
+ * reads accented characters correctly instead of guessing a legacy encoding.
4
+ *
5
+ * @param filename - The filename to save to.
6
+ * @param content - The CSV content (see `toCsv`).
7
+ * @example
8
+ * downloadCsv("members.csv", toCsv([["name"], ["Zoé"]]));
9
+ */
10
+ export declare function downloadCsv(filename: string, content: string): void;
11
+ //# sourceMappingURL=downloadCsv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloadCsv.d.ts","sourceRoot":"","sources":["../../src/csv/downloadCsv.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEnE"}
@@ -0,0 +1,13 @@
1
+ import { downloadTextBlob } from "../files/downloadText.js";
2
+ /**
3
+ * Downloads CSV content as a file. Prepends a UTF-8 byte order mark so Excel
4
+ * reads accented characters correctly instead of guessing a legacy encoding.
5
+ *
6
+ * @param filename - The filename to save to.
7
+ * @param content - The CSV content (see `toCsv`).
8
+ * @example
9
+ * downloadCsv("members.csv", toCsv([["name"], ["Zoé"]]));
10
+ */
11
+ export function downloadCsv(filename, content) {
12
+ downloadTextBlob(filename, "" + content, "text/csv");
13
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./downloadCsv.js";
2
+ export * from "./parseCsvLine.js";
3
+ export * from "./toCsv.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./downloadCsv.js";
2
+ export * from "./parseCsvLine.js";
3
+ export * from "./toCsv.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Splits a single CSV line into its cells. A cell wrapped in double quotes may
3
+ * contain the delimiter, and a doubled double quote (`""`) inside it stands for
4
+ * a literal one. Every cell is trimmed.
5
+ *
6
+ * @param line - One line of CSV (no line break).
7
+ * @param delimiter - The cell separator. Defaults to `","`.
8
+ * @returns The cells of the line, unquoted and trimmed.
9
+ * @example
10
+ * parseCsvLine('a, "b, c" ,d'); // => ["a", "b, c", "d"]
11
+ * parseCsvLine('"He said ""hi"""'); // => ['He said "hi"']
12
+ * parseCsvLine("a;b", ";"); // => ["a", "b"]
13
+ */
14
+ export declare function parseCsvLine(line: string, delimiter?: string): string[];
15
+ //# sourceMappingURL=parseCsvLine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseCsvLine.d.ts","sourceRoot":"","sources":["../../src/csv/parseCsvLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,MAAM,EAAE,CAoBpE"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Splits a single CSV line into its cells. A cell wrapped in double quotes may
3
+ * contain the delimiter, and a doubled double quote (`""`) inside it stands for
4
+ * a literal one. Every cell is trimmed.
5
+ *
6
+ * @param line - One line of CSV (no line break).
7
+ * @param delimiter - The cell separator. Defaults to `","`.
8
+ * @returns The cells of the line, unquoted and trimmed.
9
+ * @example
10
+ * parseCsvLine('a, "b, c" ,d'); // => ["a", "b, c", "d"]
11
+ * parseCsvLine('"He said ""hi"""'); // => ['He said "hi"']
12
+ * parseCsvLine("a;b", ";"); // => ["a", "b"]
13
+ */
14
+ export function parseCsvLine(line, delimiter = ",") {
15
+ const cells = [];
16
+ let current = "";
17
+ let inQuotes = false;
18
+ for (let i = 0; i < line.length; i++) {
19
+ const ch = line.charAt(i);
20
+ if (ch === '"') {
21
+ if (inQuotes && line.charAt(i + 1) === '"') {
22
+ current += '"';
23
+ i++;
24
+ }
25
+ else
26
+ inQuotes = !inQuotes;
27
+ }
28
+ else if (ch === delimiter && !inQuotes) {
29
+ cells.push(current.trim());
30
+ current = "";
31
+ }
32
+ else {
33
+ current += ch;
34
+ }
35
+ }
36
+ cells.push(current.trim());
37
+ return cells;
38
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Serializes rows to CSV. Every cell is wrapped in double quotes (any internal
3
+ * double quote doubled), so delimiters and line breaks inside a cell are safe.
4
+ * Rows are joined with `\n`.
5
+ *
6
+ * @param rows - The rows to serialize, header row included if any.
7
+ * @param delimiter - The cell separator. Defaults to `","`.
8
+ * @returns The CSV content.
9
+ * @example
10
+ * toCsv([["name", "age"], ["Ann", 32]]); // => '"name","age"\n"Ann","32"'
11
+ * toCsv([['say "hi"']]); // => '"say ""hi"""'
12
+ */
13
+ export declare function toCsv(rows: readonly (readonly (string | number)[])[], delimiter?: string): string;
14
+ //# sourceMappingURL=toCsv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toCsv.d.ts","sourceRoot":"","sources":["../../src/csv/toCsv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CACnB,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,EAAE,EAC/C,SAAS,SAAM,GACd,MAAM,CAQR"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Serializes rows to CSV. Every cell is wrapped in double quotes (any internal
3
+ * double quote doubled), so delimiters and line breaks inside a cell are safe.
4
+ * Rows are joined with `\n`.
5
+ *
6
+ * @param rows - The rows to serialize, header row included if any.
7
+ * @param delimiter - The cell separator. Defaults to `","`.
8
+ * @returns The CSV content.
9
+ * @example
10
+ * toCsv([["name", "age"], ["Ann", 32]]); // => '"name","age"\n"Ann","32"'
11
+ * toCsv([['say "hi"']]); // => '"say ""hi"""'
12
+ */
13
+ export function toCsv(rows, delimiter = ",") {
14
+ return rows
15
+ .map((row) => row
16
+ .map((cell) => `"${String(cell).replace(/"/g, '""')}"`)
17
+ .join(delimiter))
18
+ .join("\n");
19
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./array/index.js";
2
2
  export * from "./async/index.js";
3
3
  export * from "./classes/index.js";
4
+ export * from "./csv/index.js";
4
5
  export * from "./date/index.js";
5
6
  export * from "./files/index.js";
6
7
  export * from "./id/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  export * from "./array/index.js";
7
7
  export * from "./async/index.js";
8
8
  export * from "./classes/index.js";
9
+ export * from "./csv/index.js";
9
10
  export * from "./date/index.js";
10
11
  export * from "./files/index.js";
11
12
  export * from "./id/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/ts-kit",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -26,8 +26,8 @@
26
26
  "jsdom": "^29.1.1",
27
27
  "typescript": "^5.9.3",
28
28
  "vitest": "^4.1.10",
29
- "@forthtilliath/typescript-config": "0.1.1",
30
- "@forthtilliath/eslint-config": "0.7.0"
29
+ "@forthtilliath/eslint-config": "0.7.0",
30
+ "@forthtilliath/typescript-config": "0.1.1"
31
31
  },
32
32
  "scripts": {
33
33
  "dev": "tsc --watch -p tsconfig.build.json",