Skip to content

Commit 35ce3bb

Browse files
authored
Merge pull request #218 from jlewis1778/update_csv_parse
Update csv parse
2 parents 71e72a1 + 1d27bee commit 35ce3bb

4 files changed

Lines changed: 79 additions & 27 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"nanostores": "^0.11.3",
4848
"open-location-code": "^1.0.3",
4949
"osmtogeojson": "3.0.0-beta.5",
50+
"papaparse": "^5.5.3",
5051
"react": "19.1.0",
5152
"react-dom": "19.1.0",
5253
"react-icons": "^5.5.0",
@@ -68,6 +69,7 @@
6869
"@types/leaflet-draw": "^1.0.11",
6970
"@types/lodash": "^4.17.15",
7071
"@types/open-location-code": "^1.0.1",
72+
"@types/papaparse": "^5.5.2",
7173
"@types/react": "19.1.6",
7274
"@types/react-dom": "19.0.3",
7375
"@types/terraformer__arcgis": "^2.0.5",

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/maps/api/importers.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Feature, FeatureCollection, GeoJSON, Point } from "geojson";
2+
import Papa from "papaparse";
23

34
import type {
45
CustomStation,
@@ -7,42 +8,49 @@ import type {
78
} from "./types";
89

910
function parseCSV(text: string): CustomStation[] {
10-
// Expect headers including lat/lng or latitude/longitude; optional name,id
11-
const lines = text
12-
.split(/\r?\n/)
13-
.map((l) => l.trim())
14-
.filter(Boolean);
15-
if (lines.length === 0) return [];
16-
const header = lines[0]
17-
.split(/,|\t|;|\|/)
18-
.map((h) => h.trim().toLowerCase());
19-
const latIdx = header.findIndex((h) => ["lat", "latitude"].includes(h));
20-
const lngIdx = header.findIndex((h) =>
11+
const { data, errors } = Papa.parse<Record<string, string>>(text, {
12+
header: true,
13+
skipEmptyLines: true,
14+
transformHeader: (h) => h.toLowerCase().trim(),
15+
});
16+
17+
if (errors.length > 0) {
18+
throw new Error(`CSV parse error: ${errors[0].message}`);
19+
}
20+
21+
const firstRow = data[0] ?? {};
22+
const headers = Object.keys(firstRow);
23+
24+
const latKey = headers.find((h) => ["lat", "latitude"].includes(h));
25+
const lngKey = headers.find((h) =>
2126
["lng", "lon", "long", "longitude"].includes(h),
2227
);
23-
const nameIdx = header.findIndex((h) =>
28+
const nameKey = headers.find((h) =>
2429
["name", "title", "station", "label"].includes(h),
2530
);
26-
const idIdx = header.findIndex((h) =>
31+
const idKey = headers.find((h) =>
2732
["id", "station_id", "osm_id"].includes(h),
2833
);
29-
const delimiter = lines[0].includes("\t")
30-
? "\t"
31-
: lines[0].includes(";")
32-
? ";"
33-
: lines[0].includes("|")
34-
? "|"
35-
: ",";
34+
35+
if (!latKey)
36+
throw new Error("CSV missing required 'lat' or 'latitude' column");
37+
if (!lngKey)
38+
throw new Error(
39+
"CSV missing required 'lng', 'lon', 'long', or 'longitude' column",
40+
);
41+
if (!nameKey)
42+
throw new Error(
43+
"CSV missing required 'name', 'title', 'station', or 'label' column",
44+
);
3645

3746
const stations: CustomStation[] = [];
38-
for (let i = 1; i < lines.length; i++) {
39-
const cols = lines[i].split(delimiter).map((c) => c.trim());
40-
if (latIdx < 0 || lngIdx < 0) continue;
41-
const lat = parseFloat(cols[latIdx]);
42-
const lng = parseFloat(cols[lngIdx]);
47+
for (const row of data) {
48+
const lat = parseFloat(row[latKey]);
49+
const lng = parseFloat(row[lngKey]);
4350
if (!isFinite(lat) || !isFinite(lng)) continue;
44-
const name = nameIdx >= 0 ? cols[nameIdx] : undefined;
45-
const id = idIdx >= 0 && cols[idIdx] ? cols[idIdx] : `${lat},${lng}`;
51+
const name = row[nameKey];
52+
if (!name) continue;
53+
const id = idKey && row[idKey] ? row[idKey] : `${lat},${lng}`;
4654
stations.push({ id, name, lat, lng });
4755
}
4856
return stations;

tests/importers.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,28 @@ describe("parseCustomStationsFromText", () => {
6262
lng: -122.5,
6363
});
6464
});
65+
66+
it("throws error when name column is missing", () => {
67+
const csv = "lat,lng\n-122.4194,-122.4194";
68+
expect(() => parseCustomStationsFromText(csv, "text/csv")).toThrowError(
69+
/missing required 'name'/i,
70+
);
71+
});
72+
73+
it("handles multiline quoted fields", () => {
74+
const csv =
75+
'lat,lng,name\n37.7749,-122.4194,"Downtown\nSan Francisco"\n37.784,-122.41,Oakland';
76+
const stations = parseCustomStationsFromText(csv, "text/csv");
77+
expect(stations.length).toBe(2);
78+
expect(stations[0]).toMatchObject({
79+
lat: 37.7749,
80+
lng: -122.4194,
81+
name: "Downtown\nSan Francisco",
82+
});
83+
expect(stations[1]).toMatchObject({
84+
lat: 37.784,
85+
lng: -122.41,
86+
name: "Oakland",
87+
});
88+
});
6589
});

0 commit comments

Comments
 (0)