一份DNI (西班牙的国民身份证)由8个数字和一个字母组成,它们相互关联如下:
number % 23 -> letter_index
其中,要索引的字母序列为TRWAGMYFPDXBNJZSQVHLCKET
。
下面的程序允许计算给定的数字的字母,查找一些缺失的数字,并找到所有可能的DNI,给出一些数字和字母:
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])
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)
下面是一些测试:
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_计算器上找到。
发布于 2021-08-31 14:53:17
PEP257要求双引号,而不是文档字符串的单引号。
这些:
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
本身上的类方法。
这是:
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
上会更自然一些。
这个测试:
for dni, expected_result in zip(dnis_generator, expected_results):
隐藏长度不匹配的故障模式。使用zip_longest
可以解决这个问题。
认识到一个问题,例如找出所有的数字
052407??Q
实际上是表单的同余关系。
你应该能够应用等价关系恒等式,这样你才能找到所有的答案,而不是用暴力;举你的例子:
我还没有在下面的实现中说明这一点。
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()
https://codereview.stackexchange.com/questions/266535
复制相似问题