首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >DNI (西班牙国民身份证)计算器

DNI (西班牙国民身份证)计算器
EN

Code Review用户
提问于 2021-08-30 20:10:20
回答 1查看 251关注 0票数 5

一份DNI (西班牙的国民身份证)由8个数字和一个字母组成,它们相互关联如下:

代码语言:javascript
运行
复制
number % 23 -> letter_index

其中,要索引的字母序列为TRWAGMYFPDXBNJZSQVHLCKET

下面的程序允许计算给定的数字的字母,查找一些缺失的数字,并找到所有可能的DNI,给出一些数字和字母:

代码语言:javascript
运行
复制
from dataclasses import dataclass, field
from typing import Optional, List


@dataclass
class Dni:

    LENGTH_NUMS_ONLY = 8
    LENGTH = LENGTH_NUMS_ONLY + 1

    number: Optional[int] = None
    letter: Optional[str] = None
    missing_digits: List[int] = field(default_factory=lambda: [])

    def get_number_as_str(self) -> str:
        '''Return the number representing unknown digits as "?"

        For example, if number=11_011_111 and missing_digits=[2],
        returned value is "11?11111"
        '''
        number = self.number if self.number is not None else 0
        number_as_str = str(number).zfill(Dni.LENGTH_NUMS_ONLY)

        # number is converted to list so that missing digits can be replaced
        number_as_list = list(number_as_str)
        if self.missing_digits:
            for missing_digit in self.missing_digits:
                number_as_list[missing_digit] = '?'

        return ''.join(number_as_list)

    def get_letter_as_str(self) -> str:
        '''Return the letter or "?" if lettter is not known'''
        return self.letter.upper() if self.letter else '?'

    def __str__(self):
        return self.get_number_as_str() + self.get_letter_as_str()

    @classmethod
    def from_dni(clss, dni):
        return Dni(dni.number, dni.letter, [i for i in dni.missing_digits])
代码语言:javascript
运行
复制
from typing import Iterable, Generator, Optional
import itertools

from dni_calculator import Dni


