Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Unreleased
- feat: :doc:`/scripts/csvclean` adds a :code:`--remove-empty-columns` option to remove empty columns from standard output.
- feat: :doc:`/scripts/in2csv` guesses the ``ndjson`` format for files with :code:`.ndjson`, :code:`.jsonl` and :code:`.jl` extensions.
- fix: :code:`-C/--not-columns` now excludes the last column of an open-ended range (e.g. :code:`2-`).
- fix: :doc:`/scripts/csvjson` no longer errors on a row with a blank or unparseable :code:`--lat`/:code:`--lon` value, and instead writes a :code:`null` geometry for that feature. The :code:`bbox` member is omitted if no row has coordinates.
- fix: :doc:`/scripts/csvjson` no longer discards a :code:`--lat`/:code:`--lon` coordinate of :code:`0`, such as the equator or the prime meridian.

2.2.0 - December 15, 2025
-------------------------
Expand Down
12 changes: 8 additions & 4 deletions csvkit/utilities/csvjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ def generate_feature_collection(self, table):
('features', features),
]

if not self.args.no_bbox:
# a bbox of nulls is not valid GeoJSON, and bbox is optional
if not self.args.no_bbox and bounds.is_set():
items.insert(1, ('bbox', bounds.bbox()))

if self.args.crs:
Expand Down Expand Up @@ -244,11 +245,11 @@ def geometry_for_row(self, row):
try:
lon = float(row[self.lon_column])
lat = float(row[self.lat_column])
except ValueError:
except (TypeError, ValueError):
lon = None
lat = None

if lon and lat:
if lon is not None and lat is not None:
return OrderedDict([
('type', 'Point'),
('coordinates', [lon, lat]),
Expand All @@ -261,11 +262,14 @@ def __init__(self):
self.max_lon = None
self.max_lat = None

def is_set(self):
return self.min_lon is not None and self.min_lat is not None

def bbox(self):
return [self.min_lon, self.min_lat, self.max_lon, self.max_lat]

def add_feature(self, feature):
if 'geometry' in feature and 'coordinates' in feature['geometry']:
if feature.get('geometry') and 'coordinates' in feature['geometry']:
self.update_coordinates(feature['geometry']['coordinates'])

def update_lat(self, lat):
Expand Down
3 changes: 3 additions & 0 deletions examples/test_geo_no_coordinates.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
slug,latitude,longitude
unknown,,
also-unknown,,
3 changes: 3 additions & 0 deletions examples/test_geo_null.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
slug,latitude,longitude
tyler-museum,32.33396,-95.28174
unknown,,
4 changes: 4 additions & 0 deletions examples/test_geo_zero.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
slug,latitude,longitude
null-island,0,0
greenwich,51.48,0
equator,0,2.35
26 changes: 26 additions & 0 deletions tests/test_utilities/test_csvjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ def test_geojson_point(self):
self.assertIsInstance(geometry['coordinates'][0], float)
self.assertIsInstance(geometry['coordinates'][1], float)

def test_geojson_missing_coordinates(self):
geojson = json.loads(self.get_output(['--lat', 'latitude', '--lon', 'longitude',
'examples/test_geo_null.csv']))

self.assertEqual(geojson['bbox'], [-95.28174, 32.33396, -95.28174, 32.33396])
self.assertEqual(len(geojson['features']), 2)
self.assertIsNone(geojson['features'][1]['geometry'])

def test_geojson_zero_coordinates(self):
geojson = json.loads(self.get_output(['--lat', 'latitude', '--lon', 'longitude',
'examples/test_geo_zero.csv']))

# a zero coordinate is a real location (null island, Greenwich, the equator)
self.assertEqual(geojson['bbox'], [0.0, 0.0, 2.35, 51.48])
self.assertEqual(len(geojson['features']), 3)
self.assertEqual(geojson['features'][0]['geometry']['coordinates'], [0.0, 0.0])
self.assertEqual(geojson['features'][1]['geometry']['coordinates'], [0.0, 51.48])
self.assertEqual(geojson['features'][2]['geometry']['coordinates'], [2.35, 0.0])

def test_geojson_no_coordinates_at_all(self):
geojson = json.loads(self.get_output(['--lat', 'latitude', '--lon', 'longitude',
'examples/test_geo_no_coordinates.csv']))

# every row has a null geometry, so there is no bbox to report
self.assertNotIn('bbox', geojson)

def test_geojson_shape(self):
geojson = json.loads(self.get_output(['--lat', 'latitude', '--lon', 'longitude',
'--type', 'type', '--geometry', 'geojson', 'examples/test_geojson.csv']))
Expand Down