diff --git a/config.json b/config.json index 2c61446a..773d064c 100644 --- a/config.json +++ b/config.json @@ -655,6 +655,17 @@ "transforming" ] }, + { + "slug": "baffling-birthdays", + "name": "Baffling Birthdays", + "uuid": "c3fbce6a-61ce-439c-8c67-fad93561326f", + "practices": [], + "prerequisites": [], + "difficulty": 4, + "topics": [ + "dates" + ] + }, { "slug": "binary-search", "name": "Binary Search", diff --git a/exercises/practice/baffling-birthdays/.docs/instructions.md b/exercises/practice/baffling-birthdays/.docs/instructions.md new file mode 100644 index 00000000..a01ec867 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.docs/instructions.md @@ -0,0 +1,23 @@ +# Instructions + +Your task is to estimate the birthday paradox's probabilities. + +To do this, you need to: + +- Generate random birthdates. +- Check if a collection of randomly generated birthdates contains at least two with the same birthday. +- Estimate the probability that at least two people in a group share the same birthday for different group sizes. + +~~~~exercism/note +A birthdate includes the full date of birth (year, month, and day), whereas a birthday refers only to the month and day, which repeat each year. +Two birthdates with the same month and day correspond to the same birthday. +~~~~ + +~~~~exercism/caution +The birthday paradox assumes that: + +- There are 365 possible birthdays (no leap years). +- Each birthday is equally likely (uniform distribution). + +Your implementation must follow these assumptions. +~~~~ diff --git a/exercises/practice/baffling-birthdays/.docs/introduction.md b/exercises/practice/baffling-birthdays/.docs/introduction.md new file mode 100644 index 00000000..97dabd1e --- /dev/null +++ b/exercises/practice/baffling-birthdays/.docs/introduction.md @@ -0,0 +1,25 @@ +# Introduction + +Fresh out of college, you're throwing a huge party to celebrate with friends and family. +Over 70 people have shown up, including your mildly eccentric Uncle Ted. + +In one of his usual antics, he bets you £100 that at least two people in the room share the same birthday. +That sounds ridiculous — there are many more possible birthdays than there are guests, so you confidently accept. + +To your astonishment, after collecting the birthdays of just 32 guests, you've already found two guests that share the same birthday. +Accepting your loss, you hand Uncle Ted his £100, but something feels off. + +The next day, curiosity gets the better of you. +A quick web search leads you to the [birthday paradox][birthday-problem], which reveals that with just 23 people, the probability of a shared birthday exceeds 50%. + +Ah. So _that's_ why Uncle Ted was so confident. + +Determined to turn the tables, you start looking up other paradoxes; next time, _you'll_ be the one making the bets. + +~~~~exercism/note +The birthday paradox is a [veridical paradox][veridical-paradox]: even though it feels wrong, it is actually true. + +[veridical-paradox]: https://en.wikipedia.org/wiki/Paradox#Quine's_classification +~~~~ + +[birthday-problem]: https://en.wikipedia.org/wiki/Birthday_problem diff --git a/exercises/practice/baffling-birthdays/.meta/config.json b/exercises/practice/baffling-birthdays/.meta/config.json new file mode 100644 index 00000000..d9197e1e --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "resu-xuniL" + ], + "files": { + "solution": [ + "baffling_birthdays.sh" + ], + "test": [ + "baffling_birthdays.bats" + ], + "example": [ + ".meta/example.sh" + ] + }, + "blurb": "Estimate the birthday paradox's probabilities.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2539" +} diff --git a/exercises/practice/baffling-birthdays/.meta/example.sh b/exercises/practice/baffling-birthdays/.meta/example.sh new file mode 100644 index 00000000..b2111103 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/example.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +current_year=$(date -d "now" +%Y) + +main() { + local function=$1; shift + + "$function" "$@" +} + +shared_birthday () { + local -a birthdates=( "$@" ) + local -A birthdays + + for birthdate in "${birthdates[@]}"; do + birthday=${birthdate: -5} + + (( birthdays[$birthday]++ )) + done + + if (( ${#birthdays[@]} == ${#birthdates[@]} )); then + echo "false" + else + echo "true" + fi +} + +random_int() { + echo $(( RANDOM % $1 )) +} + +random_birthdates () { + local -i number=$1 + local -i random_year random_month random_day + local -a birthdates + + for (( i = 0; i < number; i++ )); do + random_year=$(( 1900 + $(random_int $(( current_year - 1900 + 1 ))) )) + random_month=$(( 1 + $(random_int 12) )) + + if (( random_year % 400 == 0 || (random_year % 4 == 0 && random_year % 100 != 0) )); then + (( random_year ++ )) + fi + + case "$random_month" in + 2) random_day=$(( 1 + $(random_int 28) )) ;; + 4|6|9|11) random_day=$(( 1 + $(random_int 30) )) ;; + *) random_day=$(( 1 + $(random_int 31) )) ;; + esac + + birthdates+=( "$(printf '%d-%02d-%02d' "$random_year" "$random_month" "$random_day")" ) + done + + echo "${birthdates[@]}" +} + +estimated_probability_of_share_birthday () { + local -i group_size=$1 + # the more you increase the 'runs' value, + # the more the result is approching the expected one + # but the more you increase the computing time! + local -i runs=2000 + local -i count=0 + + for (( i = 0; i <= runs; i++ )); do + read -ra birthdates < <(random_birthdates "$group_size") + if [[ "$(shared_birthday "${birthdates[@]}")" == "true" ]]; then + (( count++ )) + fi + done + + echo $(( count * 100 / runs )) +} + +main "$@" diff --git a/exercises/practice/baffling-birthdays/.meta/tests.toml b/exercises/practice/baffling-birthdays/.meta/tests.toml new file mode 100644 index 00000000..c76afb46 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/tests.toml @@ -0,0 +1,61 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[716dcc2b-8fe4-4fc9-8c48-cbe70d8e6b67] +description = "shared birthday -> one birthdate" + +[f7b3eb26-bcfc-4c1e-a2de-af07afc33f45] +description = "shared birthday -> two birthdates with same year, month, and day" + +[7193409a-6e16-4bcb-b4cc-9ffe55f79b25] +description = "shared birthday -> two birthdates with same year and month, but different day" + +[d04db648-121b-4b72-93e8-d7d2dced4495] +description = "shared birthday -> two birthdates with same month and day, but different year" + +[3c8bd0f0-14c6-4d4c-975a-4c636bfdc233] +description = "shared birthday -> two birthdates with same year, but different month and day" + +[df5daba6-0879-4480-883c-e855c99cdaa3] +description = "shared birthday -> two birthdates with different year, month, and day" + +[0c17b220-cbb9-4bd7-872f-373044c7b406] +description = "shared birthday -> multiple birthdates without shared birthday" + +[966d6b0b-5c0a-4b8c-bc2d-64939ada49f8] +description = "shared birthday -> multiple birthdates with one shared birthday" + +[b7937d28-403b-4500-acce-4d9fe3a9620d] +description = "shared birthday -> multiple birthdates with more than one shared birthday" + +[70b38cea-d234-4697-b146-7d130cd4ee12] +description = "random birthdates -> generate requested number of birthdates" + +[d9d5b7d3-5fea-4752-b9c1-3fcd176d1b03] +description = "random birthdates -> years are not leap years" + +[d1074327-f68c-4c8a-b0ff-e3730d0f0521] +description = "random birthdates -> months are random" + +[7df706b3-c3f5-471d-9563-23a4d0577940] +description = "random birthdates -> days are random" + +[89a462a4-4265-4912-9506-fb027913f221] +description = "estimated probability of at least one shared birthday -> for one person" + +[ec31c787-0ebb-4548-970c-5dcb4eadfb5f] +description = "estimated probability of at least one shared birthday -> among ten people" + +[b548afac-a451-46a3-9bb0-cb1f60c48e2f] +description = "estimated probability of at least one shared birthday -> among twenty-three people" + +[e43e6b9d-d77b-4f6c-a960-0fc0129a0bc5] +description = "estimated probability of at least one shared birthday -> among seventy people" diff --git a/exercises/practice/baffling-birthdays/baffling_birthdays.bats b/exercises/practice/baffling-birthdays/baffling_birthdays.bats new file mode 100644 index 00000000..3a1e1b9c --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays.bats @@ -0,0 +1,178 @@ +#!/usr/bin/env bats +load bats-extra + +@test "shared birthday -> one birthdate" { + #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("2000-01-01") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "false" +} + +@test "shared birthday -> two birthdates with same year, month, and day" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("2000-01-01" "2000-01-01") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "true" +} + +@test "shared birthday -> two birthdates with same year and month, but different day" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("2012-05-09" "2012-05-17") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "false" +} + +@test "shared birthday -> two birthdates with same month and day, but different year" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("1999-10-23" "1988-10-23") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "true" +} + +@test "shared birthday -> two birthdates with same year, but different month and day" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("2007-12-19" "2007-04-27") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "false" +} + +@test "shared birthday -> two birthdates with different year, month, and day" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=("1997-08-04" "1963-11-23") + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "false" +} + +@test "shared birthday -> multiple birthdates without shared birthday" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=( + "1966-07-29" + "1977-02-12" + "2001-12-25" + "1980-11-10" + ) + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "false" +} + +@test "shared birthday -> multiple birthdates with one shared birthday" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=( + "1966-07-29" + "1977-02-12" + "2001-07-29" + "1980-11-10" + ) + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "true" +} + +@test "shared birthday -> multiple birthdates with more than one shared birthday" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + birthdates=( + "1966-07-29" + "1977-02-12" + "2001-12-25" + "1980-07-29" + "2019-02-12" + ) + run bash baffling_birthdays.sh shared_birthday "${birthdates[@]}" + assert_success + assert_output "true" +} + +@test "random birthdates -> generate requested number of birthdates" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + generate=500 + run bash baffling_birthdays.sh random_birthdates "$generate" + assert_success + num_output_dates=$( wc -w <<< "$output" ) + assert_equal "$num_output_dates" "$generate" +} + +@test "random birthdates -> years are not leap years" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + generate=500 + seen_leap=0 + run bash baffling_birthdays.sh random_birthdates "$generate" + assert_success + read -ra output_dates <<< "$output" + for date in "${output_dates[@]}"; do + year=${date:0:4} + if (( year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) )); then + (( ++seen_leap )) + fi + done + assert_equal "${seen_leap}" 0 +} + +@test "random birthdates -> months are random" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + declare -A seen_month + generate=500 + run bash baffling_birthdays.sh random_birthdates "$generate" + assert_success + read -ra output_dates <<< "$output" + for date in "${output_dates[@]}"; do + month=${date:5:2} + + seen_month[$month]=true + done + assert_equal "${#seen_month[@]}" 12 +} + +@test "random birthdates -> days are random" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + declare -A seen_day + generate=500 + run bash baffling_birthdays.sh random_birthdates "$generate" + assert_success + read -ra output_dates <<< "$output" + for date in "${output_dates[@]}"; do + day=${date:8} + + seen_day[$day]=true + done + assert_equal "${#seen_day[@]}" 31 +} + +# "The expected probability values should be compared using some tolerance to allow for small deviations." +@test "estimated probability of at least one shared birthday -> for one person" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + # The expected probability is 0.0 + run bash baffling_birthdays.sh estimated_probability_of_share_birthday 1 + assert_success + assert_equal "$output" 0 +} + +@test "estimated probability of at least one shared birthday -> among ten people" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + # The expected probability is 11.694818 + run bash baffling_birthdays.sh estimated_probability_of_share_birthday 10 + assert_success + assert_between 10 13 +} + +@test "estimated probability of at least one shared birthday -> among twenty-three people" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + # The expected probability is 50.729723 + run bash baffling_birthdays.sh estimated_probability_of_share_birthday 23 + assert_success + assert_between 48 53 +} + +@test "estimated probability of at least one shared birthday -> among seventy people" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + # The expected probability is 99.915958 + run bash baffling_birthdays.sh estimated_probability_of_share_birthday 70 + assert_success + assert_between 98 100 +} diff --git a/exercises/practice/baffling-birthdays/baffling_birthdays.sh b/exercises/practice/baffling-birthdays/baffling_birthdays.sh new file mode 100644 index 00000000..960869f3 --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# The following comments should help you get started: +# - Bash is flexible. You may use functions or write a "raw" script. +# +# - Complex code can be made easier to read by breaking it up +# into functions, however this is sometimes overkill in bash. +# +# - You can find links about good style and other resources +# for Bash in './README.md'. It came with this exercise. +# +# Example: +# # other functions here +# # ... +# # ... +# +# main () { +# # your main function code here +# } +# +# # call main with all of the positional arguments +# main "$@" +# +# *** PLEASE REMOVE THESE COMMENTS BEFORE SUBMITTING YOUR SOLUTION *** diff --git a/exercises/practice/baffling-birthdays/bats-extra.bash b/exercises/practice/baffling-birthdays/bats-extra.bash new file mode 100644 index 00000000..faf3899e --- /dev/null +++ b/exercises/practice/baffling-birthdays/bats-extra.bash @@ -0,0 +1,653 @@ +# 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 +} + +assert_between() { + local -r start="${1}" + local -r end="${2}" + + if (( start > end )); then + return 1 + elif ! (( output >= "$1" && output <= "$2" )); then + batslib_print_kv_single_or_multi 6 \ + 'start' "${start}" \ + 'end' "${end}" \ + 'actual' "${output}" \ + | batslib_decorate 'output falls outside the inclusive range defined by start and end' \ + | fail + fi +}