Spyke
advent_of_codeΒ·Advent Of CodebyCameronDev

πŸŽ„ - 2025 DAY 5 SOLUTIONS - πŸŽ„

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

View original on programming.dev

Javascript

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

::: spoiler 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}`);

:::

4

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  
4
mykl
lemmy.world

Uiua

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

::: spoiler merging ranges Merging all the ranges first makes both parts faster. Sorting the ranges first means that you only need to compare adjacent pairs of ranges to decide whether they need to remain separate, or can be merged. If the end of the next range is < end of this range you can drop it; if the start of the next range < end of this range replace this with a new range of (start of this range, max of both ends); otherwise just add next to the merged list and repeat.. :::

(EDIT: I realised that Part1 was really slow, so rewrote it, and total time dropped from 140ms to about 4ms, which is nice.)

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_"          # split at \n\n; find all digits.
Merge  ← ⨬(⨬(⊟|βŠ‚βŠ’βŸœ(β†₯∩⊣))β—‘(β‰€βŠ“βŠ£βŠ’)|βŠ™β—Œ)β—‘(β‰€βˆ©βŠ£) # distinct/overlap/contained.
Ranges ← βŠ™β—Œβ₯⍜⊣(MergeβŠ™Β°βŠ‚)β—‘β‹…β§»βŠƒβ†™β†˜1β†β†―βˆž_2      # Merge pairs of ranges.

P₁ ← /+β‰ βˆ©βŒŸ(Λœβ¨‚1˜⊞β‰₯)βœβ‰Β°βŠŸ # membership test
Pβ‚‚ ← /+≑(+1/-)βŠ™β—Œ       # range counts
&p⍜now(βŠƒP₁ Pβ‚‚ Ranges Parse D)
4

It turns out that merging isn't necessary, it's simpler and slightly faster to just sort by starts. Part 2 then needs to remove any overlaps between adjacent ranges by setting end of each to min(end of this, start of next-1).

# AOC 2025 day 5
# 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 ← Β°βŠŸβ‰β†β†―βˆž_2∩(βŠœβ‹•βŠΈβˆŠ+@0⇑10)Β°$"_\n\n_" # split at \n\n; find all digits; sort ranges.

P₁ ← /+≑(/β†₯)β†§βŠ“βŒŸβ‰₯β‰€βˆ©Β€       # compare each against start/end arrays
Pβ‚‚ ← βŠ™β—Œ/+-⟜(β†§βŠ“β¬šβˆžβ†»β‚(+1\β†₯)) # unmerge the ranges! simpler to count.
⍜now (βŠƒP₁ Pβ‚‚ Parse D)
1

Python

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

::: spoiler 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

:::

3
discuss.tchncs.de

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.

::: spoiler 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

3

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
1

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?

::: spoiler 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)
}

:::

::: spoiler 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
}

:::

2

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

2

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 ::: spoiler 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
        }
    }
}

:::

2

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

2

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 <=.

2

Go

I started by not sorting the ranges in the input stream and try to merge everything together. It didn't work, but the same algorithm with sorted input does. I guess I'm missing something important, but I tested my code and can't find the corner case that make it fail. Bah...

::: spoiler day05.go

package main

import (
	"aoc/utils"
	"cmp"
	"fmt"
	"slices"
	"strconv"
	"strings"
)

type idrange struct {
	start, end int
}

func (rng idrange) contains(entry int) bool {
	return rng.start <= entry && entry <= rng.end
}

func (rng idrange) length() int {
	return (rng.end - rng.start) + 1
}

type rangeSet []idrange

func (r *rangeSet) addRange(rng idrange) {
	containsStartIdx := slices.IndexFunc(*r, func(elem idrange) bool {
		return elem.contains(rng.start)
	})
	containsEndIdx := slices.IndexFunc(*r, func(elem idrange) bool {
		return elem.contains(rng.end)
	})

	// The range overlaps to other ranges.
	if containsStartIdx != -1 && containsEndIdx != -1 {
		// If it is in fact contained inside one range, it is ignored.
		if containsStartIdx == containsEndIdx {
			return
		}

		before := (*r)[containsStartIdx]
		after := (*r)[containsEndIdx]
		before.end = after.end
		(*r)[containsStartIdx] = before
		*r = slices.Delete(*r, containsStartIdx+1, containsEndIdx+1)
	} else if containsEndIdx != -1 {
		// If the range's end overlaps with another range, that range is
		// extended on the front to start like the range in argument.
		after := (*r)[containsEndIdx]
		after.start = rng.start
		(*r)[containsEndIdx] = after

		smallestAfterIdx := slices.IndexFunc(*r, func(elem idrange) bool {
			return rng.start < elem.start
		})

		if smallestAfterIdx != -1 && smallestAfterIdx != containsEndIdx+1 {
			*r = slices.Delete(*r, smallestAfterIdx, containsEndIdx)
		}
	} else if containsStartIdx != -1 {
		// If the range's start overlaps with another range, that range is
		// extended on the back to start like the range in argument.
		before := (*r)[containsStartIdx]
		before.end = rng.end
		(*r)[containsStartIdx] = before

		smallestAfterIdx := slices.IndexFunc(*r, func(elem idrange) bool {
			return rng.end < elem.end
		})

		if smallestAfterIdx != -1 && smallestAfterIdx != containsStartIdx+1 {
			*r = slices.Delete(*r, containsStartIdx+1, smallestAfterIdx)
		}
	} else {
		// If the range is standalone, it is added at the right position.
		afterIdx := slices.IndexFunc(*r, func(elem idrange) bool {
			return rng.start < elem.start
		})

		if afterIdx == -1 {
			*r = append(*r, rng)
		} else {
			*r = slices.Insert(*r, afterIdx, rng)
		}
	}
}

func (r rangeSet) contains(entry int) bool {
	for _, rng := range r {
		if rng.contains(entry) {
			return true
		}
	}

	return false
}

func (r rangeSet) length() int {
	sum := 0
	for _, rng := range r {
		sum += rng.length()
	}
	return sum
}

func parseRange(line string) (idrange, error) {
	bounds := strings.Split(line, "-")
	start, err1 := strconv.Atoi(bounds[0])
	end, err2 := strconv.Atoi(bounds[1])

	if err1 != nil {
		return idrange{}, err1
	}
	if err2 != nil {
		return idrange{}, err2
	}

	return idrange{
		start: start,
		end:   end,
	}, nil
}

func parseRangeSet(input chan string) (rangeSet, error) {
	rangeSet := rangeSet{}
	ranges := []idrange{}

	for line := range input {
		if line == "" {
			break
		}

		rng, err := parseRange(line)
		if err != nil {
			return nil, err
		}

		ranges = append(ranges, rng)
	}

	slices.SortFunc(ranges, func(a, b idrange) int {
		return cmp.Compare(a.start, b.start)
	})

	for _, rng := range ranges {
		rangeSet.addRange(rng)
	}

	return rangeSet, nil
}

func getNumberChannel(input chan string) chan int {
	ch := make(chan int, cap(input))

	go func() {
		for line := range input {
			num, _ := strconv.Atoi(line)
			ch <- num
		}
		close(ch)
	}()

	return ch
}

func stepOne(input chan string) (int, error) {
	rngSet, errParse := parseRangeSet(input)
	if errParse != nil {
		return 0, errParse
	}

	numCh := getNumberChannel(input)
	count := 0

	for entry := range numCh {
		if rngSet.contains(entry) {
			count++
		}
	}

	return count, nil
}

func stepTwo(input chan string) (int, error) {
	rngSet, err := parseRangeSet(input)
	if err != nil {
		return 0, err
	}

	return rngSet.length(), nil
}

func main() {
	inputFile := utils.FilePath("day05.txt")
	utils.RunStep(utils.ONE, inputFile, stepOne)
	utils.RunStep(utils.TWO, inputFile, stepTwo)
}

:::

2

Futhark

As always with futhark, first the script that converts the input into futhark-format:

::: spoiler sed script and example output

1i [
s/\([0-9]\+\)-\([0-9]\+\)/\[\1, \2],/g
s/^\([0-9]\+\)$/\1,/
s/^$/] [/
$i ]
$d

this converts the example to this:

[
[3, 5],
[10, 14],
[16, 20],
[12, 18],
] [
1,
5,
8,
11,
17,
32,
]

:::

I ended up writing a RangeSet implementation, currently considering publishing this as a library later. The RangeSet-Implementation makes it very large, but futhark makes it run blazingly fast πŸš€ . /s

Some fiddling with existential size types made me learn the type system even better.

I discovered that futhark has a multicore-backend, which makes it run faster. :]

::: spoiler Solution with RangeSet implementation

def fst 'a 'b ((a, _b): (a, b)): a = a
def snd 'a 'b ((_a, b): (a, b)): b = b
def count 'a (p: a -> bool) (xs: []a): i64 = xs |> map (p >-> i64.bool) |> reduce (+) 0
def array2Pair 'a (pair: [2]a): (a, a) = (pair[0], pair[1])
def pair2Array 'a (p: (a, a)): [2]a = [fst p, snd p]

def rangeContains (range: [2]i64) (x: i64): bool = x >= range[0] && x <= range[1]

def isInRanges (ranges: [][2]i64) (x: i64): bool = any (`rangeContains` x) ranges

def part1 (ranges: [][2]i64) (ids: []i64): i64 = count (isInRanges ranges) ids

def insertAt [n] 'a (xs: [n]a) (insertionIndex: i64) (x: a): [n+1]a
  = let lookup = \ i -> 
      if i == insertionIndex
      then x
      else 
        if i < insertionIndex
        then xs[i]
        else xs[i-1] in
    tabulate (n + 1) lookup

def merge [n] 'a (xs: [n]a) (f: a -> a -> a) (ia: i64) (ib: i64): [n-1]a
  = let imin = i64.min ia ib in
    let imax = i64.max ia ib in
    tabulate (n - 1) \ i ->
      if i < imin
      then xs[i]
      else 
        if i == imin
        then f xs[imin] xs[imax]
        else if i < imax
          then xs[i]
          else xs[i+1]


-- invariant: fst <= snd
-- both ends are inclusive
type Range = (i64, i64) 

type~ RangeSet = ?[n]. #RangeSet [n]Range

def Range_Start = fst
def Range_End = snd
def Range_Overlaps (a: Range) (b: Range): bool 
  = Range_Start a <= Range_End b && Range_Start b <= Range_End a

-- Only works with overlapping ranges

def Range_Merge ((a_start, a_end): Range) ((b_start, b_end): Range): Range
  = (i64.min a_start b_start, i64.max a_end b_end)

def Range_Size ((start, end): Range): i64 = end - start + 1

def RangeSet_Empty: RangeSet = #RangeSet []
def RangeSet_Singleton (r: Range): RangeSet = #RangeSet [r]
def RangeSet_Unpack ((#RangeSet rs): RangeSet): ?[n].[n]Range = rs

def RangeSet_InsertionIndex (r: Range) ((#RangeSet rs): RangeSet): i64
  = fst
    ( loop (low, high) = (0, length rs)
      while low != high
      do 
        let middle = low + ((high - low) / 2) in
        let middleRange = rs[middle] in
        if Range_Start middleRange < Range_Start r
          then (middle + 1, high)
          else (low, middle)
    )


def RangeSet_IndicesMergeable (a: i64) (b: i64) ((#RangeSet rs): RangeSet): bool
  = let ra = rs[a] in
    let rb = rs[b] in
    ra `Range_Overlaps` rb

def RangeSet_NormalizeAtIndex (i: i64) ((#RangeSet rs): RangeSet): RangeSet
  = ( loop (current, (position, continue)) = (rs, (i, true))
      while continue
      do
        if position != 0 && RangeSet_IndicesMergeable (position-1) position (#RangeSet current)
        then (merge current Range_Merge (position-1) position, (position - 1, true))
        else 
          if position + 1 != length current && RangeSet_IndicesMergeable (position+1) position (#RangeSet current)
          then (merge current Range_Merge (position+1) position, (position, true))
          else (current, (position, false))
    )
    |> fst
    |> \ rs -> #RangeSet rs

def RangeSet_Add (r: Range) ((#RangeSet rs): RangeSet): RangeSet
  = let insertionIndex = RangeSet_InsertionIndex r (#RangeSet rs) in
    let rs' = (rs `insertAt` insertionIndex) r in
  RangeSet_NormalizeAtIndex insertionIndex (#RangeSet rs')

def buildRangeSet [n] (rangeArrays: [n][2]i64): RangeSet
  = let ranges = map array2Pair rangeArrays in
    loop set = copy RangeSet_Empty
    for range in ranges
    do RangeSet_Add range set

def part2 (rangeSet: RangeSet): i64
  = rangeSet
  |> RangeSet_Unpack
  |> map Range_Size
  |> reduce (+) 0

def main (ranges: [][2]i64) (ids: []i64) 
  = let rangeSet = buildRangeSet ranges in
  (part1 ranges ids, part2 rangeSet)

:::

2

(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.

::: spoiler 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
  });
}

:::

2
Chais
sh.itjust.works

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))
2
CameronDevreply
programming.dev

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)

1

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.

2

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!();
2

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(())
}
2

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!

::: spoiler 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);
}

:::

2

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'
2

You reached the end