This repository has been archived by the owner on Feb 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
163 lines (149 loc) · 4.38 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @file
* load.ts
*
* Simple, Promise-based loader for common data filetypes
*/
import * as R from 'ramda';
import { csvFormatRows, csvParse, tsvParse, tsvParseRows } from 'd3-dsv';
import axios from 'axios';
/**
* Take either single or multiple URIs; parse; return as single promise.
* @param {string[]|string} urls - One or more URIs
* @return {Promise} - Promise resolving to parsed data
*/
export default function loadData(urls: string[]|string|BlobType|BlobType[]) {
return Promise.all(
parseFilesBasedOnExt(makeStringIntoArray(urls)), // @TODO Something feels off about this.
)
.then(returnObjectIfLengthIsOne);
}
/**
* Get extension, return array containing filename and extension
* @param {string} filename - File path or URL
* @return {string[]} - Array containing full file path and its extension
*/
const getFileExtension = R.converge(Array, [
R.identity,
R.compose(R.last, R.split('.')),
]);
/**
* Extract the URL and filename from the blob
*/
const getExtIfBlob = R.cond([
[
R.and(
R.compose(R.startsWith('blob:'), R.view(R.lensProp('preview'))),
R.compose(R.either(R.equals('Object'), R.equals('File')), R.type),
),
R.converge(R.append, [
R.compose(R.last, getFileExtension, R.prop('name')),
R.compose(R.of, R.prop('preview')),
]),
],
[
R.T,
getFileExtension,
],
]);
/**
* Fetch data and parse based on ext.
* @param {string[]} fileData - Info about the file to consume
* @param {string} fileData[0] - URL to fetch
* @param {string} fileData[1] - File extension lacking dot
* @return {Promise<Object|Array>} - Promise resolving to parsed data
*/
const fetchParseData = ([url, ext]: [string, string]) => R.cond([
[
R.equals('json'),
async () => (await axios(url)).data,
],
[
R.equals('csv'),
async () => csvParse((await axios(url)).data),
],
[
R.equals('tsv'),
async () => {
const { data } = await axios(url);
if (!isAnnotated(data)) return tsvParse(data);
return atsvParse(data);
},
],
[
R.either(R.equals('atsv'), R.equals('txt')),
async () => atsvParse((await axios(url)).data),
],
[R.T, () => {
throw new Error('Unrecognised file extension');
}],
])(ext); // @TODO 😒
/**
* Fetch and parse an array of file paths/URIs
* @param {string[]} URIs - Fully-qualified file URIs
* @return {Promise[]} - Array of promises resolving to data
*/
const parseFilesBasedOnExt = R.map(R.compose(fetchParseData, getExtIfBlob));
/**
* If a string is provided, wrap in array and return.
* If already array, return identity.
* @param {string} uri - a URI string
* @return {string[]} - Array of URIs ready for processing
*/
const makeStringIntoArray = R.when(R.compose(R.not, Array.isArray), R.of);
/**
* Return an object if only item in array; otherwise return identity
* @param {Array} results - Results array containing one or more objects
* @return {Object|Array} - Either the only object in the array, or identity.
*/
const returnObjectIfLengthIsOne = R.when(R.compose(R.equals(1), R.length), R.head);
/**
* Returns whether a TSV is annotated
* @param {string} data - TSV as unparsed string
* @return {boolean} - Whether TSV has annotations denoted by '&'
*/
const isAnnotated = R.compose(
R.converge(
R.eqBy(R.length),
[
R.filter(R.isEmpty),
R.tail,
],
),
R.last,
tsvParseRows,
);
/**
* Creates a meta object from annotated TSV comments
* @param {string[][]} - Parsed TSV rows
* @return {Object} - Extracted metadata
*/
const getMeta = R.compose(
R.fromPairs,
R.map(R.compose(R.split('='), R.tail)),
R.filter(R.compose(R.gt(R.__, -1), R.indexOf('='))),
R.map(R.head),
);
/**
* Parse an annotated TSV into data and annotations
* @TODO this is way too complex and should be simplified
* @param {string} data - Unparsed ATSV string
* @return {Object} results
* @return {Object} results.meta - Annotations
* @return {Object[]} results.data - Parsed TSV data
*/
function atsvParse(data: string) {
const rows = tsvParseRows(data);
const meta = getMeta(rows);
if (rows[0][0] === '&') rows[0][0] = 'date';
const filtered = rows.filter(row => row[0].indexOf('&') === -1);
const parsed = csvParse(csvFormatRows(filtered));
return {
meta,
data: parsed,
};
}
type BlobType = {
preview: string;
name: string;
};