1
1
Fork 0

feat(jq): vehicle purchase

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2024-12-09 18:37:41 +01:00
parent 8c8963fcd1
commit 425aeafedf
Signed by: cafkafk
GPG key ID: 26C542FD97F965CE
9 changed files with 1200 additions and 0 deletions

View file

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

View file

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

114
jq/vehicle-purchase/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 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.

View file

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

View file

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

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,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'
}

View file

@ -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;