Skip to content

Instantly share code, notes, and snippets.

@FairlySadPanda
Last active December 5, 2024 02:09
Show Gist options
  • Save FairlySadPanda/bd781a37465a096700a07d62ba0f3f65 to your computer and use it in GitHub Desktop.
Save FairlySadPanda/bd781a37465a096700a07d62ba0f3f65 to your computer and use it in GitHub Desktop.
Advent Of Code 2025 - Day 1 - Clean Golang
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