|
| 1 | +package com.markkovari.adventofcode.day05 |
| 2 | + |
| 3 | +import scala.util.matching.Regex.Match |
| 4 | + |
| 5 | +final case class Resource(start: Long, end: Long, kind: ResourceKind) |
| 6 | + |
| 7 | +enum ResourceKind: |
| 8 | + case Seed, Soil, Fertilizer, Water, |
| 9 | + Light, Temperature, Humidity, Location |
| 10 | + |
| 11 | +final case class ResourceMap( |
| 12 | + from: ResourceKind, |
| 13 | + to: ResourceKind, |
| 14 | + properties: Seq[Property] |
| 15 | +) |
| 16 | + |
| 17 | +final case class Property( |
| 18 | + destinationStart: Long, |
| 19 | + sourceStart: Long, |
| 20 | + rangeLength: Long |
| 21 | +): |
| 22 | + |
| 23 | + lazy val sourceEnd: Long = sourceStart + rangeLength - 1 |
| 24 | +end Property |
| 25 | + |
| 26 | +def findNext(resource: Resource, map: ResourceMap): Seq[Resource] = |
| 27 | + val ResourceMap(from, to, properties) = map |
| 28 | + val (newResources, explore) = |
| 29 | + val initial = (Seq.empty[Resource], Option(resource)) |
| 30 | + properties.foldLeft(initial) { |
| 31 | + case ((acc, Some(explore)), prop) => |
| 32 | + val Resource(start, end, _) = explore |
| 33 | + val propStart = prop.sourceStart |
| 34 | + val propEnd = prop.sourceEnd |
| 35 | + val underRange = Option.when(start < propStart)( |
| 36 | + Resource(start, Math.min(propStart - 1, end), to) |
| 37 | + ) |
| 38 | + val overlaps = |
| 39 | + start >= propStart && start <= propEnd |
| 40 | + || end >= propStart && end <= propEnd |
| 41 | + || start <= propStart && end >= propEnd |
| 42 | + val inRange = Option.when(overlaps) { |
| 43 | + val delay = prop.destinationStart - propStart |
| 44 | + Resource( |
| 45 | + Math.max(start, propStart) + delay, |
| 46 | + Math.min(end, propEnd) + delay, |
| 47 | + to |
| 48 | + ) |
| 49 | + } |
| 50 | + val aboveRange = Option.when(end > propEnd)( |
| 51 | + Resource(Math.max(start, propEnd + 1), end, to) |
| 52 | + ) |
| 53 | + (Seq(underRange, inRange, acc).flatten, aboveRange) |
| 54 | + case ((acc, None), _) => (acc, None) |
| 55 | + } |
| 56 | + Seq(newResources, explore).flatten |
| 57 | +end findNext |
| 58 | + |
| 59 | +object ResourceMap: |
| 60 | + def buildFromLines(lines: Seq[String]): Seq[ResourceMap] = |
| 61 | + def isRangeLine(line: String) = |
| 62 | + line.forall(ch => ch.isDigit || ch.isSpaceChar) |
| 63 | + lines |
| 64 | + .filter(line => |
| 65 | + !line.isBlank && |
| 66 | + (line.endsWith("map:") || isRangeLine(line)) |
| 67 | + ) |
| 68 | + .foldLeft(Seq.empty[(String, Seq[String])]) { |
| 69 | + case (acc, line) if line.endsWith("map:") => |
| 70 | + (line, Seq.empty) +: acc |
| 71 | + case (Seq((definition, properties), last*), line) => |
| 72 | + (definition, line +: properties) +: last |
| 73 | + } |
| 74 | + .flatMap(build) |
| 75 | + |
| 76 | + def build(map: String, ranges: Seq[String]): Option[ResourceMap] = |
| 77 | + val mapRow = map.replace("map:", "").trim.split("-to-") |
| 78 | + val properties = ranges |
| 79 | + .map(line => line.split(" ").flatMap(_.toLongOption)) |
| 80 | + .collect: |
| 81 | + case Array(startFrom, startTo, range) => |
| 82 | + Property(startFrom, startTo, range) |
| 83 | + def resourceKindOf(optStr: Option[String]) = |
| 84 | + optStr.map(_.capitalize).map(ResourceKind.valueOf) |
| 85 | + for |
| 86 | + from <- resourceKindOf(mapRow.headOption) |
| 87 | + to <- resourceKindOf(mapRow.lastOption) |
| 88 | + yield ResourceMap(from, to, properties.sortBy(_.sourceStart)) |
| 89 | +end ResourceMap |
| 90 | + |
| 91 | +object Seeds: |
| 92 | + private def parseSeedsRaw(line: String): Seq[Long] = |
| 93 | + if !line.startsWith("seeds:") then Seq.empty[Long] |
| 94 | + else |
| 95 | + line |
| 96 | + .replace("seeds:", "") |
| 97 | + .trim |
| 98 | + .split(" ") |
| 99 | + .flatMap(_.toLongOption) |
| 100 | + |
| 101 | + // parse seeds without range |
| 102 | + def parseWithoutRange(line: String): Seq[Resource] = |
| 103 | + parseSeedsRaw(line).map: start => |
| 104 | + Resource(start, start, ResourceKind.Seed) |
| 105 | + |
| 106 | + // parse seeds with range |
| 107 | + def parse(line: String): Seq[Resource] = |
| 108 | + parseSeedsRaw(line) |
| 109 | + .grouped(2) |
| 110 | + .map { case Seq(start, length) => |
| 111 | + Resource(start, start + length - 1, ResourceKind.Seed) |
| 112 | + } |
| 113 | + .toSeq |
| 114 | +end Seeds |
| 115 | + |
| 116 | +def calculate(seeds: Seq[Resource], maps: Seq[ResourceMap]): Long = |
| 117 | + def inner(resource: Resource): Seq[Resource] = |
| 118 | + if resource.kind == ResourceKind.Location then Seq(resource) |
| 119 | + else |
| 120 | + val map = maps.find(_.from == resource.kind).get |
| 121 | + findNext(resource, map).flatMap(inner) |
| 122 | + seeds.flatMap(inner).minBy(_.start).start |
| 123 | +end calculate |
| 124 | + |
| 125 | +type ParseSeeds = String => Seq[Resource] |
| 126 | + |
| 127 | +def solution(input: String, parse: ParseSeeds): Long = |
| 128 | + val lines = input.linesIterator.toSeq |
| 129 | + val seeds = lines.headOption.map(parse).getOrElse(Seq.empty) |
| 130 | + val maps = ResourceMap.buildFromLines(lines) |
| 131 | + calculate(seeds, maps) |
| 132 | + |
| 133 | +def part1(input: String): Long = |
| 134 | + solution(input, Seeds.parseWithoutRange) |
| 135 | + |
| 136 | +def part2(input: String): Long = |
| 137 | + solution(input, Seeds.parse) |
0 commit comments