1
1
Fork 0

feat: solve high-score-board

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2024-12-06 14:28:36 +01:00
parent 0a4ebc2eb6
commit e82ff87a9f
Signed by: cafkafk
GPG key ID: 26C542FD97F965CE
9 changed files with 1317 additions and 0 deletions

View file

@ -0,0 +1,20 @@
{
"authors": [
"glennj"
],
"files": {
"solution": [
"high-score-board.jq"
],
"test": [
"test-high-score-board.bats"
],
"exemplar": [
".meta/exemplar.jq"
]
},
"forked_from": [
"javascript/high-score-board"
],
"blurb": "Practice jq objects by tracking high scores of an arcade game."
}

View file

@ -0,0 +1 @@
{"track":"jq","exercise":"high-score-board","id":"7bfefebfdd8f43bfa13d4d30ba9c5f24","url":"https://exercism.org/tracks/jq/exercises/high-score-board","handle":"cafkafk","is_requester":true,"auto_approve":false}

114
jq/high-score-board/HELP.md Normal file
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 high-score-board.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,43 @@
# Hints
## 1. Create a new high score board
- Create a new object using curly braces.
- Write the key as a string so the key can contain spaces.
- Separate key and value using a colon.
## 2. Add players to a score board
- Use bracket notation to add a key with a name that is stored in a variable (the argument).
- Use the assignment operator (`=`) to set a value for the new key.
- Alternately, use the [`+` operator][man-plus] to merge the incoming object with a new one.
- Remember that parentheses are needed around expressions for object keys.
## 3. Remove players from a score board
- Use the [del expression][man-del].
- Reference the key like you have done in the task before (bracket notation).
- Remember that the argument to `del` is a _index expression_ not just a string.
## 4. Increase a player's score
- First think about how to express the new value that you want to assign.
- Then use the assignment operator like in task 2 to set that new value.
- If you have not done so already, you can make use of the [arithmetic update-assignment operator][man-update-assignment] `+=`.
## 5. Apply Monday bonus points
- This would be a good place to use `to_entries`/`from_entries`, or `with_entries` to iterate over the object
- For each key, set the new value as you did in task 4.
## 6. Find the total score
- We want to iterate over the _values_ of the object.
We can use [`map_values`][man-map_values], or [`.[]`][man-brackets] to output a stream of values.
- The `add` expression can be used to find the sum of a list of numbers.
[man-plus]: https://jqlang.github.io/jq/manual/v1.7/#addition
[man-del]: https://jqlang.github.io/jq/manual/v1.7/#del
[man-update-assignment]: https://jqlang.github.io/jq/manual/v1.7/#arithmetic-update-assignment
[man-map_values]: https://jqlang.github.io/jq/manual/v1.7/#map-map_values
[man-brackets]: https://jqlang.github.io/jq/manual/v1.7/#array-object-value-iterator

View file

