Functional Weave
Code in Rust

collections.search-text@1.0.0

impl/python.py

2,920 bytes · the Python implementation · view raw

from typing import Any, List, Mapping, Optional, Sequence

# Largest integer JavaScript can hold exactly; beyond it the three languages disagree.
SAFE_INTEGER = 9007199254740991


def fold_ascii(text: str) -> str:
    """Fold A-Z to a-z and leave every other character alone.

    Deliberately not str.lower(): full Unicode case mapping differs between
    JavaScript, Python and Rust (Turkish dotted I, final sigma, sharp s), and a
    search that finds a row in one service but not in another is worse than one
    that consistently ignores accents.
    """
    out = []
    for ch in text:
        code = ord(ch)
        out.append(chr(code + 32) if 0x41 <= code <= 0x5A else ch)
    return "".join(out)


def _searchable_text(value: Any) -> Optional[str]:
    """The searchable text of a value, or None if there is nothing to search.

    Floats are skipped rather than rendered: no decimal form of them is
    identical in all three languages, so matching on one could not be pinned.
    """
    if isinstance(value, str):
        return value
    # bool before int: True would otherwise be searched as "1" here and as
    # "true" in TypeScript and Rust.
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int):
        return str(value) if abs(value) <= SAFE_INTEGER else None
    return None


def search_text(
    records: Sequence[Mapping[str, Any]],
    fields: Sequence[str],
    query: str,
) -> List[Mapping[str, Any]]:
    """The records whose text in any of ``fields`` contains ``query``, in input order.

    An empty query returns everything, because "nothing typed yet" means "show
    me everything" in every search box ever built.
    """
    if isinstance(records, (str, bytes)) or not isinstance(records, (list, tuple)):
        raise TypeError("search_text needs a list of records")
    if isinstance(fields, (str, bytes)) or not isinstance(fields, (list, tuple)):
        raise TypeError("search_text needs a list of field names")
    if any(not isinstance(f, str) for f in fields):
        raise TypeError("search_text needs a list of field names")
    # An empty field list that quietly matches nothing is the classic bug that
    # reaches users as "search is broken" with no other symptom.
    if len(fields) == 0:
        raise ValueError("search_text needs at least one field to search")
    if not isinstance(query, str):
        raise TypeError("query must be a string, received %r" % (query,))

    needle = fold_ascii(query)
    if needle == "":
        return list(records)

    matches: List[Mapping[str, Any]] = []
    for record in records:
        if not isinstance(record, dict):
            continue
        for field in fields:
            text = _searchable_text(record.get(field))
            if text is not None and needle in fold_ascii(text):
                matches.append(record)
                break
    return matches