├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── LICENSE
├── composer.json
├── psalm.xml
└── src
├── CountryPostcodeFormatter.php
├── FormatHelper
└── StripPrefix.php
├── Formatter
├── ADFormatter.php
├── AFFormatter.php
├── AIFormatter.php
├── ALFormatter.php
├── AMFormatter.php
├── AQFormatter.php
├── ARFormatter.php
├── ASFormatter.php
├── ATFormatter.php
├── AUFormatter.php
├── AXFormatter.php
├── AZFormatter.php
├── BAFormatter.php
├── BBFormatter.php
├── BDFormatter.php
├── BEFormatter.php
├── BGFormatter.php
├── BHFormatter.php
├── BLFormatter.php
├── BMFormatter.php
├── BNFormatter.php
├── BRFormatter.php
├── BTFormatter.php
├── BYFormatter.php
├── CAFormatter.php
├── CCFormatter.php
├── CHFormatter.php
├── CLFormatter.php
├── CNFormatter.php
├── COFormatter.php
├── CRFormatter.php
├── CUFormatter.php
├── CVFormatter.php
├── CXFormatter.php
├── CYFormatter.php
├── CZFormatter.php
├── DEFormatter.php
├── DKFormatter.php
├── DOFormatter.php
├── DZFormatter.php
├── ECFormatter.php
├── EEFormatter.php
├── EGFormatter.php
├── ESFormatter.php
├── ETFormatter.php
├── FIFormatter.php
├── FKFormatter.php
├── FMFormatter.php
├── FOFormatter.php
├── FRFormatter.php
├── GBFormatter.php
├── GEFormatter.php
├── GFFormatter.php
├── GGFormatter.php
├── GIFormatter.php
├── GLFormatter.php
├── GNFormatter.php
├── GPFormatter.php
├── GRFormatter.php
├── GSFormatter.php
├── GTFormatter.php
├── GUFormatter.php
├── GWFormatter.php
├── HNFormatter.php
├── HRFormatter.php
├── HTFormatter.php
├── HUFormatter.php
├── ICFormatter.php
├── IDFormatter.php
├── IEFormatter.php
├── ILFormatter.php
├── IMFormatter.php
├── INFormatter.php
├── IOFormatter.php
├── IQFormatter.php
├── IRFormatter.php
├── ISFormatter.php
├── ITFormatter.php
├── JEFormatter.php
├── JOFormatter.php
├── JPFormatter.php
├── KEFormatter.php
├── KGFormatter.php
├── KHFormatter.php
├── KRFormatter.php
├── KWFormatter.php
├── KYFormatter.php
├── KZFormatter.php
├── LAFormatter.php
├── LBFormatter.php
├── LCFormatter.php
├── LIFormatter.php
├── LKFormatter.php
├── LRFormatter.php
├── LSFormatter.php
├── LTFormatter.php
├── LUFormatter.php
├── LVFormatter.php
├── MAFormatter.php
├── MCFormatter.php
├── MDFormatter.php
├── MEFormatter.php
├── MFFormatter.php
├── MGFormatter.php
├── MHFormatter.php
├── MKFormatter.php
├── MMFormatter.php
├── MNFormatter.php
├── MPFormatter.php
├── MQFormatter.php
├── MSFormatter.php
├── MTFormatter.php
├── MUFormatter.php
├── MVFormatter.php
├── MXFormatter.php
├── MYFormatter.php
├── MZFormatter.php
├── NCFormatter.php
├── NEFormatter.php
├── NFFormatter.php
├── NGFormatter.php
├── NIFormatter.php
├── NLFormatter.php
├── NOFormatter.php
├── NPFormatter.php
├── NZFormatter.php
├── OMFormatter.php
├── PAFormatter.php
├── PEFormatter.php
├── PFFormatter.php
├── PGFormatter.php
├── PHFormatter.php
├── PKFormatter.php
├── PLFormatter.php
├── PMFormatter.php
├── PNFormatter.php
├── PRFormatter.php
├── PSFormatter.php
├── PTFormatter.php
├── PWFormatter.php
├── PYFormatter.php
├── REFormatter.php
├── ROFormatter.php
├── RSFormatter.php
├── RUFormatter.php
├── SAFormatter.php
├── SDFormatter.php
├── SEFormatter.php
├── SGFormatter.php
├── SHFormatter.php
├── SIFormatter.php
├── SJFormatter.php
├── SKFormatter.php
├── SMFormatter.php
├── SNFormatter.php
├── SVFormatter.php
├── SZFormatter.php
├── TCFormatter.php
├── TFFormatter.php
├── THFormatter.php
├── TJFormatter.php
├── TMFormatter.php
├── TNFormatter.php
├── TRFormatter.php
├── TTFormatter.php
├── TWFormatter.php
├── TZFormatter.php
├── UAFormatter.php
├── USFormatter.php
├── UYFormatter.php
├── UZFormatter.php
├── VAFormatter.php
├── VCFormatter.php
├── VEFormatter.php
├── VGFormatter.php
├── VIFormatter.php
├── VNFormatter.php
├── WFFormatter.php
├── WSFormatter.php
├── YTFormatter.php
├── ZAFormatter.php
└── ZMFormatter.php
├── InvalidPostcodeException.php
├── PostcodeFormatter.php
└── UnknownCountryException.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: BenMorel
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | env:
8 | PSALM_PHP_VERSION: "8.4"
9 | COVERAGE_PHP_VERSION: "8.4"
10 |
11 | jobs:
12 | phpunit:
13 | name: PHPUnit
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | php-version:
20 | - "8.1"
21 | - "8.2"
22 | - "8.3"
23 | - "8.4"
24 | deps:
25 | - "highest"
26 | include:
27 | - php-version: "8.1"
28 | deps: "lowest"
29 |
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 |
34 | - name: Setup PHP
35 | uses: shivammathur/setup-php@v2
36 | with:
37 | php-version: ${{ matrix.php-version }}
38 | coverage: pcov
39 |
40 | - name: Install composer dependencies
41 | uses: ramsey/composer-install@v3
42 | with:
43 | dependency-versions: ${{ matrix.deps }}
44 |
45 | - name: Run PHPUnit
46 | run: vendor/bin/phpunit
47 | if: ${{ matrix.php-version != env.COVERAGE_PHP_VERSION }}
48 |
49 | - name: Run PHPUnit with coverage
50 | run: |
51 | mkdir -p build/logs
52 | vendor/bin/phpunit --coverage-clover build/logs/clover.xml
53 | if: ${{ matrix.php-version == env.COVERAGE_PHP_VERSION }}
54 |
55 | - name: Upload coverage report to Coveralls
56 | run: vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v
57 | env:
58 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | if: ${{ matrix.php-version == env.COVERAGE_PHP_VERSION }}
60 |
61 | psalm:
62 | name: Psalm
63 | runs-on: ubuntu-latest
64 |
65 | steps:
66 | - name: Checkout
67 | uses: actions/checkout@v4
68 |
69 | - name: Setup PHP
70 | uses: shivammathur/setup-php@v2
71 | with:
72 | php-version: ${{ env.PSALM_PHP_VERSION }}
73 |
74 | - name: Install composer dependencies
75 | uses: ramsey/composer-install@v3
76 |
77 | - name: Run Psalm
78 | run: vendor/bin/psalm --no-progress
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-present Benjamin Morel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brick/postcode",
3 | "description": "A library to format and validate postcodes",
4 | "type": "library",
5 | "keywords": [
6 | "Brick",
7 | "Postcode"
8 | ],
9 | "license": "MIT",
10 | "require": {
11 | "php": "^8.1"
12 | },
13 | "require-dev": {
14 | "phpunit/phpunit": "^9.0",
15 | "php-coveralls/php-coveralls": "^2.0",
16 | "vimeo/psalm": "6.3.0"
17 | },
18 | "autoload": {
19 | "psr-4": {
20 | "Brick\\Postcode\\": "src/"
21 | }
22 | },
23 | "autoload-dev": {
24 | "psr-4": {
25 | "Brick\\Postcode\\Tests\\": "tests/"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/CountryPostcodeFormatter.php:
--------------------------------------------------------------------------------
1 | '43') {
32 | return null;
33 | }
34 |
35 | return $postcode;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Formatter/AIFormatter.php:
--------------------------------------------------------------------------------
1 | stripPrefix($postcode, 'A');
25 |
26 | if (preg_match('/^[1-9][0-9]{3}$/', $postcode) !== 1) {
27 | return null;
28 | }
29 |
30 | return $postcode;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Formatter/AUFormatter.php:
--------------------------------------------------------------------------------
1 | stripPrefix($postcode, 'B');
25 |
26 | if (preg_match('/^[0-9]{4}$/', $postcode) !== 1) {
27 | return null;
28 | }
29 |
30 | return $postcode;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Formatter/BGFormatter.php:
--------------------------------------------------------------------------------
1 | 12) {
28 | return null;
29 | }
30 |
31 | if ($matches[2] === '00') {
32 | return null;
33 | }
34 |
35 | return $postcode;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Formatter/BLFormatter.php:
--------------------------------------------------------------------------------
1 | '7') {
33 | return null;
34 | }
35 |
36 | return substr($postcode, 0, 3) . ' ' . substr($postcode, 3);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Formatter/DEFormatter.php:
--------------------------------------------------------------------------------
1 | '96944') {
35 | return null;
36 | }
37 |
38 | if ($length === 5) {
39 | return $postcode;
40 | }
41 |
42 | return $zip . '-' . substr($postcode, 5);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Formatter/FOFormatter.php:
--------------------------------------------------------------------------------
1 | getPatterns() as $pattern) {
65 | if (preg_match($pattern, $postcode, $matches) === 1) {
66 | [, $outwardCode, $areaCode, $inwardCode] = $matches;
67 |
68 | if (! in_array($areaCode, self::AREA_CODES, true)) {
69 | return null;
70 | }
71 |
72 | return $outwardCode . ' ' . $inwardCode;
73 | }
74 | }
75 |
76 | return null;
77 | }
78 |
79 | /**
80 | * Builds the regular expression patterns to check the postcode.
81 | *
82 | * Each pattern contains 3 capturing groups:
83 | *
84 | * - The outward code (e.g. WC2E) for formatting
85 | * - The area code (ex: WC) for additional checks
86 | * - The inward code (e.g. 9RZ) for formatting
87 | *
88 | * @psalm-return non-empty-string[]
89 | *
90 | * @return string[]
91 | */
92 | private function getPatterns() : array
93 | {
94 | if ($this->patterns !== null) {
95 | return $this->patterns;
96 | }
97 |
98 | $n = '[0-9]';
99 |
100 | // outward code alpha chars
101 | $alphaOut1 = '[ABCDEFGHIJKLMNOPRSTUWYZ]';
102 | $alphaOut2 = '[ABCDEFGHKLMNOPQRSTUVWXY]';
103 | $alphaOut3 = '[ABCDEFGHJKPSTUW]';
104 | $alphaOut4 = '[ABEHMNPRVWXY]';
105 |
106 | // inward code alpha chars
107 | $alphaIn = '[ABCDEFGHJLNPQRSTUWXYZ]';
108 |
109 | $outPatterns = [];
110 |
111 | // AN
112 | $outPatterns[] = '(' . $alphaOut1 . ')' . $n;
113 |
114 | // ANA
115 | $outPatterns[] = '(' . $alphaOut1 . ')' . $n . $alphaOut3;
116 |
117 | // ANN
118 | $outPatterns[] = '(' . $alphaOut1 . ')' . $n . $n;
119 |
120 | // AAN
121 | $outPatterns[] = '(' . $alphaOut1 . $alphaOut2 . ')' . $n;
122 |
123 | // AANA
124 | $outPatterns[] = '(' . $alphaOut1 . $alphaOut2 . ')' . $n . $alphaOut4;
125 |
126 | // AANN
127 | $outPatterns[] = '(' . $alphaOut1 . $alphaOut2 . ')' . $n . $n;
128 |
129 | $inPattern = $n . $alphaIn . $alphaIn;
130 |
131 | $patterns = [];
132 |
133 | foreach ($outPatterns as $outPattern) {
134 | $patterns[] = '/^(' . $outPattern . ')(' . $inPattern . ')$/';
135 | }
136 |
137 | return $this->patterns = $patterns;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Formatter/GEFormatter.php:
--------------------------------------------------------------------------------
1 | '96932') {
35 | return null;
36 | }
37 |
38 | if ($length === 5) {
39 | return $postcode;
40 | }
41 |
42 | return $zip . '-' . substr($postcode, 5);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Formatter/GWFormatter.php:
--------------------------------------------------------------------------------
1 | '3') {
32 | return null;
33 | }
34 |
35 | return 'KY' . $postcode[0] . '-' . substr($postcode, 1);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Formatter/KZFormatter.php:
--------------------------------------------------------------------------------
1 | '9498') {
26 | return null;
27 | }
28 |
29 | return $postcode;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Formatter/LKFormatter.php:
--------------------------------------------------------------------------------
1 | stripPrefix($postcode, 'L');
25 |
26 | if (preg_match('/^[0-9]{4}$/', $postcode) !== 1) {
27 | return null;
28 | }
29 |
30 | return $postcode;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Formatter/LVFormatter.php:
--------------------------------------------------------------------------------
1 | '96970') {
35 | return null;
36 | }
37 |
38 | if ($length === 5) {
39 | return $postcode;
40 | }
41 |
42 | return $zip . '-' . substr($postcode, 5);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Formatter/MKFormatter.php:
--------------------------------------------------------------------------------
1 | '96952') {
36 | return null;
37 | }
38 |
39 | if ($length === 5) {
40 | return $postcode;
41 | }
42 |
43 | return $zip . '-' . substr($postcode, 5);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Formatter/MQFormatter.php:
--------------------------------------------------------------------------------
1 | '97290') {
26 | return null;
27 | }
28 |
29 | return $postcode;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Formatter/MSFormatter.php:
--------------------------------------------------------------------------------
1 | '1350') {
31 | return null;
32 | }
33 |
34 | return 'MSR ' . $postcode;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Formatter/MTFormatter.php:
--------------------------------------------------------------------------------
1 | '98890') {
25 | return null;
26 | }
27 |
28 | return $postcode;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Formatter/NEFormatter.php:
--------------------------------------------------------------------------------
1 | isInRange($zip)) {
36 | return null;
37 | }
38 |
39 | if ($length === 5) {
40 | return $postcode;
41 | }
42 |
43 | return $zip . '-' . substr($postcode, 5);
44 | }
45 |
46 | /**
47 | * @param string $zip
48 | *
49 | * @return bool
50 | */
51 | private function isInRange(string $zip) : bool
52 | {
53 | if ($zip >= '00600' && $zip <= '00799') {
54 | return true;
55 | }
56 |
57 | if ($zip >= '00900' && $zip <= '00999') {
58 | return true;
59 | }
60 |
61 | return false;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Formatter/PSFormatter.php:
--------------------------------------------------------------------------------
1 | '98499') {
27 | return null;
28 | }
29 |
30 | return substr_replace($postcode, ' ', 3, 0);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Formatter/SGFormatter.php:
--------------------------------------------------------------------------------
1 | '1160') {
30 | return null;
31 | }
32 |
33 | return 'VG' . $postcode;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Formatter/VIFormatter.php:
--------------------------------------------------------------------------------
1 | '00851') {
35 | return null;
36 | }
37 |
38 | if ($length === 5) {
39 | return $postcode;
40 | }
41 |
42 | return $zip . '-' . substr($postcode, 5);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Formatter/VNFormatter.php:
--------------------------------------------------------------------------------
1 | '98690') {
26 | return null;
27 | }
28 |
29 | return $postcode;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Formatter/WSFormatter.php:
--------------------------------------------------------------------------------
1 | '97690') {
26 | return null;
27 | }
28 |
29 | return $postcode;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Formatter/ZAFormatter.php:
--------------------------------------------------------------------------------
1 | postcode = $postcode;
25 | $this->country = $country;
26 | }
27 |
28 | /**
29 | * Get the invalid postcode associated with this exception.
30 | *
31 | * @return string
32 | */
33 | public function getPostcode(): string
34 | {
35 | return $this->postcode;
36 | }
37 |
38 | /**
39 | * Get the country ISO2 code associated with this exception.
40 | *
41 | * @return string
42 | */
43 | public function getCountry(): string
44 | {
45 | return $this->country;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/PostcodeFormatter.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | private array $formatters = [];
18 |
19 | /**
20 | * Formats the given postcode.
21 | *
22 | * The country code and postcode are case insensitive.
23 | * The input postcode is allowed to contain space or dash separators, possibly misplaced.
24 | *
25 | * @param string $country The ISO 3166-1 alpha-2 country code.
26 | * @param string $postcode The postcode to format.
27 | *
28 | * @return string
29 | *
30 | * @throws UnknownCountryException
31 | * @throws InvalidPostcodeException
32 | */
33 | public function format(string $country, string $postcode) : string
34 | {
35 | $postcode = str_replace([' ', '-'], '', $postcode);
36 | $postcode = strtoupper($postcode);
37 |
38 | $formatter = $this->getFormatter($country);
39 |
40 | if ($formatter === null) {
41 | throw new UnknownCountryException($country);
42 | }
43 |
44 | if (preg_match('/^[A-Z0-9]+$/', $postcode) !== 1) {
45 | throw new InvalidPostcodeException($postcode, $country);
46 | }
47 |
48 | $formatted = $formatter->format($postcode);
49 |
50 | if ($formatted === null) {
51 | throw new InvalidPostcodeException($postcode, $country);
52 | }
53 |
54 | return $formatted;
55 | }
56 |
57 | /**
58 | * @param string $country
59 | *
60 | * @return bool
61 | */
62 | public function isSupportedCountry(string $country) : bool
63 | {
64 | return $this->getFormatter($country) !== null;
65 | }
66 |
67 | /**
68 | * @param string $country The ISO 3166-1 alpha-2 country code.
69 | *
70 | * @return CountryPostcodeFormatter|null The formatter, or null if the country code is unknown.
71 | */
72 | private function getFormatter(string $country) : ?CountryPostcodeFormatter
73 | {
74 | if (array_key_exists($country, $this->formatters)) {
75 | return $this->formatters[$country];
76 | }
77 |
78 | return $this->formatters[$country] = $this->doGetFormatter($country);
79 | }
80 |
81 | private function doGetFormatter(string $country): ?CountryPostcodeFormatter
82 | {
83 | $country = strtoupper($country);
84 |
85 | if (preg_match('/^[A-Z]{2}$/', $country) !== 1) {
86 | return null;
87 | }
88 |
89 | /** @var class-string $class */
90 | $class = __NAMESPACE__ . '\\Formatter\\' . $country . 'Formatter';
91 |
92 | return class_exists($class) ? new $class() : null;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/UnknownCountryException.php:
--------------------------------------------------------------------------------
1 | country = $country;
23 | }
24 |
25 | /**
26 | * Get the unknown country ISO2 code associated with this exception.
27 | *
28 | * @return string
29 | */
30 | public function getCountry(): string
31 | {
32 | return $this->country;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------