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)), )