Skip to content

Commit

Permalink
Add alphametics exercise and update TwelveDays stub. (#547)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelBunker authored Aug 25, 2023
1 parent 4510137 commit 60336f6
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 1 deletion.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,14 @@
"practices": [],
"prerequisites": [],
"difficulty": 3
},
{
"slug": "alphametics",
"name": "Alphametics",
"uuid": "49d212c6-6fa3-4e7b-95d9-595613630566",
"practices": [],
"prerequisites": [],
"difficulty": 5
}
]
},
Expand Down
31 changes: 31 additions & 0 deletions exercises/practice/alphametics/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Instructions

Write a function to solve alphametics puzzles.

[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.

Write a function to solve alphametics puzzles.

[alphametics]: https://en.wikipedia.org/wiki/Alphametics
10 changes: 10 additions & 0 deletions exercises/practice/alphametics/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"blurb": "Write a function to solve alphametics puzzles.",
"authors": ["MichaelBunker"],
"contributors": [],
"files": {
"solution": ["Alphametics.php"],
"test": ["AlphameticsTest.php"],
"example": [".meta/example.php"]
}
}
128 changes: 128 additions & 0 deletions exercises/practice/alphametics/.meta/example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

/*
* By adding type hints and enabling strict type checking, code can become
* easier to read, self-documenting and reduce the number of potential bugs.
* By default, type declarations are non-strict, which means they will attempt
* to change the original type to match the type specified by the
* type-declaration.
*
* In other words, if you pass a string to a function requiring a float,
* it will attempt to convert the string value to a float.
*
* To enable strict mode, a single declare directive must be placed at the top
* of the file.
* This means that the strictness of typing is configured on a per-file basis.
* This directive not only affects the type declarations of parameters, but also
* a function's return type.
*
* For more info review the Concept on strict type checking in the PHP track
* <link>.
*
* To disable strict typing, comment out the directive below.
*/

declare(strict_types=1);

class Alphametics
{
public function solve(string $puzzle): ?array
{
//Remove operators and filter down to just the letters
$parts = preg_split('/[+|==]/', $puzzle);
$parts = array_map('trim', $parts);
$parts = array_filter($parts);

$firstLetters = [];

foreach ($parts as $part) {
$firstLetters[] = substr($part, 0, 1);
}
$firstLetters = array_unique($firstLetters);

$sum = array_pop($parts);
$counts = $this->getLetterCounts($parts, $sum);

return $this->runPermutations($counts, $firstLetters);
}

//Run through each permutation of values.
private function runPermutations(array $letterCounts, array $firstLetters, array $numbers = []): ?array
{
$letters = array_keys($letterCounts);

//If the permutation has a value for each letter, test the permutation to see if it is a solution
if (count($letters) === count($numbers)) {
return $this->testPermutation($letterCounts, $numbers);
}
$possibleValues = [0,1,2,3,4,5,6,7,8,9];

foreach ($possibleValues as $value) {
//Setup possible values without duplicates and not using 0 as first letter value
if (in_array($value, $numbers) || ($value === 0 && in_array($letters[count($numbers)], $firstLetters))) {
continue;
}

//Add the number to the possible permutation and run function again.
//Ex. with 3 unique letters
// 1st run: $numbers = []
// 2nd run: $numbers = [1]
// 3rd run: $numbers = [1,2]
// 4th run: $numbers = [1,2,3] -> testPermutation
$result = $this->runPermutations($letterCounts, $firstLetters, [...$numbers, $value]);

if ($result) {
return $result;
}
}

return null;
}

//Test the permutation when each letter has a potential value
private function testPermutation(array $letterCounts, array $numbers): ?array
{
$letters = array_keys($letterCounts);
$counts = array_values($letterCounts);

$i = 0;

$isSolved = array_reduce($counts, function ($sum, $count) use ($numbers, &$i) {
$return = $sum + $count * $numbers[$i];
$i++;
return $return;
}, 0) == 0;

if (!$isSolved) {
return null;
}

$result = [];

foreach ($letters as $key => $letter) {
$result[$letter] = $numbers[$key];
}

return $result;
}

private function getLetterCounts(array $addends, string $sum): array
{
$counts = [];

foreach ($addends as $addend) {
$addendParts = str_split($addend);

foreach ($addendParts as $i => $letter) {
$counts[$letter] = ($counts[$letter] ?? 0) + 10 ** (count($addendParts) - 1 - $i);
}
}

$sumParts = str_split($sum);
foreach ($sumParts as $i => $letter) {
$counts[$letter] = ($counts[$letter] ?? 0) - 10 ** (count($sumParts) - 1 - $i);
}

return $counts;
}
}
33 changes: 33 additions & 0 deletions exercises/practice/alphametics/Alphametics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* By adding type hints and enabling strict type checking, code can become
* easier to read, self-documenting and reduce the number of potential bugs.
* By default, type declarations are non-strict, which means they will attempt
* to change the original type to match the type specified by the
* type-declaration.
*
* In other words, if you pass a string to a function requiring a float,
* it will attempt to convert the string value to a float.
*
* To enable strict mode, a single declare directive must be placed at the top
* of the file.
* This means that the strictness of typing is configured on a per-file basis.
* This directive not only affects the type declarations of parameters, but also
* a function's return type.
*
* For more info review the Concept on strict type checking in the PHP track
* <link>.
*
* To disable strict typing, comment out the directive below.
*/

