From 7b0cce752eca91696664c2bb0de3c5e0395f3fda Mon Sep 17 00:00:00 2001 From: Peter Pentchev Date: Tue, 10 Sep 2024 23:49:05 +0300 Subject: Add Peter Pentchev's Python solutions to 286 Also fix a couple of things in the other files: - make the TAP test functions accept an arrayref for the command to run instead of a single string, since we want to run the Python solution using `python3 -B -u /path/to/the/program` - fix the use of Test::More::skip() in the Self Spammer test function - fix a shadowed variable in the Order Game test function - correct the indentation in the documentation's "Implementation details" section - make the README file a copy of docs/index.md instead of a wrong copy of a completely different project's README file - point blog.txt to the Ringlet copy of this challenge's documentation --- challenge-286/ppentchev/python/scripts/ch-1.py | 39 +++++++++ challenge-286/ppentchev/python/scripts/ch-2.py | 109 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100755 challenge-286/ppentchev/python/scripts/ch-1.py create mode 100755 challenge-286/ppentchev/python/scripts/ch-2.py (limited to 'challenge-286/ppentchev/python/scripts') diff --git a/challenge-286/ppentchev/python/scripts/ch-1.py b/challenge-286/ppentchev/python/scripts/ch-1.py new file mode 100755 index 0000000000..a3470d1bd4 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-1.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import pathlib +import random +import typing + + +if typing.TYPE_CHECKING: + from typing import Final + + +class EmptyWordError(RuntimeError): + """An empty word was chosen, `str.split()` is not suppoed to do that.""" + + words: list[str] + """The words among which the empty word was found.""" + + def __str__(self) -> str: + """Provide a human-readable error message.""" + return f"Internal error: empty word in {self.words!r}" + + +def main() -> None: + """Find the source file, select a word, output it.""" + words: Final = pathlib.Path(__file__).read_text(encoding="UTF-8").split() + chosen_one: Final = random.choice(words) # noqa: S311 # no cryptography here + if not chosen_one: + raise EmptyWordError(words) + + print(chosen_one) # noqa: T201 # this is the whole point of this program + + +if __name__ == "__main__": + main() diff --git a/challenge-286/ppentchev/python/scripts/ch-2.py b/challenge-286/ppentchev/python/scripts/ch-2.py new file mode 100755 index 0000000000..b8c0819589 --- /dev/null +++ b/challenge-286/ppentchev/python/scripts/ch-2.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: Peter Pentchev +# SPDX-License-Identifier: BSD-2-Clause +"""Pick a random word from this script's source file and output it.""" + +from __future__ import annotations + +import enum +import os +import sys +import typing + + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + from typing import Final, Self + + +TEST_SEQUENCES: Final = [ + [2, 1, 4, 5, 6, 3, 0, 2], + [0, 5, 3, 2], + [9, 2, 1, 4, 5, 6, 0, 7, 3, 1, 3, 5, 7, 9, 0, 8], +] +"""The sample sequences from the problem.""" + + +class OrderIterState(enum.IntEnum): + """The states that an `OrderIter` object may be in.""" + + STASH = 0 + """Stash a value to pass to `min()` or `max()` later.""" + + YIELD = 1 + """Perform a `min()` or `max()` operation, yield the result.""" + + +class OrderIter: + """Iterate over the numbers of a single round.""" + + ints: Iterator[int] + """The list of integers to process.""" + + state: OrderIterState + """The current state of the iterator.""" + + stashed: int + """The last stashed number.""" + + op: Callable[[int, int], int] + """The operation to perform when yielding a value.""" + + def __init__(self, ints: Iterable[int]) -> None: + """Construct an `OrderIter` object with the specified numbers to process.""" + self.ints = iter(ints) + self.state = OrderIterState.STASH + self.stashed = -616 + self.op = min + + def __iter__(self) -> Self: + """Return the `OrderIter` object itself as an iterator.""" + return self + + def __next__(self) -> int: + """Calculate and return the next number.""" + match self.state: + case OrderIterState.STASH: + self.stashed = next(self.ints) + self.state = OrderIterState.YIELD + return next(self) + + case OrderIterState.YIELD: + res: Final = self.op(self.stashed, next(self.ints)) + self.state = OrderIterState.STASH + self.op = max if self.op == min else min + return res + + +def order_iter(ints: list[int]) -> list[int]: + """Run a single round using an `OrderIter` object.""" + return list(OrderIter(ints)) + + +def run_order_game(ints: list[int]) -> int: + """Run the order game until there is only a single integer left.""" + if not ints: + raise RuntimeError(repr(ints)) + + while len(ints) > 1: + ints = order_iter(ints) + + return ints[0] + + +def parse_stdin() -> list[int]: + """Read a line from the standard input, parse it as a list of integers.""" + return [int(word) for word in sys.stdin.readline().split() if word] + + +def main() -> None: + """Find the source file, select a word, output it.""" + if os.environ.get("PWC_FROM_STDIN", "") == "1": + print(run_order_game(parse_stdin())) # noqa: T201 # this is the whole point + else: + for ints in TEST_SEQUENCES: + print(run_order_game(ints)) # noqa: T201 # this is the whole point + + +if __name__ == "__main__": + main() -- cgit