diff --git a/jq/high-score-board/.exercism/config.json b/jq/high-score-board/.exercism/config.json new file mode 100644 index 0000000..3837cb8 --- /dev/null +++ b/jq/high-score-board/.exercism/config.json @@ -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." +} diff --git a/jq/high-score-board/.exercism/metadata.json b/jq/high-score-board/.exercism/metadata.json new file mode 100644 index 0000000..431f40f --- /dev/null +++ b/jq/high-score-board/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/jq/high-score-board/HELP.md b/jq/high-score-board/HELP.md new file mode 100644 index 0000000..6462016 --- /dev/null +++ b/jq/high-score-board/HELP.md @@ -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. \ No newline at end of file diff --git a/jq/high-score-board/HINTS.md b/jq/high-score-board/HINTS.md new file mode 100644 index 0000000..e311a2f --- /dev/null +++ b/jq/high-score-board/HINTS.md @@ -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 \ No newline at end of file diff --git a/jq/high-score-board/README.md b/jq/high-score-board/README.md new file mode 100644 index 0000000..ec50094 --- /dev/null +++ b/jq/high-score-board/README.md @@ -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. + + +```json +{ + "name": "Jane", + "age": 42, + "pets": ["cat", "fish"], + "address": {"street": "123 Main St", "city": "Springfield"} +} +``` + + + +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 \ No newline at end of file diff --git a/jq/high-score-board/bats-extra.bash b/jq/high-score-board/bats-extra.bash new file mode 100644 index 0000000..54d4807 --- /dev/null +++ b/jq/high-score-board/bats-extra.bash @@ -0,0 +1,637 @@ +# This is the source code for bats-support and bats-assert, concatenated +# * https://github.com/bats-core/bats-support +# * https://github.com/bats-core/bats-assert +# +# Comments have been removed to save space. See the git repos for full source code. + +############################################################ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +fail() { + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +batslib_is_caller() { + local -i is_mode_direct=1 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -i|--indirect) is_mode_direct=0; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + # Arguments. + local -r func="$1" + + # Check call stack. + if (( is_mode_direct )); then + [[ $func == "${FUNCNAME[2]}" ]] && return 0 + else + local -i depth + for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do + [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 + done + fi + + return 1 +} + +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} + +############################################################ + +assert() { + if ! "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion failed' \ + | fail + fi +} + +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +assert_failure() { + : "${output?}" + : "${status?}" + + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } \ + | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +assert_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( 'regexp' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( 'substring' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( 'line' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'output does not contain line' \ + | fail + fi + fi +} + +assert_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_nonempty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_nonempty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local expected + if (( use_stdin )); then + expected="$(cat -)" + else + expected="${1-}" + fi + + # Matching. + if (( is_mode_nonempty )); then + if [ -z "$output" ]; then + echo 'expected non-empty output, but output was empty' \ + | batslib_decorate 'no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + elif ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi +} + +assert_success() { + : "${output?}" + : "${status?}" + + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } \ + | batslib_decorate 'command failed' \ + | fail + fi +} + +refute() { + if "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion succeeded, but it was expected to fail' \ + | fail + fi +} + +refute_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if [[ ${lines[$idx]} =~ $unexpected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Line contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( 'substring' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( 'line' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + fi +} + +refute_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_empty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_empty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local unexpected + if (( use_stdin )); then + unexpected="$(cat -)" + else + unexpected="${1-}" + fi + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_mode_empty )); then + if [ -n "$output" ]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output non-empty, but expected no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ $output =~ $unexpected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi +} diff --git a/jq/high-score-board/bats-jq.bash b/jq/high-score-board/bats-jq.bash new file mode 100644 index 0000000..3f55da5 --- /dev/null +++ b/jq/high-score-board/bats-jq.bash @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# `bats-core` will consume both stdout and stderr for the `run` command's output. +# However `jq` prints its DEBUG output on stderr. +# +# Lines starting with `["DEBUG:",` will be prefixed with a hash and printed on file descriptor 3. +# Other lines on stderr will remain on stderr for bats to consume. +# +# See `bats-core` docs: +# - "Printing to the terminal", https://bats-core.readthedocs.io/en/stable/writing-tests.html#printing-to-the-terminal +# - "File descriptor 3", https://bats-core.readthedocs.io/en/stable/writing-tests.html#file-descriptor-3-read-this-if-bats-hangs + + +jq() { + local output stderr rc line + stderr=$(mktemp) + output=$(command jq "$@" 2> "$stderr") + rc=$? + while IFS= read -r line || [[ -n $line ]]; do + if [[ $line == '["DEBUG:",'* ]]; then + echo "# $line" >&3 + else + echo "$line" >&2 + fi + done < "$stderr" + rm -f "$stderr" + echo "$output" + return "$rc" +} diff --git a/jq/high-score-board/high-score-board.jq b/jq/high-score-board/high-score-board.jq new file mode 100644 index 0000000..c549c86 --- /dev/null +++ b/jq/high-score-board/high-score-board.jq @@ -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; diff --git a/jq/high-score-board/test-high-score-board.bats b/jq/high-score-board/test-high-score-board.bats new file mode 100644 index 0000000..073e02c --- /dev/null +++ b/jq/high-score-board/test-high-score-board.bats @@ -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 +}