├── .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 | --------------------------------------------------------------------------------