Repository: joke2k/faker
Pull request #1851 (2023-04-24 10:14:52)

Fix zh_TW ssn provider rule
### What does this change
Implement current rule of generating ssn in locale zh_TW.

### What was wrong

The rule in the repo has not updated for years and was wrong. Gender and checksum were not considered.

### How this fixes it

Based on the public information of the generating rules and implement so.


Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 56902152..efa0283e 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 18.4.0
+current_version = 18.6.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5223fe01..b7172d21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
 ## Changelog
 
-### [v18.4.0- 2023-04-06](https://github.com/joke2k/faker/compare/v18.3.4...v18.4.0)
+### [v18.6.0 - 2023-04-27](https://github.com/joke2k/faker/compare/v18.5.1...v18.6.0)
+
+* Add a separate `basic_phone_number` for `en_US`. Thanks @dlwrnc.
+
+### [v18.5.1 - 2023-04-24](https://github.com/joke2k/faker/compare/v18.5.0...v18.5.1)
+
+* Fix release.
+
+### [v18.5.0 - 2023-04-24](https://github.com/joke2k/faker/compare/v18.4.0...v18.5.0)
+
+* Add `de_DE` `swift_location_codes`. Thanks @MiloniAtal.
+
+### [v18.4.0 - 2023-04-06](https://github.com/joke2k/faker/compare/v18.3.4...v18.4.0)
 
 * Add currency symbols for all listed currencies. Thanks @evoludigit.
 
diff --git a/VERSION b/VERSION
index 5b9720f0..4b5ebaf0 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-18.4.0
+18.6.0
diff --git a/docs/conf.py b/docs/conf.py
index 5f9f1901..401ac46c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "18.4.0"
+version = "18.6.0"
 # The full version, including alpha/beta/rc tags.
-release = "18.4.0"
+release = "18.6.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/faker/__init__.py b/faker/__init__.py
index 09f055a4..bcd4ef82 100644
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -2,6 +2,6 @@ from faker.factory import Factory
 from faker.generator import Generator
 from faker.proxy import Faker
 
-VERSION = "18.4.0"
+VERSION = "18.6.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/phone_number/en_US/__init__.py b/faker/providers/phone_number/en_US/__init__.py
index bb0b77df..ac1367e8 100644
--- a/faker/providers/phone_number/en_US/__init__.py
+++ b/faker/providers/phone_number/en_US/__init__.py
@@ -37,3 +37,14 @@ class Provider(PhoneNumberProvider):
         "001-###-###-####x####",
         "001-###-###-####x#####",
     )
+
+    basic_formats = (
+        # basic 10-digit phone number format with no extensions
+        "##########",
+        "###-###-####",
+        "(###)###-####",
+    )
+
+    def basic_phone_number(self) -> str:
+        pattern: str = self.random_element(self.basic_formats)
+        return self.numerify(self.generator.parse(pattern))
diff --git a/faker/providers/ssn/zh_TW/__init__.py b/faker/providers/ssn/zh_TW/__init__.py
index 84af8f12..1b565271 100644
--- a/faker/providers/ssn/zh_TW/__init__.py
+++ b/faker/providers/ssn/zh_TW/__init__.py
@@ -1,8 +1,45 @@
 from .. import Provider as SsnProvider
 
 
-class Provider(SsnProvider):
-    ssn_formats = ("?#########",)
+def checksum(s: str) -> int:
+    def _get_alphabet_weight(c: str) -> int:
+        """A=10, B=11, ...., H=17,
+        I=34,
+        J=18, K=19, ..., N=22,
+        O=35,
+        P=23, Q=24, ..., V=29,
+        W=32,
+        X=30, Y=31, Z=33
+        """
+        if ord(c) < 73:  # A-H
+            return ord(c) - 55
+        if ord(c) == 73:  # I
+            return ord(c) - 39
+        if ord(c) < 79:  # J-N
+            return ord(c) - 56
+        if ord(c) == 79:  # O
+            return ord(c) - 44
+        if ord(c) < 87:  # P-V
+            return ord(c) - 57
+        if ord(c) == 87:  # W
+            return ord(c) - 55
+        if ord(c) < 90:  # X, Y
+            return ord(c) - 58
+        return ord(c) - 57  # Z
+
+    res = 0
+    for i, c in enumerate(s):
+        if i == 0:
+            res = _get_alphabet_weight(c) % 10 * 9 + _get_alphabet_weight(c) // 10
+        elif i < 9:
+            res += int(c) * (9 - i)
+        else:
+            res += int(c)
+    return res
 
+
+class Provider(SsnProvider):
     def ssn(self) -> str:
-        return self.bothify(self.random_element(self.ssn_formats)).upper()
+        ssn_without_last_char = self.numerify(self.random_uppercase_letter() + str(self.random_int(1, 2)) + "#######")
+        last_char = str((10 - checksum(ssn_without_last_char) % 10) % 10)
+        return ssn_without_last_char + last_char


Test patch:
diff --git a/tests/providers/test_phone_number.py b/tests/providers/test_phone_number.py
index 8fd54ec3..1bb38daf 100644
--- a/tests/providers/test_phone_number.py
+++ b/tests/providers/test_phone_number.py
@@ -364,3 +364,18 @@ class TestFrFr:
         for _ in range(num_samples):
             phone_number = faker.phone_number()
             assert any([re.match(pattern, phone_number) for pattern in patterns])
+
+
+class TestEnUs:
+    """Test En_US phone provider methods"""
+
+    def test_basic_phone_number(self, faker, num_samples):
+        pattern_no_whitespaces: Pattern = re.compile(
+            r"\d{9}",
+        )
+        pattern_dashes: Pattern = re.compile(r"\d{3}-\d{3}-\d{4}")
+        pattern_parens: Pattern = re.compile(r"\(\d{3}\)\d{3}-\d{4}")
+        patterns = [pattern_no_whitespaces, pattern_dashes, pattern_parens]
+        for _ in range(num_samples):
+            phone_number = faker.basic_phone_number()
+            assert any([re.match(pattern, phone_number) for pattern in patterns])
diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py
index dc338708..0b2b83d4 100644
--- a/tests/providers/test_ssn.py
+++ b/tests/providers/test_ssn.py
@@ -34,6 +34,7 @@ from faker.providers.ssn.pl_PL import checksum as pl_checksum
 from faker.providers.ssn.pt_BR import checksum as pt_checksum
 from faker.providers.ssn.ro_RO import ssn_checksum as ro_ssn_checksum
 from faker.providers.ssn.ro_RO import vat_checksum as ro_vat_checksum
+from faker.providers.ssn.zh_TW import checksum as tw_checksum
 from faker.utils.checksums import luhn_checksum
 
 
@@ -1271,3 +1272,25 @@ class TestLvLV(unittest.TestCase):
     def test_vat_id(self):
         for _ in range(100):
             assert re.search(r"^LV\d{11}$", self.fake.vat_id())
+
+
+class TestZhTW(unittest.TestCase):
+    num_sample_runs = 10
+
+    def setUp(self):
+        self.fake = Faker("zh_TW")
+        Faker.seed(0)
+        self.samples = [self.fake.ssn() for _ in range(self.num_sample_runs)]
+
+    def test_length(self):
+        for sample in self.samples:
+            assert len(sample) == 10
+
+    def test_gender(self):
+        """only '1' and '2' are allowed in the second char"""
+        for sample in self.samples:
+            assert sample[1] == "1" or sample[1] == "2"
+
+    def test_checksum(self):
+        for sample in self.samples:
+            assert tw_checksum(sample) % 10 == 0