this post was submitted on 05 Dec 2025
21 points (100.0% liked)

Advent Of Code

1199 readers
3 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

Day 5: Cafeteria

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

top 20 comments
sorted by: hot top controversial new old
[–] Chais@sh.itjust.works 2 points 4 days ago (1 children)

Again part 2 took me way longer than I would've liked and also than feels appropriate for the simplicity of the solution I finally came up with.
Turned out quite fast, thanks to the ranges.

Python

from pathlib import Path
from typing import List
from itertools import combinations

def parse_input(input: str) -> tuple[set[range], list[int]]:
    parts = input.split("\n\n")
    fresh = set((lambda r: range(int(r[0]), int(r[1]) + 1))(line.split("-")) for line in parts[0].splitlines())
    return (fresh, list(map(int, parts[1].splitlines())))


def merge_ranges(a: range, b: range) -> List[range]:
    if a.stop <= b.start or b.stop <= a.start:
        return [a, b]
    return [range(min(a.start, b.start), max(a.stop, b.stop))]


def part_one(input: str) -> int:
    fresh, available = parse_input(input)
    return len(list(filter(None, [any(i in r for r in fresh) for i in available])))


def part_two(input: str) -> int:
    fresh, _ = parse_input(input)
    while True:
        for a, b in combinations(fresh, 2):
            if len(m := merge_ranges(a, b)) == 1:
                fresh.remove(a)
                fresh.remove(b)
                fresh.add(m[0])
                break
        else:
            break
    return sum(map(len, fresh))


if __name__ == "__main__":
    input = Path("_2025/_5/input").read_text("utf-8")
    print(part_one(input))
    print(part_two(input))
[–] CameronDev@programming.dev 1 points 4 days ago (1 children)

That is nice and simple, power of python I guess. How quick was the pt2 solve? I could imagine that being pathalogically slow with the wrong ordering of inputs?

Eg: (99,100),(0,1),..., (95,96), (96,97), (97,98), (98,99)

[–] Chais@sh.itjust.works 2 points 4 days ago* (last edited 3 days ago)

I haven't timed it, but easily below a second.
Could that be optimised? Most certainly.

Due to the ranges being in a set, rather than a list, the input order doesn't matter anyway. And the set really does a lot of heavy lifting for making the code so concise. You'll need a bunch of boilerplate for list maintenance, especially if you continuously keep it sorted.
The set also removed 8 duplicates I had in the input.

[–] lwhjp@piefed.blahaj.zone 4 points 2 weeks ago

Haskell

IntSet was the wrong first choice for part 2 :3

import Control.Arrow  
import Data.Foldable  
import Data.Ix  

readInput :: [Char] -> ([(Int, Int)], [Int])  
readInput =  
  (map readRange *** (map read . tail))  
    . break (== "")  
    . lines  
  where  
    readRange = (read *** (read . tail)) . break (== '-')  

part1 (ranges, ids) = length $ filter (\id -> any (`inRange` id) ranges) ids  

part2 (ranges, _) = sum $ map rangeSize $ foldl' addRange [] ranges  
  where  
    addRange [] x = [x]  
    addRange (r : rs) x  
      | touching r x = addRange rs $ merge r x  
      | otherwise = r : addRange rs x  
    touching (a, b) (c, d) = not (b < c - 1 || a > d + 1)  
    merge (a, b) (c, d) = (min a c, max b d)  

main = do  
  input <- readInput <$> readFile "input05"  
  print $ part1 input  
  print $ part2 input  
[–] mykl@lemmy.world 4 points 2 weeks ago

Uiua

It took me a while to work out the merging of ranges, but I'm very pleased with the solution.

Run it here

D ← "3-5\n10-14\n16-20\n12-18\n\n1\n5\n8\n11\n17\n32"
# D ← &fras"2025day05.txt" # drop your input file on this pane and uncomment this line to test against your own data.

Parse ← ∩(βŠœβ‹•βŠΈβˆŠ+@0⇑10)Β°β–‘β‚‚βŠœβ–‘Β¬βŠΈβ¦·"\n\n"

Merge ← ⨬(
  ⨬(⊟        # -> distinct, keep both.
  | βŠ‚βŠ’βŸœ(β†₯∩⊣) # -> overlap, merge them.
  )β—‘(β‰€βŠ“βŠ£βŠ’)
| βŠ™β—Œ # -> inside, ignore it.
)β—‘(β‰€βˆ©βŠ£)

Ranges ← βŠ™β—Œβ₯⍜⊣(MergeβŠ™Β°βŠ‚)β—‘β‹…β§»βŠƒβ†™β†˜1β†β†―βˆž_2 # Merge pairs of ranges.

