Repository: joke2k/faker
Pull request #1073 (2019-12-07 19:04:21)

🍁 Add en-CA postcode_in_province() method
### What does this change

* Add `postcode_in_province` for en-CA addresses
* Remove extra `New Brunswick` from en-CA provinces
* Use tighter regex for en-CA postcode tests
* Add en-CA address source link

### What was wrong

* `postcode_in_province` seemed like a nice thing to have for parity with the US.
* The tests were too lax for the allowable en-CA postal codes.
* `New Brunswick` was listed twice.  Once is more than enough.


Patch:
diff --git a/faker/providers/address/en_CA/__init__.py b/faker/providers/address/en_CA/__init__.py
index 66c4b97c..f558103c 100644
--- a/faker/providers/address/en_CA/__init__.py
+++ b/faker/providers/address/en_CA/__init__.py
@@ -5,6 +5,11 @@ from ..en import Provider as AddressProvider
 
 class Provider(AddressProvider):
 
+    #  Source: https://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp#1449294
+    #
+    #  'W' and 'Z' are valid in non-initial position (easily verified in the
+    #  wild), but online official documentation is hard to find, so just ignore
+    #  them for now.
     postal_code_letters = (
         'A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S',
         'T', 'V', 'X', 'Y',
@@ -267,13 +272,20 @@ class Provider(AddressProvider):
     provinces = (
         'Alberta', 'British Columbia', 'Manitoba', 'New Brunswick',
         'Newfoundland and Labrador', 'Northwest Territories',
-        'New Brunswick', 'Nova Scotia', 'Nunavut', 'Ontario',
+        'Nova Scotia', 'Nunavut', 'Ontario',
         'Prince Edward Island', 'Quebec', 'Saskatchewan', 'Yukon Territory')
 
     provinces_abbr = (
         'AB', 'BC', 'MB', 'NB', 'NL', 'NT', 'NS',
         'NU', 'ON', 'PE', 'QC', 'SK', 'YT')
 
+    provinces_postcode_prefixes = {
+        'NL': ['A'], 'NS': ['B'], 'PE': ['C'], 'NB': ['E'],
+        'QC': ['G', 'H', 'J'], 'ON': ['K', 'L', 'M', 'N', 'P'],
+        'MB': ['R'], 'SK': ['S'], 'AB': ['T'], 'BC': ['V'],
+        'NU': ['X'], 'NT': ['X'], 'YT': ['Y'],
+    }
+
     city_formats = (
         '{{city_prefix}} {{first_name}}{{city_suffix}}',
         '{{city_prefix}} {{first_name}}',
@@ -316,16 +328,44 @@ class Provider(AddressProvider):
         """
         return self.random_element(self.postal_code_letters)
 
-    def postcode(self):
+    def _postcode_replace(self, postal_code_format):
         """
         Replaces all question mark ('?') occurrences with a random letter
-        from postal_code_formats then passes result to
-        numerify to insert numbers
+        from given postal_code_format, then passes result to numerify to insert
+        numbers
         """
         temp = re.sub(r'\?',
                       lambda x: self.postal_code_letter(),
-                      self.random_element(self.postal_code_formats))
+                      postal_code_format)
         return self.numerify(temp)
 
+    def postcode(self):
+        """
+        Returns a random postcode
+        """
+        return self._postcode_replace(
+            self.random_element(self.postal_code_formats))
+
+    def postcode_in_province(self, province_abbr=None):
+        """
+        Returns a random postcode within the provided province abbreviation
+        """
+        if province_abbr is None:
+            province_abbr = self.random_element(self.provinces_abbr)
+
+        if province_abbr in self.provinces_abbr:
+            postal_code_format = self.random_element(self.postal_code_formats)
+            postal_code_format = postal_code_format.replace(
+                '?',
+                self.generator.random_element(
+                    self.provinces_postcode_prefixes[province_abbr]),
+                1)
+            return self._postcode_replace(postal_code_format)
+        else:
+            raise Exception('Province Abbreviation not found in list')
+
+    def postalcode_in_province(self, province_abbr=None):
+        return self.postcode_in_province(province_abbr)
+
     def postalcode(self):
         return self.postcode()


Test patch:
diff --git a/tests/providers/test_address.py b/tests/providers/test_address.py
index e1d6a009..fe49cccb 100644
--- a/tests/providers/test_address.py
+++ b/tests/providers/test_address.py
@@ -275,22 +275,33 @@ class TestEnCA(unittest.TestCase):
 
     def setUp(self):
         self.factory = Faker('en_CA')
+        self.valid_postcode_letter_re = r'[{}]'.format(
+            ''.join(EnCaProvider.postal_code_letters))
+        self.valid_postcode_re = r"{0}[0-9]{0} ?[0-9]{0}[0-9]".format(
+            self.valid_postcode_letter_re)
 
     def test_postcode(self):
         for _ in range(100):
             postcode = self.factory.postcode()
-            assert re.match(r"[A-Z][0-9][A-Z] ?[0-9][A-Z][0-9]",
-                            postcode)
+            assert re.match(self.valid_postcode_re, postcode)
+
+    def test_postcode_in_province(self):
+        for province_abbr in EnCaProvider.provinces_abbr:
+            code = self.factory.postcode_in_province(province_abbr)
+            assert code[0] in EnCaProvider.provinces_postcode_prefixes[
+                province_abbr]
+
+        with self.assertRaises(Exception):
+            self.factory.postcode_in_province('XX')
 
     def test_postalcode(self):
         for _ in range(100):
             postalcode = self.factory.postalcode()
-            assert re.match(r"[A-Z][0-9][A-Z] ?[0-9][A-Z][0-9]",
-                            postalcode)
+            assert re.match(self.valid_postcode_letter_re, postalcode)
 
     def test_postal_code_letter(self):
         postal_code_letter = self.factory.postal_code_letter()
-        assert re.match(r"[A-Z]", postal_code_letter)
+        assert re.match(self.valid_postcode_letter_re, postal_code_letter)
 
     def test_province(self):
         province = self.factory.province()