From 0fbc92a1d37173b07a82470d6e8ff5568cd6c36e Mon Sep 17 00:00:00 2001 From: Peter Pentchev Date: Tue, 1 Oct 2024 17:59:34 +0300 Subject: Add Peter Pentchev's Rust solution and test harness for 289 --- challenge-289/ppentchev/LICENSES/BSD-2-Clause.txt | 9 + challenge-289/ppentchev/README | 86 -- challenge-289/ppentchev/REUSE.toml | 25 + challenge-289/ppentchev/rust/Cargo.lock | 912 +++++++++++++++++++++ challenge-289/ppentchev/rust/Cargo.toml | 29 + challenge-289/ppentchev/rust/run-clippy.sh | 70 ++ challenge-289/ppentchev/rust/src/bin/ch-1.rs | 178 ++++ challenge-289/ppentchev/rust/src/bin/ch-2.rs | 192 +++++ .../ppentchev/rust/src/bin/test-jumble.rs | 234 ++++++ challenge-289/ppentchev/rust/src/bin/unjumble.rs | 244 ++++++ .../ppentchev/test-data/jumbled-letters/empty.txt | 0 .../test-data/jumbled-letters/empty.txt.sorted | 0 .../test-data/jumbled-letters/numbers.txt | 2 + .../test-data/jumbled-letters/numbers.txt.sorted | 2 + .../ppentchev/test-data/jumbled-letters/short.txt | 3 + .../test-data/jumbled-letters/short.txt.sorted | 3 + .../test-data/jumbled-letters/single-line.txt | 1 + .../jumbled-letters/single-line.txt.sorted | 1 + challenge-289/ppentchev/tox.ini | 18 + 19 files changed, 1923 insertions(+), 86 deletions(-) create mode 100644 challenge-289/ppentchev/LICENSES/BSD-2-Clause.txt delete mode 100644 challenge-289/ppentchev/README create mode 100644 challenge-289/ppentchev/REUSE.toml create mode 100644 challenge-289/ppentchev/rust/Cargo.lock create mode 100644 challenge-289/ppentchev/rust/Cargo.toml create mode 100755 challenge-289/ppentchev/rust/run-clippy.sh create mode 100644 challenge-289/ppentchev/rust/src/bin/ch-1.rs create mode 100644 challenge-289/ppentchev/rust/src/bin/ch-2.rs create mode 100644 challenge-289/ppentchev/rust/src/bin/test-jumble.rs create mode 100644 challenge-289/ppentchev/rust/src/bin/unjumble.rs create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/empty.txt create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/empty.txt.sorted create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/numbers.txt create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/numbers.txt.sorted create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/short.txt create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/short.txt.sorted create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/single-line.txt create mode 100644 challenge-289/ppentchev/test-data/jumbled-letters/single-line.txt.sorted create mode 100644 challenge-289/ppentchev/tox.ini diff --git a/challenge-289/ppentchev/LICENSES/BSD-2-Clause.txt b/challenge-289/ppentchev/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..5f662b354c --- /dev/null +++ b/challenge-289/ppentchev/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/challenge-289/ppentchev/README b/challenge-289/ppentchev/README deleted file mode 100644 index b0b0bb5c11..0000000000 --- a/challenge-289/ppentchev/README +++ /dev/null @@ -1,86 +0,0 @@ - - -# Peter Pentchev's solutions to PWC 287 - -\[[Home][ringlet-home] | [GitHub][github]\] - -## Overview - -This directory contains Peter Pentchev's solutions to the two tasks in -the [Perl & Raku Weekly Challenge 287][pwc-287]. - -## General remarks - -### Task 2: Valid Number - -There are several approaches to solving this problem: - -- match the input string against a regular expression -- try to parse the input string using a pre-built parser - -Some languages have built-in constructs for building parsers, others can handle -regular expressions with ease, and some are good at both, so the approach depends on -the programming language. - -## Running the test suite - -The `tests/` subdirectory contains a Perl test suite that outputs TAP, so that -it can be run using `prove tests` (or, for the more adventurous, `prove -v tests`). - -### Task 2: Valid Number - -This task takes a string as input, so there are two ways of running the program: - -- if the `PWC_FROM_STDIN` environment variable is not set to the exact value `1`, - the program examines the seven strings given as examples, and produces seven words on - its standard output stream, each one on a line by itself. - In other words, the program must output `true\nfalse\nfalse\nfalse\ntrue\ntrue\ntrue\n` exactly. -- if the `PWC_FROM_STDIN` environment variable is set, the program reads a single line of - text from its standard input, treats it as an expression to be examined, and produces - a single word (either "true" or "false") on a line by itself on its standard output stream. - -The `PWCTest::Ch287` module in `tests/lib/` defines a `test_valid_number_default` function that -runs a program with `PWC_FROM_STDIN` unset and expects the exact output, and also -a `test_valid_number` function that runs a series of tests with different sequences, -each time running the program with `PWC_FROM_STDIN` set to 1 and feeding it the sequence. - -If the implementation in any language should provide more than one method, then -the program should honor the `PWC_METHOD` environment variable. -The value "0" indicates the use of the most natural method for the language, -the value "1" indicates the use of an alternative method, and if there are more than two, -then the values "2", "3", etc, are used to select them. -If `PWC_METHOD` is set to a non-numeric value or to a value that is higher than -the index of the last supported methods, it is ignored and the program proceeds as if -`PWC_METHOD` was set to "0". - -The `tests/02-perl-ch-2.t` test runs these functions on the Perl implementation and -produces TAP output suitable for the `prove` tool. - -## Implementation details - -### Task 2: Valid Number - -#### Perl - -The Perl solution has three major elements: - -- `$re_valid_number` - a documented (using the `/x` modifier) regular expression that - determines whether a string represents a valid number or not -- `valid_number` - a function that returns either the string "true" or the string "false" - depending on whether the argument represents a valid number -- `parse_stdin` - read a line from the standard input, return it as a string to be examined - -## Contact - -These solutions were written by [Peter Pentchev][roam]. -They are developed in [the common PWC-club GitHub repository][github]. -This documentation is hosted at [Ringlet][ringlet-home]. - -[roam]: mailto:roam@ringlet.net "Peter Pentchev" -[github]: https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-287/ppentchev "These solutions at GitHub" -[ringlet-home]: https://devel.ringlet.net/misc/perlweeklychallenge-club/287/ "This documentation at Ringlet" - -[pwc-287]: https://theweeklychallenge.org/blog/perl-weekly-challenge-287/ "The 287th Perl & Raku Weekly Challenge" diff --git a/challenge-289/ppentchev/REUSE.toml b/challenge-289/ppentchev/REUSE.toml new file mode 100644 index 0000000000..7e1aa5a123 --- /dev/null +++ b/challenge-289/ppentchev/REUSE.toml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Peter Pentchev +# SPDX-License-Identifier: BSD-2-Clause + +version = 1 +SPDX-PackageName = "pwc-289-ppentchev" +SPDX-PackageSupplier = "Peter Pentchev " +SPDX-PackageDownloadLocation = "https://devel.ringlet.net/{{ website_section }}/{{ name }}/" + +[[annotations]] +path = "rust/Cargo.lock" +precedence = "aggregate" +SPDX-FileCopyrightText = "Peter Pentchev " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "test-data/jumbled-letters/*.txt" +precedence = "aggregate" +SPDX-FileCopyrightText = "Peter Pentchev " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "test-data/jumbled-letters/*.txt.sorted" +precedence = "aggregate" +SPDX-FileCopyrightText = "Peter Pentchev " +SPDX-License-Identifier = "BSD-2-Clause" diff --git a/challenge-289/ppentchev/rust/Cargo.lock b/challenge-289/ppentchev/rust/Cargo.lock new file mode 100644 index 0000000000..6d592500c0 --- /dev/null +++ b/challenge-289/ppentchev/rust/Cargo.lock @@ -0,0 +1,912 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "perl-weekly-challenge-289" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "clap", + "clap_derive", + "itertools", + "rand", + "rstest", + "tempfile", + "tracing", + "tracing-test", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/challenge-289/ppentchev/rust/Cargo.toml b/challenge-289/ppentchev/rust/Cargo.toml new file mode 100644 index 0000000000..1376a11073 --- /dev/null +++ b/challenge-289/ppentchev/rust/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Peter Pentchev +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "perl-weekly-challenge-289" +version = "0.1.0" +rust-version = "1.62" +authors = ["Peter Pentchev "] +edition = "2021" +description = "A solution to the Perl Weekly Challenge 289" +readme = "README.md" +repository = "https://github.com/ppentchev/perlweeklychallenge-club" +license = "BSD-2-Clause" +categories = ["algorithms"] +keywords = ["perl-weekly-challenge"] + +[dependencies] +anyhow = "1" +camino = "1.1.9" +clap = "4" +clap_derive = "4" +itertools = "0.13" +rand = "0.8" +tempfile = "3" +tracing = "0.1" + +[dev-dependencies] +rstest = "0.22" +tracing-test = "0.2" diff --git a/challenge-289/ppentchev/rust/run-clippy.sh b/challenge-289/ppentchev/rust/run-clippy.sh new file mode 100755 index 0000000000..746742f2e4 --- /dev/null +++ b/challenge-289/ppentchev/rust/run-clippy.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# +# SPDX-FileCopyrightText: Peter Pentchev +# SPDX-License-Identifier: BSD-2-Clause + + +set -e + +def_cargo='cargo' + +usage() +{ + cat <&2 + exit 1 + ;; + esac +done +shift "$((OPTIND - 1))" + +# The list of allowed and ignored checks is synced with Rust 1.76. +set -x +"$cargo" clippy \ + --tests \ + -- \ + -W warnings \ + -W future-incompatible \ + -W nonstandard-style \ + -W rust-2018-compatibility \ + -W rust-2018-idioms \ + -W rust-2021-compatibility \ + -W unused \ + -W clippy::restriction \ + -A clippy::blanket_clippy_restriction_lints \ + -A clippy::std_instead_of_alloc \ + -A clippy::std_instead_of_core \ + -A clippy::implicit_return \ + -A clippy::missing_trait_methods \ + -A clippy::mod_module_files \ + -A clippy::question_mark_used \ + -A clippy::ref_patterns \ + -A clippy::semicolon_outside_block \ + -A clippy::separated_literal_suffix \ + -A clippy::single_call_fn \ + -A clippy::self_named_module_files \ + -W clippy::pedantic \ + -A clippy::module_name_repetitions \ + -W clippy::cargo \ + ${run_nursery+-W clippy::nursery} \ + "$@" diff --git a/challenge-289/ppentchev/rust/src/bin/ch-1.rs b/challenge-289/ppentchev/rust/src/bin/ch-1.rs new file mode 100644 index 0000000000..60d51d579a --- /dev/null +++ b/challenge-289/ppentchev/rust/src/bin/ch-1.rs @@ -0,0 +1,178 @@ +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +// SPDX-FileCopyrightText: Peter Pentchev +// SPDX-License-Identifier: BSD-2-Clause +//! PWC 289 task 1: Third Maximum + +use std::env; +use std::io; +use std::iter::FusedIterator; +use std::process::{ExitCode, Termination}; +use std::str::FromStr; + +use anyhow::{bail, Context, Result}; +use tracing::{debug, trace}; + +/// The example sequences from the problem to test against. +const TEST_SEQUENCES: [&[u32]; 3] = [&[5, 6, 4, 1], &[4, 5], &[1, 2, 2, 3]]; + +/// The result of the main program or an invoked subcommand. +enum MainResult { + /// Everything seems fine. + Ok, +} + +impl Termination for MainResult { + fn report(self) -> ExitCode { + match self { + Self::Ok => ExitCode::SUCCESS, + } + } +} + +/// The state for running a single number through the collected maxima. +#[derive(Debug)] +struct MaxState> { + /// The iterator over the maxima collected so far. + it: I, + + /// The value stored from the last step, if any. + stashed: Option, + + /// Have we reached the end yet? + done: bool, +} + +impl> MaxState { + /// Construct a [`MaxState`] iterator over the supplied one. + #[inline] + #[must_use] + const fn new(it: I, stashed: u32) -> Self { + Self { + it, + stashed: Some(stashed), + done: false, + } + } +} + +impl> Iterator for MaxState { + type Item = u32; + + #[inline] + fn next(&mut self) -> Option { + if self.done { + return None; + } + + match (self.it.next(), self.stashed.take()) { + (None, None) => { + self.done = true; + self.stashed = None; + None + } + + (None, Some(stashed)) => { + self.done = true; + self.stashed = None; + Some(stashed) + } + + (Some(value), None) => Some(value), + + (Some(value), Some(stashed)) if stashed > value => { + self.stashed = Some(value); + Some(stashed) + } + + (Some(value), Some(stashed)) if stashed == value => { + self.stashed = None; + Some(value) + } + + (Some(value), Some(stashed)) => { + self.stashed = Some(stashed); + Some(value) + } + } + } +} + +impl> FusedIterator for MaxState {} + +/// Run each successive number through the [`MaxState`] iterator. +#[tracing::instrument] +fn third_max(seq: &[u32]) -> Result { + debug!( + "Processing a sequence of {count} numbers", + count = seq.len(), + ); + let res = seq.iter().fold(vec![], |acc, value| { + trace!(?acc); + MaxState::new(acc.into_iter(), *value).take(3).collect() + }); + trace!(?res); + match res[..] { + [_, _, third] => Ok(third), + [first, ..] => Ok(first), + [] => bail!("No numbers at all?"), + } +} + +/// The main program - parse the environment settings, do something about it. +#[allow(clippy::print_stdout)] +fn main() -> Result { + if env::var("PWC_FROM_STDIN").unwrap_or_else(|_| String::new()) == "1" { + let mut line = String::new(); + io::stdin() + .read_line(&mut line) + .context("Could not read a line from the standard input stream")?; + let seq = line + .split_ascii_whitespace() + .map(|word| { + u32::from_str(word) + .with_context(|| format!("Could not parse '{word}' as a positive integer")) + }) + .collect::, _>>()?; + println!("{res}", res = third_max(&seq)?); + } else { + for test_seq in TEST_SEQUENCES { + println!("{res}", res = third_max(test_seq)?); + } + } + Ok(MainResult::Ok) +} + +#[cfg(test)] +#[allow(clippy::panic_in_result_fn)] +#[allow(clippy::print_stdout)] +mod tests { + use anyhow::Result; + use rstest::rstest; + use tracing_test::traced_test; + + #[traced_test] + #[rstest] + #[case(&[1], 1)] + #[case(&[1, 1], 1)] + #[case(&[1, 1, 1], 1)] + #[case(&[1, 2], 2)] + #[case(&[2, 1], 2)] + #[case(&[1, 2, 3], 1)] + #[case(&[1, 3, 2], 1)] + #[case(&[2, 1, 3], 1)] + #[case(&[2, 3, 1], 1)] + #[case(&[3, 1, 2], 1)] + #[case(&[3, 2, 1], 1)] + #[case(&[1, 2, 3, 4], 2)] + #[case(&[1, 2, 4, 3], 2)] + #[case(&[3, 4, 1, 2], 2)] + #[case(&[4, 3, 2, 1], 2)] + #[case(&[1, 2, 3, 4, 3, 2, 1, 4, 2, 4, 1, 3], 2)] + #[case(&[1, 2, 2, 1], 2)] + fn test_third_max(#[case] seq: &[u32], #[case] expected: u32) -> Result<()> { + println!(); + assert_eq!(super::third_max(seq)?, expected); + Ok(()) + } +} diff --git a/challenge-289/ppentchev/rust/src/bin/ch-2.rs b/challenge-289/ppentchev/rust/src/bin/ch-2.rs new file mode 100644 index 0000000000..974d5ac095 --- /dev/null +++ b/challenge-289/ppentchev/rust/src/bin/ch-2.rs @@ -0,0 +1,192 @@ +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +// SPDX-FileCopyrightText: Peter Pentchev +// SPDX-License-Identifier: BSD-2-Clause +//! PWC 289 task 2: Jumbled Letters + +use std::io; +use std::process::{ExitCode, Termination}; + +use anyhow::{Context, Result}; +use rand::rngs::ThreadRng; +use rand::seq::index as r_idx; + +/// The result of executing the program. +#[derive(Debug)] +enum MainResult { + /// Everything went file. + Ok, +} + +impl Termination for MainResult { + fn report(self) -> ExitCode { + match self { + Self::Ok => ExitCode::SUCCESS, + } + } +} + +/// The state of the "grab a word's characters to jumble" machine. +#[derive(Debug)] +enum State { + /// We are not in a word. + Outside, + + /// Got the first character of a word. + First(char), + + /// Inside a word. + Inside(char, Vec), +} + +/// Shuffle the letters in a single word, if there are enough of them. +#[allow(clippy::print_stdout)] +fn jumble_word(rng: &mut ThreadRng, first: char, mut letters: Vec, term_opt: Option) { + print!("{first}"); + if let Some(last) = letters.pop() { + let indices = r_idx::sample(rng, letters.len(), letters.len()); + // Let's hope rand::seq::index::sample() works correctly + #[allow(clippy::indexing_slicing)] + let res = indices + .into_iter() + .map(|idx| letters[idx]) + .collect::(); + print!("{res}{last}"); + } + if let Some(term) = term_opt { + print!("{term}"); + } +} + +/// Read a file, sort the characters in each word, display them on stdout. +#[allow(clippy::print_stdout)] +fn jumble_stdin() -> Result { + let mut rng = rand::thread_rng(); + for line_res in io::stdin().lines() { + let line = line_res.context("Could not read a line from the standard input stream")?; + let res = line + .chars() + .try_fold(State::Outside, |state, chr| -> Result { + match (state, chr.is_ascii_alphabetic()) { + (State::Outside, false) => { + print!("{chr}"); + Ok(State::Outside) + } + (State::Outside, true) => Ok(State::First(chr)), + (State::First(first), false) => { + print!("{first}{chr}"); + Ok(State::Outside) + } + (State::First(first), true) => Ok(State::Inside(first, vec![chr])), + (State::Inside(first, letters), false) => { + jumble_word(&mut rng, first, letters, Some(chr)); + Ok(State::Outside) + } + (State::Inside(first, mut letters), true) => { + letters.push(chr); + Ok(State::Inside(first, letters)) + } + } + })?; + match res { + State::Outside => (), + State::First(first) => print!("{first}"), + State::Inside(first, letters) => { + jumble_word(&mut rng, first, letters, None); + } + }; + println!(); + } + Ok(MainResult::Ok) +} + +fn main() -> Result { + jumble_stdin() +} + +#[cfg(test)] +#[allow(clippy::panic_in_result_fn)] +mod tests { + use std::env; + use std::path::PathBuf; + use std::process::{Command, Stdio}; + + use anyhow::{anyhow, Context, Result}; + use camino::Utf8PathBuf; + use rstest::rstest; + + #[derive(Debug)] + struct Programs { + /// The program to be tested. + testee: Utf8PathBuf, + + /// The program to be tested. + tester: Utf8PathBuf, + + /// The "unjumble this word" program. + unjumble: Utf8PathBuf, + } + + fn find_programs() -> Result { + let self_exe = Utf8PathBuf::from_path_buf( + env::current_exe().context("Could not find our own test executable file")?, + ) + .map_err(|obuf| { + anyhow!( + "Could not represent the test executable path {obuf} as an UTF-8 string", + obuf = obuf.display() + ) + })?; + let pdir = { + let self_parent = self_exe + .parent() + .ok_or_else(|| anyhow!("Could not determine a parent for {self_exe}"))?; + let parent_filename = self_parent + .file_name() + .ok_or_else(|| anyhow!("Could not determine a filename for {self_parent}"))?; + if parent_filename == "deps" { + self_parent + .parent() + .ok_or_else(|| anyhow!("Could not determine a parent for {self_parent}"))? + } else { + self_parent + } + }; + Ok(Programs { + testee: pdir.join("ch-2"), + tester: pdir.join("test-jumble"), + unjumble: pdir.join("unjumble"), + }) + } + + #[rstest] + fn test_jumble_bin( + #[files("../test-data/jumbled-letters/*.txt")] infile_buf: PathBuf, + ) -> Result<()> { + #[allow(clippy::print_stdout)] + { + println!(); + } + let programs = find_programs()?; + let infile = Utf8PathBuf::from_path_buf(infile_buf).map_err(|path| { + anyhow!( + "Could not parse {path} as a UTF-8 pathname", + path = path.display() + ) + })?; + assert!(Command::new(&programs.tester) + .args([ + "--unjumble-program", + programs.unjumble.as_ref(), + "--jumble-program", + programs.testee.as_ref(), + "--", + infile.as_ref(), + ]) + .stdin(Stdio::null()) + .status() + .with_context(|| format!("Could not run {tester}", tester = programs.tester))? + .success()); + Ok(()) + } +} diff --git a/challenge-289/ppentchev/rust/src/bin/test-jumble.rs b/challenge-289/ppentchev/rust/src/bin/test-jumble.rs new file mode 100644 index 0000000000..a03442b581 --- /dev/null +++ b/challenge-289/ppentchev/rust/src/bin/test-jumble.rs @@ -0,0 +1,234 @@ +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +// SPDX-FileCopyrightText: Peter Pentchev +// SPDX-License-Identifier: BSD-2-Clause +//! PWC 289 helper: run the jumble program, compare its output with the expected one. + +use std::fs::File; +use std::process::{Command, ExitCode, Stdio, Termination}; + +use anyhow::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use clap::error::ErrorKind as ClapErrorKind; +use clap::Parser; +use clap_derive::Parser; +use tempfile::Builder; + +/// The result of executing the program. +#[derive(Debug)] +enum MainResult { + /// Everything went file. + Ok, +} + +impl Termination for MainResult { + fn report(self) -> ExitCode { + match self { + Self::Ok => ExitCode::SUCCESS, + } + } +} + +/// Runtime configuration for the unjumble tool. +#[derive(Debug)] +struct Config { + /// The program to test (the one that jumbles the words). + testee: Utf8PathBuf, + + /// The program that sorts the letters in each word. + unjumble: Utf8PathBuf, + + /// The file to jumble. + filename: Utf8PathBuf, +} + +impl Config { + /// The program to test (the one that jumbles the words). + fn testee(&self) -> &Utf8Path { + &self.testee + } + + /// The program that sorts the letters in each word. + fn unjumble(&self) -> &Utf8Path { + &self.unjumble + } + + /// The file to jumble. + fn filename(&self) -> &Utf8Path { + &self.filename + } +} + +/// What we should do. +#[derive(Debug)] +enum Mode { + /// Everything was handled already. + Handled, + + /// Unjumble the words from a file. + TestJumble(Config), +} + +/// The command-line arguments. +#[derive(Debug, Parser)] +struct Cli { + /// The program to test (the one that jumbles the words). + #[clap(short('j'), long, required(true))] + jumble_program: Utf8PathBuf, + + /// The program that sorts the letters in each word. + #[clap(short('u'), long, required(true))] + unjumble_program: Utf8PathBuf, + + /// The file to jumble. + filename: Utf8PathBuf, +} + +/// Parse the command-line arguments, determine the mode of operation. +/// +/// # Errors +/// +/// Propagate command-line parsing errors. +fn try_parse() -> Result { + let args = match Cli::try_parse() { + Ok(args) => args, + Err(err) + if matches!( + err.kind(), + ClapErrorKind::DisplayHelp | ClapErrorKind::DisplayVersion + ) => + { + err.print() + .context("Could not display the help or version output")?; + return Ok(Mode::Handled); + } + Err(err) if err.kind() == ClapErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => { + err.print() + .context("Could not display the usage or version message")?; + bail!("Invalid or missing command-line options"); + } + Err(err) => return Err(err).context("Could not parse the command-line options"), + }; + Ok(Mode::TestJumble(Config { + testee: args.jumble_program, + unjumble: args.unjumble_program, + filename: args.filename, + })) +} + +/// Use `cmp(1)` to decide whether two files are the same or not. +fn same_files(src: &Utf8Path, dst: &Utf8Path) -> Result { + Ok(Command::new("cmp") + .args(["--", src.as_ref(), dst.as_ref()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .output() + .with_context(|| format!("Could not run `cmp -- {src} {dst}`"))? + .status + .success()) +} + +/// Run the jumble program ten times, sort its output, look for consistency and changes. +#[allow(clippy::print_stdout)] +fn test_jumble(cfg: &Config) -> Result { + let sorted_file = cfg.filename().with_extension("txt.sorted"); + let same = same_files(cfg.filename(), &sorted_file)?; + let changed = (1_u32..=10_u32).try_fold(false, |changed, idx| -> Result { + println!( + "Iteration {idx}: running {testee} on {infile}", + testee = cfg.testee(), + infile = cfg.filename(), + ); + let temp_obj = Builder::new() + .tempfile() + .context("Could not create a temporary file")?; + let temp_path = Utf8Path::from_path(temp_obj.path()).with_context(|| { + format!( + "Could not parse {path} as a UTF-8 path", + path = temp_obj.path().display() + ) + })?; + let temp_sorted_obj = Builder::new() + .tempfile() + .context("Could not create a temporary file")?; + let temp_sorted_path = Utf8Path::from_path(temp_sorted_obj.path()).with_context(|| { + format!( + "Could not parse {path} as a UTF-8 path", + path = temp_sorted_obj.path().display() + ) + })?; + + if !Command::new(cfg.testee()) + .stdin(File::open(cfg.filename()).with_context(|| { + format!( + "Could not open {infile} for reading", + infile = cfg.filename() + ) + })?) + .stdout( + File::options() + .write(true) + .open(temp_path) + .with_context(|| format!("Could not open {temp_path} for writing"))?, + ) + .stderr(Stdio::inherit()) + .status() + .with_context(|| format!("Could not run {testee}", testee = cfg.testee()))? + .success() + { + bail!( + "Could not run {testee} on {infile} into {temp_path}", + testee = cfg.testee(), + infile = cfg.filename() + ); + } + + if !Command::new(cfg.unjumble()) + .arg(temp_path) + .stdout( + File::options() + .write(true) + .open(temp_sorted_path) + .with_context(|| format!("Could not open {temp_sorted_path} for writing"))?, + ) + .stderr(Stdio::inherit()) + .status() + .with_context(|| format!("Could not run {tester}", tester = cfg.unjumble()))? + .success() + { + bail!( + "Could not run {tester} on {temp_path} into {temp_sorted_path}", + tester = cfg.unjumble(), + ); + } + + if !same_files(temp_sorted_path, &sorted_file)? { + bail!( + "Running {testee} on {infile} did not produce the expected words", + testee = cfg.testee(), + infile = cfg.filename() + ); + } + + let changed_now = !same_files(temp_path, cfg.filename())?; + println!("- at iteration {idx}: changed {changed_now}"); + Ok(changed || changed_now) + })?; + + if changed == same { + bail!( + "At least one run of {testee} should have changed the input file {infile}", + testee = cfg.testee(), + infile = cfg.filename() + ); + } + Ok(MainResult::Ok) +} + +fn main() -> Result { + match try_parse()? { + Mode::Handled => Ok(MainResult::Ok), + Mode::TestJumble(cfg) => test_jumble(&cfg), + } +} diff --git a/challenge-289/ppentchev/rust/src/bin/unjumble.rs b/challenge-289/ppentchev/rust/src/bin/unjumble.rs new file mode 100644 index 0000000000..9a817ac9d4 --- /dev/null +++ b/challenge-289/ppentchev/rust/src/bin/unjumble.rs @@ -0,0 +1,244 @@ +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +// SPDX-FileCopyrightText: Peter Pentchev +// SPDX-License-Identifier: BSD-2-Clause +//! PWC 289 helper: unjumble the words in a text file + +use std::fs; +use std::process::{ExitCode, Termination}; + +use anyhow::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use clap::error::ErrorKind as ClapErrorKind; +use clap::Parser; +use clap_derive::Parser; +use itertools::Itertools; + +/// The result of executing the program. +#[derive(Debug)] +enum MainResult { + /// Everything went file. + Ok, +} + +impl Termination for MainResult { + fn report(self) -> ExitCode { + match self { + Self::Ok => ExitCode::SUCCESS, + } + } +} + +/// Runtime configuration for the unjumble tool. +#[derive(Debug)] +struct Config { + /// The file to unjumble. + filename: Utf8PathBuf, +} + +impl Config { + /// Construct a [`Config`] object with the specified filename. + const fn new(filename: Utf8PathBuf) -> Self { + Self { filename } + } + + /// The file to unjumble. + fn filename(&self) -> &Utf8Path { + &self.filename + } +} + +/// What we should do. +#[derive(Debug)] +enum Mode { + /// Everything was handled already. + Handled, + + /// Unjumble the words from a file. + Unjumble(Config), +} + +/// The command-line arguments. +#[derive(Debug, Parser)] +struct Cli { + /// The file to unjumble. + filename: Utf8PathBuf, +} + +/// The state of the "grab a word's characters to sort" machine. +#[derive(Debug)] +enum State { + /// We are not in a word. + Outside, + + /// Got the first character of a word. + First(char), + + /// Inside a word. + Inside(char, Vec), +} + +/// Parse the command-line arguments, determine the mode of operation. +/// +/// # Errors +/// +/// Propagate command-line parsing errors. +fn try_parse() -> Result { + let args = match Cli::try_parse() { + Ok(args) => args, + Err(err) + if matches!( + err.kind(), + ClapErrorKind::DisplayHelp | ClapErrorKind::DisplayVersion + ) => + { + err.print() + .context("Could not display the help or version output")?; + return Ok(Mode::Handled); + } + Err(err) if err.kind() == ClapErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => { + err.print() + .context("Could not display the usage or version message")?; + bail!("Invalid or missing command-line options"); + } + Err(err) => return Err(err).context("Could not parse the command-line options"), + }; + Ok(Mode::Unjumble(Config::new(args.filename))) +} + +/// Sort the letters in a single word, if there are enough of them. +#[allow(clippy::print_stdout)] +fn unjumble_word(first: char, mut letters: Vec, term: Option) { + print!("{first}"); + if let Some(last) = letters.pop() { + letters.sort_unstable(); + print!("{letters}{last}", letters = letters.into_iter().join("")); + } + if let Some(term_chr) = term { + print!("{term_chr}"); + } +} + +/// Read a file, sort the characters in each word, display them on stdout. +#[allow(clippy::print_stdout)] +fn unjumble_file(cfg: &Config) -> Result { + let contents = fs::read_to_string(cfg.filename()).with_context(|| { + format!( + "Could not read the contents of {filename}", + filename = cfg.filename() + ) + })?; + for line in contents.lines() { + let res = line + .chars() + .try_fold(State::Outside, |state, chr| -> Result { + match (state, chr.is_ascii_alphabetic()) { + (State::Outside, false) => { + print!("{chr}"); + Ok(State::Outside) + } + (State::Outside, true) => Ok(State::First(chr)), + (State::First(first), false) => { + print!("{first}{chr}"); + Ok(State::Outside) + } + (State::First(first), true) => Ok(State::Inside(first, vec![chr])), + (State::Inside(first, letters), false) => { + unjumble_word(first, letters, Some(chr)); + Ok(State::Outside) + } + (State::Inside(first, mut letters), true) => { + letters.push(chr); + Ok(State::Inside(first, letters)) + } + } + })?; + match res { + State::Outside => (), + State::First(first) => print!("{first}"), + State::Inside(first, letters) => { + unjumble_word(first, letters, None); + } + }; + println!(); + } + Ok(MainResult::Ok) +} + +fn main() -> Result { + match try_parse()? { + Mode::Handled => Ok(MainResult::Ok), + Mode::Unjumble(cfg) => unjumble_file(&cfg), + } +} + +#[cfg(test)] +#[allow(clippy::panic_in_result_fn)] +mod tests { + use std::env; + use std::fs; + use std::path::PathBuf; + use std::process::{Command, Stdio}; + + use anyhow::{anyhow, Context, Result}; + use camino::Utf8PathBuf; + use rstest::rstest; + + fn find_program() -> Result { + let self_exe = Utf8PathBuf::from_path_buf( + env::current_exe().context("Could not find our own test executable file")?, + ) + .map_err(|obuf| { + anyhow!( + "Could not represent the test executable path {obuf} as an UTF-8 string", + obuf = obuf.display() + ) + })?; + let pdir = { + let self_parent = self_exe + .parent() + .ok_or_else(|| anyhow!("Could not determine a parent for {self_exe}"))?; + let parent_filename = self_parent + .file_name() + .ok_or_else(|| anyhow!("Could not determine a filename for {self_parent}"))?; + if parent_filename == "deps" { + self_parent + .parent() + .ok_or_else(|| anyhow!("Could not determine a parent for {self_parent}"))? + } else { + self_parent + } + }; + Ok(pdir.join("unjumble")) + } + + #[rstest] + fn test_unjumble_bin( + #[files("../test-data/jumbled-letters/*.txt")] infile_buf: PathBuf, + ) -> Result<()> { + let prog = find_program()?; + let infile = Utf8PathBuf::from_path_buf(infile_buf).map_err(|path| { + anyhow!( + "Could not parse {path} as a UTF-8 pathname", + path = path.display() + ) + })?; + let sorted_file = infile.with_extension("txt.sorted"); + + let expected = fs::read_to_string(&sorted_file) + .with_context(|| format!("Could not read the contents of {sorted_file}"))?; + let output = Command::new(&prog) + .arg(&infile) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .with_context(|| format!("Could not run `{prog} {infile}`"))?; + assert!(output.status.success()); + let result = String::from_utf8(output.stdout).with_context(|| { + format!("Could not parse the output of `{prog} {infile}` as a UTF-8 string") + })?; + assert_eq!(result, expected); + Ok(()) + } +} diff --git a/challenge-289/ppentchev/test-data/jumbled-letters/empty.txt b/challenge-289/ppentchev/test-data/jumbled-letters/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/challenge-289/ppentchev/test-data/jumbled