From 9058f48d67158bedda1e1c5f2d95aba5b37c2fb5 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 15 Oct 2020 20:57:24 +0200 Subject: [PATCH] Add a country list auto-generated from CLDR. --- address.go | 31 +++++- address_test.go | 58 +++++++++++ countries.go | 266 ++++++++++++++++++++++++++++++++++++++++++++++++ gen.go | 187 ++++++++++++++++++++++++++++++++++ 4 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 countries.go create mode 100644 gen.go diff --git a/address.go b/address.go index b5fc1c1..2990adc 100644 --- a/address.go +++ b/address.go @@ -3,7 +3,10 @@ package address -import "regexp" +import ( + "regexp" + "sort" +) // Address represents an address. type Address struct { @@ -81,6 +84,32 @@ func (f Format) CheckPostalCode(postalCode string) bool { return rx.MatchString(postalCode) } +// CheckCountryCode checks whether the given country code is valid. +// +// An empty country code is considered valid. +func CheckCountryCode(countryCode string) bool { + if countryCode == "" { + return true + } + _, ok := countries[countryCode] + return ok +} + +// GetCountryCodes returns all known country codes. +func GetCountryCodes() []string { + countryCodes := make([]string, 0, len(countries)) + for countryCode := range countries { + countryCodes = append(countryCodes, countryCode) + } + sort.Strings(countryCodes) + return countryCodes +} + +// GetCountryNames returns all known country names, keyed by country code. +func GetCountryNames() map[string]string { + return countries +} + // GetFormats returns all known address formats, keyed by country code. // // The ZZ address format represents the generic fallback. diff --git a/address_test.go b/address_test.go index 84a9892..fecc18b 100644 --- a/address_test.go +++ b/address_test.go @@ -121,6 +121,55 @@ func TestFormat_CheckPostalCode(t *testing.T) { } } +func TestCheckCountryCode(t *testing.T) { + tests := []struct { + countryCode string + want bool + }{ + // Empty value. + {"", true}, + // Valid country code. + {"FR", true}, + // Invalid country code. + {"ABCD", false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := address.CheckCountryCode(tt.countryCode) + if got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetCountryCodes(t *testing.T) { + countryCodes := address.GetCountryCodes() + for _, countryCode := range []string{"FR", "RS", "US"} { + if !contains(countryCodes, countryCode) { + t.Errorf("no %v country code found.", countryCode) + } + } +} + +func TestGetCountryNames(t *testing.T) { + got := address.GetCountryNames() + want := map[string]string{ + "FR": "France", + "RS": "Serbia", + } + for wantCode, wantName := range want { + gotName, ok := got[wantCode] + if !ok { + t.Errorf("no %v country code found.", wantCode) + } + if gotName != wantName { + t.Errorf("got %v, want %v", gotName, wantName) + } + } +} + func TestGetFormat(t *testing.T) { // Existing format. got := address.GetFormat("RS") @@ -159,3 +208,12 @@ func TestGetFormats(t *testing.T) { } } } + +func contains(a []string, x string) bool { + for _, v := range a { + if v == x { + return true + } + } + return false +} diff --git a/countries.go b/countries.go new file mode 100644 index 0000000..76f5d78 --- /dev/null +++ b/countries.go @@ -0,0 +1,266 @@ +// Code generated by go generate; DO NOT EDIT. +//go:generate go run gen.go + +package address + +// CLDRVersion is the CLDR version from which the data is derived. +const CLDRVersion = "37.0.0" + +var countries = map[string]string{ + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL": "Albania", + "DZ": "Algeria", + "AS": "American Samoa", + "AD": "Andorra", + "AO": "Angola", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua & Barbuda", + "AR": "Argentina", + "AM": "Armenia", + "AW": "Aruba", + "AC": "Ascension Island", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD": "Bangladesh", + "BB": "Barbados", + "BY": "Belarus", + "BE": "Belgium", + "BZ": "Belize", + "BJ": "Benin", + "BM": "Bermuda", + "BT": "Bhutan", + "BO": "Bolivia", + "BA": "Bosnia & Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR": "Brazil", + "IO": "British Indian Ocean Territory", + "VG": "British Virgin Islands", + "BN": "Brunei", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA": "Canada", + "IC": "Canary Islands", + "CV": "Cape Verde", + "BQ": "Caribbean Netherlands", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "EA": "Ceuta & Melilla", + "TD": "Chad", + "CL": "Chile", + "CN": "China", + "CX": "Christmas Island", + "CP": "Clipperton Island", + "CC": "Cocos (Keeling) Islands", + "CO": "Colombia", + "KM": "Comoros", + "CG": "Congo - Brazzaville", + "CD": "Congo - Kinshasa", + "CK": "Cook Islands", + "CR": "Costa Rica", + "HR": "Croatia", + "CU": "Cuba", + "CW": "Curaçao", + "CY": "Cyprus", + "CZ": "Czechia", + "CI": "Côte d’Ivoire", + "DK": "Denmark", + "DG": "Diego Garcia", + "DJ": "Djibouti", + "DM": "Dominica", + "DO": "Dominican Republic", + "EC": "Ecuador", + "EG": "Egypt", + "SV": "El Salvador", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "SZ": "Eswatini", + "ET": "Ethiopia", + "FK": "Falkland Islands", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE": "Germany", + "GH": "Ghana", + "GI": "Gibraltar", + "GR": "Greece", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT": "Guatemala", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard & McDonald Islands", + "HN": "Honduras", + "HK": "Hong Kong SAR China", + "HU": "Hungary", + "IS": "Iceland", + "IN": "India", + "ID": "Indonesia", + "IR": "Iran", + "IQ": "Iraq", + "IE": "Ireland", + "IM": "Isle of Man", + "IL": "Israel", + "IT": "Italy", + "JM": "Jamaica", + "JP": "Japan", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KI": "Kiribati", + "XK": "Kosovo", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA": "Laos", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR": "Liberia", + "LY": "Libya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao SAR China", + "MG": "Madagascar", + "MW": "Malawi", + "MY": "Malaysia", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX": "Mexico", + "FM": "Micronesia", + "MD": "Moldova", + "MC": "Monaco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MA": "Morocco", + "MZ": "Mozambique", + "MM": "Myanmar (Burma)", + "NA": "Namibia", + "NR": "Nauru", + "NP": "Nepal", + "NL": "Netherlands", + "NC": "New Caledonia", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NE": "Niger", + "NG": "Nigeria", + "NU": "Niue", + "NF": "Norfolk Island", + "KP": "North Korea", + "MK": "North Macedonia", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK": "Pakistan", + "PW": "Palau", + "PS": "Palestinian Territories", + "PA": "Panama", + "PG": "Papua New Guinea", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PN": "Pitcairn Islands", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "RW": "Rwanda", + "RE": "Réunion", + "WS": "Samoa", + "SM": "San Marino", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS": "Serbia", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SX": "Sint Maarten", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA": "South Africa", + "GS": "South Georgia & South Sandwich Islands", + "KR": "South Korea", + "SS": "South Sudan", + "ES": "Spain", + "LK": "Sri Lanka", + "BL": "St. Barthélemy", + "SH": "St. Helena", + "KN": "St. Kitts & Nevis", + "LC": "St. Lucia", + "MF": "St. Martin", + "PM": "St. Pierre & Miquelon", + "VC": "St. Vincent & Grenadines", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard & Jan Mayen", + "SE": "Sweden", + "CH": "Switzerland", + "SY": "Syria", + "ST": "São Tomé & Príncipe", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ": "Tanzania", + "TH": "Thailand", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad & Tobago", + "TA": "Tristan da Cunha", + "TN": "Tunisia", + "TR": "Turkey", + "TM": "Turkmenistan", + "TC": "Turks & Caicos Islands", + "TV": "Tuvalu", + "UM": "U.S. Outlying Islands", + "VI": "U.S. Virgin Islands", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "United Kingdom", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VA": "Vatican City", + "VE": "Venezuela", + "VN": "Vietnam", + "WF": "Wallis & Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe", +} diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..f65fa43 --- /dev/null +++ b/gen.go @@ -0,0 +1,187 @@ +// Copyright (c) 2020 Bojan Zivanovic and contributors +// SPDX-License-Identifier: MIT + +// +build ignore + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "reflect" + "sort" + "strings" + "text/template" + "time" +) + +const dataTemplate = `// Code generated by go generate; DO NOT EDIT. +//go:generate go run gen.go + +package address + +// CLDRVersion is the CLDR version from which the data is derived. +const CLDRVersion = "{{ .CLDRVersion }}" + +var countries = map[string]string{ + {{ export .Countries }} +} +` + +func main() { + log.Println("Fetching data...") + cldrVersion, err := fetchVersion() + if err != nil { + log.Fatal(err) + } + countries, err := fetchCountries() + if err != nil { + log.Fatal(err) + } + + log.Println("Processing...") + os.Remove("countries.go") + f, err := os.Create("countries.go") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + funcMap := template.FuncMap{ + "export": export, + } + t, err := template.New("data").Funcs(funcMap).Parse(dataTemplate) + if err != nil { + log.Fatal(err) + } + t.Execute(f, struct { + CLDRVersion string + Countries map[string]string + }{ + CLDRVersion: cldrVersion, + Countries: countries, + }) + + log.Println("Done.") +} + +// fetchVersion fetches the CLDR version from GitHub. +func fetchVersion() (string, error) { + data, err := fetchURL("https://raw.githubusercontent.com/unicode-cldr/cldr-localenames-full/master/package.json") + if err != nil { + return "", fmt.Errorf("fetchVersion: %w", err) + } + aux := struct { + Version string + }{} + if err := json.Unmarshal(data, &aux); err != nil { + return "", fmt.Errorf("fetchVersion: %w", err) + } + + return aux.Version, nil +} + +// fetchCountries fetches the CLDR country names from GitHub. +// +// The JSON version of CLDR data is used because it is more convenient +// to parse. See https://github.com/unicode-cldr/cldr-json for details. +func fetchCountries() (map[string]string, error) { + data, err := fetchURL("https://raw.githubusercontent.com/unicode-cldr/cldr-localenames-full/master/main/en/territories.json") + if err != nil { + return nil, fmt.Errorf("fetchCountries: %w", err) + } + aux := struct { + Main struct { + En struct { + LocaleDisplayNames struct { + Territories map[string]string + } + } + } + Version string + }{} + if err := json.Unmarshal(data, &aux); err != nil { + return nil, fmt.Errorf("fetchCountries: %w", err) + } + countries := aux.Main.En.LocaleDisplayNames.Territories + for countryCode := range countries { + if len(countryCode) > 2 { + delete(countries, countryCode) + } + if contains([]string{"EU", "EZ", "UN", "QO", "XA", "XB", "ZZ"}, countryCode) { + delete(countries, countryCode) + } + } + + return countries, nil +} + +func fetchURL(url string) ([]byte, error) { + client := http.Client{Timeout: 15 * time.Second} + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("fetchURL: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetchURL: Get %q: %v", url, resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("fetchURL: Get %q: %w", url, err) + } + + return data, nil +} + +func contains(a []string, x string) bool { + for _, v := range a { + if v == x { + return true + } + } + return false +} + +func export(i interface{}) string { + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Map: + return exportMap(v) + default: + return fmt.Sprintf("%#v", i) + } +} + +func exportMap(v reflect.Value) string { + var values []string + flipped := make(map[string]string, v.Len()) + iter := v.MapRange() + for iter.Next() { + key := iter.Key().String() + value := iter.Value().String() + values = append(values, value) + flipped[value] = key + } + sort.Slice(values, func(i, j int) bool { + // Compare Å as A to avoid having it come after Z. + v1 := strings.Replace(values[i], "Å", "A", 1) + v2 := strings.Replace(values[j], "Å", "A", 1) + return v1 < v2 + }) + + b := strings.Builder{} + for i, value := range values { + key := flipped[value] + fmt.Fprintf(&b, "%q: %#v,", key, value) + if i+1 < len(values) { + fmt.Fprintf(&b, "\n\t") + } + } + + return b.String() +}