From 425aeafedfc00f739fafda250a0f21c6d8e8ec69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Mon, 9 Dec 2024 18:37:41 +0100 Subject: [PATCH] feat(jq): vehicle purchase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- jq/vehicle-purchase/.exercism/config.json | 20 + jq/vehicle-purchase/.exercism/metadata.json | 1 + jq/vehicle-purchase/HELP.md | 114 ++++ jq/vehicle-purchase/HINTS.md | 26 + jq/vehicle-purchase/README.md | 238 +++++++ jq/vehicle-purchase/bats-extra.bash | 637 ++++++++++++++++++ jq/vehicle-purchase/bats-jq.bash | 29 + .../test-vehicle-purchase.bats | 108 +++ jq/vehicle-purchase/vehicle-purchase.jq | 27 + 9 files changed, 1200 insertions(+) create mode 100644 jq/vehicle-purchase/.exercism/config.json create mode 100644 jq/vehicle-purchase/.exercism/metadata.json create mode 100644 jq/vehicle-purchase/HELP.md create mode 100644 jq/vehicle-purchase/HINTS.md create mode 100644 jq/vehicle-purchase/README.md create mode 100644 jq/vehicle-purchase/bats-extra.bash create mode 100644 jq/vehicle-purchase/bats-jq.bash create mode 100644 jq/vehicle-purchase/test-vehicle-purchase.bats create mode 100644 jq/vehicle-purchase/vehicle-purchase.jq diff --git a/jq/vehicle-purchase/.exercism/config.json b/jq/vehicle-purchase/.exercism/config.json new file mode 100644 index 0000000..f105538 --- /dev/null +++ b/jq/vehicle-purchase/.exercism/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "vehicle-purchase.jq" + ], + "test": [ + "test-vehicle-purchase.bats" + ], + "exemplar": [ + ".meta/exemplar.jq" + ] + }, + "forked_from": [ + "javascript/vehicle-purchase" + ], + "blurb": "Learn about comparison and conditionals while preparing for your next vehicle purchase" +} diff --git a/jq/vehicle-purchase/.exercism/metadata.json b/jq/vehicle-purchase/.exercism/metadata.json new file mode 100644 index 0000000..2b71b9e --- /dev/null +++ b/jq/vehicle-purchase/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"jq","exercise":"vehicle-purchase","id":"d51f6b81e3894e8285afd1217f4afd39","url":"https://exercism.org/tracks/jq/exercises/vehicle-purchase","handle":"cafkafk","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/jq/vehicle-purchase/HELP.md b/jq/vehicle-purchase/HELP.md new file mode 100644 index 0000000..d9135b5 --- /dev/null +++ b/jq/vehicle-purchase/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 vehicle-purchase.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/vehicle-purchase/HINTS.md b/jq/vehicle-purchase/HINTS.md new file mode 100644 index 0000000..fde389b --- /dev/null +++ b/jq/vehicle-purchase/HINTS.md @@ -0,0 +1,26 @@ +# Hints + +## 1. Determine if you will need a drivers license + +- Use the [equality operator] to check whether your input equals a certain string. +- Use one of the two [boolean operators] to combine the two requirements. +- You do not need an if-statement to solve this task. You can return the boolean expression you build directly. + +## 2. Choose between two potential vehicles to buy + +- You will need the `if-then-else` [conditional expression] for this task. +- Use a [comparison operator] to determine which option comes first in dictionary order. +- Finally, construct the recommendation sentence. + You can use the concatenation or string interpolation that you learned in the Strings concept to construct the recommendation sentence. + +## 3. Calculate an estimation for the price of a used vehicle + +- Start with determining the percentage based on the age of the vehicle. + Use an `if-elsif-else` expression. +- To calculate the result, apply the percentage to the original price. + For example, `30% of x` can be calculated by multiplying `x` by `30` and dividing by `100`. + +[equality operator]: https://jqlang.github.io/jq/manual/v1.7/#==-!= +[boolean operators]: https://jqlang.github.io/jq/manual/v1.7/#and-or-not +[comparison operator]: https://jqlang.github.io/jq/manual/v1.7/#%3E-%3E=-%3C=-%3C +[conditional expression]: https://jqlang.github.io/jq/manual/v1.7/#if-then-else-end \ No newline at end of file diff --git a/jq/vehicle-purchase/README.md b/jq/vehicle-purchase/README.md new file mode 100644 index 0000000..7774440 --- /dev/null +++ b/jq/vehicle-purchase/README.md @@ -0,0 +1,238 @@ +# Vehicle Purchase + +Welcome to Vehicle Purchase 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 + +## Compare + +### Comparing Numbers + +In `jq` numbers can be compared using the following relational and equality operators. + +| Comparison | Operator | +| ---------------------- | -------- | +| Greater than | `a > b` | +| Greater than or equals | `a >= b` | +| Less than | `a < b` | +| Less than or equals | `a <= b` | +| Equals | `a == b` | +| Not equals | `a != b` | + +The result of the comparison is always a boolean value, so either `true` or `false`. + +```jq +1 < 3, # => true +2 != 2, # => false +1 == 1.0 # => true + # All numbers are floating-points, so this is different syntax + # for the exact same value. +``` + +### Comparing Strings + +The comparison operators above can also be used to compare strings. +In that case, a dictionary (lexicographical) order is applied. +The ordering is _by unicode codepoint value_. + +```jq +"Apple" > "Pear", # => false +"a" < "above", # => true +"a" == "A" # => false +``` + +You need to be careful when you compare two variables that appear to contain numeric values but are of type string. +Due to the dictionary order, the result will not be the same as comparing values of type number. + +```jq +10 < 2, # => false +"10" < "2" # => true (because "1" comes before "2") +``` + +### "Strict" Equality + +The `jq` `==` operator is like Javascript's `===` in the sense that things that "look" the same, but are of different types, are not equal. + +```jq +"3" == 3 # => false + # the value on the left has type string, + # the value on the right has type number. +``` + +### Comparing Arrays + +Two arrays are equal if all the corresponding elements are equal. + +```jq +[1, 2, 3] == [1, 2, 3] # => true +[1, 2, 3] == [1, 3, 2] # => false, different order +[1, 2, 3] == [1, 2, "3"] # => false, different types +``` + +### Comparing Objects + +Two objects are equal if they have the same key-value pairs. + +```jq +{name: "Joe", age: 42} == {age: 42, name: "Joe"} # => true +{name: "Joe", age: 42} == {age: 42, name: "Jane"} # => false +{name: "Joe", age: 42} == {age: "42", name: "Joe"} # => false +{name: "Joe", age: 42} == {age: 42, name: "Joe", height: 175} # => false + +# comparisons will drill down as deeply as required +{a: {b: {c: [1, 2]}}} == {a: {b: {c: [1, 2]}}} # => true +{a: {b: {c: [1, 2]}}} == {a: {b: {c: [1, 2, 3]}}} # => false +``` + +## Conditionals + +### If Expression + +`jq`'s **conditional expression** is `if A then B else C end`. + +`if-then-else` is a filter like all `jq` builtins: it takes an input and produces an output. + +If the expression `A` produces a "truthy" value, then the `if` filter evaluates `B`. +Otherwise it evaluates `C`. +The input to the `if` filter will be passed to `B` or `C`. + +```jq +42 | if . < 50 then "small" else "big" end # => "small" +``` + +```jq +5 | if . % 2 == 0 then . / 2 else . * 4 end # => 20 +``` + +~~~~exercism/note +The `else` clause is **optional** in the current `jq` release (version 1.7): +the following two statements are equivalent. + +```jq +if A then B else . end +if A then B end +``` + +The `else` clause is **mandatory** in the previous v1.6 release. +~~~~ + +### Nested If-Statements + +Further conditions can be added with `elif`. + +```jq +42 | if . < 33 then "small" + elif . < 66 then "medium" + else "big" + end +# => "medium" +``` + +Use as many `elif` clauses as you need. + +### Truthiness + +The only "false" values in `jq` are: `false` and `null`. +Everything else is "true", even the number zero and the empty string, array and object. + +### Boolean Operators + +The **boolean operators** `and` and `or` can be used to build complex queries. + +```jq +42 | if . < 33 or . > 66 then "big or small" + else "medium" + end +``` + +To negate, use `not`. This is a **filter** not an operator. + +```jq +42 | if (. < 33 or . > 66 | not) then "medium" + else "big or small" + end +``` + +### Alternative Operator + +The **alternative operator** allows you to specify a "default" value if an expression is false or null. + +```jq +A // B + +# This is identical to +if A then A else B end +``` + +To demonstrate + +```jq +[3, 5, 18] | add / 2 # => 13 +[] | add / 2 # => error: null (null) and number (2) cannot be divided +[] | add // 0 / 2 # => 0 +``` + +## Instructions + +In this exercise, you will write some code to help you prepare to buy a vehicle. + +You have three tasks: determine if you will need to get a license; choose between two vehicles; and estimate the acceptable price for a used vehicle. + +## 1. Determine if you will need a drivers license + +Some kinds of vehicles require a drivers license to operate them. +Assume only the kinds `"car"` and `"truck"` require a license; everything else can be operated without a license. + +Implement the `needs_license` function that takes the kind of vehicle and returns a boolean indicating whether you need a license for that kind of vehicle. + +```jq +"car" | needs_license +# => true + +"bike" | needs_license +# => false +``` + +## 2. Choose between two potential vehicles to buy + +You have evaluated your options of available vehicles. +You managed to narrow it down to two options but you need help making the final decision. +Implement the function `choose_vehicle` that takes an array of two vehicles as input and returns a decision, which is the option that comes first in dictionary order. + +```jq +["Wuling Hongguang", "Toyota Corolla"] | choose_vehicle +# => "Toyota Corolla is clearly the better choice." + +["Volkswagen Beetle", "Volkswagen Golf"] | choose_vehicle +# => "Volkswagen Beetle is clearly the better choice." +``` + +## 3. Calculate an estimation for the price of a used vehicle + +Now that you have made your decision, you want to make sure you get a fair price at the dealership. +Since you are interested in buying a used vehicle, the price depends on how old the vehicle is. +For a rough estimate, assume if the vehicle is less than 3 years old, it costs 80% of the original price it had when it was brand new. +If it is more than 10 years old, it costs 50%. +If the vehicle is at least 3 years old but not older than 10 years, it costs 70% of the original price. + +Implement the `resell_price` function that applies this logic using `if`, `elif` and `else`. +It takes an object holding the original price and the age of the vehicle and returns the estimated price in the dealership. + +```jq +{"original_price": 1000, "age": 1} | resell_price +# => 800 + +{"original_price": 1000, "age": 5} | resell_price +# => 700 + +{"original_price": 1000, "age": 15} | resell_price +# => 500 +``` + +## Source + +### Created by + +- @glennj \ No newline at end of file diff --git a/jq/vehicle-purchase/bats-extra.bash b/jq/vehicle-purchase/bats-extra.bash new file mode 100644 index 0000000..54d4807 --- /dev/null +++ b/jq/vehicle-purchase/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/vehicle-purchase/bats-jq.bash b/jq/vehicle-purchase/bats-jq.bash new file mode 100644 index 0000000..3f55da5 --- /dev/null +++ b/jq/vehicle-purchase/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/vehicle-purchase/test-vehicle-purchase.bats b/jq/vehicle-purchase/test-vehicle-purchase.bats new file mode 100644 index 0000000..162d7bc --- /dev/null +++ b/jq/vehicle-purchase/test-vehicle-purchase.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats +load bats-extra +load bats-jq + +@test requires_a_license_for_a_car { + ## task 1 + run jq -R 'include "vehicle-purchase"; needs_license' <<< 'car' + assert_success + assert_output "true" +} + +@test requires_a_license_for_a_truck { + ## task 1 + run jq -R 'include "vehicle-purchase"; needs_license' <<< 'truck' + assert_success + assert_output "true" +} + +@test does_not_require_a_license_for_a_bike { + ## task 1 + run jq -R 'include "vehicle-purchase"; needs_license' <<< 'bike' + assert_success + assert_output "false" +} + +@test does_not_require_a_license_for_a_stroller { + ## task 1 + run jq -R 'include "vehicle-purchase"; needs_license' <<< 'stroller' + assert_success + assert_output "false" +} + +@test does_not_require_a_license_for_an_e-scooter { + ## task 1 + run jq -R 'include "vehicle-purchase"; needs_license' <<< 'e-scooter' + assert_success + assert_output "false" +} + + +@test correctly_recommends_the_first_option { + ## task 2 + run jq -r 'include "vehicle-purchase"; choose_vehicle' << END_INPUT + ["Bugatti Veyron", "Ford Pinto"] + ["Chery EQ", "Kia Niro Elektro"] +END_INPUT + assert_success + assert_line --index 0 'Bugatti Veyron is clearly the better choice.' + assert_line --index 1 'Chery EQ is clearly the better choice.' +} + +@test correctly_recommends_the_second_option { + ## task 2 + run jq -r 'include "vehicle-purchase"; choose_vehicle' << END_INPUT + ["Ford Pinto", "Bugatti Veyron"] + ["2020 Gazelle Medeo", "2018 Bergamont City"] +END_INPUT + assert_success + assert_line --index 0 'Bugatti Veyron is clearly the better choice.' + assert_line --index 1 '2018 Bergamont City is clearly the better choice.' +} + +@test price_is_reduced_to_80%_for_age_below_3 { + ## task 3 + run jq 'include "vehicle-purchase"; resell_price' << END_INPUT + {"original_price": 40000, "age": 2} + {"original_price": 40000, "age": 2.5} +END_INPUT + assert_success + assert_line --index 0 '32000' + assert_line --index 1 '32000' +} + +@test price_is_reduced_to_50%_for_age_above_10 { + ## task 3 + run jq 'include "vehicle-purchase"; resell_price' << END_INPUT + {"original_price": 40000, "age": 12} +END_INPUT + assert_success + assert_output '20000' +} + +@test price_is_reduced_to_70%_for_between_3_and_10 { + ## task 3 + run jq 'include "vehicle-purchase"; resell_price' << END_INPUT + {"original_price": 25000, "age": 7} +END_INPUT + assert_success + assert_output '17500' +} + +@test works_correctly_for_threshold_age_3 { + ## task 3 + run jq 'include "vehicle-purchase"; resell_price' << END_INPUT + {"original_price": 40000, "age": 3} +END_INPUT + assert_success + assert_output '28000' +} + +@test works_correctly_for_threshold_age_10 { + ## task 3 + run jq 'include "vehicle-purchase"; resell_price' << END_INPUT + {"original_price": 25000, "age": 10} +END_INPUT + assert_success + assert_output '17500' +} diff --git a/jq/vehicle-purchase/vehicle-purchase.jq b/jq/vehicle-purchase/vehicle-purchase.jq new file mode 100644 index 0000000..258cc99 --- /dev/null +++ b/jq/vehicle-purchase/vehicle-purchase.jq @@ -0,0 +1,27 @@ +# Task 1 +# Determines whether or not you need a license to operate a certain kind of vehicle. +# +# input: {string} kind of vehicle +# output: {boolean} whether a license is required + +def needs_license: . == "car" or . == "truck"; + +# Task 2 +# Helps choosing between two options by recommending the one that +# comes first in dictionary order. +# +# input: {array of strings} options to consider +# output: {string} a sentence of advice which option to choose +def choose_vehicle: if .[0] < .[1] then .[0] else .[1] end|"\(.) is clearly the better choice."; + +# Task 3 +# Calculates an estimate for the price of a used vehicle in the dealership +# based on the original price and the age of the vehicle. +# +# input: {object} with keys "original_price" and "age" +# output: {number} expected resell price in the dealership + +def resell_price: + if .age > 10 then .original_price * 0.5 + elif .age >= 3 then .original_price * 0.7 + else .original_price * 0.8 end;