feat: grade-stat
This commit is contained in:
parent
6b317e935d
commit
e20eb8b4c4
10 changed files with 1105 additions and 0 deletions
20
jq/grade-stats/.exercism/config.json
Normal file
20
jq/grade-stats/.exercism/config.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"authors": [
|
||||
"glennj"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"grade-stats.jq"
|
||||
],
|
||||
"test": [
|
||||
"test-grade-stats.bats"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.jq"
|
||||
],
|
||||
"editor": [
|
||||
"grades.json"
|
||||
]
|
||||
},
|
||||
"blurb": "Demonstrate the reduce filter in jq"
|
||||
}
|
1
jq/grade-stats/.exercism/metadata.json
Normal file
1
jq/grade-stats/.exercism/metadata.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"track":"jq","exercise":"grade-stats","id":"8a64b3831a124a5ba7c5f7fffc62783d","url":"https://exercism.org/tracks/jq/exercises/grade-stats","handle":"cafkafk","is_requester":true,"auto_approve":false}
|
114
jq/grade-stats/HELP.md
Normal file
114
jq/grade-stats/HELP.md
Normal 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 grade-stats.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.
|
16
jq/grade-stats/HINTS.md
Normal file
16
jq/grade-stats/HINTS.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Hints
|
||||
|
||||
## 1. Translate a numeric grade to a letter grade
|
||||
|
||||
- Use an [`if-then-else` expression][jq-man-if] with as many `elif` branches as needed.
|
||||
|
||||
## 2. Count the number of students for each letter grade
|
||||
|
||||
- The `reduce` filter needs a _stream_ to iterate over.
|
||||
The input is an object.
|
||||
Use the `to_entries` expression to transform the object into an array.
|
||||
- The result object must have all the possible letter grades as keys, even if no students earned that grade.
|
||||
- The [`+=` assignment][jq-man-arith-assign] will be useful.
|
||||
|
||||
[jq-man-if]: https://jqlang.github.io/jq/manual/v1.7/#if-then-else-end
|
||||
[jq-man-arith-assign]: https://jqlang.github.io/jq/manual/v1.7/#arithmetic-update-assignment
|
153
jq/grade-stats/README.md
Normal file
153
jq/grade-stats/README.md
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Grade Stats
|
||||
|
||||
Welcome to Grade Stats 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
|
||||
|
||||
## Reduce
|
||||
|
||||
Suppose we want to sum an array of numbers.
|
||||
There are many ways to accomplish this goal.
|
||||
|
||||
- In many languages, this problem is expressed as a loop with an accumulator variable.
|
||||
- This problem can be written as a recursive function.
|
||||
In pseudo-code, we might have this.
|
||||
|
||||
```none
|
||||
function Add(X, Sum=0):
|
||||
if X is empty then
|
||||
return Sum
|
||||
else
|
||||
return Add(rest(X), Sum + first(X))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This method of dividing the problem into smaller pieces can also be described as "reducing towards the base case."
|
||||
|
||||
**Reduce** is a way to combine all the elements of a data structure into a single value.
|
||||
The process iterates over the data structure, applying a function to each element to update the accumulated result.
|
||||
|
||||
In `jq`, this process is implemented in the [`reduce` filter][jq-man-reduce].
|
||||
In other languages, it might be called "fold", "fold-left", "inject", or "aggregate".
|
||||
|
||||
The `jq` `reduce` expression looks like this.
|
||||
|
||||
```jq
|
||||
reduce STREAM_EXPRESSION as $var (INITIAL_VALUE; UPDATE_EXPRESSION)
|
||||
```
|
||||
|
||||
- `STREAM_EXPRESSION` is a _stream_ of items, each stored in the `$var` variable in turn.
|
||||
- Recall, to stream an array, use the [iterator filter `.[]`][jq-man-iterator]: `$myArray | .[]`.
|
||||
- `INITIAL_VALUE` is the starting value of the accumulated result (known as the "accumulator").
|
||||
- The `UPDATE_EXPRESSION` combines ("folds") the current value ($var) into the accumulator.
|
||||
- In the context of this expression, `.` is the value of the accumulator.
|
||||
- The output of the expression is stored into the accumulator for use in the next iteration.
|
||||
- After the last iteration, the accumulated result is the output of `reduce`.
|
||||
|
||||
Let's look at an example: adding up the numbers in an array.
|
||||
The `add` filter does just this, but we'll see how to implement it.
|
||||
|
||||
If we use `[10, 20, 30, 40]` as the input, and taking zero as the initial state, this is what each step looks like.
|
||||
|
||||
| \# | state | element | reducer | result |
|
||||
| :-: | :---: | :-----: | :-----: | :----: |
|
||||
| 1 | 0 | 10 | 0 + 10 | 10 |
|
||||
| 2 | 10 | 20 | 10 + 20 | 30 |
|
||||
| 3 | 30 | 30 | 30 + 30 | 60 |
|
||||
| 4 | 60 | 40 | 60 + 40 | 100 |
|
||||
|
||||
In `jq` syntax, this looks like this code.
|
||||
|
||||
```jq
|
||||
0 + 10 | . + 20 | . + 30 | . + 40
|
||||
```
|
||||
|
||||
We can express that with the `reduce` filter.
|
||||
|
||||
```jq
|
||||
[10, 20, 30, 40] | reduce .[] as $n (0; . + $n) # => 100
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
~~~~exercism/note
|
||||
The `add` builtin is actually [implemented with `reduce`][jq-code-add], but uses "null" as the initial state (any data type can be added to null).
|
||||
|
||||
```jq
|
||||
def add: reduce .[] as $x (null; . + $x);
|
||||
```
|
||||
|
||||
[jq-code-add]: https://github.com/jqlang/jq/blob/jq-1.7/src/builtin.jq#L11
|
||||
~~~~
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### Some things to keep in mind
|
||||
|
||||
- In the reducing expression, `.` is the accumulator.
|
||||
If the input is some object that you need to reference inside the reducing function, you need to store it in a variable.
|
||||
|
||||
```jq
|
||||
{"apple": 10, "banana": 16, "carrot": 4}
|
||||
| . as $obj
|
||||
| reduce (keys | .[]) as $key (0; . + $obj[$key]) # => 30
|
||||
```
|
||||
|
||||
- The accumulator can be of any type of data.
|
||||
For example you may want to reverse an array.
|
||||
|
||||
```jq
|
||||
["A", "B", "C", "D"]
|
||||
| reduce .[] as $elem ([]; [$elem] + .) # => ["D", "C", "B", "A"]
|
||||
```
|
||||
|
||||
[jq-man-reduce]: https://jqlang.github.io/jq/manual/v1.7/#reduce
|
||||
[jq-man-iterator]: https://jqlang.github.io/jq/manual/v1.7/#array-object-value-iterator
|
||||
|
||||
## Instructions
|
||||
|
||||
You are a teacher.
|
||||
At the end of the year, you have generated a numeric grade for each of your students.
|
||||
Now you need to translate that to a letter grade and count how many students have achieved each letter grade
|
||||
|
||||
## 1. Translate a numeric grade to a letter grade
|
||||
|
||||
The `letter_grade` function will take a numeric grade as input, and it will output the letter.
|
||||
Use these ranges:
|
||||
|
||||
| Letter | Grade |
|
||||
| :----: | ---------: |
|
||||
| A | 90% - 100% |
|
||||
| B | 80% - 89% |
|
||||
| C | 70% - 79% |
|
||||
| D | 60% - 69% |
|
||||
| F | 0% - 59% |
|
||||
|
||||
Example:
|
||||
|
||||
```jq
|
||||
75 | letter_grade # => "C"
|
||||
```
|
||||
|
||||
## 2. Count the number of students for each letter grade
|
||||
|
||||
The function `count_letter_grades` will take an object mapping student names to their grades.
|
||||
The output will be an object mapping each letter grade to the number of students with that grade.
|
||||
|
||||
Example:
|
||||
|
||||
```jq
|
||||
{"Joe": 78, "Jane": 93, "Richard": 72} | count_letter_grades
|
||||
# => {"A": 1, "B": 0, "C": 2, "D": 0, "F": 0}
|
||||
```
|
||||
|
||||
There are a few different ways to solve this.
|
||||
Use the `reduce` filter for practice.
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @glennj
|
637
jq/grade-stats/bats-extra.bash
Normal file
637
jq/grade-stats/bats-extra.bash
Normal 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
|
||||
}
|
29
jq/grade-stats/bats-jq.bash
Normal file
29
jq/grade-stats/bats-jq.bash
Normal 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"
|
||||
}
|
25
jq/grade-stats/grade-stats.jq
Normal file
25
jq/grade-stats/grade-stats.jq
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Given a numeric score between 0 and 100, output a letter grade
|
||||
# - "A" is 90% - 100%
|
||||
# - "B" is 80% - 89%
|
||||
# - "C" is 70% - 79%
|
||||
# - "D" is 60% - 69%
|
||||
# - "F" is 0% - 59%
|
||||
|
||||
def letter_grade:
|
||||
if . < 60 then "F"
|
||||
elif . < 70 then "D"
|
||||
elif . < 80 then "C"
|
||||
elif . < 90 then "B"
|
||||
else "A"
|
||||
end;
|
||||
|
||||
|
||||
# Given an object that maps a student's name to their grade,
|
||||
# generate an object that maps the letter grade to the number of
|
||||
# students with that grade
|
||||
|
||||
def count_letter_grades:
|
||||
reduce .[] as $s (
|
||||
{A: 0, B: 0, C: 0, D: 0, F: 0};
|
||||
.[$s | letter_grade] += 1
|
||||
);
|
22
jq/grade-stats/grades.json
Normal file
22
jq/grade-stats/grades.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"Alexandra": 99,
|
||||
"Beatrice": 74,
|
||||
"Bethany": 100,
|
||||
"Brian": 97,
|
||||
"Clinton": 54,
|
||||
"Emerson": 86,
|
||||
"Evan": 72,
|
||||
"Grady": 70,
|
||||
"Jane": 52,
|
||||
"Joanna": 76,
|
||||
"Kim": 75,
|
||||
"Lorraine": 48,
|
||||
"Marcy": 96,
|
||||
"Raphael": 67,
|
||||
"Raymond": 61,
|
||||
"Reynaldo": 98,
|
||||
"Shelia": 78,
|
||||
"Staci": 59,
|
||||
"Sybil": 57,
|
||||
"Taylor": 92
|
||||
}
|
88
jq/grade-stats/test-grade-stats.bats
Normal file
88
jq/grade-stats/test-grade-stats.bats
Normal file
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env bats
|
||||
load bats-extra
|
||||
load bats-jq
|
||||
|
||||
assert_key_value() {
|
||||
local key=$1 expected=$2 actual
|
||||
actual=$(jq -rc --arg key "$key" '.[$key]' <<< "$output")
|
||||
assert_equal "$actual" "$expected"
|
||||
}
|
||||
|
||||
@test 'number to letter grade A' {
|
||||
## task 1
|
||||
run jq -nc '
|
||||
include "grade-stats";
|
||||
[ range(90; 101) | letter_grade ]
|
||||
'
|
||||
assert_success
|
||||
assert_output '["A","A","A","A","A","A","A","A","A","A","A"]'
|
||||
}
|
||||
|
||||
@test 'number to letter grade B' {
|
||||
## task 1
|
||||
run jq -nc '
|
||||
include "grade-stats";
|
||||
[ range(80; 90) | letter_grade ]
|
||||
'
|
||||
assert_success
|
||||
assert_output '["B","B","B","B","B","B","B","B","B","B"]'
|
||||
}
|
||||
|
||||
@test 'number to letter grade C' {
|
||||
## task 1
|
||||
run jq -nc '
|
||||
include "grade-stats";
|
||||
[ range(70; 80) | letter_grade ]
|
||||
'
|
||||
assert_success
|
||||
assert_output '["C","C","C","C","C","C","C","C","C","C"]'
|
||||
}
|
||||
|
||||
@test 'number to letter grade D' {
|
||||
## task 1
|
||||
run jq -nc '
|
||||
include "grade-stats";
|
||||
[ range(60; 70) | letter_grade ]
|
||||
'
|
||||
assert_success
|
||||
assert_output '["D","D","D","D","D","D","D","D","D","D"]'
|
||||
}
|
||||
|
||||
@test 'number to letter grade F' {
|
||||
## task 1
|
||||
run jq -nc '
|
||||
include "grade-stats";
|
||||
[ range(0; 60) | letter_grade ]
|
||||
| (length == 60 and all(. == "F"))
|
||||
'
|
||||
assert_success
|
||||
assert_output 'true'
|
||||
}
|
||||
|
||||
@test 'aggregate the number of students by letter grade' {
|
||||
## task 2
|
||||
run jq -c '
|
||||
include "grade-stats";
|
||||
count_letter_grades
|
||||
' grades.json
|
||||
assert_success
|
||||
assert_key_value "A" 6 "$output"
|
||||
assert_key_value "B" 1 "$output"
|
||||
assert_key_value "C" 6 "$output"
|
||||
assert_key_value "D" 2 "$output"
|
||||
assert_key_value "F" 5 "$output"
|
||||
}
|
||||
|
||||
@test 'aggregate, all letter grades are present even if zero students' {
|
||||
## task 2
|
||||
run jq -c '
|
||||
include "grade-stats";
|
||||
count_letter_grades
|
||||
' <<< '{"alpha": 95, "bravo": 88}'
|
||||
assert_success
|
||||
assert_key_value "A" 1 "$output"
|
||||
assert_key_value "B" 1 "$output"
|
||||
assert_key_value "C" 0 "$output"
|
||||
assert_key_value "D" 0 "$output"
|
||||
assert_key_value "F" 0 "$output"
|
||||
}
|
Loading…
Reference in a new issue