diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..bc8609f --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,77 @@ +name: test + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: [main] + + workflow_dispatch: + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + unit_test: + name: Unit test [${{ matrix.mode }}-rust-${{ matrix.rust }}-${{ matrix.os }}] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + - macos-14 + rust: + - 1.81.0 + mode: + - debug + + include: + - os: ubuntu-24.04 + rust: 1.81.0 + mode: release + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Update rust + run: rustup install ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} + + - name: Check rust installation + run: rustc -vV + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-rust-${{ matrix.rust }}-cargo-unit-test-${{ matrix.mode }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build ${{ matrix.mode == 'release' && '--release' || '' }} --verbose + - name: Run tests + run: cargo test ${{ matrix.mode == 'release' && '--release' || '' }} --verbose + - name: Build API documentation + run: cargo doc --no-deps + + + tests_complete: + name: All tests + if: always() + # needs: [unit_test, execute_tutorials, build_documentation] + needs: [unit_test] + runs-on: ubuntu-latest + + steps: + - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' + - name: Done + run: exit 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5155ca0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +ci: + autoupdate_schedule: quarterly + autoupdate_branch: 'trunk' + autofix_prs: true + +default_language_version: + rust: 1.81.0 + +repos: +- repo: https://github.com/backplane/pre-commit-rust-hooks + rev: v1.1.0 + hooks: + - id: fmt + - id: check + - id: clippy + args: + - --all-targets + - --all-features + - -- + - -Dwarnings +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-json + - id: check-toml + - id: check-yaml + - id: check-case-conflict + - id: mixed-line-ending +- repo: https://github.com/codespell-project/codespell + rev: v2.4.0 + hooks: + - id: codespell + args: ["--ignore-words-list=crate"] +- repo: https://github.com/rhysd/actionlint + rev: v1.7.7 + hooks: + - id: actionlint diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..33520b6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,279 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrcomp" +version = "0.1.0" +dependencies = [ + "rstest", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[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.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +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 = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9455a9d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arrcomp" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] +rstest = "0.24.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b467bf1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025 Jenna Bradley + +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. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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/README.md b/README.md index 89d18d8..d4f993e 100644 --- a/README.md +++ b/README.md @@ -1 +1,81 @@ -# arrcomp \ No newline at end of file + + +List comprehension-style syntax for creating Rust array using declarative macros. + +In contrast to most Rust packages of this sort, arrcomp exclusively creates fixed size +arrays without an intermediate heap allocation. This is more performant than standard +Vector-based approaches, but places a few additional restrictions on the allowed syntax. + +`arrcomp` Syntax +---------------- + +```rust +use arrcomp::arr; + +let incremented = arr![x + 1, for x in 0..10; len 10]; +// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +let incremented_if_odd = arr![x + 1, for x in 0..10, if x % 2 == 1; len 10]; +// [None, Some(2), None, Some(4), None, Some(6), None, Some(8), None, Some(10)] +``` + +This Rust adaption provides a familiar and performant interface for creating and +modifying fixed-size arrays. `Option` types allow the use of filters even in cases where +the number of unfiltered outputs is unknown at compile time -- without any dynamic +allocations! + +The `arr!` pattern is generally expressed as `f(x), for x in iterable, if condition; len N`, +where `f(x)` and `iterable` are any [statement](https://doc.rust-lang.org/reference/statements.html), +`condition` is any statement that evaluates to a `bool`, and and `x` is any [pattern](https://doc.rust-lang.org/reference/patterns.html). Unlike Python, we must also provide a const `N` matching +the length of the provided iterable in order to ensure the output can be sized at compile time. + +Why this crate? +=============== + +When working with vectors, list comprehensions are most naturally expressed as +`filter/flat_map`s in Rust. Consider the following example, which uses `filter` and +`map` for clarity: + +```rust +use arrcomp::arr; +let incremented_vec: Vec<_> = (0..10).filter(|x| x % 2 == 1).map(|x| x + 1).collect(); + +// Converting to an array is simple. Note we have to provide the correct array length +let incremented_arr: [i32; 5] = incremented_vec.clone().try_into().unwrap(); +assert_eq!(incremented_arr.to_vec(), incremented_vec); +``` + +Note that, in the example above, we dynamically allocate a vector that gets converted to +an array. In performance-critical contexts this is undesirable. Fortunately, there is +another way: + +```rust +# use arrcomp::arr; +let mut incremented_iter = (0..10) + .into_iter() + .map(|x| if x % 2 == 1 { Some(x+1) } else { None }); + +let iter_copy = incremented_iter.clone(); + +// std::array::from_fn lets us generate the array without collecting into a vector +let arr_without_allocation: [Option; 10] = std::array::from_fn( + |_| incremented_iter.next().unwrap() +); + +assert_eq!(arr_without_allocation.to_vec(), iter_copy.collect::>()); +``` + +The default Rust syntax is a little clunky, and the inputs to `std::array::from_fn` are +limited. Furthermore, an additional variable must be created outside the function call, +as cloning the iterator inside `from_fn` resets the iterator to the beginning. + +An array comprehension provides an attractive alternative to this pattern, with a +simplified syntax that allows for arbitrary expressions for our input iterable. + +```rust +# use arrcomp::arr; +let incremented_vec = (0..10).map(|x| if x % 2 == 1 { Some(x+1) } else { None }); + +let arr_comprehension = arr![x+1, for x in 0..10, if x % 2 == 1; len 10]; +assert_eq!(arr_comprehension.to_vec(), incremented_vec.collect::>()); +``` diff --git a/src/ims/arrcomp.svg b/src/ims/arrcomp.svg new file mode 100644 index 0000000..692fadb --- /dev/null +++ b/src/ims/arrcomp.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ims/arrcomp_anim.svg b/src/ims/arrcomp_anim.svg new file mode 100644 index 0000000..b62b2a4 --- /dev/null +++ b/src/ims/arrcomp_anim.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..89c1add --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,399 @@ +#![doc(html_logo_url = "https://github.com/janbridley/arrcomp/blob/main/src/ims/arrcomp.svg")] + +/*! +List comprehension-style syntax for creating Rust array using declarative macros. + +In contrast to most Rust packages of this sort, arrcomp exclusively creates fixed size +arrays without an intermediate heap allocation. This is more performant than standard +Vector-based approaches, but places a few additional restrictions on the allowed syntax. + +`arrcomp` Syntax +---------------- + +```rust +use arrcomp::arr; + +# fn main() { +let incremented = arr![x + 1, for x in 0..10; len 10]; +// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +let incremented_if_odd = arr![x + 1, for x in 0..10, if x % 2 == 1; len 10]; +// [None, Some(2), None, Some(4), None, Some(6), None, Some(8), None, Some(10)] +# } +``` +This Rust adaption provides a familiar and performant interface for creating and +modifying fixed-size arrays. `Option` types allow the use of filters even in cases where +the number of unfiltered outputs is unknown at compile time -- without any dynamic +allocations! + +The `arr!` pattern is generally expressed as `f(x), for x in iterable, if condition; len N`, +where `f(x)` and `iterable` are any [statement](https://doc.rust-lang.org/reference/statements.html), +`condition` is any statement that evaluates to a `bool`, and and `x` is any [pattern](https://doc.rust-lang.org/reference/patterns.html). Unlike Python, we must also provide a const `N` matching +the length of the provided iterable in order to ensure the output can be sized at compile time. + +
+Note that the extended arr![f(x), for x in a, for a in b, ... for c in iterable] +syntax that Python supports is not yet supported. Nested comprehensions like +arr![arr![f(x) for x in outer] for outer in iterable] work as expected. +
+ +Why this crate? +=============== + +When working with vectors, list comprehensions are most naturally expressed as +`filter/flat_map`s in Rust. Consider the following example, which uses `filter` and +`map` for clarity: + +```rust +use arrcomp::arr; +# fn main() { +let incremented_vec: Vec<_> = (0..10).filter(|x| x % 2 == 1).map(|x| x + 1).collect(); + +// Converting to an array is simple. Note we have to provide the correct array length +let incremented_arr: [i32; 5] = incremented_vec.clone().try_into().unwrap(); +assert_eq!(incremented_arr.to_vec(), incremented_vec); +# } +``` + +Note that, in the example above, we dynamically allocate a vector that gets converted to +an array. In performance-critical contexts this is undesirable. Fortunately, there is +another way: + +```rust +# use arrcomp::arr; +# fn main() { +let mut incremented_iter = (0..10) + .into_iter() + .map(|x| if x % 2 == 1 { Some(x+1) } else { None }); + +let iter_copy = incremented_iter.clone(); + +// std::array::from_fn lets us generate the array without collecting into a vector +let arr_without_allocation: [Option; 10] = std::array::from_fn( + |_| incremented_iter.next().unwrap() +); + +assert_eq!(arr_without_allocation.to_vec(), iter_copy.collect::>()); +# } +``` + +The default Rust syntax is a little clunky, and the inputs to `std::array::from_fn` are +limited. Furthermore, an additional variable must be created outside the function call, +as cloning the iterator inside `from_fn` resets the iterator to the beginning. + +An array comprehension provides an attractive alternative to this pattern, with a +simplified syntax that allows for arbitrary expressions for our input iterable. + +```rust +# use arrcomp::arr; +# fn main() { +let incremented_vec = (0..10).map(|x| if x % 2 == 1 { Some(x+1) } else { None }); + +let arr_comprehension = arr![x+1, for x in 0..10, if x % 2 == 1; len 10]; + +assert_eq!(arr_comprehension.to_vec(), incremented_vec.collect::>()); +# } +``` + +*/ + +/** +The `arr!` macro has two branches - one for filtered comprehensions and one for +unfiltered. This distinction allows unfiltered comprehensions to return arrays of a base +type rather `Option`, simplifying cases where all values would be `Some` + +`arr![$ex, for $x in $input, if $cond0, if $cond1; len $len]` +**/ +#[macro_export] +macro_rules! arr { + ($ex:stmt, for $x:pat in $input:expr $(, if $cond:expr)+; len $len:expr) => {{ + let mut iter = $input.into_iter(); + + if $input.len() != $len { + let msg = &format!("Expected {} elements, got {}.", $len, $input.len()); + panic!("{}", msg); + } + std::array::from_fn::<_, $len, _>(|_| { + let $x = iter.next().unwrap_or_default(); + (true $(&& $cond)*).then(|| {$ex}) + }) + }}; + + + ($ex:stmt, for $x:pat in $input:expr; len $len:expr) => {{ + let mut iter = $input.into_iter(); + + if $input.len() != $len { + let msg = &format!("Expected {} elements, got {}.", $len, $input.len()); + panic!("{}", msg); + } + + std::array::from_fn::<_, $len, _>(|_| { + let $x = iter.next().unwrap_or_default(); + $ex + }) + }}; + +} + +#[cfg(test)] +mod tests { + /* + The tests here aim to cover a wide range of possible syntax statements that + users may wish to include in their list comprehensions. Our comprehension syntax + is tested against map/filter statements as one would normally use, with the + additional understanding that such patterns do not translate easily to arrays. + */ + use super::*; + use rstest::fixture; + use rstest::rstest; + + #[fixture] + fn nums() -> [i32; 5] { + [0, 99, -2, 5, 9] + } + + #[fixture] + fn nums_plus_one() -> [i32; 5] { + [1, 100, -1, 6, 10] + } + + #[fixture] + fn pairs() -> [(i32, f64); 5] { + [(0, 0.0), (99, 9900.0), (-2, 2.0), (5, 30.0), (9, 90.0)] + } + + #[rstest] + fn test_nums_identity(nums: [i32; 5]) { + assert_eq!(arr![x, for x in nums; len 5], nums); + } + + #[rstest] + fn test_nums_statement(nums: [i32; 5]) { + assert_eq!(arr![{let _ = x + 1;}, for x in nums; len 5], [(); 5]); + } + + #[rstest] + fn test_nums_incremented(nums: [i32; 5], nums_plus_one: [i32; 5]) { + assert_eq!(arr![x + 1, for x in nums; len 5], nums_plus_one); + } + + #[rstest] + fn test_nums_with_fn(nums: [i32; 5]) { + assert_eq!( + vec![arr![x.abs(), for x in nums; len 5]], + vec![nums.map(|x| x.abs())] + ); + } + + #[rstest] + fn test_nums_constant_value(nums: [i32; 5]) { + assert_eq!(arr![12.3, for _ in nums; len 5], [12.3; 5]); + } + + #[rstest] + fn test_conditional_expressions(nums: [i32; 5]) { + assert_eq!( + vec![arr![if x > 0 { 1 } else { 0 }, for x in nums; len 5]], + vec![nums.map(|x| if x > 0 { 1 } else { 0 })] + ); + } + + #[rstest] + fn test_pairs_first_element(pairs: [(i32, f64); 5]) { + assert_eq!(arr![x, for (x, _) in pairs; len 5], [0, 99, -2, 5, 9]); + } + + #[rstest] + fn test_pairs_nested(pairs: [(i32, f64); 5]) { + assert_eq!( + vec![arr![arr![y, for (_, _, y) in [(1, 33, x)]; len 1], for (x, _) in pairs; len 5]], + vec![pairs.map(|p| [p.0])] + ); + } + + #[rstest] + fn test_pairs_second_element_zeroed(pairs: [(i32, f64); 5]) { + assert_eq!(arr![y * 0.0, for (_, y) in pairs; len 5], [0.0; 5]); + } + + #[rstest] + fn test_pairs_constant_tuple(pairs: [(i32, f64); 5]) { + assert_eq!(arr![(), for _ in pairs; len 5], [(); 5]); + } + + #[rstest] + fn test_pairs_swapped_elements(pairs: [(i32, f64); 5]) { + assert_eq!( + arr![(y, x), for (x, y) in pairs; len 5], + arr![(pair.1, pair.0), for pair in pairs; len 5] + ); + } + + #[rstest] + fn test_pairs_swapped_and_scaled(pairs: [(i32, f64); 5]) { + assert_eq!( + vec![arr![(y * 2.0, x + 10), for (x, y) in pairs; len 5]], + vec![pairs.map(|(x, y)| (y * 2.0, x + 10))] + ); + } + + #[rstest] + fn test_pairs_to_arr(pairs: [(i32, f64); 5]) { + let other_variable = 43; + assert_eq!( + vec![arr![[x - other_variable, y as i32], for (x, y) in pairs; len 5]], + vec![pairs.map(|(x, y)| [x - other_variable, y as i32])] + ); + } + + #[rstest] + fn test_pairs_zipped_product(nums_plus_one: [i32; 5], pairs: [(i32, f64); 5]) { + assert_eq!( + arr![(x * z) as f64, for ((x, _), z) in pairs.into_iter().zip(nums_plus_one); len 5], + arr![y, for (_, y) in pairs; len 5] + ); + } + + #[rstest] + fn test_nums_identity_with_cond(nums: [i32; 5]) { + assert_eq!( + arr![x, for x in nums, if x % 2 == 0; len 5], + nums.map(|x| if x % 2 == 0 { Some(x) } else { None }) + ); + } + + #[rstest] + fn test_nums_statement_with_cond(nums: [i32; 5]) { + assert_eq!( + arr![{let _ = x + 1;}, for x in nums, if x > 0; len 5], + nums.map(|x| if x > 0 { Some(()) } else { None }) + ); + } + + #[rstest] + fn test_nums_incremented_with_cond(nums: [i32; 5]) { + assert_eq!( + arr![x + 1, for x in nums, if x >= 0; len 5], + nums.map(|x| if x >= 0 { Some(x + 1) } else { None }) + ); + } + + #[rstest] + fn test_nums_with_fn_with_cond(nums: [i32; 5]) { + assert_eq!( + vec![arr![x.abs(), for x in nums, if x != 0; len 5]], + vec![nums.map(|x| if x != 0 { Some(x.abs()) } else { None })] + ); + } + + #[rstest] + #[case::is_odd(|x: i32| x % 2 == 1)] + #[case::greater_than_5(|x: i32| x > 5)] + #[case::is_negative(|x: i32| x < 0)] + #[case::is_zero(|x: i32| x == 0)] + fn test_nums_constant_value_with_cond(nums: [i32; 5], #[case] cond: fn(i32) -> bool) { + assert_eq!( + vec![arr![12.3, for x in nums, if cond(x); len 5]], + vec![nums.map(|x| if cond(x) { Some(12.3) } else { None })] + ); + } + + #[rstest] + #[case::is_odd(|x: i32| x % 2 == 1)] + #[case::greater_than_5(|x: i32| x > 5)] + #[case::is_negative(|x: i32| x < 0)] + #[case::is_zero(|x: i32| x == 0)] + fn test_pairs_first_element_with_cond(pairs: [(i32, f64); 5], #[case] cond: fn(i32) -> bool) { + assert_eq!( + arr![x, for (x, _) in pairs, if cond(x); len 5], + pairs.map(|(x, _)| if cond(x) { Some(x) } else { None }) + ); + } + + #[rstest] + fn test_pairs_nested_with_cond(pairs: [(i32, f64); 5]) { + assert_eq!( + vec![arr![arr![y, for (_, _, y) in [(1, 33, x)]; len 1], + for (x, _) in pairs, if x % 2 == 0; len 5]], + vec![pairs.map(|p| if p.0 % 2 == 0 { Some([p.0]) } else { None })] + ); + } + + #[rstest] + fn test_pairs_nested_with_nested_cond(pairs: [(i32, f64); 5]) { + assert_eq!( + vec![ + arr![arr![y, for (_, _, y) in [(1, 33, x)], if y > 0; len 1], + for (x, _) in pairs, if x % 2 == 0; len 5] + ], + vec![pairs.map(|p| if p.0 % 2 == 0 { + Some([if p.0 > 0 { Some(p.0) } else { None }]) + } else { + None + })] + ); + } + + #[rstest] + fn test_pairs_second_element_zeroed_with_cond(pairs: [(i32, f64); 5]) { + assert_eq!( + arr![y * 0.0, for (_, y) in pairs, if true; len 5], + [Some(0.0); 5] + ); + } + + #[rstest] + fn test_pairs_constant_tuple_with_cond(pairs: [(i32, f64); 5]) { + assert_eq!(arr![(), for _ in pairs, if true; len 5], [Some(()); 5]); + } + + #[rstest] + fn test_pairs_swapped_elements_with_cond(pairs: [(i32, f64); 5]) { + assert_eq!( + arr![(y, x), for (x, y) in pairs, if x > 0; len 5], + arr![(pair.1, pair.0), for pair in pairs, if pair.0 > 0; len 5] + ); + } + + #[rstest] + fn test_pairs_swapped_and_scaled_with_cond(pairs: [(i32, f64); 5]) { + assert_eq!( + vec![arr![(y * 2.0, x + 10), for (x, y) in pairs, if x as f64 + y > 10.0; len 5]], + vec![pairs.map(|(x, y)| if x as f64 + y > 10.0 { + Some((y * 2.0, x + 10)) + } else { + None + })] + ); + } + + #[rstest] + fn test_pairs_to_arr_with_cond(pairs: [(i32, f64); 5]) { + let other_variable = 43; + assert_eq!( + vec![arr![ + [x - other_variable, y as i32], + for (x, y) in pairs, + if x > y as i32; len 5 + ]], + vec![pairs.map(|(x, y)| if x > y as i32 { + Some([x - other_variable, y as i32]) + } else { + None + })] + ); + } + + #[rstest] + fn test_pairs_zipped_product_with_cond(nums_plus_one: [i32; 5], pairs: [(i32, f64); 5]) { + assert_eq!( + arr![ + (x * z) as f64, + for ((x, _), z) in pairs.into_iter().zip(nums_plus_one), + if x > z; len 5 + ], + arr![y, for (x, y) in pairs, if x as f64 > y + 1.0; len 5] + ); + } +}