P₁ ← /+β‰‘βŒž(/β†₯β‰‘βŒŸ(β†§βŠ“βŒŸβ‰₯β‰€Β°βŠŸ))
Pβ‚‚ ← /+≑(+1/-)βŠ™β—Œ
βŠƒP₁ Pβ‚‚ Ranges Parse D
[–] Zikeji@programming.dev 4 points 2 weeks ago

Javascript

Short and sweet. Basically just handle all the range overlaps for part 2 and then do basic subtraction to get the answer.

spoiler

const input = require('fs').readFileSync('input-day5.txt', 'utf-8');

/** @typedef {[number, number]} Range */

/** @type {[Range[], number[]]} */
const [freshRanges, availableIngredients] = input.split("\n\n").map(l => l.split("\n")).map((l, i) => (i === 1) ? l.map(v => parseInt(v, 10)) : l.map(v => v.split('-').map(vv => parseInt(vv, 10))));


let freshOnHand = 0;

for (const ingredient of availableIngredients) {
    for (const [start, stop] of freshRanges) {
        if (ingredient >= start && ingredient <= stop) {
            freshOnHand += 1;
            break;
        }
    }
}

console.log(`Part 1 Answer: ${freshOnHand}`);

const sortedRanges = [...freshRanges].sort((a, b) => a[0] - b[0]);

const mergedRanges = [];
let current = sortedRanges[0];

for (let i = 1; i < sortedRanges.length; i++) {
    const [nextStart, nextEnd] = sortedRanges[i];
    
    if (nextStart <= current[1] + 1) {
        current = [current[0], Math.max(current[1], nextEnd)];
    } else {
        mergedRanges.push(current);
        current = [nextStart, nextEnd];
    }
}

mergedRanges.push(current);

const freshIngredientCount = mergedRanges.reduce((acc, [start, stop]) => acc + ((stop + 1) - start), 0)

console.log(`Part 2 Answer: ${freshIngredientCount}`);

[–] eco_game@discuss.tchncs.de 3 points 2 weeks ago (1 children)

Kotlin

I first solved the puzzle with my own horrible range-merging algorithm, had quite a few nasty off by one errors. I quickly replaced it with the "normal" merge-algorithm after getting the right solution.

If you really want to see my abomination, it's still in the commits on my repo.

Solution

class Day05 : Puzzle {

    val validIds = mutableSetOf<LongRange>()
    val ids = mutableListOf<Long>()

    override fun readFile() {
        val input = readInputFromFile("src/main/resources/a2025/day05.txt")
        val inputList = input.split("\n\n", limit = 2)
        validIds.addAll(
            inputList[0].lines().filter { it.isNotBlank() }
                .map { it.split("-").map { it.toLong() } }
                .map { LongRange(it[0], it[1]) }
        )
        ids.addAll(inputList[1].lines().filter { it.isNotBlank() }.map { it.toLong() })
    }

    override fun solvePartOne(): String {
        return ids.count { id -> validIds.any { id in it } }.toString()
    }

    override fun solvePartTwo(): String {
        val idsSorted = validIds.sortedBy { it.first }
        val merged = mutableSetOf<LongRange>()

        var current = idsSorted.first()
        for (i in 1..<idsSorted.size) {
            val next = idsSorted[i]

            if (next.first <= current.last) {
                current = LongRange(current.first, max(current.last, next.last()))
            } else {
                merged.add(current)
                current = next
            }
        }
        merged.add(current)
        return merged.sumOf { it.last + 1 - it.first }.toString()
    }
}

full code on Codeberg

[–] chunkystyles@sopuli.xyz 1 points 2 weeks ago

It's amusing just how similar our solutions are today. The only real issue I had today was not considering both sides of each range and only taking the last part of the range in the sort order.

var ingredientRanges: MutableList<LongRange> = mutableListOf()
var ingredients: MutableList<Long> = mutableListOf()

fun main() {
    val input = getInput(5)
    parseInput1(input)
    var count = 0L
    ingredientRanges.sortBy { it.first }
    var i = 0
    while (i < ingredientRanges.size) {
        var j = i + 1
        while (j < ingredientRanges.size && doRangesOverlap(ingredientRanges[i], ingredientRanges[j])) {
            ingredientRanges[i] = LongRange(ingredientRanges[i].first, max(ingredientRanges[i].last, ingredientRanges[j].last))
            j++
        }
        count += ingredientRanges[i].last - ingredientRanges[i].first + 1
        i = j
    }
    println(count)
}

