Functional Weave
Code in TypeScript

form.character-count@1.0.0

impl/python.py

2,699 bytes · the Python implementation · view raw

from typing import Any, Optional

from .form_character_count_types import CharacterCount, CountUnit


def _whole(value: Any) -> bool:
    # bool is an int in Python; True is not a limit of one here.
    return isinstance(value, int) and not isinstance(value, bool)


def _is_whitespace(ch: str) -> bool:
    """The Unicode White_Space property, spelled out so all three languages
    agree: str.isspace also accepts U+001C-U+001F, which JavaScript and Rust
    do not.
    """
    cp = ord(ch)
    return (
        0x09 <= cp <= 0x0D
        or cp == 0x20
        or cp == 0x85
        or cp == 0xA0
        or cp == 0x1680
        or 0x2000 <= cp <= 0x200A
        or cp == 0x2028
        or cp == 0x2029
        or cp == 0x202F
        or cp == 0x205F
        or cp == 0x3000
    )


def _count_words(text: str) -> int:
    words = 0
    in_word = False
    for ch in text:
        if _is_whitespace(ch):
            in_word = False
        elif not in_word:
            in_word = True
            words += 1
    return words


def character_count(text: str, limit: int, unit: CountUnit, threshold: Optional[int]) -> CharacterCount:
    """GOV.UK's character count: how many characters (code points) or words
    have been typed against a limit, and the sentence to show under the box.
    """
    if not isinstance(text, str):
        raise TypeError("text must be a string")
    if not _whole(limit) or limit < 1:
        raise ValueError("limit must be a whole number of at least 1, received %r" % (limit,))
    if unit not in ("characters", "words"):
        raise ValueError("unit must be characters or words, received %r" % (unit,))
    if threshold is not None and (not _whole(threshold) or threshold < 0 or threshold > 100):
        raise ValueError("threshold must be a whole number from 0 to 100, received %r" % (threshold,))
    # A Python str is indexed by code point already.
    count = _count_words(text) if unit == "words" else len(text)
    remaining = limit - count
    over = remaining < 0
    # count >= limit * threshold / 100, kept in whole numbers so 50% of 3 is 1.5, not 1.
    visible = over or threshold is None or count * 100 >= limit * threshold
    one = "word" if unit == "words" else "character"

    def noun(n: int) -> str:
        return one if n == 1 else unit

    if over:
        message = "You have %d %s too many" % (-remaining, noun(-remaining))
    else:
        message = "You have %d %s remaining" % (remaining, noun(remaining))
    return CharacterCount(
        count=count,
        remaining=remaining,
        over=over,
        visible=visible,
        message=message,
        description="You can enter up to %d %s" % (limit, noun(limit)),
    )