From 3039cd41c779fc5dbb7e743b5faf462f2e9e47ab Mon Sep 17 00:00:00 2001 From: talveg-1 Date: Sun, 17 May 2026 19:15:12 +0100 Subject: [PATCH] Add RU SNILS --- stdnum/ru/snils.py | 75 +++++++++++++++++++++++++++++++++++++ tests/test_ru_snils.doctest | 28 ++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 stdnum/ru/snils.py create mode 100644 tests/test_ru_snils.doctest diff --git a/stdnum/ru/snils.py b/stdnum/ru/snils.py new file mode 100644 index 00000000..c7035ac7 --- /dev/null +++ b/stdnum/ru/snils.py @@ -0,0 +1,75 @@ +"""СНИЛС (Страховой номер индивидуального лицевого счёта, Russian Individual insurance account number) + +More information: + +* https://en.wikipedia.org/wiki/SNILS_(Russia) +* https://ru.wikipedia.org/wiki/Страховой_номер_индивидуального_лицевого_счёта + +>>> validate('11223344595') +'112-233-445 95' +>>> validate('010-242-368 77') +'010-242-368 77' +>>> validate('010-242-368 00') +Traceback (most recent call last): + ... +InvalidChecksum: ... +""" + +from __future__ import annotations + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace.""" + return clean(number, " -").strip() + + +def calc_check_digit(number: str) -> str: + """Calculate the expected SNILS checksum digits""" + calculated_checksum: int = sum([int(c) * (9 - i) for i, c in enumerate(number[:9])]) + if calculated_checksum > 101: + calculated_checksum %= 101 + if calculated_checksum in [100, 101]: + calculated_checksum = 0 + return f"{calculated_checksum:02d}" + + +def _is_valid_checksum(number: str) -> bool: + """Determine if checksum is correct""" + checksum_digits: str = number[9:11] + if calc_check_digit(number) != checksum_digits: + return False + return True + + +def validate(number: str) -> str: + """Determine if the given number is a valid SNILS.""" + number = compact(number) + if not isdigits(number): + raise InvalidFormat() + if not len(number) == 11: + raise InvalidLength() + if not _is_valid_checksum(number): + raise InvalidChecksum() + return number + + +def is_valid(number: str) -> bool: + """Check if the number is a valid SNILS.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number: str) -> str: + """Format the number provided for output.""" + number = validate(number) + return f"{number[0:3]}-{number[3:6]}-{number[6:9]} {number[9:11]}" + + +if __name__ == "__main__": + print(format("148-481-255 85")) diff --git a/tests/test_ru_snils.doctest b/tests/test_ru_snils.doctest new file mode 100644 index 00000000..daeb1f8f --- /dev/null +++ b/tests/test_ru_snils.doctest @@ -0,0 +1,28 @@ +>>> from stdnum.ru import snils +>>> from stdnum.exceptions import * + +Checks a valid SNILS number and its formatting behavior: + +>>> snils.validate('148-481-255 85') +'14848125585' +>>> snils.format('14848125585') +'148-481-255 85' +>>> snils.compact(' 148 481 255 85 ') +'14848125585' +>>> snils.is_valid('148-481-255 85') +True + +Invalid SNILS values raise the expected exceptions: + +>>> snils.validate('148-481-255') # too short +Traceback (most recent call last): + ... +InvalidLength: ... +>>> snils.validate('148-481-255 xx') # invalid characters +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> snils.validate('148-481-255 00') # invalid checksum +Traceback (most recent call last): + ... +InvalidChecksum: ...