diff --git a/2018/src/main/java/me/zodac/advent/Day02.java b/2018/src/main/java/me/zodac/advent/Day02.java new file mode 100644 index 00000000..78c08725 --- /dev/null +++ b/2018/src/main/java/me/zodac/advent/Day02.java @@ -0,0 +1,128 @@ +/* + * BSD Zero Clause License + * + * Copyright (c) 2021-2023 zodac.me + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package me.zodac.advent; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import me.zodac.advent.util.StringUtils; + +/** + * Solution for 2018, Day 2. + * + * @see AoC 2018, Day 2 + */ +public final class Day02 { + + private Day02() { + + } + + /** + * Given some box IDs in the form of {@link String}s, we calculate the checksum by counting the number of box IDs with: + * + * + *

+ * These counts are then multiplied to return the checksum of the provided box IDs. + * + * @param boxIds the {@link String}s of the box IDs + * @return the checksum of the box IDs + */ + public static long checksumOfBoxIds(final Iterable boxIds) { + int numberOfDoubleCharacters = 0; + int numberOfTripleCharacters = 0; + + for (final String boxId : boxIds) { + if (hasAtLeastOneCharacterThatOccursExactlyTwoTimes(boxId)) { + numberOfDoubleCharacters++; + } + + if (hasAtLeastOneCharacterThatOccursExactlyThreeTimes(boxId)) { + numberOfTripleCharacters++; + } + } + + return (long) numberOfDoubleCharacters * numberOfTripleCharacters; + } + + /** + * Given a {@link List} of box IDs in the form of {@link String}s, we know there will be one pair different by only 1 character. We find + * this pair of box IDs, remove the different character, then return the common letters for the pair. + * + * @param boxIds the {@link String}s of the box IDs + * @return the common characters of the valid box ID pair + * @throws IllegalStateException thrown if a valid pair of box IDs could not be found + */ + public static String findCommonCharactersForValidBoxIds(final List boxIds) { + final int expectedLengthAfterDifferencesRemoved = boxIds.getFirst().length() - 1; + + for (int i = 0; i < boxIds.size(); i++) { + final String currentBoxId = boxIds.get(i); + + for (int j = i + 1; j < boxIds.size(); j++) { + final String nextBoxId = boxIds.get(j); + + final String differentCharactersRemoved = StringUtils.removeDifferentCharacters(currentBoxId, nextBoxId); + if (differentCharactersRemoved.length() == expectedLengthAfterDifferencesRemoved) { + return differentCharactersRemoved; + } + } + } + + throw new IllegalStateException("Could not find a valid pair of box IDs"); + } + + private static boolean hasAtLeastOneCharacterThatOccursExactlyTwoTimes(final CharSequence input) { + return hasAtLeastOneCharacterThatOccursExactly(input, 2); + } + + private static boolean hasAtLeastOneCharacterThatOccursExactlyThreeTimes(final CharSequence input) { + return hasAtLeastOneCharacterThatOccursExactly(input, 3); + } + + private static boolean hasAtLeastOneCharacterThatOccursExactly(final CharSequence input, final int times) { + final int stringLength = input.length(); + final Collection checkedCharacters = new HashSet<>(); + + for (int i = 0; i < stringLength - 1; i++) { + final char currentChar = input.charAt(i); + int count = 1; // Start count at 1 as we count the current occurance + + if (checkedCharacters.contains(currentChar)) { + continue; + } + + for (int j = i + 1; j < stringLength; j++) { + if (input.charAt(j) == currentChar) { + count++; + } + } + + if (count == times) { + return true; + } + + checkedCharacters.add(currentChar); + } + + return false; + } +} diff --git a/2018/src/test/java/me/zodac/advent/Day02Test.java b/2018/src/test/java/me/zodac/advent/Day02Test.java new file mode 100644 index 00000000..457ff498 --- /dev/null +++ b/2018/src/test/java/me/zodac/advent/Day02Test.java @@ -0,0 +1,67 @@ +/* + * BSD Zero Clause License + * + * Copyright (c) 2021-2023 zodac.me + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package me.zodac.advent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import me.zodac.advent.input.ExampleInput; +import me.zodac.advent.input.PuzzleInput; +import org.junit.jupiter.api.Test; + +/** + * Tests to verify answers for {@link Day02}. + */ +public class Day02Test { + + private static final String INPUT_FILENAME = "day02.txt"; + private static final String INPUT_FILENAME_PART_2 = "day02_2.txt"; + + @Test + void example() { + final List values = ExampleInput.readLines(INPUT_FILENAME); + + final long checksumOfBoxIds = Day02.checksumOfBoxIds(values); + assertThat(checksumOfBoxIds) + .isEqualTo(12L); + + final List valuesPart2 = ExampleInput.readLines(INPUT_FILENAME_PART_2); + + final String commonLetters = Day02.findCommonCharactersForValidBoxIds(valuesPart2); + assertThat(commonLetters) + .isEqualTo("fgij"); + } + + @Test + void part1() { + final List values = PuzzleInput.readLines(INPUT_FILENAME); + + final long checksumOfBoxIds = Day02.checksumOfBoxIds(values); + assertThat(checksumOfBoxIds) + .isEqualTo(5_880L); + } + + @Test + void part2() { + final List values = PuzzleInput.readLines(INPUT_FILENAME); + + final String commonLetters = Day02.findCommonCharactersForValidBoxIds(values); + assertThat(commonLetters) + .isEqualTo("tiwcdpbseqhxryfmgkvjujvza"); + } +} diff --git a/2018/src/test/resources/day02.txt b/2018/src/test/resources/day02.txt new file mode 100644 index 00000000..8a42990c --- /dev/null +++ b/2018/src/test/resources/day02.txt @@ -0,0 +1,7 @@ +abcdef +bababc +abbcde +abcccd +aabcdd +abcdee +ababab \ No newline at end of file diff --git a/2018/src/test/resources/day02_2.txt b/2018/src/test/resources/day02_2.txt new file mode 100644 index 00000000..46a5a083 --- /dev/null +++ b/2018/src/test/resources/day02_2.txt @@ -0,0 +1,7 @@ +abcde +fghij +klmno +pqrst +fguij +axcye +wvxyz \ No newline at end of file diff --git a/README.md b/README.md index 729fb955..d045b054 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![](https://img.shields.io/badge/2021%20⭐-19-orange) ![](https://img.shields.io/badge/2020%20⭐-0-red) ![](https://img.shields.io/badge/2019%20⭐-0-red) -![](https://img.shields.io/badge/2018%20⭐-2-orange) +![](https://img.shields.io/badge/2018%20⭐-4-orange) ![](https://img.shields.io/badge/2017%20⭐-0-red) ![](https://img.shields.io/badge/2016%20⭐-0-red) ![](https://img.shields.io/badge/2015%20⭐-50-green) @@ -176,7 +176,7 @@ The source code is released under the [BSD Zero Clause License](https://opensour | Day | Part 1 | Part 2 | |:-----------------------------------------------|---------:|---------:| | [Day 1](https://adventofcode.com/2018/day/1) | 27,792 ⭐ | 22,808 ⭐ | -| [Day 2](https://adventofcode.com/2018/day/2) | | | +| [Day 2](https://adventofcode.com/2018/day/2) | 66,502 ⭐ | 59,417 ⭐ | | [Day 3](https://adventofcode.com/2018/day/3) | | | | [Day 4](https://adventofcode.com/2018/day/4) | | | | [Day 5](https://adventofcode.com/2018/day/5) | | | diff --git a/advent-of-code-inputs b/advent-of-code-inputs index 6103f5ad..a8406e0f 160000 --- a/advent-of-code-inputs +++ b/advent-of-code-inputs @@ -1 +1 @@ -Subproject commit 6103f5adeec5e18974c1bab8a60606f4ca973653 +Subproject commit a8406e0fdb13198ea5a99ed0b1dbec7ef8c16862 diff --git a/common-utils/src/main/java/me/zodac/advent/util/StringUtils.java b/common-utils/src/main/java/me/zodac/advent/util/StringUtils.java index a187a886..dfc5b205 100644 --- a/common-utils/src/main/java/me/zodac/advent/util/StringUtils.java +++ b/common-utils/src/main/java/me/zodac/advent/util/StringUtils.java @@ -352,9 +352,54 @@ public static String lookAndSay(final String input) { return output.toString(); } + /** + * Iterates through both input {@link String}s index by index, and if the character at any index is different, that character is removed. + * + *

+ * For example, given {@code foobar} and {@code fuubar}, the returned value will be: + *

+     *   {@code fbar}
+     * 
+ * + * @param first the first {@link String} + * @param second the second {@link String} + * @return the {@link String} with any differences removed + * @throws IllegalArgumentException thrown if the two input {@link String}s do not have the same length, or if either input is {@code null} + */ + public static String removeDifferentCharacters(final String first, final String second) { + if (first == null || second == null) { + throw new IllegalArgumentException("Inputs must not be null"); + } + + if (first.length() != second.length()) { + throw new IllegalArgumentException( + String.format("Expected inputs of equal length, found %s (%s) and %s (%s)", first, first.length(), second, second.length())); + } + + final int stringLength = first.length(); + + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < stringLength; i++) { + final char charFromFirst = first.charAt(i); + final char charFromSecond = second.charAt(i); + + if (charFromFirst == charFromSecond) { + stringBuilder.append(charFromFirst); + } + } + + return stringBuilder.toString(); + } + /** * Removes the last character in the {@link String}. * + *

+ * For example, given {@code foobar}, the returned value will be: + *

+     *   {@code fooba}
+     * 
+ * * @param input the input {@link String} * @return the updated {@link String} */ @@ -365,6 +410,12 @@ public static String removeLastCharacter(final String input) { /** * Removes the last {@code numberOfCharactersToRemove} characters in the {@link String}. * + *

+ * For example, given {@code foobar} and {@code numberOfCharactersToRemove} set to 3, the returned value will be: + *

+     *   {@code foo}
+     * 
+ * * @param input the input {@link String} * @param numberOfCharactersToRemove the number of characters to remove * @return the updated {@link String} @@ -421,7 +472,10 @@ public static String replaceAtIndex(final String input, final CharSequence subSt * Sorts the individual characters in the given {@link String} alphabetically. * *

- * For example, {@code foobar} will be returned as {@code abfoor} + * For example, given {@code foobar}, the returned value will be: + *

+     *   {@code abfoor}
+     * 
* * @param input the {@link String} to sort * @return the sorted {@link String} diff --git a/common-utils/src/test/java/me/zodac/advent/util/StringUtilsTest.java b/common-utils/src/test/java/me/zodac/advent/util/StringUtilsTest.java index d7c75829..c1ed18bc 100644 --- a/common-utils/src/test/java/me/zodac/advent/util/StringUtilsTest.java +++ b/common-utils/src/test/java/me/zodac/advent/util/StringUtilsTest.java @@ -914,6 +914,87 @@ void whenLookAndSay_givenNullString_thenEmptyStringIsReturned() { .isEmpty(); } + @Test + void whenRemoveDifferentCharacters_givenEqualInputs_thenInputIsReturnedWithoutChange() { + final String first = "abcdef"; + final String second = "abcdef"; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEqualTo(first); + } + + @Test + void whenRemoveDifferentCharacters_givenInputsWithSingleDifferent_thenOnlyCommonCharactersAreReturned() { + final String first = "abcdef"; + final String second = "abcqef"; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEqualTo("abcef"); + } + + @Test + void whenRemoveDifferentCharacters_givenInputsWithNoCommonCharacters_thenEmptyStringIsReturned() { + final String first = "abcdef"; + final String second = "ghijkl"; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEmpty(); + } + + @Test + void whenRemoveDifferentCharacters_givenInputsOfDifferentLength_thenExceptionIsThrown() { + final String first = "abcdef"; + final String second = "abcdefg"; + assertThatThrownBy(() -> StringUtils.removeDifferentCharacters(first, second)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expected inputs of equal length, found abcdef (6) and abcdefg (7)"); + } + + @Test + void whenRemoveDifferentCharacters_givenInputIsEmpty_thenEmptyStringIsReturned() { + final String first = ""; + final String second = ""; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEmpty(); + } + + @Test + void whenRemoveDifferentCharacters_givenFirstInputIsBlank_thenEmptyStringIsReturned() { + final String first = " "; + final String second = "a"; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEmpty(); + } + + @Test + void whenRemoveDifferentCharacters_givenSecondInputIsBlank_thenEmptyStringIsReturned() { + final String first = "a"; + final String second = " "; + final String output = StringUtils.removeDifferentCharacters(first, second); + assertThat(output) + .isEmpty(); + } + + @Test + void whenRemoveDifferentCharacters_givenFirstInputIsNull_thenExceptionIsThrown() { + final String first = null; + final String second = "abcdef"; + assertThatThrownBy(() -> StringUtils.removeDifferentCharacters(first, second)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Inputs must not be null"); + } + + @Test + void whenRemoveDifferentCharacters_givenSecondInputIsNull_thenExceptionIsThrown() { + final String first = "abcdef"; + final String second = null; + assertThatThrownBy(() -> StringUtils.removeDifferentCharacters(first, second)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Inputs must not be null"); + } + @Test void whenRemoveLastCharacter_givenString_thenStringIsReturnedWithoutLastCharacter() { final String input = "abc";