Parcourir la source

Implement alphametics exercise (#1188)

* Implement alphametics

* Patch

* Update Alphametics to throw Exception
Zhi Yuan il y a 8 ans
Parent
révision
2eddc44a72

+ 12
- 0
config.json Voir le fichier

@@ -615,6 +615,18 @@
615 615
     {
616 616
       "core": false,
617 617
       "difficulty": 6,
618
+      "slug": "alphametics",
619
+      "topics": [
620
+        "mathematics",
621
+        "logic",
622
+        "conditionals"
623
+      ],
624
+      "unlocked_by": "secret-handshake",
625
+      "uuid": "0639a1f8-5af4-4877-95c1-5db8e97c30bf"
626
+    },
627
+    {
628
+      "core": false,
629
+      "difficulty": 6,
618 630
       "slug": "spiral-matrix",
619 631
       "topics": [
620 632
         "arrays",

+ 125
- 0
exercises/alphametics/.meta/src/reference/java/Alphametics.java Voir le fichier

@@ -0,0 +1,125 @@
1
+import java.util.ArrayList;
2
+import java.util.Arrays;
3
+import java.util.Collections;
4
+import java.util.LinkedHashMap;
5
+import java.util.LinkedHashSet;
6
+import java.util.List;
7
+import java.util.Map;
8
+import java.util.Optional;
9
+import java.util.Set;
10
+import java.util.stream.Collectors;
11
+
12
+public class Alphametics {
13
+    private final List<String> wordsToSum;
14
+    private final String wordResult;
15
+
16
+    Alphametics(String userInput) {
17
+        String[] questionAndAnswer = userInput.split("==");
18
+        wordsToSum = Arrays.stream(questionAndAnswer[0].split("\\+"))
19
+                .map(String::new)
20
+                .map(String::trim)
21
+                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
22
+        wordResult = questionAndAnswer[1].trim();
23
+    }
24
+
25
+    Map<Character, Integer> solve() throws UnsolvablePuzzleException {
26
+        AlphameticsRecursion solver = new AlphameticsRecursion(getDistinctCharacters());
27
+        solver.generate();
28
+        return solver.get().orElseThrow(UnsolvablePuzzleException::new);
29
+    }
30
+
31
+    /**
32
+     * Returns the list of distinct characters in this alphametic puzzle.
33
+     */
34
+    private List<Character> getDistinctCharacters() {
35
+        Set<Character> distinctCharacters = new LinkedHashSet<>();
36
+        wordsToSum.forEach(word -> distinctCharacters.addAll(toChar(word)));
37
+        distinctCharacters.addAll(toChar(wordResult));
38
+
39
+        return new ArrayList<>(distinctCharacters);
40
+    }
41
+
42
+    private List<Character> toChar(String toChar) {
43
+        return toChar.chars()
44
+                .mapToObj(c -> (char) c)
45
+                .collect(Collectors.toList());
46
+    }
47
+
48
+    private class AlphameticsRecursion {
49
+        private final List<Character> characters;
50
+        private LinkedHashMap<Character, Integer> validPermutation;
51
+
52
+        private AlphameticsRecursion(List<Character> characters) {
53
+            this.characters = characters;
54
+        }
55
+
56
+        private void generate() {
57
+            generate(new LinkedHashMap<>(), 0, new boolean[10]);
58
+        }
59
+
60
+        private void generate(LinkedHashMap<Character, Integer> permutation, int index, boolean[] isDigitsUsed) {
61
+            // base case
62
+            if (index == characters.size()) {
63
+                if (!isLeadingDigitZero(permutation) && isSumTally(permutation)) {
64
+                    validPermutation = new LinkedHashMap<>(permutation);
65
+                }
66
+                return;
67
+            }
68
+
69
+            for (int i = 0; i <= 9; i++) { // loop through digits 0 to 9
70
+                if (isDigitsUsed[i]) {
71
+                    continue;
72
+                }
73
+
74
+                permutation.put(characters.get(index), i);
75
+                isDigitsUsed[i] = true;
76
+                generate(permutation, index + 1, isDigitsUsed);
77
+                isDigitsUsed[i] = false;
78
+            }
79
+        }
80
+
81
+        private Optional<LinkedHashMap<Character, Integer>> get() {
82
+            return Optional.ofNullable(validPermutation);
83
+        }
84
+
85
+        /**
86
+         * Returns true if the mapping letters to digits using {@code letterToDigit} will result in having zero as a
87
+         * leading digit.
88
+         */
89
+        private boolean isLeadingDigitZero(Map<Character, Integer> letterToDigit) {
90
+            return letterToDigit.keySet().stream()
91
+                    .filter(key -> letterToDigit.get(key) == 0) // Find the character that is mapped to digit 0
92
+                    .filter(charMappedToZero -> wordResult.charAt(0) == charMappedToZero // If the first character in
93
+                                                                                         // wordResult is mapped to 0
94
+                            || wordsToSum.stream() // If the first character in any of wordsToSum is mapped to 0
95
+                            .map(word -> word.charAt(0))
96
+                            .anyMatch(character -> character == charMappedToZero))
97
+                    .count() == 1; // One letter maps to zero and is a leading character
98
+        }
99
+
100
+        /**
101
+         * Returns true if the {@code letterToDigit} solves the alphametic puzzle.
102
+         */
103
+        private boolean isSumTally(Map<Character, Integer> letterToDigit) {
104
+            long actualSum = wordsToSum.stream()
105
+                    .mapToLong(word -> mapToNumber(letterToDigit, word))
106
+                    .sum();
107
+            long expectedSum = mapToNumber(letterToDigit, wordResult);
108
+            return actualSum == expectedSum;
109
+        }
110
+
111
+        /**
112
+         * Returns the long value of {@code word}, mapped using {@code letterToDigit}.
113
+         */
114
+        private long mapToNumber(Map<Character, Integer> letterToDigit, String word) {
115
+            StringBuilder builder = new StringBuilder();
116
+
117
+            for (int i = 0; i < word.length(); i++) {
118
+                int digit = letterToDigit.get(word.charAt(i));
119
+                builder.append(digit);
120
+            }
121
+
122
+            return Long.parseLong(builder.toString());
123
+        }
124
+    }
125
+}

+ 2
- 0
exercises/alphametics/.meta/src/reference/java/UnsolvablePuzzleException.java Voir le fichier

@@ -0,0 +1,2 @@
1
+class UnsolvablePuzzleException extends Exception {
2
+}

+ 1
- 0
exercises/alphametics/.meta/src/version Voir le fichier

@@ -0,0 +1 @@
1
+1.1.0

+ 47
- 0
exercises/alphametics/README.md Voir le fichier

@@ -0,0 +1,47 @@
1
+# Alphametics
2
+
3
+Write a function to solve alphametics puzzles.
4
+
5
+[Alphametics](https://en.wikipedia.org/wiki/Alphametics) is a puzzle where
6
+letters in words are replaced with numbers.
7
+
8
+For example `SEND + MORE = MONEY`:
9
+
10
+```text
11
+  S E N D
12
+  M O R E +
13
+-----------
14
+M O N E Y
15
+```
16
+
17
+Replacing these with valid numbers gives:
18
+
19
+```text
20
+  9 5 6 7
21
+  1 0 8 5 +
22
+-----------
23
+1 0 6 5 2
24
+```
25
+
26
+This is correct because every letter is replaced by a different number and the
27
+words, translated into numbers, then make a valid sum.
28
+
29
+Each letter must represent a different digit, and the leading digit of
30
+a multi-digit number must not be zero.
31
+
32
+Write a function to solve alphametics puzzles.
33
+
34
+# Running the tests
35
+
36
+You can run all the tests for an exercise by entering
37
+
38
+```sh
39
+$ gradle test
40
+```
41
+
42
+in your terminal.
43
+
44
+
45
+## Submitting Incomplete Solutions
46
+
47
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.

+ 18
- 0
exercises/alphametics/build.gradle Voir le fichier

@@ -0,0 +1,18 @@
1
+apply plugin: "java"
2
+apply plugin: "eclipse"
3
+apply plugin: "idea"
4
+
5
+repositories {
6
+  mavenCentral()
7
+}
8
+
9
+dependencies {
10
+  testCompile "junit:junit:4.12"
11
+}
12
+
13
+test {
14
+  testLogging {
15
+    exceptionFormat = 'full'
16
+    events = ["passed", "failed", "skipped"]
17
+  }
18
+}

+ 0
- 0
exercises/alphametics/src/main/java/.keep Voir le fichier


+ 2
- 0
exercises/alphametics/src/main/java/UnsolvablePuzzleException.java Voir le fichier

@@ -0,0 +1,2 @@
1
+class UnsolvablePuzzleException extends Exception {
2
+}

+ 150
- 0
exercises/alphametics/src/test/java/AlphameticsTest.java Voir le fichier

@@ -0,0 +1,150 @@
1
+import org.junit.Ignore;
2
+import org.junit.Rule;
3
+import org.junit.Test;
4
+import org.junit.rules.ExpectedException;
5
+
6
+import java.util.Arrays;
7
+import java.util.Collections;
8
+import java.util.LinkedHashMap;
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+import static org.hamcrest.CoreMatchers.*;
13
+import static org.junit.Assert.*;
14
+
15
+public class AlphameticsTest {
16
+    @Rule
17
+    public ExpectedException expectedException = ExpectedException.none();
18
+
19
+    @Test
20
+    public void testThreeLetters() throws UnsolvablePuzzleException {
21
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
22
+        expected.put('I', 1);
23
+        expected.put('B', 9);
24
+        expected.put('L', 0);
25
+
26
+        assertEquals(expected, new Alphametics("I + BB == ILL").solve());
27
+    }
28
+
29
+    @Ignore("Remove to run test")
30
+    @Test
31
+    public void testUniqueValue() throws UnsolvablePuzzleException {
32
+        expectedException.expect(UnsolvablePuzzleException.class);
33
+        new Alphametics("A == B").solve();
34
+    }
35
+
36
+    @Ignore("Remove to run test")
37
+    @Test
38
+    public void testLeadingZero() throws UnsolvablePuzzleException {
39
+        expectedException.expect(UnsolvablePuzzleException.class);
40
+        assertNull(new Alphametics("ACA + DD == BD").solve());
41
+    }
42
+
43
+    @Ignore("Remove to run test")
44
+    @Test
45
+    public void testFourLetters() throws UnsolvablePuzzleException {
46
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
47
+        expected.put('A', 9);
48
+        expected.put('S', 2);
49
+        expected.put('M', 1);
50
+        expected.put('O', 0);
51
+
52
+        assertEquals(expected, new Alphametics("AS + A == MOM").solve());
53
+    }
54
+
55
+    @Ignore("Remove to run test")
56
+    @Test
57
+    public void testSixLetters() throws UnsolvablePuzzleException {
58
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
59
+        expected.put('N', 7);
60
+        expected.put('O', 4);
61
+        expected.put('T', 9);
62
+        expected.put('L', 1);
63
+        expected.put('A', 0);
64
+        expected.put('E', 2);
65
+
66
+        assertEquals(expected, new Alphametics("NO + NO + TOO == LATE").solve());
67
+    }
68
+
69
+    @Ignore("Remove to run test")
70
+    @Test
71
+    public void testSevenLetters() throws UnsolvablePuzzleException {
72
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
73
+        expected.put('E', 4);
74
+        expected.put('G', 2);
75
+        expected.put('H', 5);
76
+        expected.put('I', 0);
77
+        expected.put('L', 1);
78
+        expected.put('S', 9);
79
+        expected.put('T', 7);
80
+
81
+        assertEquals(expected, new Alphametics("HE + SEES + THE == LIGHT").solve());
82
+    }
83
+
84
+    @Ignore("Remove to run test")
85
+    @Test
86
+    public void testEightLetters() throws UnsolvablePuzzleException {
87
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
88
+        expected.put('S', 9);
89
+        expected.put('E', 5);
90
+        expected.put('N', 6);
91
+        expected.put('D', 7);
92
+        expected.put('M', 1);
93
+        expected.put('O', 0);
94
+        expected.put('R', 8);
95
+        expected.put('Y', 2);
96
+
97
+        assertEquals(expected, new Alphametics("SEND + MORE == MONEY").solve());
98
+    }
99
+
100
+    @Ignore("Remove to run test")
101
+    @Test
102
+    public void testTenLetters() throws UnsolvablePuzzleException {
103
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
104
+        expected.put('A', 5);
105
+        expected.put('D', 3);
106
+        expected.put('E', 4);
107
+        expected.put('F', 7);
108
+        expected.put('G', 8);
109
+        expected.put('N', 0);
110
+        expected.put('O', 2);
111
+        expected.put('R', 1);
112
+        expected.put('S', 6);
113
+        expected.put('T', 9);
114
+
115
+        assertEquals(expected, new Alphametics("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE").solve());
116
+    }
117
+
118
+    @Ignore("Remove to run test")
119
+    @Test
120
+    public void testTenLetters41Addends() throws UnsolvablePuzzleException {
121
+        LinkedHashMap<Character, Integer> expected = new LinkedHashMap<>();
122
+        expected.put('A', 1);
123
+        expected.put('E', 0);
124
+        expected.put('F', 5);
125
+        expected.put('H', 8);
126
+        expected.put('I', 7);
127
+        expected.put('L', 2);
128
+        expected.put('O', 6);
129
+        expected.put('R', 3);
130
+        expected.put('S', 4);
131
+        expected.put('T', 9);
132
+
133
+        assertEquals(expected, new Alphametics("THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + " +
134
+                "TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + " +
135
+                "HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + " +
136
+                "TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + " +
137
+                "LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + " +
138
+                "HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + " +
139
+                "HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + " +
140
+                "FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + " +
141
+                "TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + " +
142
+                "LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + " +
143
+                "FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + " +
144
+                "RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + " +
145
+                "HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + " +
146
+                "ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + " +
147
+                "HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + " +
148
+                "ROOFS + FOR + THEIR + SAFE == FORTRESSES").solve());
149
+    }
150
+}

+ 1
- 0
exercises/settings.gradle Voir le fichier

@@ -2,6 +2,7 @@ include 'accumulate'
2 2
 include 'acronym'
3 3
 include 'all-your-base'
4 4
 include 'allergies'
5
+include 'alphametics'
5 6
 include 'anagram'
6 7
 include 'armstrong-numbers'
7 8
 include 'atbash-cipher'