fun parseInput1(input: String) {
    val split = input.split("\n\n")
    split[0].lines()
        .filter { it.isNotBlank() }
        .forEach {
            val range = it.split("-")
            ingredientRanges.add(LongRange(range[0].toLong(), range[1].toLong()))
        }
    split[1].lines()
        .filter { it.isNotBlank() }
        .forEach { ingredients.add(it.toLong()) }
}

fun doRangesOverlap(left: LongRange, right: LongRange): Boolean = right.first in left || right.first - 1 == left.last
[–] ystael@beehaw.org 2 points 2 weeks ago

Nothing interesting here. I was tripped up momentarily by the fact that Common Lisp sort, while it's a destructive operation, does not leave its input argument equal to its result! Typically you see either "nondestructive, returns a value" (Python sorted()) or "destructive, leaves the object in the final state" (Python list.sort()). Old school Lisp sort returns the sorted list and the input structure is not guaranteed to be meaningful afterward. Hope you weren't using that pair for anything; it now points to hell.

(ql:quickload :str)

(defun read-inputs (filename)
  (let* ((input-lines (uiop:read-file-lines filename))
         (range-lines (remove-if-not #'(lambda (s) (find #\- s)) input-lines))
         (id-lines (remove-if #'(lambda (s) (or (zerop (length s)) (find #\- s))) input-lines)))
    (flet ((parse-range (line) (mapcar #'parse-integer (str:split "-" line))))
      (cons (mapcar #'parse-range range-lines)
            (mapcar #'parse-integer id-lines)))))

(defun fresh? (fresh-ranges id)
  "Return the first fresh range containing id, or nil if there is no such range.
  Assumes the fresh ranges are sorted to enable early exit."
  (loop for fresh-range in fresh-ranges
        when (<= (car fresh-range) id (cadr fresh-range))
          return fresh-range
        when (> (car fresh-range) id)
          return nil
        finally (return nil)))

(defun range< (range1 range2)
  (destructuring-bind (a1 b1) range1
    (destructuring-bind (a2 b2) range2
      (or (< a1 a2)
          (and (= a1 a2) (< b1 b2))))))

(defun main-1 (filename)
  (destructuring-bind (fresh-ranges . ids) (read-inputs filename)
    (let ((sorted-fresh-ranges (sort fresh-ranges #'range<)))
      (loop for id in ids
            sum (if (fresh? sorted-fresh-ranges id) 1 0)))))

(defun consolidate (fresh-ranges)
  "Remove redundancy in a sorted list of fresh ranges: emit non-overlapping ranges."
  (let ((result nil)
        (current-range (car fresh-ranges)))
    (loop for fresh-range in (cdr fresh-ranges)
          do (if (<= (car fresh-range) (cadr current-range))
                 (setf current-range (list (car current-range)
                                           (max (cadr current-range) (cadr fresh-range))))
                 (progn
                   (setf result (cons current-range result))
                   (setf current-range fresh-range)))
          finally (setf result (cons current-range result)))
    result))

(defun main-2 (filename)
  (destructuring-bind (fresh-ranges . ids) (read-inputs filename)
    (let ((sorted-fresh-ranges (sort fresh-ranges #'range<)))
      (reduce #'+
              (mapcar #'(lambda (range) (1+ (- (cadr range) (car range))))
                      (consolidate sorted-fresh-ranges))))))
[–] EnEnCode@programming.dev 2 points 2 weeks ago

Well, I guess I've decided to do AoC this year. Trivial problem, though I shamelessly stole some range-merging code from AoC 2022 (it was when I was still well in the learning phase of Rust). If you can't look at old code and wonder WTF you were thinking, have you really gotten any better?

Solution (mostly uninteresting)

pub fn day05(input: &str) -> (u64, u64) {
    let mut part1 = 0;
    let mut part2 = 0;
    let mut ranges = Vec::new();
    let mut lines = input.trim().lines();
    for line in lines.by_ref() {
        if line.is_empty() {
            break;
        }
        let range = line
            .split_once('-')
            .map(|(l, h)| [l.parse::<u64>().unwrap(), h.parse::<u64>().unwrap()])
            .unwrap();
        ranges.push(range);
    }
    let ranges = combine_ranges(ranges);
    for idx in lines {
        for r in ranges.iter() {
            if (r[0]..=r[1]).contains(&idx.parse::<u64>().unwrap()) {
                part1 += 1;
                break;
            }
        }
    }
    for r in ranges {
        part2 += r[1] - r[0] + 1;
    }
    (part1, part2)
}

combine_ranges WARNING : Hideous, triggers Clippy, not blazingly fast

fn combine_ranges(ranges: Vec<[u64; 2]>) -> Vec<[u64; 2]> {
    let mut temp_ranges = ranges;
    let mut comp_ranges: Vec<[u64; 2]> = Vec::new();
    let mut run_again: bool = true;
    while run_again {
        run_again = false;
        comp_ranges = Vec::new();
        'outer: for i in 0..temp_ranges.len() {
            for j in 0..comp_ranges.len() {
                if temp_ranges[i][0] <= comp_ranges[j][0] && comp_ranges[j][1] <= temp_ranges[i][1]
                {
                    //temp covers all of comp
                    comp_ranges[j][0] = temp_ranges[i][0];
                    comp_ranges[j][1] = temp_ranges[i][1];
                    run_again = true;
                    continue 'outer;
                } else if comp_ranges[j][0] <= temp_ranges[i][0]
                    && temp_ranges[i][1] <= comp_ranges[j][1]
                {
                    //comp covers all of temp
                    run_again = true;
                    continue 'outer;
                } else if temp_ranges[i][0] <= comp_ranges[j][0]
                    && comp_ranges[j][0] <= temp_ranges[i][1]
                    && temp_ranges[i][1] <= comp_ranges[j][1]
                {
                    //grow left
                    comp_ranges[j][0] = temp_ranges[i][0];
                    run_again = true;
                    continue 'outer;
                } else if comp_ranges[j][0] <= temp_ranges[i][0]
                    && temp_ranges[i][0] <= comp_ranges[j][1]
                    && comp_ranges[j][1] <= temp_ranges[i][1]
                {
                    //grow right
                    comp_ranges[j][1] = temp_ranges[i][1];
                    run_again = true;
                    continue 'outer;
                }
            }
            comp_ranges.push(temp_ranges[i]);
        }
        temp_ranges = comp_ranges.clone();
    }

    comp_ranges
}

[–] GiantTree@feddit.org 2 points 1 week ago* (last edited 1 week ago)

Kotlin

I like this one. The first part could be solved trivially with a naive approach. The second part is also not too complicated.
I started out with a simple merging algorithm that would check all ranges, except the one to merge, for overlaps.
With clever sorting, I can skip the search for mergable ranges and just iterate in reverse and build larger and larger ranges.
I haven't tried to merge adjacent ranges like 1..2 and 3..4 to 1..4. The solution is fast enough already when JIT/C2 compiled (200-250 Β΅s).

Code on Github

Code

class Day05 : AOCSolution {
    override val year = 2025
    override val day = 5

    override fun part1(inputFile: String): String {
        val (freshIngredients, availableIngredients) = readResourceTwoParts(inputFile)

        val freshRanges = buildRanges(freshIngredients)

        val ingredientIds = availableIngredients.lines().filterNot(String::isEmpty).map(String::toLong)

        val count = ingredientIds.count { id -> freshRanges.any { range -> id in range } }

        return count.toString()
    }

    override fun part2(inputFile: String): String {
        val (freshIngredients, _) = readResourceTwoParts(inputFile)

        val freshRanges = buildRanges(freshIngredients)

        return freshRanges.sumOf { range ->
            range.last - range.first + 1
        }.toString()
    }

    private companion object {
        private fun buildRanges(ingredients: String): List<LongRange> {
            val lines = ingredients.lines()
            val ranges = MutableList(lines.size) { i ->
                val line = lines[i]
                val hyphen = line.indexOf('-')
                val lower = line.take(hyphen).toLong()
                val upper = line.substring(hyphen + 1).toLong()
                LongRange(lower, upper)
            }

            // Sort the ranges
            // The ones with the smallest ID and the least upper end
            // get sorted to the beginning.
            // This allows for easy merging, as overlapping ranges are always adjacent
            ranges.sortWith(Comparator { r1, r2 ->
                val first = r1.first.compareTo(r2.first)
                if (first != 0) {
                    first
                } else {
                    r1.last.compareTo(r2.last)
                }
            })

            // Merge adjacent ranges backwards, modifying the list in-place
            for (i in ranges.lastIndex downTo 1) {
                val lowerRange = ranges[i - 1]
                val upperRange = ranges[i]

                // The two ranges do overlap, because the tail of the first range
                // is in the second range.
                if (upperRange.first <= lowerRange.last) {
                    ranges[i - 1] = LongRange(
                        lowerRange.first,
                        maxOf(lowerRange.last, upperRange.last)
                    )
                    ranges.removeAt(i)
                }
            }

            return ranges
        }
    }
}

[–] janAkali@lemmy.sdf.org 2 points 2 weeks ago* (last edited 2 weeks ago)

Nim

Huh, I didn't expect two easy days in a row.
Part 1 is a range check. Part 2 is a range merge.

Runtime: ~720 Β΅s

type
  AOCSolution[T,U] = tuple[part1: T, part2: U]

proc merge[T](ranges: var seq[Slice[T]]) =
  ranges.sort(cmp = proc(r1, r2: Slice[T]): int = cmp(r1.a, r2.a))
  var merged = @[ranges[0]]
  for range in ranges.toOpenArray(1, ranges.high):
    if range.a <= merged[^1].b:
      if range.b > merged[^1].b:
        merged[^1].b = range.b
    else:
      merged.add range
  ranges = merged

proc solve(input: string): AOCSolution[int, int] =
  let chunks = input.split("\n\n")
  var freshRanges = chunks[0].splitLines().mapIt:
    let t = it.split('-'); t[0].parseInt .. t[1].parseInt

  freshRanges.merge()

  block p1:
    let availableFood = chunks[1].splitLines().mapIt(parseInt it)
    for food in availableFood:
      for range in freshRanges:
        if food in range:
          inc result.part1
          break

  block p2:
    for range in freshRanges:
      result.part2 += range.b-range.a+1

Full solution at Codeberg: solution.nim

[–] Pyro@programming.dev 2 points 2 weeks ago

Python

Not too bad though part2 could become difficult if you don't realize the simple method to merge overlapping intervals.

see code

# takes a list of (start, end) ranges and merges overlapping ones
def merge_overlapping_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
    # sort ranges by start so that overlaps are adjacent
    ranges.sort(key=lambda r: r[0])
    merged_ranges = []

    # keep track of a current range that could be merged into
    curr_start, curr_end = ranges[0]
    for start, end in ranges:
        if curr_start <= start <= curr_end:
            # overlap, extend current range
            curr_end = max(curr_end, end)
        else:
            # no overlap, save current range and record the new one
            merged_ranges.append((curr_start, curr_end))
            curr_start, curr_end = start, end
    # finally, don't forget to add the last range that's being tracked
    merged_ranges.append((curr_start, curr_end))

    return merged_ranges

def part1(data: str):
    # split input
    freshness_data, ingredients_data = data.split("\n\n")

    # parse data
    freshness_ranges = [tuple(map(int, f.split('-'))) for f in freshness_data.splitlines()]
    freshness_ranges = merge_overlapping_ranges(freshness_ranges)   # optimization: merge overlapping ranges
    ingredients = [int(i) for i in ingredients_data.splitlines()]

    # checks if an ingredient is fresh
    # this could've been done with binary search, but linear search is simpler and fast enough
    def is_fresh(ingredient: int) -> bool:
        for start, end in freshness_ranges:
            if start <= ingredient <= end:
                # found
                return True
            if start > ingredient:
                # quit early since we are past any range that could've contained the ingredient
                return False
        return False

    # count all fresh ingredients
    return sum(is_fresh(i) for i in ingredients)

def part2(data: str):
    # split input
    freshness_data, _ = data.split("\n\n")

    # parse data
    freshness_ranges = [tuple(map(int, f.split('-'))) for f in freshness_data.splitlines()]
    freshness_ranges = merge_overlapping_ranges(freshness_ranges)   # merge overlapping ranges

    # return total count of fresh ingredients
    # we add +1 because both start and end are inclusive
    return sum(end - start + 1 for start, end in freshness_ranges)

sample = """3-5
10-14
16-20
12-18

1
5
8
11
17
32"""
assert part1(sample) == 3
assert part2(sample) == 14

[–] tranzystorek_io@beehaw.org 2 points 2 weeks ago

Janet

> read part2 description
> immediately think of naive set solution
> look at input
> oh...

I honestly didn't expect to one-shot part2, I'm usually pretty bad with coming up with complex solutions and coding them up in first-try :P

[–] strlcpy@lemmy.sdf.org 2 points 2 weeks ago* (last edited 2 weeks ago)

C

Repo

Sweet one. Glad the merge could be done with one n^2^/2 scan and no sorting or moving, which will make the assembly port a bit easier. Speaking of which, I'm still at day 3 there, fighting not the puzzles but the 64K .COM file limit!

Code

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

#define MR	256

#define MIN(a,b)	((a)<(b)?(a):(b))
#define MAX(a,b)	((a)>(b)?(a):(b))

struct range { uint64_t lo, hi; };

static struct range rs[MR];
static int nrs;

int
main()
{
	uint64_t p1=0,p2=0, val;
	int i,j;
	char buf[64];

        for (; fgets(buf, sizeof(buf), stdin); nrs++) {
                assert(nrs < MR);
                if (sscanf(buf, "%lu-%lu", &rs[nrs].lo, &rs[nrs].hi) != 2)
                        break;
        }

	for (i=0; i<nrs-1; i++)
	for (j=i+1; j<nrs; j++)
		if (rs[i].lo <= rs[j].hi && rs[i].hi >= rs[j].lo) {
			rs[j].lo = MIN(rs[i].lo, rs[j].lo);
			rs[j].hi = MAX(rs[i].hi, rs[j].hi);
			rs[i].lo = rs[i].hi = 0;
		}

	while (scanf("%lu", &val) == 1)
		for (i=0; i<nrs; i++)
			if (val >= rs[i].lo && val <= rs[i].hi)
				{ p1++; break; }
	
	for (i=0; i<nrs; i++)
		if (rs[i].lo)
			p2 += rs[i].hi - rs[i].lo + 1;
	
	printf("05: %lu %lu\n", p1, p2);
}

[–] Deebster@programming.dev 2 points 2 weeks ago* (last edited 2 weeks ago)

Rust

I didn't look at the input at first so tried a naive version with sets, but it was too slow even for part one. I got smarter and wrote a version with less code that runs in under half a millisecond.

type Id = usize;

struct Ingredients {
    fresh: Vec<RangeInclusive<Id>>,
    available: HashSet<Id>,
}

impl Ingredients {
    fn is_fresh(&self, id: Id) -> bool {
        self.fresh.iter().any(|range| range.contains(&id))
    }

    fn num_fresh_available(&self) -> usize {
        self.available
            .iter()
            .filter(|&&id| self.is_fresh(id))
            .count()
    }

    fn num_fresh_all(&self) -> usize {
        self.fresh
            .iter()
            .map(|range| 1 + range.end() - range.start())
            .sum()
    }
}

fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> {
    if ranges.is_empty() {
        return ranges;
    }
    ranges.sort_by_key(|range| *range.start());

    let mut merged = Vec::new();
    let mut start = ranges[0].start();
    let mut end = ranges[0].end();
    for range in ranges.iter().skip(1) {
        if range.start() > end {
            merged.push(RangeInclusive::new(*start, *end));
            start = range.start();
            end = range.end();
        }
        if range.end() > end {
            end = range.end();
        }
    }
    merged.push(RangeInclusive::new(*start, *end));

    merged
}

impl FromStr for Ingredients {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let Some((fresh_str, avail_str)) = s.split_once("\n\n") else {
            bail!("missing divider");
        };

        let fresh = fresh_str
            .lines()
            .map(|l| -> Result<RangeInclusive<_>, Self::Err> {
                let (start, end) = l.split_once('-').ok_or_eyre("bad range")?;
                let start: Id = start.parse()?;
                let end: Id = end.parse()?;
                Ok(start..=end)
            })
            .collect::<Result<_, _>>()?;

        let available = avail_str
            .lines()
            .map(|l| l.parse())
            .collect::<Result<_, _>>()?;

        Ok(Ingredients {
            fresh: merge_ranges(fresh),
            available,
        })
    }
}

::: spoiler Full code


use std::{collections::HashSet, fs, ops::RangeInclusive, str::FromStr};

use color_eyre::eyre::{OptionExt, Report, Result, bail};

#[derive(Debug, PartialEq, Eq)]
struct Ingredients {
    fresh: Vec<RangeInclusive<Id>>,
    available: HashSet<Id>,
}

type Id = usize;

impl Ingredients {
    fn is_fresh(&self, id: Id) -> bool {
        self.fresh.iter().any(|range| range.contains(&id))
    }

    fn num_fresh_available(&self) -> usize {
        self.available
            .iter()
            .filter(|&&id| self.is_fresh(id))
            .count()
    }

    fn num_fresh_all(&self) -> usize {
        self.fresh
            .iter()
            .map(|range| 1 + range.end() - range.start())
            .sum()
    }
}

fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> {
    if ranges.is_empty() {
        return ranges;
    }
    ranges.sort_by_key(|range| *range.start());

    let mut merged = Vec::new();
    let mut start = ranges[0].start();
    let mut end = ranges[0].end();
    for range in ranges.iter().skip(1) {
        if range.start() > end {
            merged.push(RangeInclusive::new(*start, *end));
            start = range.start();
            end = range.end();
        }
        if range.end() > end {
            end = range.end();
        }
    }
    merged.push(RangeInclusive::new(*start, *end));

    merged
}

impl FromStr for Ingredients {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let Some((fresh_str, avail_str)) = s.split_once("\n\n") else {
            bail!("missing divider");
        };

        let fresh = fresh_str
            .lines()
            .map(|l| -> Result<RangeInclusive<_>, Self::Err> {
                let (start, end) = l.split_once('-').ok_or_eyre("bad range")?;
                let start: Id = start.parse()?;
                let end: Id = end.parse()?;
                Ok(start..=end)
            })
            .collect::<Result<_, _>>()?;

        let available = avail_str
            .lines()
            .map(|l| l.parse())
            .collect::<Result<_, _>>()?;

        Ok(Ingredients {
            fresh: merge_ranges(fresh),
            available,
        })
    }
}

fn part1(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let ingrd = Ingredients::from_str(&input).unwrap();
    Ok(ingrd.num_fresh_available())
}

fn part2(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let ingrd = Ingredients::from_str(&input).unwrap();
    Ok(ingrd.num_fresh_all())
}

fn main() -> Result<()> {
    color_eyre::install()?;

    println!("Part 1: {}", part1("d05/input.txt")?);
    println!("Part 2: {}", part2("d05/input.txt")?);
    Ok(())
}
[–] Gobbel2000@programming.dev 2 points 2 weeks ago

Rust

Tried to outsmart part 2 by not merging ranges but just subtracting overlapping ranges from the current range's size, but then overlapping overlaps are subtracted more than once. So I ended up merging the ranges. They are kept in a list that is sorted by their starting position. Also, transforming the inclusive ranges in the input into exclusive ranges made things quite a bit easier.

View on github

use std::ops::Range;

fn parse_input(input: &str) -> (Vec<Range<u64>>, Vec<u64>) {
    let ranges: Vec<_> = input
        .lines()
        .take_while(|l| !l.is_empty())
        .map(|l| {
            let (a, b) = l.split_once('-').unwrap();
            a.parse().unwrap()..b.parse::<u64>().unwrap() + 1
        })
        .collect();
    let nums = input
        .lines()
        .skip(ranges.len() + 1)
        .map(|n| n.parse().unwrap())
        .collect();
    (ranges, nums)
}

fn part1(input: String) {
    let (ranges, nums) = parse_input(&input);
    let count = nums
        .iter()
        .filter(|n| ranges.iter().any(|r| r.contains(n)))
        .count();
    println!("{count}");
}

fn part2(input: String) {
    let (ranges, _) = parse_input(&input);
    // Ranges are added to this Vec always sorted by start and non-overlapping
    let mut merged: Vec<Range<u64>> = Vec::with_capacity(ranges.len());
    for r in ranges {
        // Find index of first intersecting range
        let first_int_o = merged.iter().position(|m| {
            // Intersection range (if any)
            let int_start = r.start.max(m.start);
            let int_end = r.end.min(m.end);
            int_start < int_end
        });
        if let Some(first_int) = first_int_o {
            // Exclusive
            let last_int = merged.len()
                - merged
                    .iter()
                    .rev()
                    .position(|m| {
                        let int_start = r.start.max(m.start);
                        let int_end = r.end.min(m.end);
                        int_start < int_end
                    })
                    .unwrap();
            // New range replacing all intersecting ranges
            let new = r.start.min(merged[first_int].start)..r.end.max(merged[last_int - 1].end);
            merged[first_int] = new;
            for i in (first_int + 1)..last_int {
                merged.remove(i);
            }
        } else {
            // Does not overlap with anything. Find index that keeps sorting
            let idx = merged
                .iter()
                .position(|m| m.start > r.start)
                .unwrap_or(merged.len());
            merged.insert(idx, r);
        }
    }
    let count = merged.iter().map(|r| r.end - r.start).sum::<u64>();
    println!("{count}");
}

util::aoc_main!();
[–] gedhrel@lemmy.world 2 points 2 weeks ago

There's an alternative approach to merging ranges. Put starting and ending points into a heap then scan it, keeping track of the number of open ranges present. Something like this:

mergeRanges rs =
    let
        -- We put in this order so we count openings before closings
        starts = map (\(a, b) -> (a, -1)) rs
        ends = map (\(a, b) -> (b, 1)) rs
        heap = H.fromList (starts ++ ends) :: H.MinHeap (Int, Int)
     in
        mergeNo heap
  where
    -- not in a range currently
    mergeNo :: H.MinHeap (Int, Int) -> [R]
    mergeNo h =
        case H.view h of
            Nothing -> []
            Just ((a, -1), h') -> mergeYes a 1 h'
    -- in a range
    mergeYes :: Int -> Int -> H.MinHeap (Int, Int) -> [R]
    mergeYes start height h =
        let Just ((v, delta), h') = H.view h
            newHeight = height - delta
         in if newHeight == 0
                then (start, v) : mergeNo h'
                else mergeYes start newHeight h'
[–] Jayjader@jlai.lu 2 points 1 week ago

(Browser-based) Javascript

The good ol' range merge is back! I've done the past few years in Rust, which has first-class "support" for inclusive ranges, so I was dreading having to re-implement things. Turns out when you can ignore lifetimes things get simpler.

Code

function part1(inputText) {
  const [freshRangeDefs, availableIds] = inputText.trim().split('\n\n');
  const parsedIds = new Set(availableIds.trim().split('\n').map(strId => Number.parseInt(strId, 10)));
  const parsedRanges = freshRangeDefs.trim().split('\n').map(rangeDef => rangeDef.split('-').map(strBound => Number.parseInt(strBound, 10)));

  for (const id of parsedIds) {
    let fresh = false;
    for (const [lowerBound, upperBound] of parsedRanges) {
      if (lowerBound <= id && id <= upperBound) {
        fresh = true;
        break;
      }
    }
    if (!fresh) {
      parsedIds.delete(id)
    }
  }
  return parsedIds.size;
}

{
  const start = performance.now();
  const result = part1(document.body.textContent);
  const end = performance.now();
  console.info({
    day: 5,
    part: 1,
    time: end - start,
    result
  });
}

function part2(inputText) {
  const [freshRangeDefs] = inputText.trim().split('\n\n');
  const parsedRanges = freshRangeDefs.trim().split('\n').map(rangeDef => rangeDef.split('-').map(strBound => Number.parseInt(strBound, 10)));
  parsedRanges.sort(([lowerA, upperA], [lowerB, upperB]) => lowerA - lowerB);
  const merged = [parsedRanges[0]];
  for (const [parsedLower, parsedUpper] of parsedRanges) {
    let inserted = false;
    for (let mIndex = 0; mIndex < merged.length; mIndex++) {
      const [mLower, mUpper] = merged[mIndex];
      if (mUpper < parsedLower) {
        // parsed is completely "above" merged
        continue
      } else if (parsedUpper < mLower) {
        // parsed is completely "below" merged
        merged.splice(mIndex, 0, [parsedLower, parsedUpper]);
        inserted = true;
        break;
      } else {
        // parsed and merged intersect somehow/somewhere)
        merged[mIndex][0] = Math.min(parsedLower, mLower)
        merged[mIndex][1] = Math.max(parsedUpper, mUpper)
        inserted = true;
        break;
      }
    }
    if (!inserted) {
      // parsed is completely "above" ALL already-merged ranges
      merged.push([parsedLower, parsedUpper]);
    }
  }
  return merged.reduce((accu, [lower, upper]) => accu + upper - lower + 1, 0)
}
{
  const exampleText = `3-5
10-14
16-20
12-18

1
5
8
11
17
32
`
  const start = performance.now();
  //   const result = part2(exampleText);
  const result = part2(document.body.textContent);
  const end = performance.now();
  console.info({
    day: 5,
    part: 2,
    time: end - start,
    result
  });
}

[–] CameronDev@programming.dev 1 points 2 weeks ago

Rust

   #[test]
    fn test_y2025_day5_part2() {
        let input = std::fs::read_to_string("input/2025/day_5.txt").unwrap();
        let (fresh, _) = input.split_once("\n\n").unwrap();
        let mut fresh = fresh
            .lines()
            .map(|l| {
                let (p1, p2) = l.split_once("-").unwrap();
                (p1.parse::<usize>().unwrap(), p2.parse::<usize>().unwrap())
            })
            .collect::<Vec<(usize, usize)>>();

        fresh.sort_by_key(|a| a.0);

        let mut non_overlapping = vec![*fresh.first().unwrap()];

        for range in fresh[1..].iter() {
            let last = *non_overlapping.last().unwrap();
            if range.0 > last.1 {
                // println!("Non overlapping: {range:?} -> {last:?}");
                non_overlapping.push((range.0, range.1));
                continue;
            }
            if range.0 <= last.1 && range.1 > last.1 {
                // println!("Overlapping: {range:?} -> {last:?}");
                let new_last = (last.0, range.1);
                non_overlapping.pop();
                non_overlapping.push(new_last);
                continue;
            }
            // println!("{range:?} is entirely within {last:?}");
        }

        let mut count = 0;
        for r in &non_overlapping {
            count += r.1 - r.0 + 1;
        }
        assert_eq!(count, 352556672963116);
        println!("{}", count);
    }

Took me way longer than it should have to work out pt2, got tripped up by a < instead of <=.