diff --git a/config.json b/config.json index b9bff82..79eb32d 100644 --- a/config.json +++ b/config.json @@ -442,6 +442,14 @@ ], "difficulty": 2 }, + { + "slug": "grade-school", + "name": "Grade School", + "uuid": "40dfdf5a-fee6-424e-984a-998111a11c06", + "practices": [], + "prerequisites": [], + "difficulty": 2 + }, { "slug": "acronym", "name": "Acronym", diff --git a/exercises/practice/grade-school/.docs/instructions.md b/exercises/practice/grade-school/.docs/instructions.md new file mode 100644 index 0000000..9a63e39 --- /dev/null +++ b/exercises/practice/grade-school/.docs/instructions.md @@ -0,0 +1,21 @@ +# Instructions + +Given students' names along with the grade that they are in, create a roster for the school. + +In the end, you should be able to: + +- Add a student's name to the roster for a grade + - "Add Jim to grade 2." + - "OK." +- Get a list of all students enrolled in a grade + - "Which students are in grade 2?" + - "We've only got Jim just now." +- Get a sorted list of all students in all grades. + Grades should sort as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name. + - "Who all is enrolled in school right now?" + - "Let me think. + We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2 and Jim in grade 5. + So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe and Jim" + +Note that all our students only have one name (It's a small town, what do you want?) and each student cannot be added more than once to a grade or the roster. +In fact, when a test attempts to add the same student more than once, your implementation should indicate that this is incorrect. diff --git a/exercises/practice/grade-school/.meta/config.json b/exercises/practice/grade-school/.meta/config.json new file mode 100644 index 0000000..b05f539 --- /dev/null +++ b/exercises/practice/grade-school/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "grade-school.jq" + ], + "test": [ + "test-grade-school.bats" + ], + "example": [ + ".meta/example.jq" + ] + }, + "blurb": "Given students' names along with the grade that they are in, create a roster for the school.", + "source": "A pairing session with Phil Battos at gSchool" +} diff --git a/exercises/practice/grade-school/.meta/example.jq b/exercises/practice/grade-school/.meta/example.jq new file mode 100644 index 0000000..ace305c --- /dev/null +++ b/exercises/practice/grade-school/.meta/example.jq @@ -0,0 +1,21 @@ +def transform: + reduce .input.students[] as $student ({seen: [], school: []}; + $student as [$name, $grade] + | .seen as $seen + | if $name | IN($seen[]) | not then + .seen += [$name] + | .school[$grade] |= (. + [$name] | unique) + end + ) + | .school +; + +def get_grade($wanted): + transform + | .[$wanted] +; + +if .property == "roster" then transform | add +elif .property == "grade" then get_grade(.input.desiredGrade) +end +| . // [] diff --git a/exercises/practice/grade-school/.meta/tests.toml b/exercises/practice/grade-school/.meta/tests.toml new file mode 100644 index 0000000..3456799 --- /dev/null +++ b/exercises/practice/grade-school/.meta/tests.toml @@ -0,0 +1,91 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[a3f0fb58-f240-4723-8ddc-e644666b85cc] +description = "Roster is empty when no student is added" + +[9337267f-7793-4b90-9b4a-8e3978408824] +description = "Add a student" +include = false + +[6d0a30e4-1b4e-472e-8e20-c41702125667] +description = "Student is added to the roster" + +[73c3ca75-0c16-40d7-82f5-ed8fe17a8e4a] +description = "Adding multiple students in the same grade in the roster" +include = false + +[233be705-dd58-4968-889d-fb3c7954c9cc] +description = "Multiple students in the same grade are added to the roster" + +[87c871c1-6bde-4413-9c44-73d59a259d83] +description = "Cannot add student to same grade in the roster more than once" +include = false + +[c125dab7-2a53-492f-a99a-56ad511940d8] +description = "A student can't be in two different grades" +include = false + +[a0c7b9b8-0e89-47f8-8b4a-c50f885e79d1] +description = "A student can only be added to the same grade in the roster once" +reimplements = "c125dab7-2a53-492f-a99a-56ad511940d8" +include = false + +[d7982c4f-1602-49f6-a651-620f2614243a] +description = "Student not added to same grade in the roster more than once" +reimplements = "a0c7b9b8-0e89-47f8-8b4a-c50f885e79d1" + +[e70d5d8f-43a9-41fd-94a4-1ea0fa338056] +description = "Adding students in multiple grades" +include = false + +[75a51579-d1d7-407c-a2f8-2166e984e8ab] +description = "Students in multiple grades are added to the roster" + +[7df542f1-57ce-433c-b249-ff77028ec479] +description = "Cannot add same student to multiple grades in the roster" +include = false + +[6a03b61e-1211-4783-a3cc-fc7f773fba3f] +description = "A student cannot be added to more than one grade in the sorted roster" +reimplements = "c125dab7-2a53-492f-a99a-56ad511940d8" +include = false + +[c7ec1c5e-9ab7-4d3b-be5c-29f2f7a237c5] +description = "Student not added to multiple grades in the roster" +reimplements = "6a03b61e-1211-4783-a3cc-fc7f773fba3f" + +[d9af4f19-1ba1-48e7-94d0-dabda4e5aba6] +description = "Students are sorted by grades in the roster" + +[d9fb5bea-f5aa-4524-9d61-c158d8906807] +description = "Students are sorted by name in the roster" + +[180a8ff9-5b94-43fc-9db1-d46b4a8c93b6] +description = "Students are sorted by grades and then by name in the roster" + +[5e67aa3c-a3c6-4407-a183-d8fe59cd1630] +description = "Grade is empty if no students in the roster" + +[1e0cf06b-26e0-4526-af2d-a2e2df6a51d6] +description = "Grade is empty if no students in that grade" + +[2bfc697c-adf2-4b65-8d0f-c46e085f796e] +description = "Student not added to same grade more than once" + +[66c8e141-68ab-4a04-a15a-c28bc07fe6b9] +description = "Student not added to multiple grades" + +[c9c1fc2f-42e0-4d2c-b361-99271f03eda7] +description = "Student not added to other grade for multiple grades" + +[1bfbcef1-e4a3-49e8-8d22-f6f9f386187e] +description = "Students are sorted by name in a grade" diff --git a/exercises/practice/grade-school/bats-extra.bash b/exercises/practice/grade-school/bats-extra.bash new file mode 100644 index 0000000..54d4807 --- /dev/null +++ b/exercises/practice/grade-school/bats-extra.bash @@ -0,0 +1,637 @@ +# This is the source code for bats-support and bats-assert, concatenated +# * https://github.com/bats-core/bats-support +# * https://github.com/bats-core/bats-assert +# +# Comments have been removed to save space. See the git repos for full source code. + +############################################################ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +fail() { + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +batslib_is_caller() { + local -i is_mode_direct=1 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -i|--indirect) is_mode_direct=0; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + # Arguments. + local -r func="$1" + + # Check call stack. + if (( is_mode_direct )); then + [[ $func == "${FUNCNAME[2]}" ]] && return 0 + else + local -i depth + for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do + [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 + done + fi + + return 1 +} + +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} + +############################################################ + +assert() { + if ! "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion failed' \ + | fail + fi +} + +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +assert_failure() { + : "${output?}" + : "${status?}" + + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } \ + | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +assert_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( 'regexp' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( 'substring' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( 'line' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'output does not contain line' \ + | fail + fi + fi +} + +assert_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_nonempty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_nonempty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local expected + if (( use_stdin )); then + expected="$(cat -)" + else + expected="${1-}" + fi + + # Matching. + if (( is_mode_nonempty )); then + if [ -z "$output" ]; then + echo 'expected non-empty output, but output was empty' \ + | batslib_decorate 'no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + elif ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi +} + +assert_success() { + : "${output?}" + : "${status?}" + + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } \ + | batslib_decorate 'command failed' \ + | fail + fi +} + +refute() { + if "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion succeeded, but it was expected to fail' \ + | fail + fi +} + +refute_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if [[ ${lines[$idx]} =~ $unexpected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Line contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( 'substring' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( 'line' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + fi +} + +refute_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_empty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_empty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local unexpected + if (( use_stdin )); then + unexpected="$(cat -)" + else + unexpected="${1-}" + fi + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_mode_empty )); then + if [ -n "$output" ]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output non-empty, but expected no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ $output =~ $unexpected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi +} diff --git a/exercises/practice/grade-school/bats-jq.bash b/exercises/practice/grade-school/bats-jq.bash new file mode 100644 index 0000000..3f55da5 --- /dev/null +++ b/exercises/practice/grade-school/bats-jq.bash @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# `bats-core` will consume both stdout and stderr for the `run` command's output. +# However `jq` prints its DEBUG output on stderr. +# +# Lines starting with `["DEBUG:",` will be prefixed with a hash and printed on file descriptor 3. +# Other lines on stderr will remain on stderr for bats to consume. +# +# See `bats-core` docs: +# - "Printing to the terminal", https://bats-core.readthedocs.io/en/stable/writing-tests.html#printing-to-the-terminal +# - "File descriptor 3", https://bats-core.readthedocs.io/en/stable/writing-tests.html#file-descriptor-3-read-this-if-bats-hangs + + +jq() { + local output stderr rc line + stderr=$(mktemp) + output=$(command jq "$@" 2> "$stderr") + rc=$? + while IFS= read -r line || [[ -n $line ]]; do + if [[ $line == '["DEBUG:",'* ]]; then + echo "# $line" >&3 + else + echo "$line" >&2 + fi + done < "$stderr" + rm -f "$stderr" + echo "$output" + return "$rc" +} diff --git a/exercises/practice/grade-school/grade-school.jq b/exercises/practice/grade-school/grade-school.jq new file mode 100644 index 0000000..6d0c541 --- /dev/null +++ b/exercises/practice/grade-school/grade-school.jq @@ -0,0 +1 @@ +"Remove this line and implement your solution" | halt_error diff --git a/exercises/practice/grade-school/test-grade-school.bats b/exercises/practice/grade-school/test-grade-school.bats new file mode 100644 index 0000000..89c846a --- /dev/null +++ b/exercises/practice/grade-school/test-grade-school.bats @@ -0,0 +1,462 @@ +#!/usr/bin/env bats +# generated on 2024-07-23T00:57:34Z +load bats-extra +load bats-jq + +@test 'Roster is empty when no student is added' { + #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [] + } + } +END_INPUT + + assert_success + expected='[]' + assert_equal "$output" "$expected" +} + +@test 'Student is added to the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Aimee", + 2 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Aimee"]' + assert_equal "$output" "$expected" +} + +@test 'Multiple students in the same grade are added to the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "Paul", + 2 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Blair","James","Paul"]' + assert_equal "$output" "$expected" +} + +@test 'Student not added to same grade in the roster more than once' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "James", + 2 + ], + [ + "Paul", + 2 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Blair","James","Paul"]' + assert_equal "$output" "$expected" +} + +@test 'Students in multiple grades are added to the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Chelsea", + 3 + ], + [ + "Logan", + 7 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Chelsea","Logan"]' + assert_equal "$output" "$expected" +} + +@test 'Student not added to multiple grades in the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "James", + 3 + ], + [ + "Paul", + 3 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Blair","James","Paul"]' + assert_equal "$output" "$expected" +} + +@test 'Students are sorted by grades in the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Jim", + 3 + ], + [ + "Peter", + 2 + ], + [ + "Anna", + 1 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Anna","Peter","Jim"]' + assert_equal "$output" "$expected" +} + +@test 'Students are sorted by name in the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Peter", + 2 + ], + [ + "Zoe", + 2 + ], + [ + "Alex", + 2 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Alex","Peter","Zoe"]' + assert_equal "$output" "$expected" +} + +@test 'Students are sorted by grades and then by name in the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "roster", + "input": { + "students": [ + [ + "Peter", + 2 + ], + [ + "Anna", + 1 + ], + [ + "Barb", + 1 + ], + [ + "Zoe", + 2 + ], + [ + "Alex", + 2 + ], + [ + "Jim", + 3 + ], + [ + "Charlie", + 1 + ] + ] + } + } +END_INPUT + + assert_success + expected='["Anna","Barb","Charlie","Alex","Peter","Zoe","Jim"]' + assert_equal "$output" "$expected" +} + +@test 'Grade is empty if no students in the roster' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [], + "desiredGrade": 1 + } + } +END_INPUT + + assert_success + expected='[]' + assert_equal "$output" "$expected" +} + +@test 'Grade is empty if no students in that grade' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [ + [ + "Peter", + 2 + ], + [ + "Zoe", + 2 + ], + [ + "Alex", + 2 + ], + [ + "Jim", + 3 + ] + ], + "desiredGrade": 1 + } + } +END_INPUT + + assert_success + expected='[]' + assert_equal "$output" "$expected" +} + +@test 'Student not added to same grade more than once' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "James", + 2 + ], + [ + "Paul", + 2 + ] + ], + "desiredGrade": 2 + } + } +END_INPUT + + assert_success + expected='["Blair","James","Paul"]' + assert_equal "$output" "$expected" +} + +@test 'Student not added to multiple grades' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "James", + 3 + ], + [ + "Paul", + 3 + ] + ], + "desiredGrade": 2 + } + } +END_INPUT + + assert_success + expected='["Blair","James"]' + assert_equal "$output" "$expected" +} + +@test 'Student not added to other grade for multiple grades' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [ + [ + "Blair", + 2 + ], + [ + "James", + 2 + ], + [ + "James", + 3 + ], + [ + "Paul", + 3 + ] + ], + "desiredGrade": 3 + } + } +END_INPUT + + assert_success + expected='["Paul"]' + assert_equal "$output" "$expected" +} + +@test 'Students are sorted by name in a grade' { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + + run jq -c -f grade-school.jq << 'END_INPUT' + { + "property": "grade", + "input": { + "students": [ + [ + "Franklin", + 5 + ], + [ + "Bradley", + 5 + ], + [ + "Jeff", + 1 + ] + ], + "desiredGrade": 5 + } + } +END_INPUT + + assert_success + expected='["Bradley","Franklin"]' + assert_equal "$output" "$expected" +} diff --git a/generate_tests.json b/generate_tests.json index 50c5ae1..fa1deae 100644 --- a/generate_tests.json +++ b/generate_tests.json @@ -25,6 +25,7 @@ "allergies", "atbash-cipher", "difference-of-squares", + "grade-school", "resistor-color", "run-length-encoding", "zebra-puzzle"