from typing import Any, List def _whole(value: Any) -> bool: # bool is an int in Python; True is not a count of one here. return isinstance(value, int) and not isinstance(value, bool) def _is_separator(ch: str) -> bool: # Only these two are separators: they are how card and sort-code numbers # are printed. return ch == " " or ch == "-" def mask(value: str, visible: int, mask_char: str, keep_separators: bool) -> str: """Mask all but the last ``visible`` characters of ``value``. Characters are Unicode code points (a Python str is already indexed by code point), so an emoji counts once and the length never changes. """ if not isinstance(value, str): raise TypeError("mask needs a string, received %r" % (value,)) if not _whole(visible): raise TypeError("visible must be a whole number, received %r" % (visible,)) if visible < 0: raise ValueError("visible must be 0 or greater, received %d" % (visible,)) if not isinstance(mask_char, str) or len(mask_char) != 1: raise ValueError('maskChar must be exactly one character, received "%s"' % (mask_char,)) out: List[str] = [""] * len(value) shown = 0 # Walk from the end so "the last n" is counted without a second pass. for i in range(len(value) - 1, -1, -1): ch = value[i] if keep_separators and _is_separator(ch): out[i] = ch elif shown < visible: out[i] = ch shown += 1 else: out[i] = mask_char return "".join(out)