utilful 2.2.4 → 2.4.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 +45 -39
- package/dist/csv.d.mts +114 -13
- package/dist/csv.mjs +245 -76
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/json.mjs +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,26 +50,24 @@ declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[]
|
|
|
50
50
|
|
|
51
51
|
#### `createCSV`
|
|
52
52
|
|
|
53
|
-
Converts an array of objects to a comma-separated values (CSV) string
|
|
53
|
+
Converts an array of objects to a comma-separated values (CSV) string. You can either specify which columns to include explicitly, or let the function automatically infer all columns from your data.
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
+
// With explicit columns
|
|
56
57
|
declare function createCSV<T extends Record<string, unknown>>(
|
|
57
|
-
data: T[],
|
|
58
|
+
data: readonly T[],
|
|
58
59
|
columns: readonly (keyof T)[],
|
|
59
|
-
options?:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/** @default '\n' */
|
|
67
|
-
lineEnding?: string
|
|
68
|
-
}
|
|
60
|
+
options?: CSVCreateOptions
|
|
61
|
+
): string
|
|
62
|
+
|
|
63
|
+
// With automatic column inference
|
|
64
|
+
declare function createCSV<T extends Record<string, unknown>>(
|
|
65
|
+
data: readonly T[],
|
|
66
|
+
options?: CSVCreateOptions
|
|
69
67
|
): string
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
**Example:**
|
|
70
|
+
**Example with explicit columns:**
|
|
73
71
|
|
|
74
72
|
```ts
|
|
75
73
|
const data = [
|
|
@@ -77,12 +75,32 @@ const data = [
|
|
|
77
75
|
{ name: 'Jane', age: '25', city: 'Boston' }
|
|
78
76
|
]
|
|
79
77
|
|
|
78
|
+
// Only include 'name' and 'age' columns
|
|
80
79
|
const csv = createCSV(data, ['name', 'age'])
|
|
81
80
|
// name,age
|
|
82
81
|
// John,30
|
|
83
82
|
// Jane,25
|
|
84
83
|
```
|
|
85
84
|
|
|
85
|
+
**Example with automatic column inference:**
|
|
86
|
+
|
|
87
|
+
When you omit the `columns` parameter, `createCSV` automatically collects all unique keys from your data in first-seen order. This is particularly useful when working with data that has varying structures:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const rows = [
|
|
91
|
+
{ name: 'John', age: '30' },
|
|
92
|
+
{ name: 'Jane', city: 'Boston' },
|
|
93
|
+
{ name: 'Bob', age: '40', city: 'Chicago' }
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
// All columns are automatically detected: name, age, city
|
|
97
|
+
const csv = createCSV(rows)
|
|
98
|
+
// name,age,city
|
|
99
|
+
// John,30,
|
|
100
|
+
// Jane,,Boston
|
|
101
|
+
// Bob,40,Chicago
|
|
102
|
+
```
|
|
103
|
+
|
|
86
104
|
#### `parseCSV`
|
|
87
105
|
|
|
88
106
|
Parses a comma-separated values (CSV) string into an array of objects.
|
|
@@ -119,7 +137,7 @@ const csv = `
|
|
|
119
137
|
name,age
|
|
120
138
|
John,30
|
|
121
139
|
Jane,25
|
|
122
|
-
|
|
140
|
+
`.trim()
|
|
123
141
|
|
|
124
142
|
const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
|
|
125
143
|
```
|
|
@@ -134,7 +152,7 @@ Recursively assigns missing properties from defaults to the source object. The s
|
|
|
134
152
|
|
|
135
153
|
**Key Features:**
|
|
136
154
|
|
|
137
|
-
- **
|
|
155
|
+
- **Nullish handling**: `null` and `undefined` values in source are replaced with defaults
|
|
138
156
|
- **Array concatenation**: Arrays are concatenated (source + defaults)
|
|
139
157
|
- **Deep merging**: Nested objects are recursively merged
|
|
140
158
|
- **Type safety**: Preserves TypeScript types
|
|
@@ -171,7 +189,7 @@ const result = defu(
|
|
|
171
189
|
// Result: { items: ['a', 'b', 'c', 'd'] }
|
|
172
190
|
```
|
|
173
191
|
|
|
174
|
-
**
|
|
192
|
+
**Handling null/undefined:**
|
|
175
193
|
|
|
176
194
|
```ts
|
|
177
195
|
const result = defu(
|
|
@@ -455,8 +473,6 @@ else
|
|
|
455
473
|
|
|
456
474
|
The `Result` type represents either success (`Ok`) or failure (`Err`).
|
|
457
475
|
|
|
458
|
-
**Type Definition:**
|
|
459
|
-
|
|
460
476
|
```ts
|
|
461
477
|
type Result<T, E> = Ok<T> | Err<E>
|
|
462
478
|
```
|
|
@@ -485,8 +501,6 @@ const result = new Err('Something went wrong')
|
|
|
485
501
|
|
|
486
502
|
Shorthand function to create an `Ok` result. Use it to wrap a successful value.
|
|
487
503
|
|
|
488
|
-
**Type Definition:**
|
|
489
|
-
|
|
490
504
|
```ts
|
|
491
505
|
declare function ok<T>(value: T): Ok<T>
|
|
492
506
|
```
|
|
@@ -495,8 +509,6 @@ declare function ok<T>(value: T): Ok<T>
|
|
|
495
509
|
|
|
496
510
|
Shorthand function to create an `Err` result. Use it to wrap an error value.
|
|
497
511
|
|
|
498
|
-
**Type Definition:**
|
|
499
|
-
|
|
500
512
|
```ts
|
|
501
513
|
declare function err<E extends string = string>(err: E): Err<E>
|
|
502
514
|
declare function err<E = unknown>(err: E): Err<E>
|
|
@@ -506,8 +518,6 @@ declare function err<E = unknown>(err: E): Err<E>
|
|
|
506
518
|
|
|
507
519
|
Wraps a function that might throw an error and returns a `Result` with the result of the function.
|
|
508
520
|
|
|
509
|
-
**Type Definition:**
|
|
510
|
-
|
|
511
521
|
```ts
|
|
512
522
|
declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
|
|
513
523
|
declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
|
|
@@ -517,25 +527,28 @@ declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T
|
|
|
517
527
|
|
|
518
528
|
Unwraps a `Result`, `Ok`, or `Err` value and returns the value or error in an object. If the result is an `Ok`, the object contains the value and an `undefined` error. If the result is an `Err`, the object contains an `undefined` value and the error.
|
|
519
529
|
|
|
520
|
-
**Example:**
|
|
521
|
-
|
|
522
530
|
```ts
|
|
523
|
-
|
|
524
|
-
|
|
531
|
+
declare function unwrapResult<T>(result: Ok<T>): { value: T, error: undefined }
|
|
532
|
+
declare function unwrapResult<E>(result: Err<E>): { value: undefined, error: E }
|
|
533
|
+
declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
|
|
525
534
|
```
|
|
526
535
|
|
|
527
|
-
**
|
|
536
|
+
**Example:**
|
|
528
537
|
|
|
529
538
|
```ts
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
|
|
539
|
+
const result = toResult(() => JSON.parse('{"foo":"bar"}'))
|
|
540
|
+
const { value, error } = unwrapResult(result)
|
|
533
541
|
```
|
|
534
542
|
|
|
535
543
|
#### `tryCatch`
|
|
536
544
|
|
|
537
545
|
A simpler alternative to `toResult` + `unwrapResult`. It executes a function that might throw an error and directly returns the result in a `ResultData` format. Works with both synchronous functions and promises.
|
|
538
546
|
|
|
547
|
+
```ts
|
|
548
|
+
declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
|
|
549
|
+
declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
|
|
550
|
+
```
|
|
551
|
+
|
|
539
552
|
**Example:**
|
|
540
553
|
|
|
541
554
|
```ts
|
|
@@ -548,13 +561,6 @@ const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
|
|
|
548
561
|
const { value, error } = await tryCatch(fetch('https://api.example.com/data').then(r => r.json()))
|
|
549
562
|
```
|
|
550
563
|
|
|
551
|
-
**Type Definition:**
|
|
552
|
-
|
|
553
|
-
```ts
|
|
554
|
-
declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
|
|
555
|
-
declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
|
|
556
|
-
```
|
|
557
|
-
|
|
558
564
|
### String
|
|
559
565
|
|
|
560
566
|
#### `template`
|
package/dist/csv.d.mts
CHANGED
|
@@ -4,10 +4,32 @@
|
|
|
4
4
|
*/
|
|
5
5
|
type CSVRow<T extends string = string> = Record<T, string>;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
7
|
+
* Options for the `createCSV` function.
|
|
8
|
+
*/
|
|
9
|
+
interface CSVCreateOptions {
|
|
10
|
+
/** @default ',' */
|
|
11
|
+
delimiter?: string;
|
|
12
|
+
/** @default true */
|
|
13
|
+
addHeader?: boolean;
|
|
14
|
+
/** @default false */
|
|
15
|
+
quoteAll?: boolean;
|
|
16
|
+
/** @default '\n' */
|
|
17
|
+
lineEnding?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Converts an array of objects to a comma-separated values (CSV) string.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* When `columns` is omitted, the function automatically infers columns by collecting
|
|
24
|
+
* the union of all keys across all objects in first-seen order. This means if different
|
|
25
|
+
* objects have different keys, all keys will be included in the CSV. Objects missing
|
|
26
|
+
* certain keys will have empty values for those columns.
|
|
27
|
+
*
|
|
28
|
+
* When `columns` is provided explicitly, only those columns are included in the output,
|
|
29
|
+
* allowing you to control column order and filter out unwanted properties.
|
|
9
30
|
*
|
|
10
31
|
* @example
|
|
32
|
+
* // With explicit columns
|
|
11
33
|
* const data = [
|
|
12
34
|
* { name: 'John', age: '30', city: 'New York' },
|
|
13
35
|
* { name: 'Jane', age: '25', city: 'Boston' }
|
|
@@ -17,17 +39,56 @@ type CSVRow<T extends string = string> = Record<T, string>;
|
|
|
17
39
|
* // name,age
|
|
18
40
|
* // John,30
|
|
19
41
|
* // Jane,25
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // With inferred columns (union of all keys in first-seen order)
|
|
45
|
+
* const rows = [
|
|
46
|
+
* { name: 'John', age: '30' },
|
|
47
|
+
* { name: 'Jane', city: 'Boston' },
|
|
48
|
+
* ]
|
|
49
|
+
*
|
|
50
|
+
* const csv = createCSV(rows)
|
|
51
|
+
* // name,age,city
|
|
52
|
+
* // John,30,
|
|
53
|
+
* // Jane,,Boston
|
|
20
54
|
*/
|
|
21
|
-
declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: readonly (keyof T)[], options?:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
55
|
+
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], columns: readonly (keyof T)[], options?: CSVCreateOptions): string;
|
|
56
|
+
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], options?: CSVCreateOptions): string;
|
|
57
|
+
/**
|
|
58
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* This function yields CSV content as strings, including line endings.
|
|
62
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const data = [
|
|
66
|
+
* { name: 'John', age: '30' },
|
|
67
|
+
* { name: 'Jane', age: '25' }
|
|
68
|
+
* ]
|
|
69
|
+
*
|
|
70
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
71
|
+
* console.log(chunk)
|
|
72
|
+
* }
|
|
73
|
+
*/
|
|
74
|
+
declare function createCSVStream<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): AsyncIterable<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
80
|
+
* all chunks into a single string. Note that the result will have a
|
|
81
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const data = [
|
|
85
|
+
* { name: 'John', age: '30' },
|
|
86
|
+
* { name: 'Jane', age: '25' }
|
|
87
|
+
* ]
|
|
88
|
+
*
|
|
89
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
90
|
+
*/
|
|
91
|
+
declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): Promise<string>;
|
|
31
92
|
/**
|
|
32
93
|
* Escapes a value for a CSV string.
|
|
33
94
|
*
|
|
@@ -74,5 +135,45 @@ declare function parseCSV<Header extends string>(csv?: string | null | undefined
|
|
|
74
135
|
*/
|
|
75
136
|
strict?: boolean;
|
|
76
137
|
}): CSVRow<Header>[];
|
|
138
|
+
/**
|
|
139
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
143
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
144
|
+
* across chunk boundaries.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
148
|
+
*
|
|
149
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
150
|
+
* console.log(row)
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
declare function parseCSVStream<Header extends string>(chunks: AsyncIterable<string> | Iterable<string>, options?: {
|
|
154
|
+
delimiter?: string;
|
|
155
|
+
trim?: boolean;
|
|
156
|
+
strict?: boolean;
|
|
157
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
158
|
+
/**
|
|
159
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
163
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
164
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
168
|
+
*
|
|
169
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
170
|
+
* console.log(row)
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
declare function parseCSVFromLines<Header extends string>(lines: AsyncIterable<string> | Iterable<string>, options?: {
|
|
174
|
+
delimiter?: string;
|
|
175
|
+
trim?: boolean;
|
|
176
|
+
strict?: boolean;
|
|
177
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
77
178
|
//#endregion
|
|
78
|
-
export { CSVRow, createCSV, escapeCSVValue, parseCSV };
|
|
179
|
+
export { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/csv.mjs
CHANGED
|
@@ -1,29 +1,87 @@
|
|
|
1
1
|
//#region src/csv.ts
|
|
2
|
+
const COMMA = ",";
|
|
3
|
+
const DOUBLE_QUOTE = "\"";
|
|
4
|
+
const NEWLINE = "\n";
|
|
5
|
+
const CARRIAGE_RETURN = "\r";
|
|
6
|
+
const ESCAPED_QUOTE = "\"\"";
|
|
7
|
+
const SPACE = " ";
|
|
8
|
+
const TAB = " ";
|
|
9
|
+
function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
10
|
+
let columns;
|
|
11
|
+
let options;
|
|
12
|
+
if (Array.isArray(columnsOrOptions)) {
|
|
13
|
+
columns = columnsOrOptions;
|
|
14
|
+
options = maybeOptions;
|
|
15
|
+
} else {
|
|
16
|
+
columns = inferColumns(data);
|
|
17
|
+
options = columnsOrOptions ?? {};
|
|
18
|
+
}
|
|
19
|
+
if (columns.length === 0 && data.length === 0) return "";
|
|
20
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
21
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
22
|
+
if (addHeader) {
|
|
23
|
+
const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
|
|
24
|
+
if (data.length === 0) return header;
|
|
25
|
+
const bodyLines = data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll));
|
|
26
|
+
return header + lineEnding + bodyLines.join(lineEnding);
|
|
27
|
+
}
|
|
28
|
+
return data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll)).join(lineEnding);
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
31
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* This function yields CSV content as strings, including line endings.
|
|
35
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
5
36
|
*
|
|
6
37
|
* @example
|
|
7
38
|
* const data = [
|
|
8
|
-
* { name: 'John', age: '30'
|
|
9
|
-
* { name: 'Jane', age: '25'
|
|
39
|
+
* { name: 'John', age: '30' },
|
|
40
|
+
* { name: 'Jane', age: '25' }
|
|
10
41
|
* ]
|
|
11
42
|
*
|
|
12
|
-
* const
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* // Jane,25
|
|
43
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
44
|
+
* console.log(chunk)
|
|
45
|
+
* }
|
|
16
46
|
*/
|
|
17
|
-
function
|
|
18
|
-
const { delimiter =
|
|
47
|
+
async function* createCSVStream(data, columns, options = {}) {
|
|
48
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
19
49
|
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
20
|
-
|
|
50
|
+
if (addHeader) yield encodeCSVHeader(columns.map(String), delimiter, quoteAll) + lineEnding;
|
|
51
|
+
for await (const row of data) yield encodeCSVRow(row, columns, delimiter, quoteAll) + lineEnding;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
58
|
+
* all chunks into a single string. Note that the result will have a
|
|
59
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const data = [
|
|
63
|
+
* { name: 'John', age: '30' },
|
|
64
|
+
* { name: 'Jane', age: '25' }
|
|
65
|
+
* ]
|
|
66
|
+
*
|
|
67
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
68
|
+
*/
|
|
69
|
+
async function createCSVAsync(data, columns, options = {}) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
for await (const chunk of createCSVStream(data, columns, options)) chunks.push(chunk);
|
|
72
|
+
return chunks.join("");
|
|
73
|
+
}
|
|
74
|
+
function encodeCSVHeader(columns, delimiter, quoteAll) {
|
|
75
|
+
return columns.map((col) => escapeCSVValue(col, {
|
|
21
76
|
delimiter,
|
|
22
77
|
quoteAll
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
78
|
+
})).join(delimiter);
|
|
79
|
+
}
|
|
80
|
+
function encodeCSVRow(row, columns, delimiter, quoteAll) {
|
|
81
|
+
return columns.map((key) => escapeCSVValue(row[key], {
|
|
82
|
+
delimiter,
|
|
83
|
+
quoteAll
|
|
84
|
+
})).join(delimiter);
|
|
27
85
|
}
|
|
28
86
|
/**
|
|
29
87
|
* Escapes a value for a CSV string.
|
|
@@ -38,12 +96,118 @@ function createCSV(data, columns, options = {}) {
|
|
|
38
96
|
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
39
97
|
*/
|
|
40
98
|
function escapeCSVValue(value, options = {}) {
|
|
41
|
-
const { delimiter =
|
|
99
|
+
const { delimiter = COMMA, quoteAll = false } = options;
|
|
42
100
|
if (value == null) return "";
|
|
43
101
|
const coercedValue = String(value);
|
|
44
|
-
|
|
102
|
+
const hasQuote = coercedValue.includes(DOUBLE_QUOTE);
|
|
103
|
+
if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) return `${DOUBLE_QUOTE}${hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue}${DOUBLE_QUOTE}`;
|
|
45
104
|
return coercedValue;
|
|
46
105
|
}
|
|
106
|
+
var CSVParserCore = class {
|
|
107
|
+
delimiter;
|
|
108
|
+
trim;
|
|
109
|
+
strict;
|
|
110
|
+
onRow;
|
|
111
|
+
currentRow = [];
|
|
112
|
+
currentRowQuotedFlags = [];
|
|
113
|
+
currentField = "";
|
|
114
|
+
inQuotes = false;
|
|
115
|
+
isFieldQuoted = false;
|
|
116
|
+
currentRowNumber = 1;
|
|
117
|
+
headerRaw;
|
|
118
|
+
headerQuotedFlags;
|
|
119
|
+
headers;
|
|
120
|
+
constructor(options, onRow) {
|
|
121
|
+
const { delimiter = COMMA, trim = true, strict = true } = options;
|
|
122
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
123
|
+
this.delimiter = delimiter;
|
|
124
|
+
this.trim = trim;
|
|
125
|
+
this.strict = strict;
|
|
126
|
+
this.onRow = onRow;
|
|
127
|
+
}
|
|
128
|
+
push(chunk) {
|
|
129
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
130
|
+
const character = chunk[i];
|
|
131
|
+
const nextCharacter = i + 1 < chunk.length ? chunk[i + 1] : "";
|
|
132
|
+
if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
|
|
133
|
+
if (character === DOUBLE_QUOTE) if (this.currentField.length === 0 && !this.inQuotes) {
|
|
134
|
+
this.inQuotes = true;
|
|
135
|
+
this.isFieldQuoted = true;
|
|
136
|
+
} else if (this.inQuotes && nextCharacter === DOUBLE_QUOTE) {
|
|
137
|
+
this.currentField += DOUBLE_QUOTE;
|
|
138
|
+
i++;
|
|
139
|
+
} else if (this.inQuotes) this.inQuotes = false;
|
|
140
|
+
else this.currentField += character;
|
|
141
|
+
else if (character === this.delimiter && !this.inQuotes) this.appendField();
|
|
142
|
+
else if ((character === NEWLINE || character === CARRIAGE_RETURN) && !this.inQuotes) {
|
|
143
|
+
if (character === CARRIAGE_RETURN && nextCharacter === NEWLINE) i++;
|
|
144
|
+
this.appendRow();
|
|
145
|
+
} else this.currentField += character;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finish() {
|
|
149
|
+
if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
|
|
150
|
+
if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
|
|
151
|
+
}
|
|
152
|
+
appendField() {
|
|
153
|
+
this.currentRow.push(this.currentField);
|
|
154
|
+
this.currentRowQuotedFlags.push(this.isFieldQuoted);
|
|
155
|
+
this.currentField = "";
|
|
156
|
+
this.isFieldQuoted = false;
|
|
157
|
+
}
|
|
158
|
+
appendRow() {
|
|
159
|
+
this.appendField();
|
|
160
|
+
if (!this.headerRaw) {
|
|
161
|
+
this.headerRaw = this.currentRow;
|
|
162
|
+
this.headerQuotedFlags = this.currentRowQuotedFlags;
|
|
163
|
+
this.processHeaderRow();
|
|
164
|
+
} else this.processDataRow(this.currentRow, this.currentRowQuotedFlags);
|
|
165
|
+
this.currentRow = [];
|
|
166
|
+
this.currentRowQuotedFlags = [];
|
|
167
|
+
this.currentRowNumber++;
|
|
168
|
+
}
|
|
169
|
+
processHeaderRow() {
|
|
170
|
+
const headerRow = this.headerRaw;
|
|
171
|
+
const headerQuotedFlags = this.headerQuotedFlags ?? [];
|
|
172
|
+
const headers = this.trim ? headerRow.map((h, i) => headerQuotedFlags[i] ? h : h.trim()) : headerRow;
|
|
173
|
+
if (headers.filter((h) => h.length === 0).length > 0) {
|
|
174
|
+
const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
|
|
175
|
+
throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
|
|
176
|
+
}
|
|
177
|
+
const headerSet = /* @__PURE__ */ new Set();
|
|
178
|
+
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
180
|
+
else headerSet.add(header);
|
|
181
|
+
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
182
|
+
this.headers = headers;
|
|
183
|
+
}
|
|
184
|
+
processDataRow(fieldValues, quotedFlags) {
|
|
185
|
+
if (!this.headers) throw new Error("CSVParserCore: headers not initialized");
|
|
186
|
+
const headers = this.headers;
|
|
187
|
+
const isFieldPopulated = this.trim ? (field, wasQuoted) => wasQuoted ? field.length > 0 : field.trim().length > 0 : (field) => field.length > 0;
|
|
188
|
+
const hasMultipleFields = fieldValues.length > 1;
|
|
189
|
+
const hasAnyPopulatedField = fieldValues.some((field, idx) => isFieldPopulated(field, quotedFlags[idx] ?? false));
|
|
190
|
+
if (!hasMultipleFields && !hasAnyPopulatedField) return;
|
|
191
|
+
if (fieldValues.length > headers.length) {
|
|
192
|
+
const fieldsExceedingHeaders = fieldValues.slice(headers.length);
|
|
193
|
+
const excessQuotedFlags = quotedFlags.slice(headers.length);
|
|
194
|
+
const containsNonEmptyOverflow = fieldsExceedingHeaders.some((field, idx) => isFieldPopulated(field, excessQuotedFlags[idx] ?? false));
|
|
195
|
+
if (this.strict && containsNonEmptyOverflow) {
|
|
196
|
+
const expectedCount = headers.length;
|
|
197
|
+
const actualCount = fieldValues.length;
|
|
198
|
+
const excessCount = actualCount - expectedCount;
|
|
199
|
+
throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const rowEntries = headers.map((header, columnIndex) => {
|
|
203
|
+
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
204
|
+
const wasQuoted = quotedFlags[columnIndex] ?? false;
|
|
205
|
+
return [header, this.trim && !wasQuoted ? untrimmedValue.trim() : untrimmedValue];
|
|
206
|
+
});
|
|
207
|
+
const rowObject = Object.fromEntries(rowEntries);
|
|
208
|
+
this.onRow(rowObject);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
47
211
|
/**
|
|
48
212
|
* Parses a comma-separated values (CSV) string into an array of objects.
|
|
49
213
|
*
|
|
@@ -61,67 +225,72 @@ function escapeCSVValue(value, options = {}) {
|
|
|
61
225
|
function parseCSV(csv, options = {}) {
|
|
62
226
|
if (!csv?.trim()) return [];
|
|
63
227
|
const rows = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
228
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
229
|
+
rows.push(row);
|
|
230
|
+
});
|
|
231
|
+
parser.push(csv);
|
|
232
|
+
parser.finish();
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
240
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
241
|
+
* across chunk boundaries.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
245
|
+
*
|
|
246
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
247
|
+
* console.log(row)
|
|
248
|
+
* }
|
|
249
|
+
*/
|
|
250
|
+
async function* parseCSVStream(chunks, options = {}) {
|
|
251
|
+
const queue = [];
|
|
252
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
253
|
+
queue.push(row);
|
|
254
|
+
});
|
|
255
|
+
for await (const chunk of chunks) {
|
|
256
|
+
parser.push(chunk);
|
|
257
|
+
while (queue.length > 0) yield queue.shift();
|
|
92
258
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
259
|
+
parser.finish();
|
|
260
|
+
while (queue.length > 0) yield queue.shift();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
264
|
+
*
|
|
265
|
+
* @remarks
|
|
266
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
267
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
268
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
272
|
+
*
|
|
273
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
274
|
+
* console.log(row)
|
|
275
|
+
* }
|
|
276
|
+
*/
|
|
277
|
+
async function* parseCSVFromLines(lines, options) {
|
|
278
|
+
yield* parseCSVStream(lines, options);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Infers column names from data by collecting the union of keys
|
|
282
|
+
* across all rows in first-seen order.
|
|
283
|
+
*/
|
|
284
|
+
function inferColumns(rows) {
|
|
285
|
+
const seenColumns = /* @__PURE__ */ new Set();
|
|
286
|
+
const columns = [];
|
|
287
|
+
for (const row of rows) if (row && typeof row === "object") for (const columnName of Object.keys(row)) {
|
|
288
|
+
if (seenColumns.has(columnName)) continue;
|
|
289
|
+
seenColumns.add(columnName);
|
|
290
|
+
columns.push(columnName);
|
|
102
291
|
}
|
|
103
|
-
|
|
104
|
-
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
105
|
-
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
106
|
-
else headerSet.add(header);
|
|
107
|
-
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
108
|
-
const isFieldPopulated = trim ? (field) => field.trim().length > 0 : (field) => field.length > 0;
|
|
109
|
-
return rows.slice(1).filter((row) => row.length > 1 || row.some(isFieldPopulated)).map((fieldValues, rowIndex) => {
|
|
110
|
-
if (fieldValues.length > headers.length) {
|
|
111
|
-
const containsNonEmptyOverflow = fieldValues.slice(headers.length).some(isFieldPopulated);
|
|
112
|
-
if (strict && containsNonEmptyOverflow) {
|
|
113
|
-
const expectedCount = headers.length;
|
|
114
|
-
const actualCount = fieldValues.length;
|
|
115
|
-
const excessCount = actualCount - expectedCount;
|
|
116
|
-
throw new SyntaxError(`CSV row ${rowIndex + 2} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return Object.fromEntries(headers.map((header, columnIndex) => {
|
|
120
|
-
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
121
|
-
return [header, trim ? untrimmedValue.trim() : untrimmedValue];
|
|
122
|
-
}));
|
|
123
|
-
});
|
|
292
|
+
return columns;
|
|
124
293
|
}
|
|
125
294
|
|
|
126
295
|
//#endregion
|
|
127
|
-
export { createCSV, escapeCSVValue, parseCSV };
|
|
296
|
+
export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MaybeArray, toArray } from "./array.mjs";
|
|
2
|
-
import { CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
@@ -9,4 +9,4 @@ import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSla
|
|
|
9
9
|
import { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
|
|
12
|
-
export { AutocompletableString, BrandedType, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toArray } from "./array.mjs";
|
|
2
|
-
import { createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
@@ -9,4 +9,4 @@ import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTraili
|
|
|
9
9
|
import { Err, Ok, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
|
|
12
|
-
export { Err, Ok, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { Err, Ok, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/json.mjs
CHANGED
|
@@ -21,11 +21,11 @@ function tryParseJSON(value) {
|
|
|
21
21
|
*/
|
|
22
22
|
function cloneJSON(value) {
|
|
23
23
|
if (typeof value !== "object" || value === null) return value;
|
|
24
|
-
if (Array.isArray(value)) return value.map((
|
|
24
|
+
if (Array.isArray(value)) return value.map((element) => typeof element !== "object" || element === null ? element : cloneJSON(element));
|
|
25
25
|
const result = {};
|
|
26
|
-
for (const
|
|
27
|
-
const
|
|
28
|
-
result[
|
|
26
|
+
for (const key in value) {
|
|
27
|
+
const propertyValue = value[key];
|
|
28
|
+
result[key] = typeof propertyValue !== "object" || propertyValue === null ? propertyValue : cloneJSON(propertyValue);
|
|
29
29
|
}
|
|
30
30
|
return result;
|
|
31
31
|
}
|
package/package.json
CHANGED