Last active
December 5, 2024 02:09
-
-
Save FairlySadPanda/bd781a37465a096700a07d62ba0f3f65 to your computer and use it in GitHub Desktop.
Advent Of Code 2025 - Day 1 - Clean Golang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"flag" | |
"fmt" | |
"os" | |
"path/filepath" | |
"slices" | |
"strconv" | |
"strings" | |
) | |
// Try to always capture magic strings, no-matter how benign. | |
const ( | |
preOSXMac = "\r" | |
windows = "\r\n" | |
mostUnix = "\n" | |
delimiter = " " | |
) | |
type elfInput struct { | |
listA []int | |
listB []int | |
listBFrequencies map[int]int | |
} | |
type elfOutput struct { | |
TotalDistance int | |
Similarity int | |
} | |
func main() { | |
// In general declaring vars together is tidier, and doing it early is easier to | |
// read. | |
var ( | |
// Define two flags. We'll use these for file in and out locations. | |
inputFile = flag.String("input-file", "", "the input file for the job") | |
outputFile = flag.String("output-file", "", "the output file for the job") | |
) | |
// We need to parse the flags here, this adds data to the pointers above. | |
flag.Parse() | |
// This is just for safety. We should put validation code as early as possible; | |
// validation tends to clutter in functions that focus on doing things. | |
if ext := filepath.Ext(*inputFile); ext != ".txt" { | |
fmt.Printf("cannot load file with extension '%s': please use a txt file", ext) | |
os.Exit(1) | |
} | |
// Grab the rows from the input file. | |
rows, err := ingestFromInput(*inputFile) | |
if err != nil { | |
fmt.Printf("unable to ingest from input: %s", err) | |
os.Exit(1) | |
} | |
data, err := processRows(rows) | |
if err != nil { | |
fmt.Printf("unable to process rows: %s", err) | |
os.Exit(1) | |
} | |
out, err := json.Marshal(calculateResults(data)) | |
if err != nil { | |
fmt.Printf("marshalling output for output: %s", err) | |
os.Exit(1) | |
} | |
if err := outputData(out, *outputFile); err != nil { | |
fmt.Printf("unable to output to %s: %s", *outputFile, err) | |
os.Exit(1) | |
} | |
} | |
func calculateResults(data *elfInput) *elfOutput { | |
out := &elfOutput{} | |
// We only need to walk the slices once. | |
for idx, a := range data.listA { | |
b := data.listB[idx] | |
// We got max and min in Go 1.21, and this is a great use of them. | |
out.TotalDistance += max(a, b) - min(a, b) | |
// It's normal to evaluate the second result of the return for maps, | |
// but in this instance the default of the first result is useful. | |
freq := data.listBFrequencies[a] | |
if freq == 0 { | |
continue | |
} | |
out.Similarity += a * freq | |
} | |
return out | |
} | |
func ingestFromInput(input string) ([]string, error) { | |
// os.ReadFile can cope with relative inputs, so we can just grab the file here. | |
file, err := os.ReadFile(input) | |
if err != nil { | |
return nil, fmt.Errorf("unable read specified file '%s': %w", input, err) | |
} | |
var ( | |
fileStr = string(file) | |
newLine string | |
) | |
// It's always better to use a switch rather than an if-else chain. | |
// Idiomatic Go code tends to avoid the else symbol. | |
switch { | |
// Check for Windows first; it's not because Windows is better, it's that its format contains the other two. | |
case strings.Contains(fileStr, windows): | |
newLine = windows | |
case strings.Contains(fileStr, mostUnix): | |
newLine = mostUnix | |
// For absolute completeness, let's just make sure that the elves are not using pre-OSX Macintoshes. | |
case strings.Contains(fileStr, preOSXMac): | |
newLine = preOSXMac | |
default: | |
return nil, fmt.Errorf("unable read specified file '%s': unable to parse rows; no recognised newline format", input) | |
} | |
// We can break the file processing out into a sub function, as the processing does | |
// not really care about where the data came from. | |
return strings.Split(fileStr, newLine), nil | |
} | |
func processRows(rows []string) (*elfInput, error) { | |
out := &elfInput{ | |
listA: make([]int, 0, len(rows)), | |
listB: make([]int, 0, len(rows)), | |
listBFrequencies: map[int]int{}, | |
} | |
for idx, row := range rows { | |
cols := strings.Split(row, delimiter) | |
if len(cols) != 2 { | |
continue | |
} | |
a, err := strconv.Atoi(cols[0]) | |
if err != nil { | |
return nil, fmt.Errorf("unable to parse row %v: %w", idx, err) | |
} | |
b, err := strconv.Atoi(cols[1]) | |
if err != nil { | |
return nil, fmt.Errorf("unable to parse row %v: %w", idx, err) | |
} | |
out.listA = append(out.listA, a) | |
out.listB = append(out.listB, b) | |
out.listBFrequencies[b]++ | |
} | |
// For simplicity we can sort at the end, although there are certainly faster routes available. | |
slices.Sort(out.listA) | |
slices.Sort(out.listB) | |
return out, nil | |
} | |
func outputData(out []byte, output string) error { | |
// Writefile is a write not an append, so happily we don't need to delete old data first. | |
// Note that the 0o777 denotes the file's access rights (in Unix). | |
if err := os.WriteFile(output, out, 0o777); err != nil { | |
return fmt.Errorf("unable to write to file '%s': %w", output, err) | |
} | |
return nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment