Skip to content

Commit

Permalink
feat: add 2023/07–09
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsknn committed Oct 16, 2024
1 parent 92c4a3c commit 5ccdbc2
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ Though note that the test assertions use my personal results
(using [Benjamin Peinhardt's _Learn OTP w/ Gleam_ repo](https://github.com/bcpeinhardt/learn_otp_with_gleam)).

- [`year_2023/day_06.gleam`](./src/year_2023/day_06.gleam) × [adventofcode.com/2023/day/6](https://adventofcode.com/2023/day/6)
- [`year_2023/day_07.gleam`](./src/year_2023/day_07.gleam) × [adventofcode.com/2023/day/7](https://adventofcode.com/2023/day/7)
- I'm quite pleased with the approach I came up with:
calculating the rank of a hand type by counting the number of different cards,
and calculating the rank of cards by handling the cards as (hexadecimal) numbers.
- [`year_2023/day_08.gleam`](./src/year_2023/day_08.gleam) × [adventofcode.com/2023/day/8](https://adventofcode.com/2023/day/8)
- [`year_2023/day_09.gleam`](./src/year_2023/day_09.gleam) × [adventofcode.com/2023/day/9](https://adventofcode.com/2023/day/9)
123 changes: 123 additions & 0 deletions src/year_2023/day_07.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import gleam/dict
import gleam/int
import gleam/list
import gleam/option
import gleam/order
import gleam/string

type Hand {
Hand(hand_type_rank: Int, cards_rank: Int, bid: Int)
}

pub fn part_1(input: String) -> Int {
input
|> parse_input
|> list.map(fn(pair) {
let #(hand, bid) = pair

Hand(
hand_type_rank: hand |> rank_hand_type,
cards_rank: hand |> rank_cards,
bid:,
)
})
|> calculate_total_bids
}

pub fn part_2(input: String) -> Int {
input
|> parse_input
|> list.map(fn(pair) {
let #(hand, bid) = pair

Hand(
hand_type_rank: hand |> rank_hand_type_with_jokers,
cards_rank: hand |> string.replace(each: "J", with: "1") |> rank_cards,
bid:,
)
})
|> calculate_total_bids
}

/// Returns `List(#(hand, bid))`
fn parse_input(input: String) -> List(#(String, Int)) {
input
|> string.trim
|> string.split("\n")
|> list.map(fn(line) {
let assert [hand, bid] = line |> string.split(" ")
let assert Ok(bid) = bid |> int.parse

#(hand, bid)
})
}

/// High number = high rank
fn rank_hand_type(hand: String) -> Int {
let card_counts =
hand
|> string.to_graphemes
|> list.fold(dict.new(), fn(counts, card) {
counts
|> dict.upsert(card, fn(prev_count) {
{ prev_count |> option.unwrap(0) } + 1
})
})
|> dict.values
|> list.sort(by: order.reverse(int.compare))

case card_counts {
[5] -> 7
[4, 1] -> 6
[3, 2] -> 5
[3, 1, 1] -> 4
[2, 2, 1] -> 3
[2, 1, 1, 1] -> 2
_ -> 1
}
}

/// High number = high rank
fn rank_hand_type_with_jokers(hand: String) -> Int {
let normal_rank = hand |> rank_hand_type

let other_cards =
hand
|> string.to_graphemes
|> list.unique
|> list.filter(fn(card) { card != "J" })
let alternative_ranks =
other_cards
|> list.map(fn(card) {
hand
|> string.replace(each: "J", with: card)
|> rank_hand_type
})

[normal_rank, ..alternative_ranks]
|> list.fold(0, int.max)
}

/// High number = high rank
fn rank_cards(hand: String) -> Int {
let hex =
hand
|> string.replace(each: "A", with: "E")
|> string.replace(each: "K", with: "D")
|> string.replace(each: "Q", with: "C")
|> string.replace(each: "J", with: "B")
|> string.replace(each: "T", with: "A")

let assert Ok(rank) = int.base_parse(hex, 16)
rank
}

fn calculate_total_bids(hands: List(Hand)) -> Int {
hands
|> list.sort(fn(a, b) {
int.compare(a.hand_type_rank, b.hand_type_rank)
|> order.break_tie(int.compare(a.cards_rank, b.cards_rank))
})
|> list.index_map(fn(hand, index) { hand.bid * { index + 1 } })
|> int.sum
}
113 changes: 113 additions & 0 deletions src/year_2023/day_08.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import gleam/dict.{type Dict}
import gleam/iterator.{type Iterator}
import gleam/list
import gleam/pair
import gleam/string

type Node {
Node(left: String, right: String)
}

pub fn part_1(input: String) -> Int {
let #(directions, nodes) = input |> parse_input

calculate_steps(
from: "AAA",
until: fn(next_label) { next_label == "ZZZ" },
following: directions,
and: nodes,
)
}

pub fn part_2(input: String) -> Int {
let #(directions, nodes) = input |> parse_input

let starting_labels =
nodes
|> dict.keys
|> list.filter(fn(label) { label |> string.ends_with("A") })

// Feels bold to assume that each starting label has only one valid ending label,
// but works at least with my puzzle input, so :shrug:
let steps =
starting_labels
|> list.map(fn(starting_label) {
calculate_steps(
from: starting_label,
until: fn(next_label) { next_label |> string.ends_with("Z") },
following: directions,
and: nodes,
)
})

least_common_multiple(of: steps)
}

fn parse_input(input: String) -> #(Iterator(String), Dict(String, Node)) {
let assert [directions, nodes] =
input
|> string.trim
|> string.split("\n\n")

let directions =
directions
|> string.to_graphemes
|> iterator.from_list
|> iterator.cycle

let nodes =
nodes
|> string.replace(each: " = (", with: ", ")
|> string.replace(each: ")", with: "")
|> string.split("\n")
|> list.fold(dict.new(), fn(nodes, line) {
let assert [label, left, right] = line |> string.split(", ")

nodes |> dict.insert(label, Node(left:, right:))
})

#(directions, nodes)
}

fn calculate_steps(
from starting_label: String,
until is_end_label: fn(String) -> Bool,
following directions: Iterator(String),
and nodes: Dict(String, Node),
) -> Int {
directions
|> iterator.fold_until(from: #(starting_label, 0), with: fn(acc, direction) {
let #(label, steps_taken) = acc

let assert Ok(node) = dict.get(nodes, label)
let next_label = case direction {
"L" -> node.left
___ -> node.right
}
let next_acc = #(next_label, steps_taken + 1)

case next_label |> is_end_label {
True -> list.Stop(next_acc)
False -> list.Continue(next_acc)
}
})
|> pair.second
}

/// Hat tip to https://stackoverflow.com/a/147539/1079869
fn least_common_multiple(of numbers: List(Int)) -> Int {
numbers |> list.fold(1, lcm)
}

/// Least common multiple
fn lcm(a: Int, b: Int) -> Int {
a * b / gcd(a, b)
}

/// Greatest common divisor
fn gcd(a: Int, b: Int) -> Int {
case b {
0 -> a
_ -> gcd(b, a % b)
}
}
58 changes: 58 additions & 0 deletions src/year_2023/day_09.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import gleam/bool
import gleam/int
import gleam/iterator
import gleam/list
import gleam/string

pub fn part_1(input: String) -> Int {
input
|> parse_input
|> list.map(fn(numbers) {
numbers
|> to_diff_sequences
|> list.filter_map(list.last)
|> int.sum
})
|> int.sum
}

pub fn part_2(input: String) -> Int {
input
|> parse_input
|> list.map(fn(numbers) {
numbers
|> to_diff_sequences
|> list.filter_map(list.first)
|> list.fold(0, fn(prev_number, number) { number - prev_number })
})
|> int.sum
}

fn parse_input(input: String) -> List(List(Int)) {
input
|> string.trim
|> string.split("\n")
|> list.map(fn(line) {
line
|> string.split(" ")
|> list.filter_map(int.parse)
})
}

fn to_diff_sequences(numbers: List(Int)) -> List(List(Int)) {
iterator.unfold(numbers, fn(prev_numbers) {
use <- bool.guard(
when: prev_numbers |> list.all(fn(n) { n == 0 }),
return: iterator.Done,
)

let next_numbers =
prev_numbers
|> list.window_by_2
|> list.map(fn(pair) { pair.1 - pair.0 })

iterator.Next(prev_numbers, next_numbers)
})
|> iterator.to_list
|> list.reverse
}
33 changes: 33 additions & 0 deletions test/year_2023/day_07_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import glacier/should
import gleam/result
import utils
import year_2023/day_07

pub fn part_1_with_example_input_test() {
day_07.part_1(example_input)
|> should.equal(6440)
}

pub fn part_2_with_example_input_test() {
day_07.part_2(example_input)
|> should.equal(5905)
}

pub fn full_input_test() {
utils.read_input(2023, 7)
|> result.map(fn(input) {
day_07.part_1(input)
|> should.equal(251_927_063)

day_07.part_2(input)
|> should.equal(255_632_664)
})
}

const example_input = "
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"
Loading

0 comments on commit 5ccdbc2

Please sign in to comment.