From 1d4905b7a81212fdebb3500292a29f22822e4c54 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Mon, 29 Jun 2026 16:05:43 -0700 Subject: [PATCH] [list-ops] Add a Jinja test template --- exercises/practice/list-ops/.meta/template.j2 | 127 +++++++++++++++ exercises/practice/list-ops/.meta/tests.toml | 2 + exercises/practice/list-ops/list_ops.bats | 151 ++++++++---------- 3 files changed, 197 insertions(+), 83 deletions(-) create mode 100644 exercises/practice/list-ops/.meta/template.j2 diff --git a/exercises/practice/list-ops/.meta/template.j2 b/exercises/practice/list-ops/.meta/template.j2 new file mode 100644 index 00000000..cfab38cf --- /dev/null +++ b/exercises/practice/list-ops/.meta/template.j2 @@ -0,0 +1,127 @@ +{{ header }} +# shellcheck disable=SC2329 # "This function is never invoked." + +bash_version=$((10 * BASH_VERSINFO[0] + BASH_VERSINFO[1])) +if (( bash_version < 43 )); then + echo "This exercise requires at least bash version 4.3" >&2 + exit 4 +fi + +## append entries to a list and return the new list + +setup() { source list_ops.sh; } + +{% for idx, case in cases %} +@test "{{ case["descriptions"] | join(": ") }}" { + {% if idx == 0 %}# {% endif %}[[ $BATS_RUN_SKIPPED == "true" ]] || skip +{%- if case["property"] == "append" %} + local list1=({{ case["input"]["list1"] | join(" ") }}) + local list2=({{ case["input"]["list2"] | join(" ") }}) + list::{{ case["property"] }} list1 "${list2[@]}" + assert_equal "${{ "{" }}#list1[@]}" {{ case["expected"] | length }} + assert_equal "${{ "{" }}list1[*]}" "{{ case["expected"] | join(" ") }}" +{%- elif case["property"] == "filter" %} + local list=({{ case["input"]["list"] | join(" ") }}) + local result=() + {%- if case["input"]["function"] == "(x) -> x modulo 2 == 1" %} + isOdd () { (( $1 % 2 == 1 )); } + list::{{ case["property"] }} isOdd list result + {%- else %} + UNKNOWN FILTER + {%- endif %} + assert_equal "${{ "{" }}#result[@]}" {{ case["expected"] | length }} + assert_equal "${{ "{" }}result[*]}" "{{ case["expected"] | join(" ") }}" +{%- elif case["property"] == "length" %} + {#- Skipped. bash array length syntax covers it: ${#ary[@]} #} +{%- elif case["property"] == "map" %} + local list=({{ case["input"]["list"] | join(" ") }}) + local result=() + {%- if case["input"]["function"] == "(x) -> x + 1" %} + incr () { echo $(( $1 + 1 )); } + list::{{ case["property"] }} incr list result + {%- else %} + UNKNOWN FILTER + {%- endif %} + assert_equal "${{ "{" }}#result[@]}" {{ case["expected"] | length }} + assert_equal "${{ "{" }}result[*]}" "{{ case["expected"] | join(" ") }}" +{%- elif case["property"] in ("foldl", "foldr") %} + {%- if case["property"] == "foldl" %} + {%- set param1, param2 = "acc", "elem" %} + {%- else %} + {%- set param1, param2 = "elem", "acc" %} + {%- endif %} + local list=({{ case["input"]["list"] | join(" ") }}) + local result=() + {%- if case["input"]["function"] == "(acc, el) -> el + acc" %} + add () { + local {{ param1 }}=$1 {{ param2 }}=$2 + echo $(( {{ param1 }} + {{ param2 }} )) + } + result=$(list::{{ case["property"] }} add {{ case["input"]["initial"] }} list) + assert_equal "$result" "{{ case["expected"] }}" + {%- elif case["input"]["function"] == "(acc, el) -> el * acc" %} + mult () { + local {{ param1 }}=$1 {{ param2 }}=$2 + echo $(( {{ param1 }} * {{ param2 }} )) + } + result=$(list::{{ case["property"] }} mult {{ case["input"]["initial"] }} list) + assert_equal "$result" "{{ case["expected"] }}" + {%- elif case["input"]["function"] == "(acc, el) -> el / acc" %} + # For this test, we need a div function that performs + # floating point arithmetic to preserve fractions. + div () { + local {{ param1 }}=$1 {{ param2 }}=$2 + echo "$elem / $acc" | bc -l + } + answer=$(list::{{ case["property"] }} div 24 list) + result=$(printf '%.1f' "$answer") + assert_equal "$result" "{{ case["expected"] | float | round(1) }}" + {%- else %} + UNKNOWN FUNCTION {{ case["input"]["function"] }} + {%- endif %} +{%- elif case["property"] == "reverse" %} + local list=({{ case["input"]["list"] | join(" ") }}) + local result=() + list::{{ case["property"] }} list result + assert_equal "${{ "{" }}#result[@]}" {{ case["expected"] | length }} + assert_equal "${{ "{" }}result[*]}" "{{ case["expected"] | join(" ") }}" +{%- else %} + UNKNOWN PROPERTY {{ case["property"] }} +{%- endif %} +} +{% endfor %} + +# track-specific test + +@test "foldl not just numbers" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + local list=(H e l l o " " W o r l d "!") + local result + concat () { + local acc=$1 elem=$2 + echo "${acc}${elem}" + } + result=$(list::foldl concat "" list) + assert_equal "$result" 'Hello World!' +} + +@test "foldr not just numbers" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + local list=(H e l l o " " W o r l d "!") + local result + concat () { + local elem=$1 acc=$2 + echo "${acc}${elem}" + } + result=$(list::foldr concat "" list) + assert_equal "$result" '!dlroW olleH' +} + +@test "reverse with special characters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + local list=("R*" "l*") + local result=() + list::reverse list result + assert_equal "${{ "{" }}#result[@]}" 2 + assert_equal "${result[*]}" "l* R*" +} diff --git a/exercises/practice/list-ops/.meta/tests.toml b/exercises/practice/list-ops/.meta/tests.toml index 9f917a42..aeda0607 100644 --- a/exercises/practice/list-ops/.meta/tests.toml +++ b/exercises/practice/list-ops/.meta/tests.toml @@ -101,3 +101,5 @@ description = "non-empty list" [40872990-b5b8-4cb8-9085-d91fc0d05d26] description = "list of lists is not flattened" +include = false +comment = "bash does not support nested lists" diff --git a/exercises/practice/list-ops/list_ops.bats b/exercises/practice/list-ops/list_ops.bats index 485e0b8a..212019ea 100644 --- a/exercises/practice/list-ops/list_ops.bats +++ b/exercises/practice/list-ops/list_ops.bats @@ -1,9 +1,8 @@ #!/usr/bin/env bats -# shellcheck disable=SC2329 # "This function is never invoked." - load bats-extra -# local version: 2.4.0.0 +# generated on 2026-06-30T00:18:40+00:00 +# shellcheck disable=SC2329 # "This function is never invoked." bash_version=$((10 * BASH_VERSINFO[0] + BASH_VERSINFO[1])) if (( bash_version < 43 )); then @@ -15,8 +14,9 @@ fi setup() { source list_ops.sh; } -@test "append empty lists" { - #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + +@test "append entries to a list and return the new list: empty lists" { + # [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list1=() local list2=() list::append list1 "${list2[@]}" @@ -24,7 +24,7 @@ setup() { source list_ops.sh; } assert_equal "${list1[*]}" "" } -@test "append list to empty list" { +@test "append entries to a list and return the new list: list to empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list1=() local list2=(1 2 3 4) @@ -33,7 +33,7 @@ setup() { source list_ops.sh; } assert_equal "${list1[*]}" "1 2 3 4" } -@test "append empty list to list" { +@test "append entries to a list and return the new list: empty list to list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list1=(1 2 3 4) local list2=() @@ -42,21 +42,16 @@ setup() { source list_ops.sh; } assert_equal "${list1[*]}" "1 2 3 4" } -@test "append non-empty lists" { +@test "append entries to a list and return the new list: non-empty lists" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip - local l1=(1 2) - local l2=(2 3 4 5) - list::append l1 "${l2[@]}" - assert_equal "${#l1[@]}" 6 - assert_equal "${l1[*]}" "1 2 2 3 4 5" + local list1=(1 2) + local list2=(2 3 4 5) + list::append list1 "${list2[@]}" + assert_equal "${#list1[@]}" 6 + assert_equal "${list1[*]}" "1 2 2 3 4 5" } -## concatenate a list of lists -# N/A: bash arrays are strictly one-dimensional - -## filter list returning only values that satisfy the filter function - -@test "filter empty list" { +@test "filter list returning only values that satisfy the filter function: empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=() local result=() @@ -66,9 +61,9 @@ setup() { source list_ops.sh; } assert_equal "${result[*]}" "" } -@test "filter non-empty list" { +@test "filter list returning only values that satisfy the filter function: non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip - local list=(1 2 3 4 5) + local list=(1 2 3 5) local result=() isOdd () { (( $1 % 2 == 1 )); } list::filter isOdd list result @@ -76,62 +71,54 @@ setup() { source list_ops.sh; } assert_equal "${result[*]}" "1 3 5" } -## returns the length of a list -# N/A: bash array length syntax covers it: ${#ary[@]} - -## map: a list of elements whose values equal the list value -## transformed by the mapping function - -@test "map empty list" { +@test "return a list of elements whose values equal the list value transformed by the mapping function: empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=() local result=() incr () { echo $(( $1 + 1 )); } list::map incr list result - assert_equal "${#result[@]}" ${#list[@]} + assert_equal "${#result[@]}" 0 assert_equal "${result[*]}" "" } -@test "map non-empty list" { +@test "return a list of elements whose values equal the list value transformed by the mapping function: non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 3 5 7) local result=() incr () { echo $(( $1 + 1 )); } list::map incr list result - assert_equal "${#result[@]}" ${#list[@]} + assert_equal "${#result[@]}" 4 assert_equal "${result[*]}" "2 4 6 8" } -## folds (reduces) the given list from the left with a function - -@test "foldl empty list" { +@test "folds (reduces) the given list from the left with a function: empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=() - local result + local result=() mult () { local acc=$1 elem=$2 - echo $(( elem * acc )) + echo $(( acc * elem )) } result=$(list::foldl mult 2 list) assert_equal "$result" "2" } -@test "foldl direction independent function applied to non-empty list" { +@test "folds (reduces) the given list from the left with a function: direction independent function applied to non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 2 3 4) - local result + local result=() add () { local acc=$1 elem=$2 - echo $(( elem + acc )) + echo $(( acc + elem )) } result=$(list::foldl add 5 list) assert_equal "$result" "15" } -@test "foldl direction dependent function applied to non-empty list" { +@test "folds (reduces) the given list from the left with a function: direction dependent function applied to non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 2 3 4) - local result answer + local result=() # For this test, we need a div function that performs # floating point arithmetic to preserve fractions. div () { @@ -143,26 +130,10 @@ setup() { source list_ops.sh; } assert_equal "$result" "64.0" } -# track-specific test -@test "foldl not just numbers" { - [[ $BATS_RUN_SKIPPED == "true" ]] || skip - local list=(H e l l o " " W o r l d "!") - local result - concat () { - local acc=$1 elem=$2 - echo "${acc}${elem}" - } - result=$(list::foldl concat "" list) - assert_equal "$result" 'Hello World!' -} - -## folds (reduces) the given list from the right with a function -# Note the order of the arguments to the given functions! - -@test "foldr empty list" { +@test "folds (reduces) the given list from the right with a function: empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=() - local result + local result=() mult () { local elem=$1 acc=$2 echo $(( elem * acc )) @@ -171,10 +142,10 @@ setup() { source list_ops.sh; } assert_equal "$result" "2" } -@test "foldr direction independent function applied to non-empty list" { +@test "folds (reduces) the given list from the right with a function: direction independent function applied to non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 2 3 4) - local result + local result=() add () { local elem=$1 acc=$2 echo $(( elem + acc )) @@ -183,10 +154,12 @@ setup() { source list_ops.sh; } assert_equal "$result" "15" } -@test "foldr direction dependent function applied to non-empty list" { +@test "folds (reduces) the given list from the right with a function: direction dependent function applied to non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 2 3 4) - local answer result + local result=() + # For this test, we need a div function that performs + # floating point arithmetic to preserve fractions. div () { local elem=$1 acc=$2 echo "$elem / $acc" | bc -l @@ -196,44 +169,56 @@ setup() { source list_ops.sh; } assert_equal "$result" "9.0" } -# track-specific test -@test "foldr not just numbers" { - [[ $BATS_RUN_SKIPPED == "true" ]] || skip - local list=(H e l l o " " W o r l d "!") - local result - concat () { - local elem=$1 acc=$2 - echo "${acc}${elem}" - } - result=$(list::foldr concat "" list) - assert_equal "$result" '!dlroW olleH' -} - -## reverse the elements of the list - -@test "reverse empty list" { +@test "reverse the elements of the list: empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=() local result=() list::reverse list result - assert_equal "${#result[@]}" ${#list[@]} + assert_equal "${#result[@]}" 0 assert_equal "${result[*]}" "" } -@test "reverse non-empty list" { +@test "reverse the elements of the list: non-empty list" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=(1 3 5 7) local result=() list::reverse list result - assert_equal "${#result[@]}" ${#list[@]} + assert_equal "${#result[@]}" 4 assert_equal "${result[*]}" "7 5 3 1" } + +# track-specific test + +@test "foldl not just numbers" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + local list=(H e l l o " " W o r l d "!") + local result + concat () { + local acc=$1 elem=$2 + echo "${acc}${elem}" + } + result=$(list::foldl concat "" list) + assert_equal "$result" 'Hello World!' +} + +@test "foldr not just numbers" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + local list=(H e l l o " " W o r l d "!") + local result + concat () { + local elem=$1 acc=$2 + echo "${acc}${elem}" + } + result=$(list::foldr concat "" list) + assert_equal "$result" '!dlroW olleH' +} + @test "reverse with special characters" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip local list=("R*" "l*") local result=() list::reverse list result - assert_equal "${#result[@]}" ${#list[@]} + assert_equal "${#result[@]}" 2 assert_equal "${result[*]}" "l* R*" }