package zip import ( _ "embed" "math" "strconv" "strings" ) //go:embed testdata/simplemaps_uszips_basicv1.79/uszips.csv var csv string type Zip struct { Lat float64 Lng float64 City string State string } var zips map[string]Zip func init() { if len(zips) > 0 { return } zips = map[string]Zip{} trim := func(s string) string { return strings.Trim(s, `"`) } for _, line := range strings.Split(csv, "\n")[1:] { strs := strings.Split(line, ",") if len(strs) < 5 { continue } zip := trim(strs[0]) lat, _ := strconv.ParseFloat(trim(strs[1]), 32) lng, _ := strconv.ParseFloat(trim(strs[2]), 32) city := trim(alphafy(strs[3])) state := strings.ToUpper(trim(strs[4])) zips[zip] = Zip{ Lat: lat, Lng: lng, City: city, State: state, } } } func alphafy(s string) string { bs := make([]byte, 0, len(s)+1) for i := range s { if (s[i] < 'a' || s[i] > 'z') && (s[i] < 'A' || s[i] > 'Z') { } else { bs = append(bs, s[i]) } } return strings.ToLower(string(bs)) } func (zip Zip) MilesTo(other Zip) int { // c**2 = a**2 + b**2 // 69.2 mi per lat // 60.0 mi per lng return int(math.Sqrt( math.Pow(69.2*(math.Abs(zip.Lat)-math.Abs(other.Lat)), 2) + math.Pow(60.0*(math.Abs(zip.Lng)-math.Abs(other.Lng)), 2), )) } func Get(zip string) Zip { if z, ok := zips[zip]; ok { return z } zipAsI, err := strconv.Atoi(strings.Split(zip, "-")[0]) if err != nil { return Zip{} } for i := 0; i < 5; i++ { j := i - 2 if z2, ok := zips[strconv.Itoa(zipAsI+j)]; ok { return z2 } } return Zip{} } func GetStatesWithin(zip Zip, miles int) []string { states := map[string]struct{}{} for _, other := range zips { if zip.MilesTo(other) <= miles { states[other.State] = struct{}{} } } result := []string{} for state := range states { result = append(result, state) } return result } func FromCityState(city, state string) Zip { city = alphafy(city) state = strings.ToUpper(state) for _, z := range zips { if z.City == city && z.State == state { return z } } return Zip{} }