Source code for pain001.validation.charset

# Copyright (C) 2023-2026 Pain001. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""ISO 20022 restricted character-set validation and transliteration.

Most ISO 20022 text fields are limited to the "Latin character set"
(also called the SWIFT-x / FIN-X set). Banks routinely reject messages
that carry characters outside it — accented names, em dashes, currency
symbols pasted from spreadsheets — even when the XML is otherwise
XSD-valid. This module detects those characters and offers a best-effort
transliteration to a compliant form.

The permitted set is::

    a-z  A-Z  0-9  /  -  ?  :  (  )  .  ,  '  +  and space
"""

import unicodedata

#: The ISO 20022 "Latin character set" permitted in restricted text fields.
ISO20022_ALLOWED_CHARACTERS = frozenset(
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-?:().,'+ "
)


def find_invalid_characters(value: str) -> list[str]:
    """Return the characters in ``value`` outside the ISO 20022 set.

    Args:
        value: The text to inspect.

    Returns:
        A sorted list of the unique disallowed characters. Empty when the
        value is fully compliant.

    Example:
        >>> find_invalid_characters("Jose Muller & Co")
        ['&']
    """
    return sorted(
        {ch for ch in value if ch not in ISO20022_ALLOWED_CHARACTERS}
    )


def is_valid_charset(value: str) -> bool:
    """Report whether ``value`` uses only ISO 20022 permitted characters.

    Args:
        value: The text to check.

    Returns:
        ``True`` if every character is permitted, ``False`` otherwise.

    Example:
        >>> is_valid_charset("Invoice 12345")
        True
        >>> is_valid_charset("Café")
        False
    """
    return not find_invalid_characters(value)


[docs] def sanitize_to_charset(value: str, replacement: str = " ") -> str: """Transliterate ``value`` into the ISO 20022 character set. Accented Latin letters are decomposed to their base letter (``é`` becomes ``e``, ``ü`` becomes ``u``). Any character that still falls outside the permitted set is replaced with ``replacement``. Args: value: The text to transliterate. replacement: The string substituted for characters that cannot be transliterated (default: a single space). Returns: A string containing only ISO 20022 permitted characters. Example: >>> sanitize_to_charset("Café Münchën") 'Cafe Munchen' >>> sanitize_to_charset("A & B", replacement="and") 'A and B' """ decomposed = unicodedata.normalize("NFKD", value) stripped = "".join( ch for ch in decomposed if not unicodedata.combining(ch) ) return "".join( ch if ch in ISO20022_ALLOWED_CHARACTERS else replacement for ch in stripped )