From 5e1e3a93f31a78ddfc29c31e286353b8f45b98aa Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Fri, 5 Dec 2025 12:22:15 -0500 Subject: [PATCH 1/8] day 5 2025 writeup --- docs/2025/puzzles/day05.md | 225 +++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 9c415694b..fd4cbbac1 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -2,10 +2,235 @@ import Solver from "../../../../../website/src/components/Solver.js" # Day 5: Cafeteria +by [Bulby](https://github.com/TheDrawingCoder-Gamer) + ## Puzzle description https://adventofcode.com/2025/day/5 +## Solution Summary + +* Parse input into list of ranges and list of ingredient IDs +* For part 1, count the amount of ingredients that are fresh +* For part 2, find the total size of all of the ranges combined + * This will require special handling to prevent overlap! + +## Parsing + +We'll need to end up doing more fancy stuff with our range type, so let's define it as a case class: + +```scala +final case class LRange(start: Long, end: Long) +``` + +Let's define our parsed input as a tuple: + +```scala +type Input = (List[LRange], List[Long]) +``` + +Parsing isn't that bad then. Note that here I'm using `runtimeChecked`, which isn't stable yet. +It will end up being stable when Scala 3.8.0 comes out, but if you're following along at home and don't want +to add `@experimental` to everything, you can replace it with `: @unchecked`. + +```scala +def parse(str: String): Input = + val Array(ranges, ings) = str.split("\n\n").runtimeChecked + ( + ranges.linesIterator.map: + case s"$s-$e" => LRange(s.toLong, e.toLong) + .toList, + ings.linesIterator.map(_.toLong).toList + ) +``` + +## Part 1 + +We need to see if a range contains an ingredient, so let's add a method to `LRange`: + +```scala +final case class LRange(start: Long, end: Long): + def contains(n: Long): Boolean = n >= start && n <= end +``` + +Then we can easily do part 1: + +```scala +def part1(input: String): Long = + val (ranges, ingredients) = parse(input) + ingredients.count(ing => ranges.exists(_.contains(ing))).toLong +``` + +## Part 2 + +Part 2 is a lot more complicated, but I've done [much worse](https://adventofcode.com/2021/day/22), so it wasn't too bad. + +My common library has implementations for ranges that can intersect and combine with each other, but I'll reimplement them here +just for completeness. + + +We'll need to add a way to count the amount of values in a range: +```scala +final case class LRange(start: Long, end: Long): + // ... + def size: Long = end - start + 1 +``` + +Alright, now the hard part: intersection and union. +We'll need to add a couple methods to `LRange` to handle intersection and union: + +```scala +final case class LRange(start: Long, end: Long): +``` +For intersection, there are more or less 4 cases: +* Overlaps on the left edge +* Overlaps on the right edge +* Overlaps internally +* Overlaps entirely + +Let's walk through each case. + +When we overlap on the left edge, it will look something like this: +``` + *--------* - Us +*-------* - Them +``` + +* Our start >= their start, <= their end +* Our end >= their start, >= their end + +When we overlap on the right edge, it will look something like this: +``` +*-------* - Us + *--------* - Them +``` + +* Our start <= their start, <= their end +* Our end >= their start, <= their end + +When we overlap interally, it will look something like this: + +``` +*--------------* - Us + *-------* - Them +``` + +* Our start <= their start, <= their end +* Our end >= their start, >= their end + +When we are entirely overlapped, it will look something like this: + +``` + *-------* - Us +*--------------* - Them +``` + +* Our start >= their start, <= their end +* Our end >= their start, <= their end + +In all cases, our start is <= their end, and our end is >= their start. +We can also observe that to get the overlap, we take the maximum start, and the minimum end. + +So let's add that intersect method: + +```scala + infix def intersect + ( + t: LRange + ): Option[LRange] = + Option.when(end >= t.start && start <= t.end): + LRange(start max t.start, end min t.end) +``` + +Now we needed that intersect to implement difference. + +The intersect will clamp any points that go outside our bound to our edge, meaning we can compare easier. + +We can do simple math to make the set - we find the points before and after the hole. If we didn't intersect at all, +then we can just return a singleton set with `this`. + +```scala + infix def - + ( + that: LRange + ): Set[LRange] = + this intersect that match + case Some(hole) => + var daSet = Set.empty[LRange] + // if start == hole.start then there won't be any points left before the hole starts + if start != hole.start then daSet += LRange(start, hole.start - 1) + // if end == hole.end then there won't be any points left after the hole ends + if end != hole.end then daSet += LRange(hole.end + 1, end) + daSet + case _ => Set(this) +``` + +Now, for part 2, we'll just need to iteratively combine all these ranges. + +```scala +def part2(input: String): Long = + val (ranges, _) = parse(input) + val combinedRanges = + ranges.foldLeft(Set.empty[LRange]): (acc, range) => + // remove the new range from everything first to prevent overlaps + val removed = acc.flatMap(_ - range) + // then add it seperately + removed + range + // toIterator to prevent Set from deduplicating our result + combinedRanges.toIterator.map(_.size).sum +``` + +## Final Code + +```scala +type Input = (List[LRange], List[Long]) + +final case class LRange(start: Long, end: Long): + def contains(n: Long): Boolean = n >= start && n <= end + + def size: Long = end - start + 1 + + infix def intersect + ( + t: LRange + ): Option[LRange] = + Option.when(end >= t.start && start <= t.end): + LRange(start max t.start, end min t.end) + + infix def - + ( + that: LRange + ): Set[LRange] = + this intersect that match + case Some(hole) => + var daSet = Set.empty[LRange] + if start != hole.start then daSet += LRange(start, hole.start - 1) + if end != hole.end then daSet += LRange(hole.end + 1, end) + daSet + case _ => Set(this) + +def parse(str: String): Input = + val Array(ranges, ings) = str.split("\n\n").runtimeChecked + ( + ranges.linesIterator.map: + case s"$s-$e" => LRange(s.toLong, e.toLong) + .toList, + ings.linesIterator.map(_.toLong).toList + ) + +def part1(input: String): Long = + val (ranges, ingredients) = parse(input) + ingredients.count(ing => ranges.exists(_.contains(ing))).toLong + +def part2(input: String): Long = + val (ranges, _) = parse(input) + val combinedRanges = + ranges.foldLeft(Set.empty[LRange]): (acc, range) => + val removed = acc.flatMap(_ - range) + removed + range + combinedRanges.toIterator.map(_.size).sum +``` + ## Solutions from the community Share your solution to the Scala community by editing this page. From 592b5184c7d2ac49600c077493e372be54e4be40 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Fri, 5 Dec 2025 12:47:02 -0500 Subject: [PATCH 2/8] fix weird wording in intersect part --- docs/2025/puzzles/day05.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index fd4cbbac1..9f4ea397a 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -142,9 +142,8 @@ So let's add that intersect method: LRange(start max t.start, end min t.end) ``` -Now we needed that intersect to implement difference. - -The intersect will clamp any points that go outside our bound to our edge, meaning we can compare easier. +We needed that intersect to implement difference. The intersect will clamp any points that go outside our bound to +our edge, meaning we can compare easier. We can do simple math to make the set - we find the points before and after the hole. If we didn't intersect at all, then we can just return a singleton set with `this`. From 8920649ac827b48893a9aea4426c2d1cd38ef66d Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Fri, 5 Dec 2025 17:16:12 -0500 Subject: [PATCH 3/8] Cats mention --- docs/2025/puzzles/day05.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 9f4ea397a..a452d3ff9 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -179,6 +179,14 @@ def part2(input: String): Long = combinedRanges.toIterator.map(_.size).sum ``` +It's worth noting that this is similar in concept to a disjoint set, which is a Set of Sets where every Set is disjoint from each other. +Here, I'm collecting the ranges and making sure that every range is disjoint from each other. + +Cats collections has an implementation of Disjoint Sets, and it also has an implementation of a Discrete Interval Encoding Tree, +which lets you hold a collection of ranges. This encodes types that are fully ordered, and have a predecessor and successor function. +This is true for all integral types. You could very easily rework the code to use Diet instead of disjoint sets, and infact it supports +extracting the disjoint ranges from itself. + ## Final Code ```scala From 87b32ad8d455fe9cf6bd3384c945ec1fce36213e Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sun, 7 Dec 2025 01:10:59 -0500 Subject: [PATCH 4/8] diff not union, no numeric range, link to diet sample --- docs/2025/puzzles/day05.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index a452d3ff9..2ebbb1655 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -23,6 +23,9 @@ We'll need to end up doing more fancy stuff with our range type, so let's define final case class LRange(start: Long, end: Long) ``` +We can't use `NumericRange` because getting the size returns an `Int`, and it will throw for ranges bigger than `Int.MaxValue`. +We have to handle ranges with a size greater than `Int.MaxValue`, so we'll need to roll our own here. + Let's define our parsed input as a tuple: ```scala @@ -65,7 +68,7 @@ def part1(input: String): Long = Part 2 is a lot more complicated, but I've done [much worse](https://adventofcode.com/2021/day/22), so it wasn't too bad. -My common library has implementations for ranges that can intersect and combine with each other, but I'll reimplement them here +My common library has implementations for ranges that can intersect and subtract each other, but I'll reimplement them here just for completeness. @@ -77,7 +80,7 @@ final case class LRange(start: Long, end: Long): ``` Alright, now the hard part: intersection and union. -We'll need to add a couple methods to `LRange` to handle intersection and union: +We'll need to add a couple methods to `LRange` to handle intersection and difference: ```scala final case class LRange(start: Long, end: Long): @@ -184,8 +187,8 @@ Here, I'm collecting the ranges and making sure that every range is disjoint fro Cats collections has an implementation of Disjoint Sets, and it also has an implementation of a Discrete Interval Encoding Tree, which lets you hold a collection of ranges. This encodes types that are fully ordered, and have a predecessor and successor function. -This is true for all integral types. You could very easily rework the code to use Diet instead of disjoint sets, and infact it supports -extracting the disjoint ranges from itself. +This is true for all integral types. I have a sample of Day 5 [implemented entirely using Diet and Range](https://github.com/TheDrawingCoder-Gamer/adventofcode2024/blob/02f203be69a8d31b48c5c6080e1642c4df51cbe6/core/shared/src/main/scala/gay/menkissing/advent/y2025/Day05.scala) +for those curious. ## Final Code From 8ce860c44694642e849d76088252a86237dade9c Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sun, 7 Dec 2025 11:16:52 -0500 Subject: [PATCH 5/8] toIterator is deprecated? --- docs/2025/puzzles/day05.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 2ebbb1655..7fcda7bad 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -79,7 +79,7 @@ final case class LRange(start: Long, end: Long): def size: Long = end - start + 1 ``` -Alright, now the hard part: intersection and union. +Alright, now the hard part: intersection and difference. We'll need to add a couple methods to `LRange` to handle intersection and difference: ```scala @@ -178,8 +178,8 @@ def part2(input: String): Long = val removed = acc.flatMap(_ - range) // then add it seperately removed + range - // toIterator to prevent Set from deduplicating our result - combinedRanges.toIterator.map(_.size).sum + // iterator to prevent Set from deduplicating our result + combinedRanges.iterator.map(_.size).sum ``` It's worth noting that this is similar in concept to a disjoint set, which is a Set of Sets where every Set is disjoint from each other. From 9171f3b236b7d7b20dfdd2acb54c245e4d0e152d Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sun, 7 Dec 2025 12:05:03 -0500 Subject: [PATCH 6/8] update diet permalink --- docs/2025/puzzles/day05.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 7fcda7bad..87f71cb32 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -187,7 +187,7 @@ Here, I'm collecting the ranges and making sure that every range is disjoint fro Cats collections has an implementation of Disjoint Sets, and it also has an implementation of a Discrete Interval Encoding Tree, which lets you hold a collection of ranges. This encodes types that are fully ordered, and have a predecessor and successor function. -This is true for all integral types. I have a sample of Day 5 [implemented entirely using Diet and Range](https://github.com/TheDrawingCoder-Gamer/adventofcode2024/blob/02f203be69a8d31b48c5c6080e1642c4df51cbe6/core/shared/src/main/scala/gay/menkissing/advent/y2025/Day05.scala) +This is true for all integral types. I have a sample of Day 5 [implemented entirely using Diet and Range](https://github.com/TheDrawingCoder-Gamer/adventofcode2024/blob/dcbe2e97d93f18649c98b9afb333dd9f0e8ebcde/core/shared/src/main/scala/gay/menkissing/advent/y2025/Day05.scala) for those curious. ## Final Code From 0769f0037c3be79de100947540c0ff6bbf6006b1 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sun, 7 Dec 2025 12:05:58 -0500 Subject: [PATCH 7/8] actually fix toIterator lmao --- docs/2025/puzzles/day05.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 87f71cb32..0988b7ed0 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -238,7 +238,7 @@ def part2(input: String): Long = ranges.foldLeft(Set.empty[LRange]): (acc, range) => val removed = acc.flatMap(_ - range) removed + range - combinedRanges.toIterator.map(_.size).sum + combinedRanges.iterator.map(_.size).sum ``` ## Solutions from the community From 0d0c903528b49056ddb9a2d3a9e0329433a8a413 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sun, 7 Dec 2025 12:42:22 -0500 Subject: [PATCH 8/8] replace diet sample with alt writeup link --- docs/2025/puzzles/day05.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2025/puzzles/day05.md b/docs/2025/puzzles/day05.md index 0988b7ed0..c11ff2d0f 100644 --- a/docs/2025/puzzles/day05.md +++ b/docs/2025/puzzles/day05.md @@ -187,7 +187,7 @@ Here, I'm collecting the ranges and making sure that every range is disjoint fro Cats collections has an implementation of Disjoint Sets, and it also has an implementation of a Discrete Interval Encoding Tree, which lets you hold a collection of ranges. This encodes types that are fully ordered, and have a predecessor and successor function. -This is true for all integral types. I have a sample of Day 5 [implemented entirely using Diet and Range](https://github.com/TheDrawingCoder-Gamer/adventofcode2024/blob/dcbe2e97d93f18649c98b9afb333dd9f0e8ebcde/core/shared/src/main/scala/gay/menkissing/advent/y2025/Day05.scala) +This is true for all integral types. I have a writeup of Day 5 [implemented entirely using Diet and Range](https://thedrawingcoder-gamer.github.io/aoc-writeups/2025/day05.html) for those curious. ## Final Code