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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func main() {
if err := postcode.Validate("10007"); err != nil {
// Treat error.
}

postCodeValidator := NewPostCodeValidator()
if isValid := postCodeValidator.ValidatePostalCode("FR", "1000"); !isValid {
// Do stuff here
}
}
```

Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
module github.com/adrg/postcode

go 1.14

require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
gotest.tools v2.2.0+incompatible
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
195 changes: 195 additions & 0 deletions postcode_by_country.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package postcode

import (
"errors"
"regexp"
"strings"
)

type PostCodeValidator struct {
CountryRegexMapping map[string]string
}

// Regexes taken from https://github.com/melwynfurtado/postcode-validator
func NewPostCodeValidator() *PostCodeValidator {
return &PostCodeValidator{
CountryRegexMapping: map[string]string{
"GB": `^(?i)([A-Z]){1}([0-9][0-9]|[0-9]|[A-Z][0-9][A-Z]|[A-Z][0-9][0-9]|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-z][A-z]){1}$`,
"JE": `^JE\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$`,
"GG": `^GY\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$`,
"IM": `^IM\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$$`,
"US": `^([0-9]{5})(?:-([0-9]{4}))?$`,
"CA": `^([ABCEGHJKLMNPRSTVXY][0-9][ABCEGHJKLMNPRSTVWXYZ])\s*([0-9][ABCEGHJKLMNPRSTVWXYZ][0-9])$`,
"IE": `^([AC-FHKNPRTV-Y][0-9]{2}|D6W)[ -]?[0-9AC-FHKNPRTV-Y]{4}$`,
"DE": `^\d{5}$`,
"JP": `^\d{3}-\d{4}$`,
"FR": `\d{2}[ ]?\d{3}`,
"AU": `^\d{4}$`,
"IT": `^\d{5}$`,
"CH": `^\d{4}$`,
"AT": `^(?!0)\d{4}$`,
"ES": `^(?:0[1-9]|[1-4]\d|5[0-2])\d{3}$`,
"NL": `^\d{4}[ ]?[A-Z]{2}$`,
"BE": `^\d{4}$`,
"DK": `^\d{4}$`,
"SE": `^(SE-)?\d{3}[ ]?\d{2}$`,
"NO": `^\d{4}$`,
"BR": `^\d{5}[\-]?\d{3}$`,
"PT": `^\d{4}([\-]\d{3})?$`,
"FI": `^(FI-|AX-)?\d{5}$`,
"AX": `^22\d{3}$`,
"KR": `^\d{5}$`,
"CN": `^\d{6}$`,
"TW": `^\d{3}(\d{2})?$`,
"SG": `^\d{6}$`,
"DZ": `^\d{5}$`,
"AD": `^AD\d{3}$`,
"AR": `^([A-HJ-NP-Z])?\d{4}([A-Z]{3})?$`,
"AM": `^(37)?\d{4}$`,
"AZ": `^\d{4}$`,
"BH": `^((1[0-2]|[2-9])\d{2})?$`,
"BD": `^\d{4}$`,
"BB": `^(BB\d{5})?$`,
"BY": `^\d{6}$`,
"BM": `^[A-Z]{2}[ ]?[A-Z0-9]{2}$`,
"BA": `^\d{5}$`,
"IO": `^BBND 1ZZ$`,
"BN": `^[A-Z]{2}[ ]?\d{4}$`,
"BG": `^\d{4}$`,
"KH": `^\d{5}$`,
"CV": `^\d{4}$`,
"CL": `^\d{7}$`,
"CR": `^(\d{4,5}|\d{3}-\d{4})$`,
"HR": `^(HR-)?\d{5}$`,
"CY": `^\d{4}$`,
"CZ": `^\d{3}[ ]?\d{2}$`,
"DO": `^\d{5}$`,
"EC": `^([A-Z]\d{4}[A-Z]|(?:[A-Z]{2})?\d{6})?$`,
"EG": `^\d{5}$`,
"EE": `^\d{5}$`,
"FO": `^\d{3}$`,
"GE": `^\d{4}$`,
"GR": `^\d{3}[ ]?\d{2}$`,
"GL": `^39\d{2}$`,
"GT": `^\d{5}$`,
"HT": `^\d{4}$`,
"HN": `^(?:\d{5})?$`,
"HU": `^\d{4}$`,
"IS": `^\d{3}$`,
"IN": `^\d{6}$`,
"ID": `^\d{5}$`,
"IL": `^\d{5,7}$`,
"JO": `^\d{5}$`,
"KZ": `^\d{6}$`,
"KE": `^\d{5}$`,
"KW": `^\d{5}$`,
"LA": `^\d{5}$`,
"LV": `^(LV-)?\d{4}$`,
"LB": `^(\d{4}([ ]?\d{4})?)?$`,
"LI": `^(948[5-9])|(949[0-7])$`,
"LT": `^(LT-)?\d{5}$`,
"LU": `^(L-)?\d{4}$`,
"MK": `^\d{4}$`,
"MY": `^\d{5}$`,
"MV": `^\d{5}$`,
"MT": `^[A-Z]{3}[ ]?\d{2,4}$`,
"MU": `^((\d|[A-Z])\d{4})?$`,
"MX": `^\d{5}$`,
"MD": `^\d{4}$`,
"MC": `^980\d{2}$`,
"MA": `^\d{5}$`,
"NP": `^\d{5}$`,
"NZ": `^\d{4}$`,
"NI": `^((\d{4}-)?\d{3}-\d{3}(-\d{1})?)?$`,
"NG": `^(\d{6})?$`,
"OM": `^(PC )?\d{3}$`,
"PA": `^\d{4}$`,
"PK": `^\d{5}$`,
"PY": `^\d{4}$`,
"PH": `^\d{4}$`,
"PL": `^\d{2}-\d{3}$`,
"PR": `^00[679]\d{2}([ \-]\d{4})?$`,
"RO": `^\d{6}$`,
"RU": `^\d{6}$`,
"SM": `^4789\d$`,
"SA": `^\d{5}$`,
"SN": `^\d{5}$`,
"SK": `^\d{3}[ ]?\d{2}$`,
"SI": `^(SI-)?\d{4}$`,
"ZA": `^\d{4}$`,
"LK": `^\d{5}$`,
"TJ": `^\d{6}$`,
"TH": `^\d{5}$`,
"TN": `^\d{4}$`,
"TR": `^\d{5}$`,
"TM": `^\d{6}$`,
"UA": `^\d{5}$`,
"UY": `^\d{5}$`,
"UZ": `^\d{6}$`,
"VA": `^00120$`,
"VE": `^\d{4}$`,
"ZM": `^\d{5}$`,
"AS": `^96799$`,
"CC": `^6799$`,
"CK": `^\d{4}$`,
"RS": `^\d{5,6}$`,
"ME": `^8\d{4}$`,
"CS": `^\d{5}$`,
"YU": `^\d{5}$`,
"CX": `^6798$`,
"ET": `^\d{4}$`,
"FK": `^FIQQ 1ZZ$`,
"NF": `^2899$`,
"FM": `^(9694[1-4])([ \-]\d{4})?$`,
"GF": `^9[78]3\d{2}$`,
"GN": `^\d{3}$`,
"GP": `^9[78][01]\d{2}$`,
"GS": `^SIQQ 1ZZ$`,
"GU": `^969[123]\d([ \-]\d{4})?$`,
"GW": `^\d{4}$`,
"HM": `^\d{4}$`,
"IQ": `^\d{5}$`,
"KG": `^\d{6}$`,
"LR": `^\d{4}$`,
"LS": `^\d{3}$`,
"MG": `^\d{3}$`,
"MH": `^969[67]\d([ \-]\d{4})?$`,
"MN": `^\d{6}$`,
"MP": `^9695[012]([ \-]\d{4})?$`,
"MQ": `^9[78]2\d{2}$`,
"NC": `^988\d{2}$`,
"NE": `^\d{4}$`,
"VI": `^008(([0-4]\d)|(5[01]))([ \-]\d{4})?$`,
"VN": `^\d{6}$`,
"PF": `^987\d{2}$`,
"PG": `^\d{3}$`,
"PM": `^9[78]5\d{2}$`,
"PN": `^PCRN 1ZZ$`,
"PW": `^96940$`,
"RE": `^9[78]4\d{2}$`,
"SH": `^(ASCN|STHL) 1ZZ$`,
"SJ": `^\d{4}$`,
"SO": `^\d{5}$`,
"SZ": `^[HLMS]\d{3}$`,
"TC": `^TKCA 1ZZ$`,
"WF": `^986\d{2}$`,
"XK": `^\d{5}$`,
"YT": `^976\d{2}$`,
"INTL": `^(?:[A-Z0-9]+([- ]?[A-Z0-9]+)*)?$`,
},
}
}

func (v *PostCodeValidator) ValidatePostalCode(countryCode, postalCode string) (bool, error) {
regexForCountry, ok := v.CountryRegexMapping[strings.ToUpper(countryCode)]
if !ok {
return false, errors.New("country code not found")
}

regex, err := regexp.Compile(regexForCountry)
if err != nil {
return false, errors.New("regex does not compile " + err.Error())
}

return regex.MatchString(strings.ToUpper(postalCode)), nil
}
103 changes: 103 additions & 0 deletions postcode_by_country_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package postcode

import (
"testing"

"gotest.tools/assert"
)

func TestValidateByCountry(t *testing.T) {
testCases := []struct {
description string
country string
postCode string
expected bool
errorExpected bool
}{
{
description: "Paris, France",
country: "FR",
postCode: "75008",
expected: true,
},
{
description: "Brussels, Belgium",
country: "BE",
postCode: "1000",
expected: true,
},
{
description: "Utrecht, The Netherlands",
country: "NL",
postCode: "3511 ax",
expected: true,
},
{
description: "Utrecht, The Netherlands, Alt",
country: "NL",
postCode: "3511AX",
expected: true,
},
{
description: "Hannover, Germany",
country: "DE",
postCode: "30179",
expected: true,
},
{
description: "Vilnius, Lithuania",
country: "LT",
postCode: "LT-00200",
expected: true,
},
{
description: "Empty postal code",
country: "FR",
postCode: "",
expected: false,
},
{
description: "Short postal code",
country: "FR",
postCode: "A",
expected: false,
},
{
description: "Non-existant country code",
country: "TY",
postCode: "TY 1234",
expected: false,
errorExpected: true,
},
{
description: "Non-existant postal code format",
country: "US",
postCode: "11111111111",
expected: false,
},
{
description: "Postal code for wrong country",
country: "FR",
postCode: "1111",
expected: false,
},
{
description: "UK Postcode",
country: "GB",
postCode: "KT111AT",
expected: true,
},
}

postCodeValidator := NewPostCodeValidator()

for _, testCase := range testCases {
t.Run(testCase.description, func(tt *testing.T) {
isValid, err := postCodeValidator.ValidatePostalCode(testCase.country, testCase.postCode)
if !testCase.errorExpected {
assert.NilError(tt, err)
}
assert.Equal(tt, isValid, testCase.expected)
})
}
}
18 changes: 12 additions & 6 deletions postcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package postcode

import (
"testing"

"gotest.tools/assert"
)

func TestValidate(t *testing.T) {
Expand Down Expand Up @@ -51,21 +53,25 @@ func TestValidate(t *testing.T) {
expected: ErrShort,
},
{
description: "Inexistent country code",
description: "Non-existant country code",
input: "TY 1234",
expected: ErrInvalidCountry,
},
{
description: "Inexistent postal code format",
description: "Non-existant postal code format",
input: "11111111111",
expected: ErrInvalidFormat,
},
{
description: "Valid UK postcode",
input: "KT11 1AT",
expected: nil,
},
}

for _, testCase := range testCases {
t.Logf("Validating `%s` (%s)", testCase.input, testCase.description)
if err := Validate(testCase.input); err != testCase.expected {
t.Errorf("expected: %v; got: %v", testCase.expected, err)
}
t.Run(testCase.description, func(tt *testing.T) {
assert.Equal(tt, Validate(testCase.input), testCase.expected)
})
}
}