1
1
Fork 0

feat(jq): recursive functions

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2024-12-09 19:34:58 +01:00
parent 215e3d084b
commit a7087bbb67
Signed by: cafkafk
GPG key ID: 26C542FD97F965CE
9 changed files with 1090 additions and 0 deletions

View file

@ -0,0 +1,17 @@
{
"authors": [
"glennj"
],
"files": {
"solution": [
"recursive-functions.jq"
],
"test": [
"test-recursive-functions.bats"
],
"exemplar": [
".meta/exemplar.jq"
]
},
"blurb": "Demonstrate recursive functions in jq"
}

View file

@ -0,0 +1 @@
{"track":"jq","exercise":"recursive-functions","id":"60bbca372124453881c36ee439c17dc4","url":"https://exercism.org/tracks/jq/exercises/recursive-functions","handle":"cafkafk","is_requester":true,"auto_approve":false}

View file

@ -0,0 +1,114 @@
# Help
## Running the tests
Each exercise contains a test file.
Run the tests using the `bats` program.
```bash
bats test-hello-world.bats
```
`bats` will need to be installed.
See the [Testing on the Bash track][bash] page for instructions to install `bats` for your system.
### bats is implemented in bash
The bats file is a bash script, with some special functions recognized by the `bats` command.
You'll see some tests that look like
```sh
jq -f some-exercise.jq <<< "{some,json,here}"
```
That `<<<` syntax is a bash [Here String][here-string].
It sends the string on the right-hand side into the standard input of the program on the left-hand side.
It is ([approximately][so]) the same as
```sh
echo "{some,json,here}" | jq -f some-exercise.jq
```
## Help for assert functions
The tests use functions from the [bats-assert][bats-assert] library.
Help for the various `assert*` functions can be found there.
## Skipped tests
Solving an exercise means making all its tests pass.
By default, only one test (the first one) is executed when you run the tests.
This is intentional, as it allows you to focus on just making that one test pass.
Once it passes, you can enable the next test by commenting out or removing the
[[ $BATS_RUN_SKIPPED == true ]] || skip
annotations prepending other tests.
## Overriding skips
To run all tests, including the ones with `skip` annotations, you can run:
```bash
BATS_RUN_SKIPPED=true bats test-some-exercise.bats
```
It can be convenient to use a wrapper function to save on typing: in `bash` you can do:
```bash
bats() {
BATS_RUN_SKIPPED=true command bats *.bats
}
```
Then run tests with just:
```bash
bats
```
## Debugging in `jq`
`jq` comes with a handy [`debug`][debug] filter.
Use it while you are developing your exercise solutions to inspect the data that is currently in the jq pipline.
See the [debugging doc][debugging] for more details.
[bash]: https://exercism.org/docs/tracks/bash/tests
[bats-assert]: https://github.com/bats-core/bats-assert
[here-string]: https://www.gnu.org/software/bash/manual/bash.html#Here-Strings
[so]: https://unix.stackexchange.com/a/80372/4667
[debug]: https://jqlang.github.io/jq/manual/v1.7/#debug
[debugging]: https://exercism.org/docs/tracks/jq/debugging
## Submitting your solution
You can submit your solution using the `exercism submit recursive-functions.jq` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [jq track's documentation](https://exercism.org/docs/tracks/jq)
- The [jq track's programming category on the forum](https://forum.exercism.org/c/programming/jq)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
## Need more help?
- Go to the [Exercism Community forum](https://forum.exercism.org) to get support and ask questions (or just chat!)
- Use the [Exercism Support](https://forum.exercism.org/c/support/8) category if you face any issues with working in the web editor, or downloading or submitting your exercises locally.
- Use the [Programming:jq](https://forum.exercism.org/c/programming/jq/133) category for jq-specific topics.
- Join the community on [Exercism's Discord server](https://exercism.org/r/discord).
- [StackOverflow](https://stackoverflow.com/questions/tagged/jq) can be used to search for your problem and see if it has been answered already.
You can also ask and answer questions.
- [Github issue tracker](https://github.com/exercism/jq/issues) is where we track our development and maintainance of `jq` exercises in exercism.
If none of the above links help you, feel free to post an issue here.

View file

@ -0,0 +1,30 @@
# Hints
## General
- Try to split a problem into a base case and a recursive case
For example, let's say you want to count how many cookies are there in the cookie jar with a recursive approach
A base case is an empty jar - it has zero cookies.
If the jar is not empty, then the number of cookies in the jar is equal to one cookie plus the number of cookies in the jar after removing one cookie.
## 1. Implement a function to add the numbers in an array
- The recursion is to iterate over elements in the array, adding each to an accumulator.
- The base case is an empty array, which has sum equal to zero.
- The recursive case adds the first element to the sum of the rest of the array.
- Recall array indexing and slicing from [the Arrays concept][arrays-concept] to separate the first array element from the rest of the elements.
## 2. Reverse an array
- The recursion is to iterate over elements in the array, adding each to another array in reverse order.
- The base case is an empty array.
- Remember that the [`+` operator][manual-addition] concatenates when applied to arrays.
## 3. Map an array
- The recursion is to iterate over elements in the array, adding to an accumulator array the result of applying the filter to the element.
- To apply the filter parameter to an element, use the usual pipe syntax: `.element | filter`.
- The base case is an empty array.
[arrays-concept]: https://exercism.org/tracks/jq/concepts/arrays
[manual-addition]: https://jqlang.github.io/jq/manual/v1.7/#addition

View file

@ -0,0 +1,113 @@
# Recursive Functions
Welcome to Recursive Functions on Exercism's jq Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Recursion
**Recursive functions** are functions that call themselves.
A _recursive function_ needs to have at least one _base case_ and at least one _recursive case_.
A **base case** returns a value without calling the function again.
A **recursive case** calls the function again, modifying the input so that it will at some point match the base case.
Here is an example that counts the elements of an array.
```jq
def count:
if length == 0 then
0 # base case
else
1 + (.[1:] | count) # recursive case
end;
([] | count), # => 0
([11, 22, 33] | count) # => 3
```
A _recursive function_ can have many _base cases_ and/or many _recursive cases_.
For example [the Fibonacci sequence][wiki-fibonacci] is a recursive sequence with two _base cases_.
```jq
def fibonacci:
if . == 0 then
0
elif . == 1 then
1
else
(. - 1 | fibonacci) + (. - 2 | fibonacci)
end;
10 | fibonacci # => 55
```
Counting the number of occurrences of some given value `x` in a list has two _recursive cases_.
```jq
def count_occurrences(x):
if length == 0 then
0
elif first == x then
1 + (.[1:] | count_occurrences(x))
else
(.[1:] | count_occurrences(x))
end;
[11, 22, 33, 22, 44] | count_occurrences(22) # => 2
```
In practice, iterating over lists and other enumerable data structures is most often done using builtin functions,
such as `map` and `reduce`, or by [using streams][map-implementation] like `[.[] | select(...)]`.
Under the hood, some builtins are [implemented using recursion][walk-implementation].
[map-implementation]: https://github.com/jqlang/jq/blob/jq-1.7/src/builtin.jq#L3
[walk-implementation]: https://github.com/jqlang/jq/blob/jq-1.7/src/builtin.jq#L248
[wiki-fibonacci]: https://en.wikipedia.org/wiki/Fibonacci_number
## Instructions
In this exercise you're going to implement some recursive functions.
## 1. Implement a function to add the numbers in an array
We'll re-implement the builtin `add` filter to practice recursion.
The _base case_ is that an empty array has a sum of **zero**.
Implement it yourself with a recursive function; don't use the builtin `add`.
```jq
[5, 4, 6, 10] | array_add # => 25
```
## 2. Reverse an array
We'll re-implement the builtin `reverse` filter.
The _base case_ is that an empty array reversed is an empty array.
Implement it yourself with a recursive function; don't use the builtin `reverse`.
```sh
[5, 4, 6, 10] | array_reverse # => [10, 6, 4, 5]
```
## 3. Map an array
We'll re-implement the builtin `map` filter.
The function takes a filter as a parameter, run that filter for each element of the input array, and return the outputs in a new array.
The _base case_ is that an empty array maps to an empty array.
Implement it yourself with a recursive function; don't use the builtin `map`.
```sh
[5, 4, 6, 10] | array_map(. + 10) # => [15, 14, 16, 20]
```
## Source
### Created by
- @glennj

View file

@ -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 <zoltan dot tombol at gmail dot com>
#
# 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
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#
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
}

View file

@ -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"
}

View file

@ -0,0 +1,45 @@
# Add the elements of the input array, and return the sum.
#
# Example:
# [1, 2, 3] | array_add # => 6
def array_add:
if .|length == 0 then
0
elif .|length == 1 then
.[0]
else
.[0] + (.[1:]|array_add)
end
;
# Reverse the input array, and return the result in a new array.
#
# Example:
# [1, 2, 3] | array_reverse # => [3, 2, 1]
def array_reverse:
if .|length == 0 then
[]
elif .|length == 1 then
[.[0]]
else
[.[-1]] + (.[:-1]|array_reverse)
end
;
# Run the filter `f` for each element of the input array,
# and return the outputs in a new array.
#
# Example:
# [1, 2, 3] | array_map(. + 1) # => [2, 3, 4]
def array_map(f):
if .|length == 0 then
[]
elif .|length == 1 then
[.[0]|f]
else
[(.[0]|f)] + (.[1:]|array_map(f))
end
;

View file

@ -0,0 +1,104 @@
#!/usr/bin/env bats
load bats-extra
load bats-jq
assert_key_value() {
local result
result=$(echo "$output" | jq -r --arg key "$2" --argjson val "$1" '.[$key] == $val')
[[ $result == "true" ]]
}
@test "Sum of empty array" {
## task 1
run jq -n '
include "recursive-functions";
[] | array_add
'
assert_success
assert_output 0
}
@test "Sum of array of one number" {
## task 1
run jq -n '
include "recursive-functions";
[42] | array_add
'
assert_success
assert_output 42
}
@test "Sum of array of numbers" {
## task 1
run jq -n '
include "recursive-functions";
[1, 2, 3, 4] | array_add
'
assert_success
assert_output 10
}
@test "Reverse an empty array" {
## task 2
run jq -cn '
include "recursive-functions";
[] | array_reverse
'
assert_success
assert_output '[]'
}
@test "Reverse an array of one number" {
## task 2
run jq -cn '
include "recursive-functions";
[42] | array_reverse
'
assert_success
assert_output '[42]'
}
@test "Reverse an array of numbers" {
## task 2
run jq -cn '
include "recursive-functions";
[1, 2, 3, 4] | array_reverse
'
assert_success
assert_output '[4,3,2,1]'
}
@test "Map an empty array" {
## task 3
run jq -cn '
include "recursive-functions";
[] | array_map(. * 2)
'
assert_success
assert_output '[]'
}
@test "Map an array of one number" {
## task 3
run jq -cn '
include "recursive-functions";
[42] | array_map(. * 2)
'
assert_success
assert_output '[84]'
}
@test "Map an array of numbers" {
## task 3
run jq -cn '
include "recursive-functions";
[1, 2, 3, 4] | array_map(. * 2)
'
assert_success
assert_output '[2,4,6,8]'
}
# vim: sw=4 ts=8