class DniCalculator:

    _LETTERS = 'TRWAGMYFPDXBNJZSQVHLCKET'

    def find_letter(self, dni: Dni) -> Optional[Dni]:
        '''Find the letter corresponding to the given dni

        Example:
            find_letter(Dni(11111111)) -> 11111111H

        Args:
            dni: The dni whose letter is to be found.
                All of its digits have to be present
        '''
        if dni.missing_digits and len(dni.missing_digits) != 0:
            print(f'Invalid dni given: "{dni}". '
                + 'There cannot be missing numbers when finding a letter')
            return None
        if dni.letter is not None:
            if self._check_valid(dni):
                print(f'The given dni is already complete and valid: "{dni}"')
                return Dni.from_dni(dni)
            else:
                print(f'Letter provided. Wont look for it "{dni}"')
                return None
        res_dni = Dni.from_dni(dni)
        res_dni.letter = self._get_letter(dni.number)
        return res_dni

    def _get_letter(self, dni_number: int) -> str:
        '''Return the letter corresponding to the given dni_number'''
        return self._LETTERS[dni_number % 23]

    def _check_valid(self, dni: Dni) -> bool:
        '''Check whether the given dni is valid

        Args:
            dni: A Dni with all digits and letter present
                Example: Dni(11_111_111, 'H')
        '''
        return self._get_letter(dni.number) == dni.letter

    def find_missing_num(self, dni: Dni) -> Optional[Dni]:
        '''Find the first complete dni valid for the given dni

        Args:
            dni: The dni for which to find the missing numbers

                It should have at least one missing digit.
                It also has to contain the letter.

                Examples:
                    Dni(11_111_011, 'H', [5])
                    Dni(11_100_111, 'H', [3, 4])
        '''
        return next(self.find_all_possible_dnis(dni), None)

    def find_all_possible_dnis(self, dni: Dni) -> Generator[Dni, None, None]:
        '''Find the all of the valid dnis for the given dni

        Args:
            dni: The dni for which to find the missing numbers

                It should have at least one missing digit.
                It also has to contain the letter.

                Examples:
                    Dni(11_111_011, 'H', [5])
                    Dni(11_100_111, 'H', [3, 4])
        '''
        if dni.letter is None:
            print(f'Cannot fing missing numbers if no letter is given: "{dni}"')
            return None

        num_missing_digits = len(dni.missing_digits)
        if num_missing_digits == 0:
            if self._check_valid(dni):
                print(f'The given dni is already complete and valid: "{dni}"')
                yield Dni.from_dni(dni)
                return None
            else:
                print(f'All digits provided. Unable to find missing ones "{dni}"')
                return None

        res_dni = Dni.from_dni(dni)
        missing_digits = res_dni.missing_digits
        res_dni.missing_digits = []
        prev_digits_to_check = 0
        for digits_to_check in self._get_generator_for_digits(missing_digits):
            res_dni.number -= prev_digits_to_check
            res_dni.number += digits_to_check
            prev_digits_to_check = digits_to_check
            if self._check_valid(res_dni):
                yield Dni.from_dni(res_dni)

    def _get_generator_for_digit(self, digit_pos: int) -> Generator[int, None, None]:
        '''Return the different value the digit at position digit_pos can have

        Examples:
            digit_pos=7 -> 0, 1, 2, ..., 8, 9
            digit_pos=6 -> 0, 10, 20, ..., 80, 90
            digit_pos=0 -> 0, 10_000_000, ..., 90_000_000

        Args:
            digit_pos: A number from 0 to 9
        '''
        power = Dni.LENGTH_NUMS_ONLY - digit_pos
        for number in range(0, 10 ** power, 10 ** (power-1)):
            yield number

    def _get_generator_for_digits(self, digits_pos: Iterable[int]) -> Generator[int, None, None]:
        '''Returns all combinations of values the digits at position digits_pos can have

        Examples:
            digit_pos=(7,) -> 0, 1, 2, ..., 8, 9
            digit_pos=(6,) -> 0, 10, 20, ..., 80, 90
            digit_pos=(0,) -> 0, 10_000_000, ..., 90_000_000
            digit_pos=(6, 7) -> 0, 1, 2, ..., 98, 99
            digit_pos=(5, 7) -> 0, 1, 2, ..., 9, 100, 101, ... 109, 200, ..., 908, 909
            digit_pos=(0, 5) -> 0, 10_000_000, 10_000_100, 10_000_200, ..., 90_000_900

        Args:
            digits_pos: An iterable whose values have to be between 0 and 9
                For the output to be in an expectable order, the iterable
                has to return the numbers in increasing order.

                For example, if digits_pos=(7, 6), the yielded values will be:
                0, 10, 20, 30, ... 90, 1, 11, 21, ...
        '''
        digits_generators = [self._get_generator_for_digit(digit_pos)
                             for digit_pos in digits_pos]
        digits_generator = itertools.product(*digits_generators)
        # return digits_generator
        return map(sum, digits_generator)

下面是一些测试:

代码语言:javascript
运行
复制
from dni_calculator import Dni, DniCalculator

dni_calc = DniCalculator()

assert dni_calc.find_letter(Dni(11_111_111)) == Dni(11_111_111, 'H')
assert dni_calc.find_letter(Dni(71_091_510)) == Dni(71_091_510, 'M')
assert dni_calc.find_letter(Dni(82_416_679)) == Dni(82_416_679, 'C')

assert dni_calc.find_missing_num(Dni(11_111_011, 'H', missing_digits=[5])) == Dni(11_111_111, 'H')
assert dni_calc.find_missing_num(Dni(75_623_008, 'E', missing_digits=[5])) == Dni(75_623_608, 'E')
assert dni_calc.find_missing_num(Dni(20_602_203, 'M', missing_digits=[3, 6])) == Dni(20_612_283, 'M')

dnis_generator = dni_calc.find_all_possible_dnis(Dni(5240700, 'Q', missing_digits=[6, 7]))
expected_results = (
    Dni(5240704, 'Q'),
    Dni(5240727, 'Q'),
    Dni(5240750, 'Q'),
    Dni(5240773, 'Q'),
    Dni(5240796, 'Q'),
 )
for dni, expected_result in zip(dnis_generator, expected_results):
    assert dni == expected_result

使用提供简单CLI接口的完整代码可在m-alorda/dni_计算器上找到。

EN

回答 1

Code Review用户

回答已采纳

发布于 2021-08-31 14:53:17

Basics

PEP257要求双引号,而不是文档字符串的单引号。

