Skip to content

Commit

Permalink
Add SBN provider (#1806)
Browse files Browse the repository at this point in the history
  • Loading branch information
dag2226 committed Mar 1, 2023
1 parent fb32d0b commit 4112a9d
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
53 changes: 53 additions & 0 deletions faker/providers/sbn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import List, Tuple

from faker.providers.sbn.rules import RegistrantRule

from .. import BaseProvider
from .rules import RULES
from .sbn import SBN, SBN9


class Provider(BaseProvider):
"""Generates fake SBNs. These are the precursor to the ISBN and are
largely similar to ISBN-10.
See https://www.isbn-international.org/content/what-isbn for the
format of ISBNs. SBNs have no EAN prefix or Registration Group.
"""

def _body(self) -> List[str]:
"""Generate the information required to create an SBN"""

reg_pub_len: int = SBN.MAX_LENGTH - 1

# Generate a registrant/publication combination
reg_pub: str = self.numerify("#" * reg_pub_len)

# Use rules to separate the registrant from the publication
rules: List[RegistrantRule] = RULES
registrant, publication = self._registrant_publication(reg_pub, rules)
return [registrant, publication]

@staticmethod
def _registrant_publication(reg_pub: str, rules: List[RegistrantRule]) -> Tuple[str, str]:
"""Separate the registration from the publication in a given
string.
:param reg_pub: A string of digits representing a registration
and publication.
:param rules: A list of RegistrantRules which designate where
to separate the values in the string.
:returns: A (registrant, publication) tuple of strings.
"""
for rule in rules:
if rule.min <= reg_pub[:-1] <= rule.max:
reg_len = rule.registrant_length
break
else:
raise Exception("Registrant/Publication not found in registrant " "rule list.")
registrant, publication = reg_pub[:reg_len], reg_pub[reg_len:]
return registrant, publication

def sbn9(self, separator: str = "-") -> str:
registrant, publication = self._body()
sbn = SBN9(registrant, publication)
return sbn.format(separator)
5 changes: 5 additions & 0 deletions faker/providers/sbn/en_US/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .. import Provider as SBNProvider


class Provider(SBNProvider):
pass
24 changes: 24 additions & 0 deletions faker/providers/sbn/rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
This module exists solely to figure how long a registrant/publication
number may be within an SBN. It's the same as the ISBN implementation
for ean 978, reg_group 0.
"""

from collections import namedtuple
from typing import List

RegistrantRule = namedtuple("RegistrantRule", ["min", "max", "registrant_length"])

# Structure: RULES = [Rule1, Rule2, ...]
RULES: List[RegistrantRule] = [
RegistrantRule("0000000", "1999999", 2),
RegistrantRule("2000000", "2279999", 3),
RegistrantRule("2280000", "2289999", 4),
RegistrantRule("2290000", "6479999", 3),
RegistrantRule("6480000", "6489999", 7),
RegistrantRule("6490000", "6999999", 3),
RegistrantRule("7000000", "8499999", 4),
RegistrantRule("8500000", "8999999", 5),
RegistrantRule("9000000", "9499999", 6),
RegistrantRule("9500000", "9999999", 7),
]
49 changes: 49 additions & 0 deletions faker/providers/sbn/sbn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
This module is responsible for generating the check digit and formatting
SBN numbers.
"""
from typing import Any, Optional


class SBN:
MAX_LENGTH = 9

def __init__(
self,
registrant: Optional[str] = None,
publication: Optional[str] = None,
) -> None:
self.registrant = registrant
self.publication = publication


class SBN9(SBN):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.check_digit = self._check_digit()

def _check_digit(self) -> str:
"""Calculate the check digit for SBN-9.
SBNs use the same check digit calculation as ISBN. See
https://en.wikipedia.org/wiki/International_Standard_Book_Number
for calculation. Only modification is weights range from 1 to 9
instead of 1 to 10.
"""
weights = range(1, 9)
body = "".join([part for part in [self.registrant, self.publication] if part is not None])
remainder = sum(int(b) * w for b, w in zip(body, weights)) % 11
check_digit = "X" if remainder == 10 else str(remainder)
return str(check_digit)

def format(self, separator: str = "") -> str:
return separator.join(
[
part
for part in [
self.registrant,
self.publication,
self.check_digit,
]
if part is not None
]
)
62 changes: 62 additions & 0 deletions tests/providers/test_sbn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from faker.providers.sbn import SBN9
from faker.providers.sbn.en_US import Provider as SBNProvider
from faker.providers.sbn.rules import RegistrantRule


class TestISBN9:
def test_check_digit_is_correct(self):
sbn = SBN9(registrant="340", publication="01381")
assert sbn.check_digit == "X"
sbn = SBN9(registrant="06", publication="230125")
assert sbn.check_digit == "2"
sbn = SBN9(registrant="10103", publication="202")
assert sbn.check_digit == "3"

def test_format_length(self):
sbn = SBN9(registrant="4516", publication="7331")
assert len(sbn.format()) == 9
sbn = SBN9(registrant="451", publication="10036")
assert len(sbn.format()) == 9


class TestProvider:
prov = SBNProvider(None)

def test_reg_pub_separation(self):
r1 = RegistrantRule("0000000", "0000001", 1)
r2 = RegistrantRule("0000002", "0000003", 2)
assert self.prov._registrant_publication("00000000", [r1, r2]) == (
"0",
"0000000",
)
assert self.prov._registrant_publication("00000010", [r1, r2]) == (
"0",
"0000010",
)
assert self.prov._registrant_publication("00000019", [r1, r2]) == (
"0",
"0000019",
)
assert self.prov._registrant_publication("00000020", [r1, r2]) == (
"00",
"000020",
)
assert self.prov._registrant_publication("00000030", [r1, r2]) == (
"00",
"000030",
)
assert self.prov._registrant_publication("00000031", [r1, r2]) == (
"00",
"000031",
)
assert self.prov._registrant_publication("00000039", [r1, r2]) == (
"00",
"000039",
)

def test_rule_not_found(self):
with pytest.raises(Exception):
r = RegistrantRule("0000000", "0000001", 1)
self.prov._registrant_publication("0000002", [r])
1 change: 1 addition & 0 deletions tests/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def test_find_available_providers(self):
"faker.providers.phone_number",
"faker.providers.profile",
"faker.providers.python",
"faker.providers.sbn",
"faker.providers.ssn",
"faker.providers.user_agent",
],
Expand Down

0 comments on commit 4112a9d

Please sign in to comment.