diff --git a/config.json b/config.json index af0ff89..a0a92b2 100644 --- a/config.json +++ b/config.json @@ -757,6 +757,14 @@ "prerequisites": [], "difficulty": 7 }, + { + "slug": "alphametics", + "name": "Alphametics", + "uuid": "cc316db6-9007-40fe-af64-92f00d5f148e", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "change", "name": "Change", diff --git a/exercises/practice/alphametics/.docs/instructions.md b/exercises/practice/alphametics/.docs/instructions.md new file mode 100644 index 0000000..596878a --- /dev/null +++ b/exercises/practice/alphametics/.docs/instructions.md @@ -0,0 +1,59 @@ +# Instructions + +Given an alphametics puzzle, find the correct solution. + +[Alphametics][alphametics] is a puzzle where letters in words are replaced with numbers. + +For example `SEND + MORE = MONEY`: + +```text + S E N D + M O R E + +----------- +M O N E Y +``` + +Replacing these with valid numbers gives: + +```text + 9 5 6 7 + 1 0 8 5 + +----------- +1 0 6 5 2 +``` + +This is correct because every letter is replaced by a different number and the +words, translated into numbers, then make a valid sum. + +Each letter must represent a different digit, and the leading digit of a +multi-digit number must not be zero. + +## Input + +Your script will receive the alphametic puzzle as a single string via +standard input (stdin). The puzzle string follows the format +`WORD1 + WORD2 + ... == RESULT`. + +For example: + +```text +SEND + MORE == MONEY +``` + +## Output + +If the puzzle has a solution, print a single line consisting of +space-separated `LETTER=DIGIT` pairs, sorted alphabetically by letter. + +For the example above, the output should be: + +```text +D=7 E=5 M=1 N=6 O=0 R=8 S=9 Y=2 +``` + +If the puzzle has multiple valid solutions, printing any one of them is +acceptable. + +If the puzzle has no solution, print nothing (an empty line). + +[alphametics]: https://en.wikipedia.org/wiki/Alphametics diff --git a/exercises/practice/alphametics/.meta/config.json b/exercises/practice/alphametics/.meta/config.json new file mode 100644 index 0000000..f10fa3c --- /dev/null +++ b/exercises/practice/alphametics/.meta/config.json @@ -0,0 +1,21 @@ +{ + "blurb": "Given an alphametics puzzle, find the correct solution.", + "authors": [ + "kirtisrivastava22" + ], + "contributors": [], + "files": { + "solution": [ + "alphametics.awk" + ], + "test": [ + "test-alphametics.bats" + ], + "example": [ + ".meta/example.awk" + ] + }, + "icon": "crossword", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Alphametics" +} diff --git a/exercises/practice/alphametics/.meta/example.awk b/exercises/practice/alphametics/.meta/example.awk new file mode 100644 index 0000000..82e534d --- /dev/null +++ b/exercises/practice/alphametics/.meta/example.awk @@ -0,0 +1,103 @@ +#!/usr/bin/awk -f +# +# Alphametics solver. +# +# Reads a puzzle from stdin in the form: +# WORD1 + WORD2 + ... == RESULT +# (a single "=" is also accepted) +# +# Prints one line of space-separated LETTER=DIGIT pairs, sorted +# alphabetically by letter, for a valid solution. Prints nothing +# if no solution exists. +# +# Approach: process the puzzle column by column, from the rightmost +# (units) digit to the leftmost, tracking the carry. Within each +# column, only the letters that appear for the first time are +# assigned digits (via backtracking), then the column's arithmetic +# is checked immediately. This prunes the search far more +# aggressively than trying full permutations of every letter. +function colchar(word, col, L) { + L = length(word) + if (col < L) return substr(word, L - col, 1) + return "" +} +# Try every valid digit for newl[idx..m], then verify/recurse. +function assign(idx, m, colletters, ncol, hasres, rch, newl, col, carr, + d, letter, i, colsum, total, digit_needed, newcarry, required) { + if (idx > m) { + colsum = 0 + for (i = 1; i <= ncol; i++) colsum += assigned[colletters[i]] + total = colsum + carr + digit_needed = total % 10 + newcarry = int(total / 10) + required = hasres ? assigned[rch] : 0 + if (digit_needed != required) return 0 + return solve(col + 1, newcarry) + } + letter = newl[idx] + for (d = 0; d <= 9; d++) { + if (used[d]) continue + if (leading[letter] && d == 0) continue + assigned[letter] = d + used[d] = 1 + if (assign(idx + 1, m, colletters, ncol, hasres, rch, newl, col, carr)) return 1 + delete assigned[letter] + used[d] = 0 + } + return 0 +} +function solve(col, carry, + colletters, ncol, i, ch, hasres, rch, newl, m, j, exists) { + if (col == maxlen) return (carry == 0) + ncol = 0 + for (i = 1; i <= nwords; i++) { + ch = colchar(addends[i], col) + if (ch != "") colletters[++ncol] = ch + } + hasres = (col < length(result)) + rch = hasres ? colchar(result, col) : "" + m = 0 + for (i = 1; i <= ncol; i++) { + ch = colletters[i] + if (!(ch in assigned)) { + exists = 0 + for (j = 1; j <= m; j++) if (newl[j] == ch) { exists = 1; break } + if (!exists) newl[++m] = ch + } + } + if (hasres && !(rch in assigned)) { + exists = 0 + for (j = 1; j <= m; j++) if (newl[j] == rch) { exists = 1; break } + if (!exists) newl[++m] = rch + } + return assign(1, m, colletters, ncol, hasres, rch, newl, col, carry) +} +{ + line = $0 + split(line, parts, /\s*==\s*/) + lhs = parts[1] + result = parts[2] + nwords = split(lhs, addends, /\s*\+\s*/) + maxlen = length(result) + if (length(result) > 1) leading[substr(result, 1, 1)] = 1 + for (i = 1; i <= nwords; i++) { + L = length(addends[i]) + if (L > maxlen) maxlen = L + if (L > 1) leading[substr(addends[i], 1, 1)] = 1 + } + if (solve(0, 0)) { + n = 0 + for (l in assigned) letters[++n] = l + for (i = 1; i <= n; i++) + for (j = i + 1; j <= n; j++) + if (letters[j] < letters[i]) { + t = letters[i]; letters[i] = letters[j]; letters[j] = t + } + out = "" + for (i = 1; i <= n; i++) { + if (i > 1) out = out " " + out = out letters[i] "=" assigned[letters[i]] + } + print out + } +} diff --git a/exercises/practice/alphametics/.meta/template.j2 b/exercises/practice/alphametics/.meta/template.j2 new file mode 100644 index 0000000..520a828 --- /dev/null +++ b/exercises/practice/alphametics/.meta/template.j2 @@ -0,0 +1,13 @@ +{{ header }} +{% for idx, case in cases %} +@test "{{ case.description }}" { + {% if idx == 0 %}# {% endif %}[[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "{{ case.input.puzzle }}" + assert_success + {% if case.expected %} + assert_output "{% for letter, digit in case.expected | dictsort %}{{ letter }}={{ digit }}{% if not loop.last %} {% endif %}{% endfor %}" + {% else %} + refute_output + {% endif %} +} +{% endfor %} diff --git a/exercises/practice/alphametics/.meta/tests.toml b/exercises/practice/alphametics/.meta/tests.toml new file mode 100644 index 0000000..ca6fc6a --- /dev/null +++ b/exercises/practice/alphametics/.meta/tests.toml @@ -0,0 +1,37 @@ +# This is an auto-generated file. +# +# This file is synced by a bot with exercism/problem-specifications data. +# Anything added to this file will be overwritten unless the settings +# indicate otherwise. All added comments will be preserved. +# +# See https://github.com/exercism/docs/blob/main/building/tracks/tests.md + +[e0c08b07-9028-4d5f-91e1-d178fead8e1a] +description = "puzzle with three letters" + +[a504ee41-cb92-4ec2-9f11-c37e95ab3f25] +description = "solution must have unique value for each letter" + +[4e3b81d2-be7b-4c5c-9a80-cd72bc6d465a] +description = "leading zero solution is invalid" + +[8a3e3168-d1ee-4df7-94c7-b9c54845ac3a] +description = "puzzle with two digits final carry" + +[a9630645-15bd-48b6-a61e-d85c4021cc09] +description = "puzzle with four letters" + +[3d905a86-5a52-4e4e-bf80-8951535791bd] +description = "puzzle with six letters" + +[4febca56-e7b7-4789-97b9-530d09ba95f0] +description = "puzzle with seven letters" + +[12125a75-7284-4f9a-a5fa-191471e0d44f] +description = "puzzle with eight letters" + +[fb05955f-38dc-477a-a0b6-5ef78969fffa] +description = "puzzle with ten letters" + +[9a101e81-9216-472b-b458-b513a7adacf7] +description = "puzzle with ten letters and 199 addends" diff --git a/exercises/practice/alphametics/alphametics.awk b/exercises/practice/alphametics/alphametics.awk new file mode 100644 index 0000000..82d9841 --- /dev/null +++ b/exercises/practice/alphametics/alphametics.awk @@ -0,0 +1,11 @@ +#!/usr/bin/awk -f +# +# Read an alphametics puzzle from stdin, e.g.: +# SEND + MORE == MONEY +# +# Print a single line of space-separated LETTER=DIGIT pairs, sorted +# alphabetically by letter, for a valid solution (e.g. "D=7 E=5 M=1 N=6 +# O=0 R=8 S=9 Y=2"). Print nothing if no solution exists. + +{ +} diff --git a/exercises/practice/alphametics/bats-extra.bash b/exercises/practice/alphametics/bats-extra.bash new file mode 100644 index 0000000..54d4807 --- /dev/null +++ b/exercises/practice/alphametics/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/alphametics/test-alphametics.bats b/exercises/practice/alphametics/test-alphametics.bats new file mode 100644 index 0000000..105b710 --- /dev/null +++ b/exercises/practice/alphametics/test-alphametics.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats +load bats-extra + +# generated on 2026-08-13T17:52:46+00:00 + +@test "puzzle with three letters" { + # [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "I + BB == ILL" + assert_success + + assert_output "B=9 I=1 L=0" + +} + +@test "solution must have unique value for each letter" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "A == B" + assert_success + + refute_output + +} + +@test "leading zero solution is invalid" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "ACA + DD == BD" + assert_success + + refute_output + +} + +@test "puzzle with two digits final carry" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "A + A + A + A + A + A + A + A + A + A + A + B == BCC" + assert_success + + assert_output "A=9 B=1 C=0" + +} + +@test "puzzle with four letters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "AS + A == MOM" + assert_success + + assert_output "A=9 M=1 O=0 S=2" + +} + +@test "puzzle with six letters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "NO + NO + TOO == LATE" + assert_success + + assert_output "A=0 E=2 L=1 N=7 O=4 T=9" + +} + +@test "puzzle with seven letters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "HE + SEES + THE == LIGHT" + assert_success + + assert_output "E=4 G=2 H=5 I=0 L=1 S=9 T=7" + +} + +@test "puzzle with eight letters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "SEND + MORE == MONEY" + assert_success + + assert_output "D=7 E=5 M=1 N=6 O=0 R=8 S=9 Y=2" + +} + +@test "puzzle with ten letters" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE" + assert_success + + assert_output "A=5 D=3 E=4 F=7 G=8 N=0 O=2 R=1 S=6 T=9" + +} + +@test "puzzle with ten letters and 199 addends" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run awk -f alphametics.awk <<< "THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES" + assert_success + + assert_output "A=1 E=0 F=5 H=8 I=7 L=2 O=6 R=3 S=4 T=9" + +} +