from typing import Any, Dict, List, Mapping, Sequence # Largest integer JavaScript can hold exactly; beyond it the three languages disagree. SAFE_INTEGER = 9007199254740991 def _group_name_of(value: Any, key: str) -> str: """The name of the group a value belongs to. Each language has its own default string conversion and they disagree (Python prints True where JavaScript prints true, and 1.0 where JavaScript prints 1), so the rendering is spelled out here instead of inherited. """ # Absent and null are the same thing: a document that omits a field and one # that nulls it mean the same to every reader. if value is None: return "" if isinstance(value, str): return value # bool before int: in Python True is an int, and "1" is not the group a # JavaScript or Rust caller would have got for the same document. if isinstance(value, bool): return "true" if value else "false" if isinstance(value, int): if abs(value) > SAFE_INTEGER: raise ValueError('cannot group by the out-of-range number %d at "%s"' % (value, key)) return str(value) if isinstance(value, float): raise TypeError('cannot group by the fractional number %r at "%s"' % (value, key)) raise TypeError('cannot group by the list or map at "%s"' % (key,)) def group_by_key(records: Sequence[Mapping[str, Any]], key: str) -> Dict[str, List[Mapping[str, Any]]]: """Group ``records`` by the value at ``key``. Records keep their input order inside each group, so grouping a sorted list never unsorts it - the property callers actually depend on. """ if isinstance(records, (str, bytes)) or not isinstance(records, (list, tuple)): raise TypeError("group_by_key needs a list of records") if not isinstance(key, str) or key == "": raise TypeError("group_by_key needs a non-empty key name") groups: Dict[str, List[Mapping[str, Any]]] = {} for record in records: value = record.get(key) if isinstance(record, dict) else None name = _group_name_of(value, key) groups.setdefault(name, []).append(record) return groups def count_by_key(records: Sequence[Mapping[str, Any]], key: str) -> Dict[str, int]: """How many records fall in each group, without carrying the records themselves.""" return {name: len(group) for name, group in group_by_key(records, key).items()}