#!/usr/bin/env python3 """Find all US zip codes within n miles of a central zip code, reporting the zipcode, city, state and (center) distance.""" import csv import functools import math # import pprint import sys @functools.total_ordering class Zipcode: """Hold one zipcode.""" def __init__(self, *, zipno, city, state, latitude, longitude): """Initialize.""" # zipno is a string, because some zipcodes start with a leading zero, and we don't want to lose that self.zipno = zipno self.city = city self.state = state self.latitude = latitude self.longitude = longitude def __str__(self): """Convert to a string.""" return f'{self.zipno}, {self.city}, {self.state}' def __repr__(self): """Convert to a string for debugging.""" return str(self) + f', {self.latitude}, {self.longitude}' def __eq__(self, other): """Compare latitude, and longitude - for the sake of sorting.""" return self.latitude == other.latitude and self.longitude == other.longitude def __lt__(self, other): """Compare latitude and longitude - for the sake of sorting.""" if self.latitude < other.latitude: return True if self.longitude < other.longitude: return True return False # based on "improved accuracy" at http://www.meridianworlddata.com/distance-calculation.asp def approximate_distance_in_miles(self, other): """Approximate the distance in miles, from one lat/long to another.""" scale = 69.1 x = scale * (other.latitude - self.latitude) y = scale * (other.longitude - self.longitude) * math.cos(self.latitude / 57.3) return (x ** 2 + y ** 2) ** 0.5 def gen_zipcodes_1(): """Generate zipcodes from a .csv file.""" # This came from https://public.opendatasoft.com/explore/dataset/us-zip-code-latitude-and-longitude/table/ # I suspect the data may be off, because Santa Clarita came up within 40 miles of Laguna Niguel, but it should be more like # 97 miles according to Google Maps. Granted, Google Maps is not as the crow flies, but it still shouldn't differ by that # much. with open('us-zip-code-latitude-and-longitude.csv', newline='') as csvfile: csv_reader = csv.reader(csvfile, delimiter=';', quotechar='"') for row in csv_reader: if row[0] == 'Zip': continue # zipno, city, state, latitude, longitude zipcode = Zipcode(zipno=row[0], city=row[1], state=row[2], latitude=row[3], longitude=row[4]) yield zipcode def gen_zipcodes_2(): """Generate zipcodes from a .csv file.""" # This came from https://simplemaps.com/data/us-zips with open('uszips.csv', newline='') as csvfile: csv_reader = csv.reader(csvfile, delimiter=',', quotechar='"') for row in csv_reader: if row[0] == 'zip': continue # zipno, city, state, latitude, longitude zipcode = Zipcode(zipno=row[0], city=row[3], state=row[4], latitude=float(row[1]), longitude=float(row[2])) yield zipcode def main(): """Start the ball rolling.""" center_zipno = None max_distance = None while sys.argv[1:]: if sys.argv[1] == '--center-zip': # center_zip is a string, because some zipcodes start with a leading zero, and we don't want to lose that center_zipno = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--max-distance': max_distance = float(sys.argv[2]) del sys.argv[1] all_good = True if center_zipno is None: print('--center-zip is a required option', file=sys.stderr) all_good = False if max_distance is None: print('--max-distance is a required option', file=sys.stderr) all_good = False if not all_good: print('One or more items in preflight check failed', file=sys.stderr) sys.exit(1) zipcodes = list(gen_zipcodes_2()) zipcodes.sort() zipcodes_by_zip = {zipcode.zipno: zipcode for zipcode in zipcodes} # pprint.pprint(zipcodes) # pprint.pprint(zipcodes_by_zip) center_zipcode = zipcodes_by_zip[center_zipno] result_zipcodes = [] for zipcode in zipcodes: approx_distance = center_zipcode.approximate_distance_in_miles(zipcode) if approx_distance <= max_distance: tuple_ = (approx_distance, zipcode) result_zipcodes.append(tuple_) result_zipcodes.sort() print('Distance in miles, Zipcode, City, State') for approx_distance, result_zipcode in result_zipcodes: print(f'{approx_distance:.1f}, {repr(result_zipcode)}') main()