-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
127 lines (109 loc) · 3.79 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import {map} from "@softwareventures/array";
import type {ReadonlyTable} from "@softwareventures/table";
import regexEscape = require("escape-string-regexp");
type ParseState = "none" | "after-comma" | "in-linebreak" | "in-quote" | "after-quote";
interface ParseData {
readonly state: ParseState;
readonly result: ReadonlyTable;
readonly record: readonly string[];
readonly field: string;
}
function changeState(parseData: ParseData, state: ParseState): ParseData {
return {
state,
result: parseData.result,
record: parseData.record,
field: parseData.field
};
}
function appendText(parseData: ParseData, text: string): ParseData {
return {
state: parseData.state,
result: parseData.result,
record: parseData.record,
field: parseData.field + text
};
}
function endField(parseData: ParseData): ParseData {
return {
state: parseData.state,
result: parseData.result,
record: parseData.record.concat([parseData.field]),
field: ""
};
}
function endRecord(parseData: ParseData): ParseData {
return {
state: parseData.state,
result: parseData.result.concat([parseData.record.concat([parseData.field])]),
record: [],
field: ""
};
}
function endData(parseData: ParseData): ParseData {
if (parseData.record.length === 0 && parseData.field.length === 0) {
return parseData;
} else {
return endRecord(parseData);
}
}
export interface Configuration {
readonly separator?: string;
readonly quote?: string;
}
const defaultSeparator = ",";
const defaultQuote = '"';
export const csv: Configuration = {
separator: defaultSeparator,
quote: defaultQuote
};
export const tsv: Configuration = {
separator: "\t",
quote: defaultQuote
};
const initial: ParseData = {
state: "none",
result: [],
record: [],
field: ""
};
export function parse(data: string, configuration?: Configuration): ReadonlyTable {
const separator = configuration?.separator ?? defaultSeparator;
const quote = configuration?.quote ?? defaultQuote;
const resultState = String(data)
.split("")
.reduce((data: ParseData, char: string) => {
if (data.state === "after-comma" && char === " ") {
return data;
} else if (data.state === "in-linebreak" && char === "\n") {
return changeState(data, "none");
} else if (data.state === "after-quote" && char === quote) {
return changeState(appendText(data, quote), "in-quote");
} else if (data.state === "in-quote") {
if (char === quote) {
return changeState(data, "after-quote");
} else {
return appendText(data, char);
}
} else if (char === separator) {
return changeState(endField(data), "after-comma");
} else if (char === "\r") {
return changeState(endRecord(data), "in-linebreak");
} else if (char === "\n") {
return changeState(endRecord(data), "none");
} else if (char === quote) {
return changeState(data, "in-quote");
} else {
return changeState(appendText(data, char), "none");
}
}, initial);
return endData(resultState).result;
}
export function write(table: ReadonlyTable, configuration?: Configuration): string {
const separator = configuration?.separator ?? defaultSeparator;
const quote = configuration?.quote ?? defaultQuote;
const quoteRegex = new RegExp(regexEscape(quote), "gu");
return map(table, row =>
map(row, field => quote + field.replace(quoteRegex, quote + quote) + quote).join(separator)
).join("\r\n");
}