@ -0,0 +1,320 @@
# High Score Board
Welcome to High Score Board 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
## Objects
A JSON **object** is, to use terminology from other languages, a "hash", "map", or "dictionary".
JSON defines an _object_ as:
> An object is an unordered set of **name**/**value** pairs.
> An object begins with `{` left brace and ends with `}` right brace.
> Each _name_ is followed by `:` colon and the _name/value_ pairs are separated by `,` comma.
The _name_ **must be a string**.
Another word for _name_ is _key_.
The _value_ can be of any JSON type.
Different _values_ in the same object can be of different types, like this example.
<!-- prettier-ignore -->
```json
{
"name": "Jane",
"age": 42,
"pets": ["cat", "fish"],
"address": {"street": "123 Main St", "city": "Springfield"}
}
```
<!-- prettier-ignore-end -->
Note that there **must not** be a comma following the _last_ key-value pair.
### Creating objects
Use braces to collect the name/value pairs.
Even though the names must be strings, they do not need to be quoted if the names are **identifier-like** (composed of alphanumeric characters and underscore, and not started with a digit).
```jq
{name: "Jane", age: 42}
```
It is valid to use keys that are not _identifier-like_.
Just quote them.
```jq
{"first name": "Jane", "last name": "Lopez", age: 42}
```
If the name is the result of an expression, the expression **must** be in parentheses.
```sh
$ echo "Jane" | jq -Rc '{.: 42}'
# verbose error message ...
$ echo "Jane" | jq -Rc '{(.): 42}'
{"Jane":42}
```
### Indexing
Values are retrieved from an object with **dot notation**.
```jq
{name: "Jane", age: 42} | .age # => 42
```
If you cannot refer to the key as an identifier, use **bracket notation**.
```jq
"name" as $key | {name: "Jane", age: 42} | .$key # => error
"name" as $key | {name: "Jane", age: 42} | .[$key] # => "Jane"
```
### Adding or changing key-value pairs
To add a new key-value pair to an array, or to update the value of an existing key, use the `=` assignment operator, with an index expression on the left-hand side.
```jq
{name: "Jane", age: 42} | .sport = "tennis" | .age = 21
# => {
# "name": "Jane",
# "age": 21,
# "sport": "tennis"
# }
```
The `+` operator will _merge_ objects.
```jq
{Richard: 54} + {Jane: 42}
# => {
# "Richard": 54,
# "Jane": 42
# }
```
### Removing a key
Use the `del` function to remove a key.
It returns the updated object.
```jq
{name: "Jane", age: 42} | del(.age) # => {"name": "Jane"}
```
The parameter to `del` is an **index expression** (using dot- or bracket-notation) that resolves to a key in the object.
`jq` calls it a **path expression**.
It is not sufficient to just give a string.
```jq
{name: "Jane", age: 42} | del(name) # error: name/0 is not defined
{name: "Jane", age: 42} | del("name") # error: Invalid path expression with result "name"
{name: "Jane", age: 42} | del(.name) # OK
{name: "Jane", age: 42} | del(.["name"]) # OK
```
### Membership
- To test if the object has a key, use the `has` function.
```jq
{name: "Jane", age: 42} as $example
|
($example | has("name")), # => true
($example | has("sport")) # => false
```
- Test if a key is in an object with `in`.
```jq
{name: "Jane", age: 42} as $example
|
("name" | in($example)), # => true
("sport" | in($example)) # => false
```
### List all the keys
Use the `keys` function to output a list of all the keys.
```jq
{name: "Jane", age: 42} | keys # => ["age", "name"]
```
Note that `keys` will _sort_ the keys.
To retrieve the keys in the original order, use `keys_unsorted`.
There is no equivalent function to list all the _values_.
However the `.[]` filter outputs the object values as a _stream_, and that stream can be captured with the `[...]` array constructor.
```jq
[{first: "Jane", last: "Lopez", status: "awesome!"} | .[]]
# => ["Jane", "Lopez", "awesome!"]
```
### Iterating
- The `map_values(filter)` function applies the filter to each _value_ in the object.
```jq
{first: "Jane", last: "Lopez", status: "awesome!"}
| map_values(ascii_upcase)
# => {"first": "JANE", "last": "LOPEZ", "status": "AWESOME!"}
```
- To iterate over an object, we must first convert it to an array of key-value objects.
The `to_entries` function does that.
```jq
{name: "Jane", age: 42} | to_entries'
# => [
# {
# "key": "name",
# "value": "Jane"
# },
# {
# "key": "age",
# "value": 42
# }
# ]
```
At this point, we can use array iteration functions, like `map`.
- The `from_entries` function is the inverse: convert an array of key-value objects into an object.
```jq
[
{"key":"name", "value":"Jane"},
{"key":"age", "value":42}
] | from_entries # =>{"name": "Jane", "age": 42}
```
- To apply a filter to _each_ key-value pair in an object, use the `with_entries(filter)` function.
For example, given an object that maps a name to an age, the keys and values can be swapped as follows.
```jq
{"Jane": 42, "Richard": 54}
| with_entries({key: (.value | tostring), value: .key})
```
outputs
```json
{
"42": "Jane",
"54": "Richard"
}
```
`with_entries(filter)` is the same as
```jq
to_entries | map(filter) | from_entries
```
## Instructions
In this exercise, you are implementing a way to keep track of the high scores for the most popular game in your local arcade hall.
You have 6 functions to implement, mostly related to manipulating an object that holds high scores.
## 1. Create a new high score board
Write a function `create_score_board` which creates an object that serves as a high score board.
The keys of this object will be the names of the players, the values will be their scores.
For testing purposes, you want to directly include one entry in the object.
This initial entry should consist of `"The Best Ever"` as player name and `1000000` as score.
```jq
create_score_board
# returns an object with one initial entry
```
## 2. Add players to a score board
To add a player to the high score board, implement the function `add_player`.
It takes a score board as input, and needs two parameters: the player name and the player's score.
The function outputs the score board object with the new player added.
```jq
{"José Valim", 486373}
| add_player("Dave Thomas"; 0)
# => {"Dave Thomas": 0, "José Valim": 486373} -- in some order
```
## 3. Remove players from a score board
If players violate the rules of the arcade hall, they are manually removed from the high score board.
Implement `remove_player` which takes a board as input and one parameter, the name of the player to remove.
This function should remove the entry for the given player from the board and output the new board.
If the player was not on the board in the first place, nothing should happen to the board; it should be returned as is.
```q
{"Dave Thomas": 0} | remove_player("Dave Thomas")
# => {}
{"Dave Thomas": 0} | remove_player("Rose Fanaras")
# => {"Dave Thomas": 0}
```
## 4. Increase a player's score
If a player finishes another game at the arcade hall, a certain amount of points will be added to the previous score on the board.
Implement `update_score`, which takes a score board as input, and needs two parameters: the player name and the amount of score to add.
The function should return the score board after the update was done.
```jq
{"Freyja Ćirić": 12771000} | update_score("Freyja Ćirić"; 73)
# => {"Freyja Ćirić": 12771073}
```
## 5. Apply Monday bonus points
The arcade hall keeps a separate score board on Mondays.
At the end of the day, each player on that board gets 100 additional points.
Implement the function `apply_monday_bonus`.
The function adds the bonus points for each player that is listed on that board.
```jq
{
"Dave Thomas": 44,
"Freyja Ćirić": 539,
"José Valim": 265
}
| apply_monday_bonus
# => {"Dave Thomas": 144, "Freyja Ćirić": 639, "José Valim": 365}
```
## 6. Find the total score
Different arcade halls compete with each other to determine who has the best players.
The arcade with the highest total score wins the honor.
Write a function `total_score`.
It takes a score board as input, and outputs the sum of all the players' scores.
```jq
{
"Dave Thomas": 44,
"Freyja Ćirić": 539,
"José Valim": 265
}
| total_score
# => 848
```
## 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,11 @@
def create_score_board: {"The Best Ever": 1000000};
def add_player(player; score): . + {"\(player)": score};
def remove_player(player): .|del(.["\(player)"]);
def update_score(player; points): .["\(player)"] = (.["\(player)"] + points);
def apply_monday_bonus: .|map_values(. + 100);
def total_score: if . == {} then 0 else [.[]]|add end;

View file

@ -0,0 +1,142 @@
#!/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 creates_a_new_board_with_a_test_entry {
## task 1
run jq -n -c '
include "high-score-board";
create_score_board
'
assert_success
assert_output '{"The Best Ever":1000000}'
}
@test adds_a_player_and_score_to_the_board {
## task 2
run jq '
include "high-score-board";
add_player("Jesse Johnson"; 1337)
' << END_INPUT
{
"Amil Pastorius": 99373,
"Min-seo Shin": 0
}
END_INPUT
assert_success
assert_key_value "Amil Pastorius" 99373 "$output"
assert_key_value "Min-seo Shin" 0 "$output"
assert_key_value "Jesse Johnson" 1337 "$output"
}
@test removes_a_player_from_the_score_board {
## task 3
run jq -c '
include "high-score-board";
remove_player("Jesse Johnson")
' << END_INPUT
{
"Amil Pastorius": 99373,
"Min-seo Shin": 0,
"Jesse Johnson": 1337
}
END_INPUT
assert_success
assert_output '{"Amil Pastorius":99373,"Min-seo Shin":0}'
}
@test does_nothing_if_the_player_is_not_on_the_board {
## task 3
run jq -c '
include "high-score-board";
remove_player("Bruno Santangelo")
' << END_INPUT
{
"Amil Pastorius": 99373,
"Min-seo Shin": 0,
"Jesse Johnson": 1337
}
END_INPUT
assert_success
assert_output '{"Amil Pastorius":99373,"Min-seo Shin":0,"Jesse Johnson":1337}'
}
@test increases_a_players_score {
## task 4
run jq '
include "high-score-board";
.
| update_score("Min-seo Shin"; 1999)
| update_score("Jesse Johnson"; 1337)
' << END_INPUT
{
"Amil Pastorius": 99373,
"Min-seo Shin": 0,
"Jesse Johnson": 1337
}
END_INPUT
assert_success
assert_key_value "Amil Pastorius" 99373 "$output"
assert_key_value "Min-seo Shin" 1999 "$output"
assert_key_value "Jesse Johnson" 2674 "$output"
}
@test adds_100_points_for_all_players {
## task 5
run jq '
include "high-score-board";
apply_monday_bonus
' << END_INPUT
{
"Amil Pastorius": 345,
"Min-seo Shin": 19,
"Jesse Johnson": 122
}
END_INPUT
assert_success
assert_key_value "Amil Pastorius" 445 "$output"
assert_key_value "Min-seo Shin" 119 "$output"
assert_key_value "Jesse Johnson" 222 "$output"
}
@test does_nothing_if_the_score_board_is_empty {
## task 5
run jq -c '
include "high-score-board";
apply_monday_bonus
' <<< '{}'
assert_success
assert_output '{}'
}
@test total_score {
## task 6
run jq -c '
include "high-score-board";
total_score
' << END_INPUT
{
"Amil Pastorius": 345,
"Min-seo Shin": 19,
"Jesse Johnson": 122
}
END_INPUT
assert_success
assert_output 486
}
@test total_score_empty_board {
## task 6
run jq -c '
include "high-score-board";
total_score
' <<< '{}'
assert_success
assert_output 0
}