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 = 9007199254740991def 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) if0x41 <= code <= 0x5Aelse 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 elseNonereturnNonedef 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)) ornot isinstance(records, (list, tuple)):
raise TypeError("search_text needs a list of records")
if isinstance(fields, (str, bytes)) ornot 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")
ifnot 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:
ifnot isinstance(record, dict):
continuefor field in fields:
text = _searchable_text(record.get(field))
if text isnotNoneand needle in fold_ascii(text):
matches.append(record)
breakreturn matches