这些:

代码语言:javascript
运行
复制
LENGTH_NUMS_ONLY = 8
LENGTH = LENGTH_NUMS_ONLY + 1

应该被输入为ClassVar[int]

按照惯例,类方法的类引用变量是cls,而不是clss

from_dni实际上并不是指cls,但它应该这样做:与其通过Dni进行构造,不如构建cls。除此之外,我更希望这是一个实例方法copy,它返回self的一个副本。

理想情况下,Dni应该通过将frozen=true传递给它的装饰器使其不可变。这将暴露出一些设计缺陷:目前,DniCalculator负责Dni.missing_digits的变异,但它不应该这样做。

_get_generator_for_digit可能是静态的,或者更好的是Dni本身上的类方法。

这是:

代码语言:javascript
运行
复制
print(f'Invalid dni given: "{self}". '
      + 'There cannot be missing numbers when finding a letter')

不需要+,可以使用隐式级联。

find_letter应该是raise而不是print。印刷可以在外部进行。

错误:lettter -> letter

[i for i in dni.missing_digits]可以是list(dni.missing_digits)

_check_valid作为@property is_valid本身在Dni上会更自然一些。

这个测试:

代码语言:javascript
运行
复制
for dni, expected_result in zip(dnis_generator, expected_results):

隐藏长度不匹配的故障模式。使用zip_longest可以解决这个问题。

同步性加速

认识到一个问题,例如找出所有的数字

代码语言:javascript
运行
复制
052407??Q

实际上是表单的同余关系

5,240,700 + 10x + y \equiv 16 \mod 23

你应该能够应用等价关系恒等式,这样你才能找到所有的答案,而不是用暴力;举你的例子:

y \equiv 16 - 5,240,700 \mod 23
y \equiv 4 \mod 23
y = 4 + 23 n, 0 \le n \le 4

我还没有在下面的实现中说明这一点。

建议

代码语言:javascript
运行
复制
from dataclasses import dataclass
from typing import Iterable, Optional, ClassVar, Tuple, Sequence
import itertools


