diff --git a/config.json b/config.json index 0552d884..4fedb633 100644 --- a/config.json +++ b/config.json @@ -33,7 +33,6 @@ ] }, "exercises": { - "concept": [], "practice": [ { "slug": "hello-world", @@ -1172,10 +1171,17 @@ "practices": [], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "c423440e-d374-462e-baa9-e81dcbc085f3", + "practices": [], + "prerequisites": [], + "difficulty": 7 } ] }, - "concepts": [], "key_features": [ { "title": "Automation", @@ -1209,12 +1215,12 @@ } ], "tags": [ - "used_for/scripts", - "paradigm/procedural", "execution_mode/interpreted", - "typing/weak", + "paradigm/procedural", "platform/linux", "platform/mac", - "platform/windows" + "platform/windows", + "typing/weak", + "used_for/scripts" ] } diff --git a/exercises/practice/circular-buffer/.docs/instructions.md b/exercises/practice/circular-buffer/.docs/instructions.md new file mode 100644 index 00000000..2ba1fda2 --- /dev/null +++ b/exercises/practice/circular-buffer/.docs/instructions.md @@ -0,0 +1,58 @@ +# Instructions + +A circular buffer, cyclic buffer or ring buffer is a data structure that uses a single, fixed-size buffer as if it were connected end-to-end. + +A circular buffer first starts empty and of some predefined length. +For example, this is a 7-element buffer: + +```text +[ ][ ][ ][ ][ ][ ][ ] +``` + +Assume that a 1 is written into the middle of the buffer (exact starting location does not matter in a circular buffer): + +```text +[ ][ ][ ][1][ ][ ][ ] +``` + +Then assume that two more elements are added — 2 & 3 — which get appended after the 1: + +```text +[ ][ ][ ][1][2][3][ ] +``` + +If two elements are then removed from the buffer, the oldest values inside the buffer are removed. +The two elements removed, in this case, are 1 & 2, leaving the buffer with just a 3: + +```text +[ ][ ][ ][ ][ ][3][ ] +``` + +If the buffer has 7 elements then it is completely full: + +```text +[5][6][7][8][9][3][4] +``` + +When the buffer is full an error will be raised, alerting the client that further writes are blocked until a slot becomes free. + +When the buffer is full, the client can opt to overwrite the oldest data with a forced write. +In this case, two more elements — A & B — are added and they overwrite the 3 & 4: + +```text +[5][6][7][8][9][A][B] +``` + +3 & 4 have been replaced by A & B making 5 now the oldest data in the buffer. +Finally, if two elements are removed then what would be returned is 5 & 6 yielding the buffer: + +```text +[ ][ ][7][8][9][A][B] +``` + +Because there is space available, if the client again uses overwrite to store C & D then the space where 5 & 6 were stored previously will be used not the location of 7 & 8. +7 is still the oldest element and the buffer is once again full. + +```text +[C][D][7][8][9][A][B] +``` diff --git a/exercises/practice/circular-buffer/.meta/config.json b/exercises/practice/circular-buffer/.meta/config.json new file mode 100644 index 00000000..a3fbfcd4 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "circular_buffer.sh" + ], + "test": [ + "circular_buffer.bats" + ], + "example": [ + ".meta/example.sh" + ] + }, + "blurb": "A data structure that uses a single, fixed-size buffer as if it were connected end-to-end.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Circular_buffer" +} diff --git a/exercises/practice/circular-buffer/.meta/example.sh b/exercises/practice/circular-buffer/.meta/example.sh new file mode 100644 index 00000000..ce3aa2d5 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/example.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Namerefs require bash version 4.3 or greater +bash_version=$((10 * BASH_VERSINFO[0] + BASH_VERSINFO[1])) +if (( bash_version < 43 )); then + echo "This library requires at least bash version 4.3" >&2 + return 4 +fi + +# I'm storing all the data for all circular buffers in a single global +# associative array. +# Empty cells are denoted by empty strings. +# Therefore, an empty string cannot be stored in this circular buffer + +declare -gA _buffer_data=() + +buffer::new() { + local name=$1 + local size=$2 + _buffer_data[size,$name]=$size + buffer::clear "$name" +} + +buffer::destroy() { + local name=$1 + for ((idx = 0; idx < _buffer_data[size,$name]; idx++)); do + unset "_buffer_data[$idx,$name]" + done + unset "_buffer_data[read,$name]" + unset "_buffer_data[write,$name]" + unset "_buffer_data[size,$name]" +} + +buffer::clear() { + local name=$1 + _buffer_data[read,$name]=0 + _buffer_data[write,$name]=0 + for ((idx = 0; idx < _buffer_data[size,$name]; idx++)); do + _buffer_data[$idx,$name]='' + done +} + +buffer::is_empty() { + local name=$1 + local idx=${_buffer_data[read,$name]} + [[ -z ${_buffer_data[$idx,$name]} ]] +} + +buffer::is_full() { + local name=$1 + local idx=${_buffer_data[write,$name]} + [[ -n ${_buffer_data[$idx,$name]} ]] +} + +buffer::read() { + local name=$1 + local -n _read_value=$2 + if buffer::is_empty "$name"; then + return 1 + fi + + local idx=${_buffer_data[read,$name]} + _read_value=${_buffer_data[$idx,$name]} + _buffer_data[$idx,$name]='' + + buffer::incr_pointer "$name" read +} + +buffer::write() { + local overwrite=false + # To use `getopts` in a function, these vars must be local. + local OPTIND OPTARG + while getopts :f opt; do + case $opt in + f) overwrite=true ;; + '?') return 2 ;; + esac + done + shift $((OPTIND - 1)) + + local name=$1 item=$2 + if buffer::is_full "$name"; then + if $overwrite; then + # Read to free up a cell. + # shellcheck disable=SC2034 + local tmp + buffer::read "$name" tmp + else + return 1 + fi + fi + + local idx=${_buffer_data[write,$name]} + _buffer_data[$idx,$name]=$item + + buffer::incr_pointer "$name" write +} + +buffer::incr_pointer() { + local name=$1 p=$2 + _buffer_data[$p,$name]=$(( (_buffer_data[$p,$name] + 1) % _buffer_data[size,$name] )) +} diff --git a/exercises/practice/circular-buffer/.meta/tests.toml b/exercises/practice/circular-buffer/.meta/tests.toml new file mode 100644 index 00000000..0fb3143d --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/tests.toml @@ -0,0 +1,52 @@ +# 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. + +[28268ed4-4ff3-45f3-820e-895b44d53dfa] +description = "reading empty buffer should fail" + +[2e6db04a-58a1-425d-ade8-ac30b5f318f3] +description = "can read an item just written" + +[90741fe8-a448-45ce-be2b-de009a24c144] +description = "each item may only be read once" + +[be0e62d5-da9c-47a8-b037-5db21827baa7] +description = "items are read in the order they are written" + +[2af22046-3e44-4235-bfe6-05ba60439d38] +description = "full buffer can't be written to" + +[547d192c-bbf0-4369-b8fa-fc37e71f2393] +description = "a read frees up capacity for another write" + +[04a56659-3a81-4113-816b-6ecb659b4471] +description = "read position is maintained even across multiple writes" + +[60c3a19a-81a7-43d7-bb0a-f07242b1111f] +description = "items cleared out of buffer can't be read" + +[45f3ae89-3470-49f3-b50e-362e4b330a59] +description = "clear frees up capacity for another write" + +[e1ac5170-a026-4725-bfbe-0cf332eddecd] +description = "clear does nothing on empty buffer" + +[9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b] +description = "overwrite acts like write on non-full buffer" + +[880f916b-5039-475c-bd5c-83463c36a147] +description = "overwrite replaces the oldest item on full buffer" + +[bfecab5b-aca1-4fab-a2b0-cd4af2b053c3] +description = "overwrite replaces the oldest item remaining in buffer following a read" + +[9cebe63a-c405-437b-8b62-e3fdc1ecec5a] +description = "initial clear does not affect wrapping around" diff --git a/exercises/practice/circular-buffer/bats-extra.bash b/exercises/practice/circular-buffer/bats-extra.bash new file mode 100644 index 00000000..54d48070 --- /dev/null +++ b/exercises/practice/circular-buffer/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/exercises/practice/circular-buffer/circular_buffer.bats b/exercises/practice/circular-buffer/circular_buffer.bats new file mode 100644 index 00000000..f6d52409 --- /dev/null +++ b/exercises/practice/circular-buffer/circular_buffer.bats @@ -0,0 +1,292 @@ +#!/usr/bin/env bats +load bats-extra + +bash_version=$((10 * BASH_VERSINFO[0] + BASH_VERSINFO[1])) +if (( bash_version < 43 )); then + echo "This exercise requires at least bash version 4.3" >&2 + exit 4 +fi + +## append entries to a list and return the new list + +setup() { source circular_buffer.sh; } + +@test "reading empty buffer should fail" { + #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" fail + + buffer::destroy buff +} + +@test "can read an item just written" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 'A' && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 'A' + + buffer::destroy buff +} + +@test "each item may only be read once" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 'A' && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 'A' + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" fail + + buffer::destroy buff +} + +@test "items are read in the order they are written" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 2 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::destroy buff +} + +@test "full buffer can't be written to" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" fail + + buffer::destroy buff +} + +@test "a read frees up capacity for another write" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::destroy buff +} + +@test "read position is maintained even across multiple writes" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 3 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::write buff 3 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 3 + + buffer::destroy buff +} + +@test "items cleared out of buffer can't be read" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::clear buff + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" fail + + buffer::destroy buff +} + +@test "clear frees up capacity for another write" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::clear buff + + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::destroy buff +} + +@test "clear does nothing on empty buffer" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 1 + + buffer::clear buff + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::destroy buff +} + +@test "overwrite acts like write on non-full buffer" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 2 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + + buffer::write -f buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::destroy buff +} + +@test "overwrite replaces the oldest item on full buffer" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 2 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + + buffer::write -f buff 3 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 2 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 3 + + buffer::destroy buff +} + +@test "overwrite replaces the oldest item remaining in buffer following a read" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 3 + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 3 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 1 + + buffer::write buff 4 && result=ok || result=fail + assert_equal "$result" ok + buffer::write -f buff 5 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 3 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 4 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 5 + + buffer::destroy buff +} + +@test "initial clear does not affect wrapping around" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + buffer::new buff 2 + + buffer::clear buff + + buffer::write buff 1 && result=ok || result=fail + assert_equal "$result" ok + buffer::write buff 2 && result=ok || result=fail + assert_equal "$result" ok + buffer::write -f buff 3 && result=ok || result=fail + assert_equal "$result" ok + buffer::write -f buff 4 && result=ok || result=fail + assert_equal "$result" ok + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 3 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" ok + assert_equal "$value" 4 + + buffer::read buff value && result=ok || result=fail + assert_equal "$result" fail + + buffer::destroy buff +} diff --git a/exercises/practice/circular-buffer/circular_buffer.sh b/exercises/practice/circular-buffer/circular_buffer.sh new file mode 100644 index 00000000..ddc0e15e --- /dev/null +++ b/exercises/practice/circular-buffer/circular_buffer.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# Namerefs require bash version 4.3 or greater +bash_version=$((10 * BASH_VERSINFO[0] + BASH_VERSINFO[1])) +if (( bash_version < 43 )); then + echo "This library requires at least bash version 4.3" >&2 + return 4 +fi + +# Create a new circular buffer with the given name and size. +buffer::new() { + local name=$1 size=$2 + echo "Implement me" >&2 + exit 1 +} + +# Optionally, destroy an existing circular buffer. +# Your implementation may not need to do anything; if so leave this function empty. +buffer::destroy() { + local name=$1 +} + +# Wipe out any existing data in the buffer. +buffer::clear() { + local name=$1 + echo "Implement me" >&2 + exit 1 +} + +# Find the oldest element in the buffer. +# Set the given variable to hold the value. +# Remove the element from the buffer. +# Return a failure status if the buffer is empty, otherwise return success. +buffer::read() { + local name=$1 + local -n _read_value=$2 + echo "Implement me" >&2 + exit 1 +} + +# Write a new value into the buffer. +# Accept the `-f` option to force write ("overwrite"). +# Return a failure status if the buffer is full and `-f` is not given, +# otherwise return success. +buffer::write() { + local name=$1 item=$2 + echo "Implement me" >&2 + exit 1 +}