diff --git a/mathstream/README.md b/mathstream/README.md index e3abb68..6602dd5 100644 --- a/mathstream/README.md +++ b/mathstream/README.md @@ -10,7 +10,7 @@ source venv/bin/activate pip install -e . ``` -Create digit files anywhere you like (the examples below use `instance/log`), then construct `StreamNumber` objects, and call the helpers: +Create digit files anywhere you like (the examples below use `instance/log`), or supply ad-hoc literals, then construct `StreamNumber` objects and call the helpers: ```python from mathstream import ( @@ -26,8 +26,8 @@ from mathstream import ( ) a = StreamNumber("instance/log/huge.txt") -b = StreamNumber("instance/log/tiny.txt") -e = StreamNumber("instance/log/exponent.txt") +b = StreamNumber(literal="34567") +e = StreamNumber(literal="3") print("sum =", "".join(add(a, b).stream())) print("difference =", "".join(sub(a, b).stream())) @@ -43,7 +43,7 @@ Each arithmetic call writes its result back into `instance/log` (configurable vi ## Core Concepts -- **StreamNumber(path)** – Wraps a digit text file. The file may include an optional leading sign (`+` or `-`). Whitespace is ignored; digits must otherwise be contiguous. +- **StreamNumber(path | literal=...)** – Wraps a digit text file or creates one for an integer literal inside `LOG_DIR`. Literal operands are persisted as `literal_.txt`, so repeated runs reuse the same staged file (note that `clear_logs()` removes these cache files too). - **`.stream(chunk_size)`** – Yields strings of digits with the provided chunk size. Operations in `mathstream.engine` consume these streams to avoid loading the entire number at once. - **Automatic staging** – Outputs are stored under `LOG_DIR` with hashes based on input file paths, letting you compose operations without manual bookkeeping. - **Sign-aware** – Addition, subtraction, multiplication, division (`//` behavior), modulo, and exponentiation (non-negative exponents) all respect operand sign. Division/modulo follow Python’s floor-division rules. diff --git a/mathstream/number.py b/mathstream/number.py index a3cc79e..22d1cf4 100644 --- a/mathstream/number.py +++ b/mathstream/number.py @@ -1,13 +1,55 @@ import hashlib from pathlib import Path +from typing import Optional, Union LOG_DIR = Path("./instance/log") + +def _ensure_log_dir() -> None: + LOG_DIR.mkdir(parents=True, exist_ok=True) + + +def _canonicalize_literal(value: str) -> str: + raw = value.strip() + if not raw: + raise ValueError("Literal value cannot be empty.") + + sign = "" + digits = raw + if raw[0] in "+-": + sign = "-" if raw[0] == "-" else "" + digits = raw[1:] + + if not digits or not digits.isdigit(): + raise ValueError(f"Literal must be an integer string, got: {value!r}") + + digits = digits.lstrip("0") or "0" + if digits == "0": + sign = "" + return f"{sign}{digits}" + + class StreamNumber: - def __init__(self, file_path): - self.path = Path(file_path) - if not self.path.exists(): - raise FileNotFoundError(self.path) + def __init__( + self, + file_path: Optional[Union[str, Path]] = None, + *, + literal: Optional[str] = None, + ): + if (file_path is None) == (literal is None): + raise ValueError("Provide exactly one of file_path or literal.") + + if literal is not None: + normalized = _canonicalize_literal(literal) + _ensure_log_dir() + literal_hash = hashlib.sha1(normalized.encode()).hexdigest()[:10] + self.path = LOG_DIR / f"literal_{literal_hash}.txt" + self.path.write_text(normalized, encoding="utf-8") + else: + self.path = Path(file_path) + if not self.path.exists(): + raise FileNotFoundError(self.path) + self.hash = hashlib.sha1(str(self.path).encode()).hexdigest()[:10] def __repr__(self): @@ -21,6 +63,7 @@ class StreamNumber: def write_stage(self, stage, data: str): """Write intermediate stage result.""" + _ensure_log_dir() stage_file = LOG_DIR / f"{self.hash}_stage_{stage}.bin" with open(stage_file, "wb") as f: f.write(data.encode()) diff --git a/test.py b/test.py index 69d8e60..26c13b7 100644 --- a/test.py +++ b/test.py @@ -52,6 +52,8 @@ def main() -> None: negative = write_number("negative", "-1200") exponent = write_number("power", "5") negative_divisor = write_number("neg_divisor", "-34567") + literal_even = StreamNumber(literal="2000") + literal_odd = StreamNumber(literal="-3") # Showcase the core operations. total = add(big, small) @@ -63,6 +65,7 @@ def main() -> None: neg_mod_pos = mod(negative, small) pos_mod_neg = mod(small, negative) neg_mod_neg = mod(negative, negative_divisor) + literal_combo = add(literal_even, literal_odd) print("Operands stored under:", NUMBERS_DIR) check("huge + tiny", total, "98765432123491356") @@ -74,10 +77,13 @@ def main() -> None: check("negative % tiny", neg_mod_pos, "33367") check("tiny % negative", pos_mod_neg, "-233") check("negative % neg_divisor", neg_mod_neg, "-1200") + check("literal_even + literal_odd", literal_combo, "1997") check_bool("is_even(negative)", is_even(negative), True) check_bool("is_even(tiny)", is_even(small), False) check_bool("is_odd(tiny)", is_odd(small), True) check_bool("is_odd(negative)", is_odd(negative), False) + check_bool("is_even(literal_even)", is_even(literal_even), True) + check_bool("is_odd(literal_odd)", is_odd(literal_odd), True) if __name__ == "__main__":