@dataclass(frozen=True)
class Dni:
    LETTERS: ClassVar[str] = 'TRWAGMYFPDXBNJZSQVHLCKE'
    LENGTH_NUMS_ONLY: ClassVar[int] = 8
    LENGTH: ClassVar[int] = LENGTH_NUMS_ONLY + 1

    digits: Tuple[Optional[int], ...]
    letter: Optional[str] = None

    @classmethod
    def from_number(cls, x: int, letter: Optional[str] = None) -> 'Dni':
        if x >= 10**cls.LENGTH_NUMS_ONLY:
            raise ValueError(f'{x} has incorrect length')
        return cls(
            digits=tuple(int(c) for c in str(x)),
            letter=letter,
        )

    @classmethod
    def parse(cls, s: str) -> 'Dni':
        *digits, letter = s
        if len(digits) != cls.LENGTH_NUMS_ONLY:
            raise ValueError(f'{s} has incorrect length')

        return cls(
            digits=tuple(
                None if d == '?' else int(d) for d in digits
            ),
            letter=None if letter == '?' else letter,
        )

    @property
    def is_complete(self) -> bool:
        return (
            self.letter is not None and
            not any(d is None for d in self.digits)
        )

    @property
    def number(self) -> int:
        x = 0
        for digit in self.digits:
            if digit is None:
                raise ValueError('Partial number cannot be linearised')
            x = 10*x + digit
        return x

    @property
    def number_as_str(self) -> str:
        """Return the number representing unknown digits as "?"

        For example, if number=11_011_111 and missing_digits=[2],
        returned value is "11?11111"
        """

        number_as_list = []
        for d in self.digits:
            number_as_list += '?' if d is None else str(d)

        return ''.join(number_as_list)

    @property
    def letter_as_str(self) -> str:
        """Return the letter or "?" if letter is not known"""
        if self.letter is None:
            return '?'
        return self.letter

    def __str__(self) -> str:
        return self.number_as_str + self.letter_as_str

    def copy(self) -> 'Dni':
        return Dni(self.digits)

    @classmethod
    def _get_letter(cls, number: int) -> str:
        return cls.LETTERS[number % len(cls.LETTERS)]

    @property
    def expected_letter(self) -> str:
        """Find the letter corresponding to the given dni

        Example:
            expected_letter(Dni(11111111)) -> 11111111H

        Args:
            dni: The dni whose letter is to be found.
                All of its digits have to be present
        """
        return self._get_letter(self.number)

    @classmethod
    def _get_generator_for_digit(cls, digit_pos: int) -> Sequence[int]:
        """Return the different value the digit at position digit_pos can have

        Examples:
            digit_pos=7 -> 0, 1, 2, ..., 8, 9
            digit_pos=6 -> 0, 10, 20, ..., 80, 90
            digit_pos=0 -> 0, 10_000_000, ..., 90_000_000

        Args:
            digit_pos: A number from 0 to 9
        """
        power = cls.LENGTH_NUMS_ONLY - digit_pos
        return range(0, 10**power, 10**(power - 1))

    @classmethod
    def _get_generator_for_digits(cls, digits_pos: Iterable[int]) -> Iterable[int]:
        """Returns all combinations of values the digits at position digits_pos can have

        Examples:
            digit_pos=(7,) -> 0, 1, 2, ..., 8, 9
            digit_pos=(6,) -> 0, 10, 20, ..., 80, 90
            digit_pos=(0,) -> 0, 10_000_000, ..., 90_000_000
            digit_pos=(6, 7) -> 0, 1, 2, ..., 98, 99
            digit_pos=(5, 7) -> 0, 1, 2, ..., 9, 100, 101, ... 109, 200, ..., 908, 909
            digit_pos=(0, 5) -> 0, 10_000_000, 10_000_100, 10_000_200, ..., 90_000_900

        Args:
            digits_pos: An iterable whose values have to be between 0 and 9
                For the output to be in an expectable order, the iterable
                has to return the numbers in increasing order.

                For example, if digits_pos=(7, 6), the yielded values will be:
                0, 10, 20, 30, ... 90, 1, 11, 21, ...
        """
        digits_generators = (cls._get_generator_for_digit(digit_pos)
                             for digit_pos in digits_pos)
        digits_generator = itertools.product(*digits_generators)
        return map(sum, digits_generator)

    @property
    def missing_num(self) -> Optional['Dni']:
        """Find the first complete dni valid for the given dni

        Args:
            dni: The dni for which to find the missing numbers

                It should have at least one missing digit.
                It also has to contain the letter.

                Examples:
                    Dni(11_111_011, 'H', [5])
                    Dni(11_100_111, 'H', [3, 4])
        """
        return next(self.all_possible_dnis, None)

    @property
    def all_possible_dnis(self) -> Iterable['Dni']:
        """Find the all of the valid dnis for the given dni

        Args:
            dni: The dni for which to find the missing numbers

                It should have at least one missing digit.
                It also has to contain the letter.

                Examples:
                    Dni(11_111_011, 'H', [5])
                    Dni(11_100_111, 'H', [3, 4])
        """

        base = sum(
            0 if d is None else d * 10**(self.LENGTH_NUMS_ONLY - i)
            for i, d in enumerate(self.digits, 1)
        )

        digits_pos = [i for i, d in enumerate(self.digits) if d is None]
        for addend in self._get_generator_for_digits(digits_pos):
            number = addend + base
            letter = self._get_letter(number)
            if self.letter is None or letter == self.letter:
                yield Dni.from_number(number, letter)


def test():
    assert Dni.from_number(11_111_111).expected_letter == 'H'
    assert Dni.from_number(71_091_510).expected_letter == 'M'
    assert Dni.from_number(82_416_679).expected_letter == 'C'

    dni = Dni.parse('11111?11H').missing_num
    assert dni.letter == 'H'
    assert dni.number == 11_111_111

    dni = Dni.parse('75623?08E').missing_num
    assert dni.letter == 'E'
    assert dni.number == 75_623_608

    dni = Dni.parse('206?22?3M').missing_num
    assert dni.letter == 'M'
    assert dni.number == 20_612_283

    dnis_generator = Dni.parse('052407??Q').all_possible_dnis
    expected_numbers = (
        5240704,
        5240727,
        5240750,
        5240773,
        5240796,
    )
    for dni, expected_number in itertools.zip_longest(dnis_generator, expected_numbers):
        assert dni.number == expected_number
        assert dni.letter == 'Q'


if __name__ == '__main__':
    test()
票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/266535

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档