diff --git a/day-1/main.go b/day-1/main.go index 9c46907..9910db7 100644 --- a/day-1/main.go +++ b/day-1/main.go @@ -37,8 +37,8 @@ func part1() { fmt.Println(" Answer: How many measurements are larger than the previous measurement?") input, err := os.Open("./input.txt") - defer input.Close() check(err) + defer input.Close() scanner := bufio.NewScanner(input) @@ -67,8 +67,8 @@ func part2() { fmt.Println(" Answer: How many sums are larger than the previous sum?") input, err := os.Open("./input.txt") - defer input.Close() check(err) + defer input.Close() scanner := bufio.NewScanner(input) diff --git a/day-1/main_test.go b/day-1/main_test.go index d06a15b..900238d 100644 --- a/day-1/main_test.go +++ b/day-1/main_test.go @@ -1,14 +1,14 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestPrintPip(t *testing.T) { DEBUG = false str := GetPip(1, 1, 1, 100) - assert.StrEquals(t, str, "", "Should return an empty string") + assert.EqualsStr(t, str, "", "Should return an empty string") DEBUG = true cases := []struct { @@ -24,7 +24,7 @@ func TestPrintPip(t *testing.T) { for _, c := range cases { str := GetPip(c.first, c.previous, c.current, c.total) - assert.StrEquals(t, str, c.expected, c.msg) + assert.EqualsStr(t, str, c.expected, c.msg) } } diff --git a/day-2/main_test.go b/day-2/main_test.go index 0afaaf4..ca50ee9 100644 --- a/day-2/main_test.go +++ b/day-2/main_test.go @@ -1,11 +1,11 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestDay2(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/day-3/main_test.go b/day-3/main_test.go index 96840de..5bdb917 100644 --- a/day-3/main_test.go +++ b/day-3/main_test.go @@ -1,11 +1,11 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestDay3(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/day-4/main_test.go b/day-4/main_test.go index a30f81c..f03c2a0 100644 --- a/day-4/main_test.go +++ b/day-4/main_test.go @@ -1,11 +1,11 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestDay4(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/day-6/main_test.go b/day-6/main_test.go index 298b3fd..3a1d01a 100644 --- a/day-6/main_test.go +++ b/day-6/main_test.go @@ -1,11 +1,11 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestDay6(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/day-7/main_test.go b/day-7/main_test.go index c8df2ab..f2fded0 100644 --- a/day-7/main_test.go +++ b/day-7/main_test.go @@ -1,11 +1,11 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) func TestDay7(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/day-8/main.go b/day-8/main.go new file mode 100644 index 0000000..584354b --- /dev/null +++ b/day-8/main.go @@ -0,0 +1,232 @@ +/* Day 8: Seven Segment Search */ +package main + +import ( + "AoC2021/utils/arrays" + "AoC2021/utils/bits" + "AoC2021/utils/log" + "AoC2021/utils/timer" + "bufio" + "math" + "os" + "strings" +) + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func main() { + log.Println(log.NORMAL, "--- Day 8: Seven Segment Search ---") + timer.Start() + part1() + timer.Tick() + log.Println(log.NORMAL) + + part2() + timer.Tick() + log.Println(log.NORMAL) +} + +func part1() { + log.Println(log.NORMAL, "* Part 1 *") + log.Println(log.NORMAL, " Goal: For now, focus on the easy digits.") + log.Println(log.NORMAL, " Answer: In the output values, how many times do digits 1, 4, 7, or 8 appear?") + + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + ones := 0 + fours := 0 + sevens := 0 + eights := 0 + + for scanner.Scan() { + fields := strings.Split(scanner.Text(), " | ") + if len(fields) != 2 { + panic("Read invalid input line") + } + _ = strings.Fields(fields[0]) + // count all the ones, fours, sevens, and eights in the output strings based on how many segments are lit up + outputDigits := strings.Fields(fields[1]) + for _, d := range outputDigits { + switch len(d) { + case 2: + ones += 1 + case 3: + sevens += 1 + case 4: + fours += 1 + case 7: + eights += 1 + } + } + lines++ + } + + total := ones + fours + sevens + eights + log.Printf(log.NORMAL, "Read %d lines from input\n", lines) + log.Printf(log.NORMAL, "We counted %d ones, %d fours, %d sevens, and %d eights for a total of %d\n", ones, fours, sevens, eights, total) +} + +func part2() { + log.Println(log.NORMAL, "* Part 2 *") + log.Println(log.NORMAL, " Goal: For each entry, determine all of the wire/segment connections and decode the four-digit output values") + log.Println(log.NORMAL, " Answer: What do you get if you add up all of the output values?") + + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + total := 0 + + for scanner.Scan() { + fields := strings.Split(scanner.Text(), " | ") + if len(fields) != 2 { + panic("Read invalid input line") + } + inputFields := strings.Fields(fields[0]) + segMap := createPatternMap(arrays.MapStrToInt(inputFields, ParseField)) + log.Println(log.DEBUG, "Created pattern", segMap) + + outputFields := strings.Fields(fields[1]) + + fieldValue := 0 + fieldsCount := len(outputFields) + for idx, field := range outputFields { + fValue := segMap[ParseField(field)] + log.Printf(log.DIAGNOSTIC, "field %s => %d; ", field, fValue) + fieldValue += fValue * int(math.Pow10(fieldsCount-idx-1)) + } + total += fieldValue + + log.Println(log.DIAGNOSTIC) + log.Printf(log.DIAGNOSTIC, "Unscrambled %s => %d\n", outputFields, fieldValue) + lines++ + } + + log.Printf(log.NORMAL, "Read %d lines from input\n", lines) + log.Printf(log.NORMAL, "Calculated total is %d\n", total) +} + +// Represent a segment as a 7-bit value +/* Segment values are aligned as below +* aaaa +* b c +* b c +* dddd +* e f +* e f +* gggg + +* segment 'a' is found in [0, 2, 3, 5, 6, 7, 8, 9] but not [1, 4] +* segment 'b' is found in [0, 4, 5, 6, 8, 9] but not [1, 2, 3, 7] +* segment 'c' is found in [0, 1, 2, 3, 4, 7, 8, 9] but not [5, 6] +* segment 'd' is found in [2, 3, 4, 5, 6, 8, 9] but not [0, 1, 7] +* segment 'e' is found in [0, 2, 6, 8] but not [1, 3, 4, 5, 7, 9] +* segment 'f' is found in [0, 1, 3, 4, 5, 6, 7, 8, 9] but not [2] +* segment 'g' is found in [0, 2, 3, 5, 6, 8, 9] but not [1, 4, 7] + */ + +// parse a field from a string value to a bit-map int value (eg 'cda' to b1101 => 13) +func ParseField(field string) int { + value := 0 + for _, c := range field { + value += 1 << int(rune(c)-rune('a')) + } + return value +} + +// func getDefaultMap() map[int]int { +// return map[int]int{ +// ParseField("abcefg"): 0, +// ParseField("cf"): 1, +// ParseField("acdeg"): 2, +// ParseField("acdfg"): 3, +// ParseField("bcdf"): 4, +// ParseField("abdfg"): 5, +// ParseField("abdefg"): 6, +// ParseField("acf"): 7, +// ParseField("abcdefg"): 8, +// ParseField("abcdfg"): 9} +// } + +// Given a list of a all 10 possible 7-segment displays create a map that can be used to unscramble values +func createPatternMap(input []int) map[int]int { + // initialize our translation map that will convert from character to character (eg wire 'a' actually goes to segment 'c') + segMap := make(map[int]int) + + // first find and map all the values that can be known by counting segments + // find the only pattern that matches '1' + one := arrays.FindFirstInt(input, bits.ByBitCount(2)) + + // next find the one that matches '7' + seven := arrays.FindFirstInt(input, bits.ByBitCount(3)) + + // next find the one that matches '4' + four := arrays.FindFirstInt(input, bits.ByBitCount(4)) + + // the one that matches '8' will be the only one with 7 segments but tells us nothing about what those segments map to + eight := arrays.FindFirstInt(input, bits.ByBitCount(7)) + + // the two segments in '1' will match 'c' or 'f' but we don't know yet + // the additional segment in '7' will map to segment 'a' + segMap['a'] = seven - one + // the two additional segments in '4' will match 'b' or 'd' but we're not sure which ones yet + + // all of '0', '6' and '9' have 6 segments + // the six-segment field that shares only 1 side with '1' is '6' + six := arrays.FindFirstInt(input, func(v int) bool { return bits.ByBitCount(6)(v) && bits.MatchingOnBits(one, v) == 1 }) + + // and we know that the segment not in six is 'c' + segMap['c'] = eight - six + // and the other value 'f' will be the other segment in '1' + segMap['f'] = one - segMap['c'] + // but we can't yet place '0' or '9' with what we know + + // all of '2', '3', and '5' have 5 segments + // three is the only 5 segment field that matches 3 bits in '7' + three := arrays.FindFirstInt(input, func(v int) bool { return bits.ByBitCount(5)(v) && bits.MatchingOnBits(seven, v) == 3 }) + // the additional two segments are 'd' ang 'g' but we can't place them yet + // five is the other 5 segment field that matches 3 bits in '4' + five := arrays.FindFirstInt(input, func(v int) bool { return bits.ByBitCount(5)(v) && v != three && bits.MatchingOnBits(four, v) == 3 }) + // segments 'b', 'd', and 'g' can't yet be placed + // but segment 'e' is what we get by subtracting five from six + segMap['e'] = six - five + + // the last five segment number must be 2 + two := arrays.FindFirstInt(input, func(v int) bool { return bits.ByBitCount(5)(v) && v != three && v != five }) + + // segment 'g' is the only one that doesn't appear in '1', '4', or '7' and is not 'e' + segMap['g'] = eight - bits.BitwiseAndA(one, four, seven, segMap['e']) + // segment 'd' is '3' minus 'a', 'c', 'f', and 'g' + segMap['d'] = three - segMap['a'] - segMap['c'] - segMap['f'] - segMap['g'] + + // then we know that zero is eight minus 'd' + zero := eight - segMap['d'] + // and nine will be eight minus 'e' + nine := eight - segMap['e'] + + log.Println(log.DIAGNOSTIC, "segMap", segMap) + + return map[int]int{ + zero: 0, + one: 1, + two: 2, + three: 3, + four: 4, + five: 5, + six: 6, + seven: 7, + eight: 8, + nine: 9, + } +} diff --git a/day-8/main_test.go b/day-8/main_test.go new file mode 100644 index 0000000..c5e8e71 --- /dev/null +++ b/day-8/main_test.go @@ -0,0 +1,10 @@ +package main + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestDay8ParseField(t *testing.T) { + assert.EqualsInt(t, ParseField("abc"), 7, "should parse fields correctly") +} diff --git a/main_test.go.template b/main_test.go.template index 62153ec..ec00cc1 100644 --- a/main_test.go.template +++ b/main_test.go.template @@ -1,7 +1,7 @@ package main import ( - assert "AoC2021/utils/testing" + "AoC2021/utils/assert" "testing" ) diff --git a/makefile b/makefile index 06f2d7d..50f7f74 100644 --- a/makefile +++ b/makefile @@ -11,4 +11,7 @@ build: cd day-$(day); go run . test: - cd day-$(day); go test \ No newline at end of file + cd day-$(day); go test + +test-utils: + go test ./utils/* \ No newline at end of file diff --git a/utils/arrays/arrays.go b/utils/arrays/arrays.go new file mode 100644 index 0000000..5c2713c --- /dev/null +++ b/utils/arrays/arrays.go @@ -0,0 +1,49 @@ +package arrays + +func FindFirst(list []string, filter func(string) bool) string { + for _, item := range list { + if filter(item) { + return item + } + } + panic("No item found that matches criteria") +} + +func FindFirstInt(list []int, filter func(int) bool) int { + for _, item := range list { + if filter(item) { + return item + } + } + panic("No item found that matches criteria") +} + +func FindAll(list []string, filter func(string) bool) []string { + var filteredList []string + for _, item := range list { + if filter(item) { + filteredList = append(filteredList, item) + } + } + + return filteredList +} + +func FindAllInt(list []int, filter func(int) bool) []int { + var filteredList []int + for _, item := range list { + if filter(item) { + filteredList = append(filteredList, item) + } + } + + return filteredList +} + +func MapStrToInt(input []string, fn func(string) int) []int { + var retVals []int + for _, s := range input { + retVals = append(retVals, fn(s)) + } + return retVals +} diff --git a/utils/arrays/arrays_test.go b/utils/arrays/arrays_test.go new file mode 100644 index 0000000..e9c95a7 --- /dev/null +++ b/utils/arrays/arrays_test.go @@ -0,0 +1,47 @@ +package arrays + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestFindFirst(t *testing.T) { + list := []string{"foo", "bar", "baz"} + filter := func(s string) bool { return s == "baz" } + item := FindFirst(list, filter) + + assert.EqualsStr(t, item, "baz", "should find item baz") +} + +func TestFindFirstInt(t *testing.T) { + list := []int{1, 3, 4, 5} + filter := func(i int) bool { return i%2 == 0 } + item := FindFirstInt(list, filter) + + assert.EqualsInt(t, item, 4, "should find even number") +} + +func TestFindAll(t *testing.T) { + list := []string{"hello", "world", "foo"} + filter := func(s string) bool { return len(s) > 3 } + items := FindAll(list, filter) + + assert.EqualsInt(t, len(items), 2, "should filter lists of strings") +} + +func TestFindAllInt(t *testing.T) { + list := []int{1, 3, 99, 200, 9000} + filter := func(i int) bool { return i > 50 } + items := FindAllInt(list, filter) + + assert.EqualsInt(t, len(items), 3, "should filter lists of ints") +} + +func TestMapStrToInt(t *testing.T) { + list := []string{"alice", "bob", "carol"} + mapFn := func(s string) int { return int(s[0]) } + + items := MapStrToInt(list, mapFn) + + assert.EqualsInt(t, items[0], int('a'), "should run map function across all strings") +} diff --git a/utils/testing/assert.go b/utils/assert/assert.go similarity index 54% rename from utils/testing/assert.go rename to utils/assert/assert.go index 6375cf7..46a621c 100644 --- a/utils/testing/assert.go +++ b/utils/assert/assert.go @@ -9,9 +9,16 @@ func IsTrue(t *testing.T, expr bool, message string) { } } -func StrEquals(t *testing.T, actual, expected, message string) { +func EqualsStr(t *testing.T, actual, expected, message string) { if actual != expected { t.Logf("Expected value '%s' but got '%s': %s", expected, actual, message) t.Fail() } } + +func EqualsInt(t *testing.T, actual, expected int, message string) { + if actual != expected { + t.Logf("Expected value '%d' but got '%d': %s", expected, actual, message) + t.Fail() + } +} diff --git a/utils/bits/bits.go b/utils/bits/bits.go new file mode 100644 index 0000000..b5b0971 --- /dev/null +++ b/utils/bits/bits.go @@ -0,0 +1,54 @@ +package bits + +func GetBitCount(val int) int { + count := 0 + for val != 0 { + count += val & 1 + val >>= 1 + } + + return count +} + +// returns the number of matching bits in the two value's binary expression +func MatchingOnBits(v1 int, v2 int) int { + count := 0 + for v1 != 0 && v2 != 0 { + if v1&1 == 1 && v2&1 == 1 { + count += 1 + } + v1 >>= 1 + v2 >>= 1 + } + + return count +} + +func BitwiseAnd(v1 int, v2 int) int { + val := 0 + for block := 1; v1 != 0 || v2 != 0; block <<= 1 { + if v1&1 == 1 || v2&1 == 1 { + val += block + } + if v1 > 0 { + v1 >>= 1 + } + if v2 > 0 { + v2 >>= 1 + } + } + return val +} + +func BitwiseAndA(a ...int) int { + total := 0 + for _, i := range a { + total = BitwiseAnd(total, i) + } + + return total +} + +func ByBitCount(size int) func(int) bool { + return func(val int) bool { return GetBitCount(val) == size } +} diff --git a/utils/bits/bits_test.go b/utils/bits/bits_test.go new file mode 100644 index 0000000..3e5698c --- /dev/null +++ b/utils/bits/bits_test.go @@ -0,0 +1,28 @@ +package bits + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestGetBitCount(t *testing.T) { + assert.EqualsInt(t, GetBitCount(0), 0, "should count zero bits") + assert.EqualsInt(t, GetBitCount(15), 4, "should count bits") + assert.EqualsInt(t, GetBitCount(16), 1, "should not count empty bits") +} +func TestMatchingOnBits(t *testing.T) { + assert.EqualsInt(t, MatchingOnBits(5 /*101*/, 13 /*1101*/), 2, "should match 2 bits") +} + +func TestBitwiseAnd(t *testing.T) { + assert.EqualsInt(t, BitwiseAnd(7 /*111*/, 18 /*10010*/), 23 /*10111*/, "should bitwise add") +} + +func TestBitwiseAndA(t *testing.T) { + assert.EqualsInt(t, BitwiseAndA(1, 4, 8, 12), 13 /*1101*/, "should bitwise add lists") +} + +func TestByBitCount(t *testing.T) { + twoBitFilter := ByBitCount(2) + assert.IsTrue(t, twoBitFilter(17 /*10001*/), "should count bits") +} diff --git a/utils/filters/filters.go b/utils/filters/filters.go new file mode 100644 index 0000000..742734f --- /dev/null +++ b/utils/filters/filters.go @@ -0,0 +1,5 @@ +package filters + +func ByLength(size int) func(string) bool { + return func(val string) bool { return len(val) == size } +} diff --git a/utils/log/log.go b/utils/log/log.go index d7f319e..d36f2b4 100644 --- a/utils/log/log.go +++ b/utils/log/log.go @@ -3,9 +3,10 @@ package log import "fmt" const ( - NONE = 0 - NORMAL = 1 - DEBUG = 2 + NONE = 0 + NORMAL = 1 + DEBUG = 2 + DIAGNOSTIC = 3 ) var LOG_LEVEL = NORMAL