declare(strict_types=1);

class Alphametics
{
public function solve(string $puzzle): ?array
{
throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__));
}
}
134 changes: 134 additions & 0 deletions exercises/practice/alphametics/AlphameticsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

/*
* By adding type hints and enabling strict type checking, code can become
* easier to read, self-documenting and reduce the number of potential bugs.
* By default, type declarations are non-strict, which means they will attempt
* to change the original type to match the type specified by the
* type-declaration.
*
* In other words, if you pass a string to a function requiring a float,
* it will attempt to convert the string value to a float.
*
* To enable strict mode, a single declare directive must be placed at the top
* of the file.
* This means that the strictness of typing is configured on a per-file basis.
* This directive not only affects the type declarations of parameters, but also
* a function's return type.
*
* For more info review the Concept on strict type checking in the PHP track
* <link>.
*
* To disable strict typing, comment out the directive below.
*/

declare(strict_types=1);

class AlphameticsTest extends PHPUnit\Framework\TestCase
{
private Alphametics $alphametics;

public static function setUpBeforeClass(): void
{
require_once 'Alphametics.php';
}

public function setUp(): void
{
$this->alphametics = new Alphametics();
}

public function testSolveThreeLetterPuzzle(): void
{
$this->assertEquals(['I' => 1, 'B' => 9, 'L' => 0], $this->alphametics->solve('I + BB == ILL'));
}

public function testSolutionsMustHaveUniqueValuesForLetters(): void
{
$this->assertEquals(null, $this->alphametics->solve('A == B'));
}

public function testLeadingZerosAreInvalid(): void
{
$this->assertEquals(null, $this->alphametics->solve('ACA + DD == BD'));
}

public function testPuzzleWithTwoDigitsFinalCarry(): void
{
$result = $this->alphametics->solve('A + A + A + A + A + A + A + A + A + A + A + B == BCC');
$this->assertEquals(['A' => 9, 'B' => 1, 'C' => 0], $result);
}

public function testPuzzleWithFourLetters(): void
{
$result = $this->alphametics->solve('AS + A == MOM');
$this->assertEquals(['A' => 9, 'S' => 2, 'M' => 1, 'O' => 0], $result);
}

public function testPuzzleWithSixLetters(): void
{
$result = $this->alphametics->solve('NO + NO + TOO == LATE');
$this->assertEquals(['N' => 7, 'O' => 4, 'T' => 9, 'L' => 1, 'A' => 0, 'E' => 2], $result);
}

public function testPuzzleWithSevenLetter(): void
{
$result = $this->alphametics->solve('HE + SEES + THE == LIGHT');
$this->assertEquals(['E' => 4, 'G' => 2, 'H' => 5, 'I' => 0, 'L' => 1, 'S' => 9, 'T' => 7], $result);
}

public function testPuzzleWithEightLetters(): void
{
$result = $this->alphametics->solve('SEND + MORE == MONEY');
$this->assertEquals(['S' => 9, 'E' => 5, 'N' => 6, 'D' => 7, 'M' => 1, 'O' => 0, 'R' => 8, 'Y' => 2], $result);
}

public function testPuzzleWithTenLetters(): void
{
$result = $this->alphametics->solve('AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE');
$this->assertEquals([
'A' => 5,
'D' => 3,
'E' => 4,
'F' => 7,
'G' => 8,
'N' => 0,
'O' => 2,
'R' => 1,
'S' => 6,
'T' => 9
], $result);
}

public function testPuzzleWithTenLettersAnd199Addends(): void
{
$puzzle = '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';
$result = $this->alphametics->solve($puzzle);
$this->assertEquals([
'A' => 1,
'E' => 0,
'F' => 5,
'H' => 8,
'I' => 7,
'L' => 2,
'O' => 6,
'R' => 3,
'S' => 4,
'T' => 9
], $result);
}
}
2 changes: 1 addition & 1 deletion exercises/practice/twelve-days/TwelveDays.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

class TwelveDays
{
public static function recite(int $start, int $end): string
public function recite(int $start, int $end): string
{
throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__));
}
Expand Down

0 comments on commit 60336f6

Please sign in to comment.