diff --git a/awk/.package-name_test.awk b/awk/.package-name_test.awk index f5fe2df5..d41f75e0 100644 --- a/awk/.package-name_test.awk +++ b/awk/.package-name_test.awk @@ -1,4 +1,4 @@ -#!/usr/bin/gawk --lint --file +#!/usr/bin/gawk --bignum --lint --file @include "awkunit" @include "test-cases" diff --git a/awk/README.md b/awk/README.md index aefafb64..1ad62c64 100644 --- a/awk/README.md +++ b/awk/README.md @@ -26,3 +26,4 @@ - [leap](./leap/README.md) - [armstrong-numbers](./armstrong-numbers/README.md) - [grains](./grains/README.md) +- [triangle](./triangle/README.md) diff --git a/awk/triangle/.exercism/config.json b/awk/triangle/.exercism/config.json new file mode 100644 index 00000000..142fe540 --- /dev/null +++ b/awk/triangle/.exercism/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "IsaacG" + ], + "files": { + "solution": [ + "triangle.awk" + ], + "test": [ + "test-triangle.bats" + ], + "example": [ + ".meta/example.awk" + ] + }, + "blurb": "Determine if a triangle is equilateral, isosceles, or scalene.", + "source": "The Ruby Koans triangle project, parts 1 & 2", + "source_url": "https://web.archive.org/web/20220831105330/http://rubykoans.com" +} diff --git a/awk/triangle/.exercism/metadata.json b/awk/triangle/.exercism/metadata.json new file mode 100644 index 00000000..72a6daca --- /dev/null +++ b/awk/triangle/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"awk","exercise":"triangle","id":"398f063987f84b3ca8834ac137467208","url":"https://exercism.org/tracks/awk/exercises/triangle","handle":"vpayno","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/awk/triangle/.lint_default_vars b/awk/triangle/.lint_default_vars new file mode 100644 index 00000000..0a69f81a --- /dev/null +++ b/awk/triangle/.lint_default_vars @@ -0,0 +1 @@ +-v type=equilateral diff --git a/awk/triangle/0 b/awk/triangle/0 new file mode 100644 index 00000000..d623c964 --- /dev/null +++ b/awk/triangle/0 @@ -0,0 +1 @@ +[error]: not a triangle [7 3 2] diff --git a/awk/triangle/HELP.md b/awk/triangle/HELP.md new file mode 100644 index 00000000..73eab5a9 --- /dev/null +++ b/awk/triangle/HELP.md @@ -0,0 +1,99 @@ +# 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 +gawk -f some-exercise.awk <<< "some,input,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,input,here" | gawk -f some-exercise.awk +``` + +## 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 +``` + +[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 + +## Submitting your solution + +You can submit your solution using the `exercism submit triangle.awk` 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 [AWK track's documentation](https://exercism.org/docs/tracks/awk) +- [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. + +Places to look for help for AWK questions: + +* [Stack Overflow `awk` tag][so]. +* check the Resources section of the [Stack Overflow `awk` tag into page][so-info]. +* raise an issue at the [exercism/awk][github] Github repository. +* IRC: `irc://irc.liberachat.net/#awk`, `irc://irc.liberachat.net/#exercism` + * see [Libera.chat][libera] if you're unfamiliar with IRC. + + +[so]: https://stackoverflow.com/tags/awk +[so-info]: https://stackoverflow.com/tags/awk/info +[github]: https://github.com/exercism/awk +[libera]: https://libera.chat \ No newline at end of file diff --git a/awk/triangle/README.md b/awk/triangle/README.md new file mode 100644 index 00000000..b48f810d --- /dev/null +++ b/awk/triangle/README.md @@ -0,0 +1,51 @@ +# Triangle + +Welcome to Triangle on Exercism's AWK Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Instructions + +Determine if a triangle is equilateral, isosceles, or scalene. + +An _equilateral_ triangle has all three sides the same length. + +An _isosceles_ triangle has at least two sides the same length. +(It is sometimes specified as having exactly two sides the same length, but for the purposes of this exercise we'll say at least two.) + +A _scalene_ triangle has all sides of different lengths. + +## Note + +For a shape to be a triangle at all, all sides have to be of length > 0, and the sum of the lengths of any two sides must be greater than or equal to the length of the third side. + +In equations: + +Let `a`, `b`, and `c` be sides of the triangle. +Then all three of the following expressions must be true: + +```text +a + b ≥ c +b + c ≥ a +a + c ≥ b +``` + +See [Triangle Inequality][triangle-inequality] + +[triangle-inequality]: https://en.wikipedia.org/wiki/Triangle_inequality + +## Source + +### Created by + +- @IsaacG + +### Based on + +The Ruby Koans triangle project, parts 1 & 2 - https://web.archive.org/web/20220831105330/http://rubykoans.com + +### My Solution + +- [my solution](./triangle.awk) +- [awkunit tests](./triangle_test.awk) +- [test cases](./test-cases.awk) +- [run-tests](./run-tests-awk.txt) diff --git a/awk/triangle/awkunit.awk b/awk/triangle/awkunit.awk new file mode 120000 index 00000000..38a08adf --- /dev/null +++ b/awk/triangle/awkunit.awk @@ -0,0 +1 @@ +../.lib/awkunit.awk \ No newline at end of file diff --git a/awk/triangle/bats-extra.bash b/awk/triangle/bats-extra.bash new file mode 100644 index 00000000..54d48070 --- /dev/null +++ b/awk/triangle/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/awk/triangle/run-tests-awk.txt b/awk/triangle/run-tests-awk.txt new file mode 100644 index 00000000..c38b73cf --- /dev/null +++ b/awk/triangle/run-tests-awk.txt @@ -0,0 +1,287 @@ +Running automated test file(s): + + +=============================================================================== + +AWKLIBPATH=/usr/lib/x86_64-linux-gnu/gawk:../.lib + +/usr/lib/x86_64-linux-gnu/gawk +filefuncs.so +fnmatch.so +fork.so +inplace.so +intdiv.so +ordchr.so +readdir.so +readfile.so +revoutput.so +revtwoway.so +rwarray.so +time.so + +../.lib +awkunit.awk +awkunit.so + +gawk -v type=equilateral --lint --file=./awkunit.awk < /dev/null > /dev/null +gawk: ./awkunit.awk:3: warning: `load' is a gawk extension +gawk: warning: function `assertEquals' defined but never called directly +gawk: warning: function `assert' defined but never called directly +gawk: ./awkunit.awk:26: warning: reference to uninitialized variable `_assert_exit' + +real 0m0.004s +user 0m0.001s +sys 0m0.003s + +gawk -v type=equilateral --lint --file=./test-cases.awk < /dev/null > /dev/null + +real 0m0.003s +user 0m0.002s +sys 0m0.001s + +gawk -v type=equilateral --lint --file=./triangle.awk < /dev/null > /dev/null +gawk: ./triangle.awk:3: warning: `load' is a gawk extension +gawk: ./triangle.awk:70: warning: `switch' is a gawk extension +gawk: ./triangle.awk:71: warning: `case' is a gawk extension + +real 0m0.005s +user 0m0.004s +sys 0m0.001s + +exit 0 + +=============================================================================== + +Running: bats ./test-triangle.bats +1..21 +ok 1 all sides are equal, equilateral +ok 2 any side is unequal +ok 3 no sides are equal, equilateral +ok 4 all zero sides is not a triangle +ok 5 sides may be floats, equilateral +ok 6 last two sides are equal +ok 7 first two sides are equal +ok 8 first and last sides are equal +ok 9 equilateral triangles are also isosceles +ok 10 no sides are equal, isosceles +ok 11 first triangle inequality violation +ok 12 second triangle inequality violation +ok 13 third triangle inequality violation +ok 14 sides may be floats, isosceles +ok 15 no sides are equal, scalene +ok 16 all sides are equal, scalene +ok 17 first and second sides are equal +ok 18 first and third sides are equal +ok 19 second and third sides are equal +ok 20 may not violate triangle inequality +ok 21 sides may be floats, scalene + +real 0m0.894s +user 0m0.603s +sys 0m0.318s + +exit 0 + +=============================================================================== + +AWKLIBPATH=/usr/lib/x86_64-linux-gnu/gawk:../.lib + +/usr/lib/x86_64-linux-gnu/gawk +filefuncs.so +fnmatch.so +fork.so +inplace.so +intdiv.so +ordchr.so +readdir.so +readfile.so +revoutput.so +revtwoway.so +rwarray.so +time.so + +../.lib +awkunit.awk +awkunit.so + +Running: gawk -v type=equilateral --file ./triangle_test.awk && printf \n%s\n Tests Passed! || printf \n%s\n Tests Failed! + +Running 8 tests... + +Test 1: + input -> [1] + output -> [true] + result -> passed + +Test 2: + input -> [0] + output -> [false] + result -> passed + +Test 3: + input -> [1 1 1] + output -> [true] + result -> passed + +Test 4: + input -> [1 2 1] + output -> [false] + result -> passed + +Test 5: + input -> [4 3 4] + output -> [true] + result -> passed + +Test 6: + input -> [1 3 1] + output -> [false] + result -> passed + +Test 7: + input -> [5 4 6] + output -> [true] + result -> passed + +Test 8: + input -> [4 3 3] + output -> [false] + result -> passed + +Running 5 test cases + +Test 9: + input -> [0.5 0.5 0.5] + output -> [true] + result -> passed + +Test 10: + input -> [0 0 0] + output -> [false] + result -> passed + +Test 11: + input -> [2 2 2] + output -> [true] + result -> passed + +Test 12: + input -> [2 3 2] + output -> [false] + result -> passed + +Test 13: + input -> [5 4 6] + output -> [false] + result -> passed + +Running 10 test cases + +Test 14: + input -> [1.5 2.5 1.0] + output -> [false] + result -> passed + +Test 15: + input -> [4 4 3] + output -> [true] + result -> passed + +Test 16: + input -> [4 4 4] + output -> [true] + result -> passed + +Test 17: + input -> [1 1 3] + output -> [false] + result -> passed + +Test 18: + input -> [3 1 1] + output -> [false] + result -> passed + +Test 19: + input -> [0.5 0.4 0.5] + output -> [true] + result -> passed + +Test 20: + input -> [3 4 4] + output -> [true] + result -> passed + +Test 21: + input -> [4 3 4] + output -> [true] + result -> passed + +Test 22: + input -> [1 3 1] + output -> [false] + result -> passed + +Test 23: + input -> [2 3 4] + output -> [false] + result -> passed + +Running 7 test cases + +Test 24: + input -> [4 4 3] + output -> [false] + result -> passed + +Test 25: + input -> [4 4 4] + output -> [false] + result -> passed + +Test 26: + input -> [7 3 2] + output -> [false] + result -> passed + +Test 27: + input -> [3 4 3] + output -> [false] + result -> passed + +Test 28: + input -> [0.5 0.4 0.6] + output -> [true] + result -> passed + +Test 29: + input -> [4 3 3] + output -> [false] + result -> passed + +Test 30: + input -> [5 4 6] + output -> [true] + result -> passed + + +30 out of 30 tests passed! + +real 0m0.009s +user 0m0.004s +sys 0m0.005s + +Tests Passed! + +exit 0 + +=============================================================================== + +Running: misspell . + +real 0m0.036s +user 0m0.042s +sys 0m0.019s + +=============================================================================== + diff --git a/awk/triangle/test-cases.awk b/awk/triangle/test-cases.awk new file mode 100644 index 00000000..cca0d8fc --- /dev/null +++ b/awk/triangle/test-cases.awk @@ -0,0 +1,38 @@ +#!/usr/bin/gawk --lint --file +# test-cases.awk + +# key: input +# value: output + +BEGIN { + equilateral_cases["2 2 2"]="true" + equilateral_cases["0.5 0.5 0.5"]="true" + + equilateral_cases["0 0 0"]="false" + equilateral_cases["2 3 2"]="false" + equilateral_cases["5 4 6"]="false" + + isosceles_cases["3 4 4"]="true" + isosceles_cases["4 4 3"]="true" + isosceles_cases["4 3 4"]="true" + isosceles_cases["4 4 4"]="true" + isosceles_cases["0.5 0.4 0.5"]="true" + + isosceles_cases["2 3 4"]="false" + isosceles_cases["1 1 3"]="false" + isosceles_cases["1 3 1"]="false" + isosceles_cases["3 1 1"]="false" + isosceles_cases["1.5 2.5 1.0"]="false" + + scalene_cases["5 4 6"]="true" + scalene_cases["0.5 0.4 0.6"]="true" + + scalene_cases["4 4 4"]="false" + scalene_cases["4 4 3"]="false" + scalene_cases["3 4 3"]="false" + scalene_cases["4 3 3"]="false" + scalene_cases["7 3 2"]="false" + + # add exit here to keep it from waiting for input + exit 0 +} diff --git a/awk/triangle/test-triangle.bats b/awk/triangle/test-triangle.bats new file mode 100644 index 00000000..22c6feb2 --- /dev/null +++ b/awk/triangle/test-triangle.bats @@ -0,0 +1,155 @@ +#!/usr/bin/env bats +load bats-extra + +# Test returns true if the triangle is equilateral + +@test "all sides are equal, equilateral" { + #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=equilateral <<< "2 2 2" + assert_success + assert_output "true" +} + +@test "any side is unequal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=equilateral <<< "2 3 2" + assert_success + assert_output "false" +} + +@test "no sides are equal, equilateral" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=equilateral <<< "5 4 6" + assert_success + assert_output "false" +} + +@test "all zero sides is not a triangle" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=equilateral <<< "0 0 0" + assert_success + assert_output "false" +} + +@test "sides may be floats, equilateral" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=equilateral <<< "0.5 0.5 0.5" + assert_success + assert_output "true" +} + +# Test returns true if the triangle is isosceles + +@test "last two sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "3 4 4" + assert_success + assert_output "true" +} + +@test "first two sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "4 4 3" + assert_success + assert_output "true" +} + +@test "first and last sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "4 3 4" + assert_success + assert_output "true" +} + +@test "equilateral triangles are also isosceles" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "4 4 4" + assert_success + assert_output "true" +} + +@test "no sides are equal, isosceles" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "2 3 4" + assert_success + assert_output "false" +} + +@test "first triangle inequality violation" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "1 1 3" + assert_success + assert_output "false" +} + +@test "second triangle inequality violation" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "1 3 1" + assert_success + assert_output "false" +} + +@test "third triangle inequality violation" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "3 1 1" + assert_success + assert_output "false" +} + +@test "sides may be floats, isosceles" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=isosceles <<< "0.5 0.4 0.5" + assert_success + assert_output "true" +} + +# Test returns true if the triangle is scalene + +@test "no sides are equal, scalene" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "5 4 6" + assert_success + assert_output "true" +} + +@test "all sides are equal, scalene" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "4 4 4" + assert_success + assert_output "false" +} + +@test "first and second sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "4 4 3" + assert_success + assert_output "false" +} + +@test "first and third sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "3 4 3" + assert_success + assert_output "false" +} + +@test "second and third sides are equal" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "4 3 3" + assert_success + assert_output "false" +} + +@test "may not violate triangle inequality" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "7 3 2" + assert_success + assert_output "false" +} + +@test "sides may be floats, scalene" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run gawk -f triangle.awk -v type=scalene <<< "0.5 0.4 0.6" + assert_success + assert_output "true" +} diff --git a/awk/triangle/triangle.awk b/awk/triangle/triangle.awk new file mode 100644 index 00000000..e6fb2ada --- /dev/null +++ b/awk/triangle/triangle.awk @@ -0,0 +1,110 @@ +#!/usr/bin/gawk --bignum --lint --file + +@load "ordchr" + +# These variables are initialized on the command line (using '-v'): +# - type -> ^(equilateral|isosceles|scalene)$ + +function is_triangle(s) { + a = s[1] + b = s[2] + c = s[3] + + # true if + # - all sides are >0 + # - the sum of any two sides is greater than or equal to the other side + return (a > 0) && (b > 0) && (c > 0) && (a + b >= c) && (a + c >= b) && (b + c >= a) +} + +function is_equilateral(s) { + a = s[1] + b = s[2] + c = s[3] + + # true if all sides are equal + return (a == b) && (a == c) +} + +function is_isosceles(s) { + a = s[1] + b = s[2] + c = s[3] + + # true if any two sides are equal + # every equilateral triangle is also an isosceles triangle + return (a == b) || (b == c) || (a == c) +} + +function is_scalene(s) { + a = s[1] + b = s[2] + c = s[3] + + # true of no side is equal to another side + return (a != b) && (a != c) && (b != c) +} + +function to_faux_bool(condition) { + # converts >=1 to true and 0 to false + return condition ? "true" : "false" +} + +function triangle(input) { + result = "false" + + if (!match(input, /^[0-9]+[.]?[0-9]* [0-9]+[.]?[0-9]* [0-9]+[.]?[0-9]*$/)) { + return "error:invalid data [" input "], expected 3 space separated positive numbers" + } + + # already tested that we have a correct input, we don't need to check that + # we got 3 numbers. + _ = split(input, sides, " ") + + if (!is_triangle(sides)) { + # the tests don't care if the triangles are valid, + # that error is mixed in with the type tests + # return "error:not a triangle [" input "]" + return "false" + } + + switch (type) { + case "equilateral": + result = to_faux_bool(is_equilateral(sides)) + break + case "isosceles": + result = to_faux_bool(is_isosceles(sides)) + break + case "scalene": + result = to_faux_bool(is_scalene(sides)) + break + } + + return result +} + +BEGIN { +} + +{ + # let's make sure direction isn't being used to inject code + if (! match(type, /^(equilateral|isosceles|scalene)$/)) { + print "error: invalid type [" type "], only supported types are ^(equilateral|isosceles|scalene)$" #> /dev/stderr + exit 1 + } + + result = triangle($0) + + if (match(result, /^error:/)) { + _ = split(result, parts, ":") + level = parts[1] + message = parts[2] + + print "[" level "]: " message #> /dev/stderr + exit 1 + } + + print result +} + +END { +} diff --git a/awk/triangle/triangle_test.awk b/awk/triangle/triangle_test.awk new file mode 100644 index 00000000..ad78c466 --- /dev/null +++ b/awk/triangle/triangle_test.awk @@ -0,0 +1,227 @@ +#!/usr/bin/gawk --bignum --lint --file + +@include "awkunit" +@include "test-cases" +@include "triangle" + +passed = 0 +testCount = 0 + +function _debugTestPre() { + printf "Test %s:\n", (passed + 1) + printf " input -> [%s]\n", input +} + +function _debugTestPost() { + passed = passed + 1 + printf " output -> [%s]\n", got + printf " result -> passed\n\n" +} + +function testTriangle_to_faux_bool_one() { + input = 1 # not-zero is true + want = "true" + + _debugTestPre() + got = to_faux_bool(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_to_faux_bool_two() { + input = 0 # zero is false + want = "false" + + _debugTestPre() + got = to_faux_bool(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_equilateral_one() { + input = "1 1 1" + want = "true" + + # _ = split(input, a, " ") + + type = "equilateral" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_equilateral_two() { + input = "1 2 1" + want = "false" + + # _ = split(input, a, " ") + + type = "equilateral" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_isosceles_one() { + input = "4 3 4" + want = "true" + + # _ = split(input, a, " ") + + type = "isosceles" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_isosceles_two() { + input = "1 3 1" + want = "false" + + # _ = split(input, a, " ") + + type = "isosceles" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_scalene_one() { + input = "5 4 6" + want = "true" + + # _ = split(input, a, " ") + + type = "scalene" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function testTriangle_scalene_two() { + input = "4 3 3" + want = "false" + + # _ = split(input, a, " ") + + type = "scalene" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() +} + +function casesTriangle_equilateral() { + printf "Running %d test cases\n\n", length(equilateral_cases) + caseNum = 0 + + # Associative arrays don't preserve insert order. + for (key in equilateral_cases) { + input = key + want = equilateral_cases[key] + + # _ = split(input, a, " ") + + type = "equilateral" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() + } +} + +function casesTriangle_isosceles() { + printf "Running %d test cases\n\n", length(isosceles_cases) + caseNum = 0 + + # Associative arrays don't preserve insert order. + for (key in isosceles_cases) { + input = key + want = isosceles_cases[key] + + # _ = split(input, a, " ") + + type = "isosceles" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() + } +} + +function casesTriangle_scalene() { + printf "Running %d test cases\n\n", length(scalene_cases) + caseNum = 0 + + # Associative arrays don't preserve insert order. + for (key in scalene_cases) { + input = key + want = scalene_cases[key] + + # _ = split(input, a, " ") + + type = "scalene" + + _debugTestPre() + got = triangle(input) + + assertEquals(got, want) + _debugTestPost() + } +} + +BEGIN { + exit 0 +} + +END { + cmd = "grep --no-filename --count ^function\\ test *_test.awk" + cmd | getline testCount + + printf "\nRunning %d tests...\n\n", testCount + + testCount = testCount + length(equilateral_cases) + length(isosceles_cases) + length(scalene_cases) + + # running tests with a lot of duplication or not covered by test-cases file + testTriangle_to_faux_bool_one() + testTriangle_to_faux_bool_two() + testTriangle_equilateral_one() + testTriangle_equilateral_two() + testTriangle_isosceles_one() + testTriangle_isosceles_two() + testTriangle_scalene_one() + testTriangle_scalene_two() + + # running tests with reduced duplication + casesTriangle_equilateral() + casesTriangle_isosceles() + casesTriangle_scalene() + + print "\n" passed " out of " testCount " tests passed!" + + # add exit here to keep it from looping + exit 0 +} diff --git a/awk/update_readmes b/awk/update_readmes index fdfad310..1de7ba9d 100755 --- a/awk/update_readmes +++ b/awk/update_readmes @@ -79,14 +79,15 @@ EOF fi if git grep -q -- '-M'; then - printf -- "%s" " -M" >.lint_default_vars + printf -- "%s" " -M " >.lint_default_vars git add .lint_default_vars else : >.lint_default_vars fi if git grep -q -- '-v '; then - git grep -- '-v ' *.bats | head -n 1 | sed -r -e 's/.*(-v [a-z]+[=].+) ?.*/\1/g' | tr -d '\n' >>.lint_default_vars + git grep -- '-v ' *.bats | head -n 1 | sed -r -e 's/.*(-v [a-z]+[=][a-zA-Z0-9]+)\b.*$/\1/g' | tr -d '\n' >>.lint_default_vars + printf -- "%s" " " >>.lint_default_vars git add .lint_default_vars fi