Skip to main content

· 3 min read

My solution for https://adventofcode.com/2022/day/13

import zio.*
import zio.stream.*

import scala.annotation.tailrec

object Day13 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

// My beautiful parser only works on single digits 😭
// Fix it? NO!
// Hack the encoding!
// Example, turns "10" into ":" which is 10 higher than "0" as a Char
extension (s: String) {
def needsEncoding(line: String): List[String] = {
line
.sliding(2)
.filterNot(c => c.contains('['))
.filterNot(c => c.contains(']'))
.filterNot(c => c.contains(','))
.toList
}

@tailrec
def encodeLoop(str: String, toReplace: List[String]): String = toReplace match {
case Nil => str
case h :: Nil => str.replace(h, (h.toInt + 48).toChar.toString)
case h :: t => encodeLoop(str.replace(h, (h.toInt + 48).toChar.toString), t)
}

def rawPacket: List[Any] = {
val hacked = encodeLoop(s, needsEncoding(s))
hacked.toList.drop(1).dropRight(1).map(_.toString).filterNot(_ == ",").map {
case "[" => "["
case "]" => "]"
case n => n.toCharArray.head - 48
}
}
}

// Handy methods to go back to the String version of the input
// for when you don't realize you've been parsing 10 as 1,0 for a day,
// and you start questioning your life and what it all means.
extension (l: List[Any]) {
def subPacket(la: List[Any]): String = la
.map {
case e: Int => e.toString
case l: List[Any] => "[" + subPacket(l) + "]"
}
.mkString(",")

def toRawPacket: String = "[" + subPacket(l) + "]"
}

// Good for data that occupies one character width 😃
@tailrec
def parsePacket(lst: List[Any]): List[Any] = {
if (lst.contains("[")) {
val indexed = lst.zipWithIndex
val open = indexed.findLast(_._1 == "[").get._2
val close = indexed.drop(open).find(_._1 == "]").get._2
val spliced =
(lst.take(open) :+ lst.slice(open + 1, close)) ++ lst.drop(close + 1)
if (spliced.contains("[")) {
parsePacket(spliced)
} else {
spliced
}
} else lst

}

// Did we succeed? Did we fail? Or did we just *maybe* fail?
def innerCompare(packets: (List[Any], List[Any])): Option[Boolean] = {
if (packets._1.isEmpty && packets._2.nonEmpty) return Some(true)
if (packets._1.nonEmpty && packets._2.isEmpty) return Some(false)

(for {
left <- packets._1.headOption
right <- packets._2.headOption
} yield {
(left, right) match {
case (l: Int, r: Int) if l == r => innerCompare(packets._1.tail, packets._2.tail)
case (l: Int, r: Int) => Some(l < r)
case (_: List[Any], r: Int) => innerCompare(packets._1, List(r) +: packets._2.drop(1))
case (l: Int, _: List[Any]) => innerCompare(List(l) +: packets._1.drop(1), packets._2)
case (l: List[Any], r: List[Any]) => innerCompare(l, r)
}
}).flatten

}

// Where the magic happens
def compare(packets: (List[Any], List[Any])): Boolean = {
if (packets._1.isEmpty && packets._2.nonEmpty) return true
if (packets._1.nonEmpty && packets._2.isEmpty) return false

(for {
left <- packets._1.headOption
right <- packets._2.headOption
} yield {
(left, right) match {
case (l: Int, r: Int) if l == r => compare(packets._1.tail, packets._2.tail)
case (l: Int, r: Int) => l < r
case (l: List[Any], r: Int) => compare(packets._1, List(r) +: packets._2.tail)
case (l: Int, r: List[Any]) => compare(List(l) +: packets._1.tail, packets._2)
case (l: List[Any], r: List[Any]) => {
// Need to distinguish against a fail from empty vs a fail from actually failing
// in order to recurse and not lose information
innerCompare(l, r).getOrElse(compare(packets._1.tail, packets._2.tail))
}
}
}).getOrElse(false)

}

val data = "day-13.test"

// I'm not even going to clean this up.
override def run: ZIO[Any, Any, Any] = for {
_ <- source(data)
.split(_ == "")
.map(_.map(_.rawPacket))
.map(_.map(parsePacket))
.map(c => (c.head, c.last))
.map(compare)
.zipWithIndex
.filter(_._1 == true)
.map(_._2 + 1)
.runSum
.debug("Answer pt 1")
_ <- (source(data) ++ ZStream("[[2]]", "[[6]]"))
.filterNot(_ == "")
.map(_.rawPacket)
.map(parsePacket)
.runCollect
.map(
_.sortWith((a, b) => compare(a, b)).zipWithIndex
.filter((l, i) => l == List(List(2)) || l == List(List(6)))
.map(_._2 + 1)
.product
)
.debug("Answer pt 2")
} yield ExitCode.success

}

· 3 min read

My solution for https://adventofcode.com/2022/day/12

import zio.*
import zio.stream.*

object Day12 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

type Position = (Int, Int)

// Naming stuff is hard
case class Thing(
position: Position,
visited: Seq[Position],
altitude: Int,
target: Position
) {

val hasArrived: Boolean = position == target
val stepsTaken: Int = visited.length

// Safely bounded neighbors
def neighbors(grid: Array[Array[Int]]): Seq[Thing] = {
val up: Position = (position._1, position._2 + 1)
val down: Position = (position._1, position._2 - 1)
val left: Position = (position._1 - 1, position._2)
val right: Position = (position._1 + 1, position._2)
Seq(
Thing.neighborApply(up, grid),
Thing.neighborApply(left, grid),
Thing.neighborApply(down, grid),
Thing.neighborApply(right, grid)
).flatten
}

// Don't visit places we've been, or are geographically locked from
private def canVisit(grid: Array[Array[Int]]): Seq[Thing] = neighbors(grid)
.filterNot(n => visited.contains(n.position))
.filter(n => n.altitude - this.altitude <= 1)

private def moveTo(that: Thing): Thing = that.copy(
visited = this.visited :+ that.position,
target = this.target
)

def branch(grid: Array[Array[Int]]): Seq[Thing] = if (hasArrived) {
Seq.empty
} else {
canVisit(grid).map(moveTo)
}

}

object Thing {

// Only use to init neighbors - lacks info to safely propagate
def neighborApply(
position: Position,
grid: Array[Array[Int]]
): Option[Thing] = {
if (
grid.head.indices
.contains(position._1) && grid.indices.contains(position._2)
) {
Some(
Thing(
position = position,
visited = Seq.empty,
target = (0, 0),
altitude = grid(position._2)(position._1)
)
)
} else {
None
}
}

}

def charLocations(c: Char, grid: Array[Array[Char]]): Array[(Int, Int)] =
grid
.map(_.zipWithIndex.filter(_._1 == c))
.zipWithIndex
.filter(_._1.nonEmpty)
.map { case (arr, y) => (arr.map(_._2).head, y) }

val data = "day-12.data"

override def run: ZIO[Scope, Any, Any] = for {
shortestArrival <- FiberRef.make[Int](Int.MaxValue)
visited <- FiberRef.make[List[Position]](List.empty)
branchingQueue <- Queue.unbounded[Thing]
arrivalQueue <- Queue.unbounded[Thing]
charGrid <- source(data)
.map(_.toCharArray)
.runCollect
.map(_.toArray)
altGrid <- ZIO.attempt {
charGrid.map { rows =>
rows
.map {
case 'S' => 'a'
case 'E' => 'z'
case any => any
}
.map(_.toInt - 97)
}
}
initCoords <- ZIO
.attempt {
val sPosition = charLocations('S', charGrid).head
val aPositions = charLocations('a', charGrid)
val ePosition = charLocations('E', charGrid).head
(
sPosition +: aPositions.toList,
ePosition
)
}
_ <- branchingQueue.offerAll(
initCoords._1
.map { pos =>
Thing(
position = pos,
visited = Seq.empty,
target = initCoords._2,
altitude = altGrid(pos._2)(pos._1)
)
}
// .take(1) // use just the S position for part 1
)
_ <- branchingQueue.take
.flatMap { item =>
for {
shortest <- shortestArrival.get
visits <- visited.get
_ <- visited
.set(visits :+ item.position)
.when(!visits.contains(item.position))
_ <-
branchingQueue
.offerAll(
item
.branch(altGrid)
.filter(_.stepsTaken < shortest)
)
.when(!visits.contains(item.position))
_ <- arrivalQueue.offer(item).when(item.hasArrived)
_ <- shortestArrival
.set(item.stepsTaken)
.when(item.hasArrived && item.stepsTaken < shortest)
} yield ()
}
.repeatUntilZIO(_ =>
branchingQueue.isEmpty
) *> branchingQueue.shutdown
_ <- arrivalQueue.takeAll
.map(_.map(_.stepsTaken).min)
.debug("Min")
} yield ExitCode.success

}

· 3 min read

My solution for https://adventofcode.com/2022/day/11

import zio.*
import zio.metrics.Metric
import zio.metrics.MetricState.Counter
import zio.stream.*

object Day11 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode)

case class Monkey(
id: Int,
startingItems: Seq[Long],
op: Long => Long,
test: Long => Boolean,
action: Boolean => Int,
mailbox: Queue[Long],
mod: Long
) {

// Hack the Metric system for counters }:-)
private val monkeyMetrics =
Metric.counterInt(id.toString)

// On start, we'll send out initial items to our mailbox
def initMailbox: ZIO[Any, Nothing, Unit] =
mailbox.offerAll(startingItems).unit

def inspect(
passTo: Int => Queue[Long],
worryLevel: Long
): ZIO[Any, Nothing, Unit] =
for {
mail <-
mailbox.takeAll
.map { items =>
items
.map(op)
.map { w =>
worryLevel match {
case 3 =>
Math.floor(w / worryLevel).toLong // Method for pt.1
case _ => w % worryLevel // Method for pt.2
}
}
.map(w => (action(test(w)), w)) // (monkeyId, item)
}
_ <- monkeyMetrics.update(mail.length) // Mwahahahahaha!
_ <- ZIO.foreachDiscard(mail)((mId, item) => passTo(mId).offer(item))
} yield ()
}

// Get all of our Monkey INfo into a single-line
val formatInput: ZPipeline[Any, Nothing, String, String] =
ZPipeline.splitLines
.filter(_.nonEmpty)
.grouped(6)
.map(_.mkString(""))

// Create a Queue "mailbox" for each monkey
val setupMailboxes: ZPipeline[Any, Nothing, String, (Queue[Long], String)] =
ZPipeline.mapZIO[Any, Nothing, String, (Queue[Long], String)] { line =>
Queue.unbounded[Long].map(q => (q, line))
}

// Functions all the way down
val parseMonkey: ZPipeline[Any, Nothing, (Queue[Long], String), Monkey] =
ZPipeline.map[(Queue[Long], String), Monkey] { case (q, line) =>
val parseOpVal: String => Long => Long =
str => default => if (str.equals("old")) default else str.toLong
line match {
case s"Monkey $id: Starting items: $items Operation: new = old $op $opVal Test: divisible by $testVal If true: throw to monkey $onTrueId If false: throw to monkey $onFalseId" => {
Monkey(
id = id.toInt,
startingItems = items.split(",").map(_.trim.toLong),
op = op match {
case "+" => (arg: Long) => arg + parseOpVal(opVal)(arg)
case "*" => (arg: Long) => arg * parseOpVal(opVal)(arg)
},
test = (arg: Long) => (arg % testVal.toInt) == 0,
action =
(arg: Boolean) => if (arg) onTrueId.toInt else onFalseId.toInt,
mailbox = q,
mod = testVal.toInt
)
}
}
}

// Iterate one round of monkey mayhem
def oneRound(
monkeyChunk: Chunk[Monkey],
mailMap: Map[Int, Queue[Long]],
worryLevel: Long
): ZIO[Any, Nothing, Unit] =
ZIO.foreachDiscard(monkeyChunk.map(m => m.inspect(mailMap(_), worryLevel)))(
identity
)

// Check our metric counters for the stats, find the product of the highest two.
def monkeyStats(monkeyIds: Chunk[Int]): ZIO[Any, Nothing, Double] = for {
counts <-
ZIO
.foreach(monkeyIds)(id =>
Metric.counterInt(id.toString).value.map(_.count)
)
} yield counts.sortBy(-_).take(2).product

val data = "day-11.data"

override def run: ZIO[Any, Any, Any] = for {
monkeyChunk <-
source(data)
.via(formatInput)
.via(setupMailboxes)
.via(parseMonkey)
.runCollect
.map(_.sortBy(_.id))
_ <-
ZIO.foreachDiscard(monkeyChunk)(_.initMailbox)
mailboxes <-
ZIO
.foreach(monkeyChunk)(m => ZIO.succeed((m.id, m.mailbox)))
.map(_.toMap)
monkeyMod <-
ZIO.succeed(monkeyChunk.map(_.mod).product) // .as(3) // use 3 for part 1
_ <-
ZIO.foreachDiscard(1 to 10000) { i =>
oneRound(monkeyChunk, mailboxes, monkeyMod)
}
_ <-
monkeyStats(monkeyChunk.map(_.id))
.debug("(Too much) Monkey Business")
} yield ExitCode.success

}

· 2 min read

My solution for https://adventofcode.com/2022/day/10

import zio.*
import zio.stream.*

object Day10 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

case class CPU(register: Int, cycle: Int) {
def noop: CPU =
this.copy(cycle = cycle + 1)

def addx(v: Int) =
this.copy(register = register + v, cycle = cycle + 2)

def signalStrength: Int =
register * cycle

def render(pixel: Int): String = {
if ((register - 1 to register + 1).contains(pixel)) "#" else "."
}
}

val commandParser: ZPipeline[Any, Nothing, String, (String, Int)] =
ZPipeline.map[String, (String, Int)] {
case "noop" => "noop" -> 0
case s"addx $v" => "addx" -> v.toInt
case _ => throw new Exception("Unrecognized command")
}

val cycleStream: ZPipeline[Any, Nothing, (String, Int), Chunk[CPU]] =
ZPipeline.mapAccum[(String, Int), CPU, Chunk[CPU]](CPU(1, 0)) {
(state, cmd) =>
cmd._1 match {
case "noop" => (state.noop, Chunk(state.noop))
case "addx" =>
(state.addx(cmd._2), Chunk(state.noop, state.noop.noop))
}
}

val interestingCycles: Set[Int] =
(20 to 220 by 40).toSet

val cycleFilter: ZPipeline[Any, Nothing, CPU, CPU] =
ZPipeline.filter[CPU](cpu => interestingCycles.contains(cpu.cycle))

val data = "day-10.data"

override def run: ZIO[Any, Any, Any] = for {
_ <- source(data)
.via(commandParser >>> cycleStream)
.flattenChunks
.via(cycleFilter)
.map(_.signalStrength)
.runSum
.debug("Answer pt.1")
_ <- source(data)
.via(commandParser >>> cycleStream)
.flattenChunks
.grouped(40)
.map(_.zipWithIndex.map((cpu, pixel) => cpu.render(pixel)).mkString)
.debug("Answer pt.2")
.runDrain
} yield ExitCode.success

}

· 3 min read

My solution for https://adventofcode.com/2022/day/9

import zio.*
import zio.stream.*

import scala.annotation.tailrec
import scala.language.postfixOps

object Day9 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

type Position = (Int, Int)
extension (p: Position) {

def add(that: Position): Position = (p._1 + that._1, p._2 + that._2)
def towards(that: Position): Position = (that._1 - p._1, that._2 - p._2)
def distanceFrom(that: Position): Double = {
Math.abs(
Math.sqrt(
Math.pow(that._1 - p._1, 2) + Math.pow(that._2 - p._2, 2)
)
)
}

def diagonalTo(that: Position): Boolean = {
p._1 != that._1 && p._2 != that._2
}

def adjacentTo(that: Position): Boolean = {
p.distanceFrom(that) <= 1 || (p.distanceFrom(that) > 1 && p.distanceFrom(
that
) < 2 && p.diagonalTo(that))
}

// Read the directions and directly you will be directed in the right direction.
def follow(target: Position): Position = {
// It they're not touching...
if (!p.adjacentTo(target)) {
// ... move p towards target via a unit distance...
val unitDistance = p.towards(target) match {
case (0, 0) => (0, 0)
case (0, y) => (0, y / Math.abs(y))
case (x, 0) => (x / Math.abs(x), 0)
case (x, y) => (x / Math.abs(x), y / Math.abs(y))
}
p.add(unitDistance)
} else p // ... otherwise, dont move.
}
}

type Snek = Array[Position]
type Visits = Set[Position]

object Snek {
def apply(size: Int): Snek = Array.fill[Position](size)((0, 0))
}

extension (s: Snek) {

// Move one
def move(direction: String): (Snek, Visits) = {

val newHead: Position = direction match {
case "U" => (s.head._1, s.head._2 + 1)
case "D" => (s.head._1, s.head._2 - 1)
case "L" => (s.head._1 - 1, s.head._2)
case "R" => (s.head._1 + 1, s.head._2)
}

val slithered =
s.tail.foldLeft(Array(newHead))((snk, elem) =>
snk :+ elem.follow(snk.last)
)

(slithered, Set(s.last, slithered.last))
}

// Move one...a bunch of times.
@tailrec
def loop(
direction: String,
amount: Int,
snek: Snek = s,
accum: Visits = Set.empty
): (Snek, Visits) = {
if (amount == 0) {
(snek, accum)
} else {
val moved = snek.move(direction)
loop(direction, amount - 1, moved._1, accum ++ moved._2)
}
}

}

// No FiberRefs, today!
def snekOps(size: Int) =
ZPipeline.mapAccum[(String, Int), (Snek, Visits), Visits](
(Snek(size), Set((0, 0)))
) { case (state, cmd) =>
val movedSnek = state._1.loop(cmd._1, cmd._2)
(movedSnek, movedSnek._2)
}

val data = "day-9.data"

override def run: ZIO[Any, Any, Any] = for {
_ <- source(data)
.map { case s"$dir $amnt" =>
(dir, amnt.toInt)
}
.via(snekOps(2))
.runCollect
.map(_.toSet.flatten.size)
.debug("Answer pt1")
_ <- source(data)
.map { case s"$dir $amnt" =>
(dir, amnt.toInt)
}
.via(snekOps(10))
.runCollect
.map(_.toSet.flatten.size)
.debug("Answer pt2")
} yield ExitCode.success

}

· 2 min read

My solution for https://adventofcode.com/2022/day/8

import zio.*
import zio.stream.*

object Day8 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

type Grid = Array[Array[Int]]
object Grid {
def apply(arr: Array[Array[Int]]): Grid = arr
}

extension (g: Grid) {

def empty: Grid = Array.fill(g.length)(Array.fill(g.head.length)(0))

def colToRow(col: Int): Array[Int] = g.map(arr => arr(col))

def seenGrid: Grid = {
val result: Grid = g.empty
for (i <- g.indices) {
for (j <- g.head.indices) {
if (
i == 0 || i == g.indices.max || j == 0 || j == g.head.indices.max
) {
result(i)(j) = 1
} else {
val thisTree = g(i)(j)

val seenByRow = g(i).take(j).forall(_ < thisTree) ||
g(i).drop(j + 1).forall(_ < thisTree)

val colRow = g.colToRow(j)
val seenByCol =
colRow.take(i).forall(_ < thisTree) ||
colRow.drop(i + 1).forall(_ < thisTree)

result(i)(j) = if (seenByRow || seenByCol) 1 else 0
}
}
}
result
}

def scenicGrid: Grid = {
val result: Grid = g.empty
for (i <- g.indices) {
for (j <- g.head.indices) {
val thisTree = g(i)(j)
val colRow = g.colToRow(j)

// Since we don't have takeUntil, add an offset if needed
def offset(arr: Array[Int]) = if (arr.exists(_ >= thisTree)) 1 else 0
val sizeOp: Array[Int] => Int =
arr => arr.takeWhile(_ < thisTree).length + offset(arr)

val up = sizeOp(colRow.take(i).reverse)
val down = sizeOp(colRow.drop(i + 1))
val left = sizeOp(g(i).take(j).reverse)
val right = sizeOp(g(i).drop(j + 1))

result(i)(j) = up * down * left * right
}
}
result
}

def sumElements: Int = g.map(_.sum).sum
def maxElement: Int = g.map(_.max).max

}

val data = "day-8.data"
override def run: ZIO[Any, Any, Any] = for {
grid <- source(data)
.map(line => line.toArray.map(_.toString.toInt))
.runCollect
.map(d => Grid(d.toArray))
_ <- Console.printLine(grid.seenGrid.sumElements) // Part 1
_ <- Console.printLine(grid.scenicGrid.maxElement) // Part 2
} yield ExitCode.success

}

· 2 min read

My solution for https://adventofcode.com/2022/day/7

import zio.*
import zio.stream.*

object Day7 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

type Path = List[String]

case class Directory(
path: Path
)

case class File(
path: Path,
size: Long
)

case class ElfFS(
dirs: List[Directory],
files: List[File],
currentPath: Path
)

object ElfFS {
def apply(): ElfFS =
ElfFS(List.empty, List.empty, List.empty)
}

extension (efs: ElfFS) {

// Add a directory, if we haven't seen it before
def mkDirectoryIfNotExists(dir: String): ElfFS = {
val dirAtPath = Directory(efs.currentPath :+ dir)
if (efs.dirs.contains(dirAtPath)) {
efs
} else {
efs.copy(dirs = efs.dirs :+ dirAtPath)
}
}

// Add a file, if we haven't seen it before
def mkFileIfNotExists(name: String, size: Long): ElfFS = {
val fileAtPath = File(efs.currentPath :+ name, size)
if (efs.files.contains(fileAtPath)) {
efs
} else {
efs.copy(files = efs.files :+ fileAtPath)
}
}

// Process the input
def process(line: String): ElfFS = line match {
case s"$$ cd $path" =>
path match {
case ".." => efs.copy(currentPath = efs.currentPath.dropRight(1))
case _ => efs.copy(currentPath = efs.currentPath :+ path)
}
case "$ ls" => efs
case s"dir $dir" => mkDirectoryIfNotExists(dir)
case s"$fSize $fName" => mkFileIfNotExists(fName, fSize.toLong)
}

def sumFilesUnder(dir: Directory): Long = {
efs.files
.filter(f => f.path.mkString.startsWith(dir.path.mkString))
.map(_.size)
.sum
}

def sumResult(sizeLimit: Long): Long = {
efs.dirs
.map(sumFilesUnder)
.filter(_ <= sizeLimit)
.sum
}

def findMinFolderSize: Long = {
val capacity: Long = 70000000
val spaceNeeded: Long = 30000000
val totalUsed = efs.files.map(_.size).sum
val unused = capacity - totalUsed
val needToDelete = spaceNeeded - unused

efs.dirs
.map(sumFilesUnder)
.filter(_ >= needToDelete)
.min
}

}

val data = "day-7.data"
override def run: ZIO[Scope, Any, Any] = for {
elfRef <- FiberRef.make(ElfFS())
_ <- source(data)
.foreach(line => elfRef.getAndUpdate(efs => efs.process(line)))
_ <- elfRef.get
.map(efs => efs.sumResult(100000))
.debug("Answer pt.1")
_ <- elfRef.get
.map(_.findMinFolderSize)
.debug("Answer pt.2")
} yield ExitCode.success

}

· One min read

My solution for https://adventofcode.com/2022/day/6

import zio.*
import zio.stream.*

object Day6 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, Byte] =
fileName => ZStream.fromFileName(fileName)

val data = "day-6.data"
val nDistinct = 14
override def run: ZIO[Any, Any, Any] = for {
_ <- source(data)
.map(byte => new String(Array(byte)))
.zipWithIndex
.sliding(nDistinct)
.filter(_.map(_._1).toSet.size == nDistinct)
.take(1)
.map(_.map(_._2).max + 1)
.debug(s"Answer for marker length $nDistinct")
.runDrain
} yield ExitCode.success

}

· 3 min read

My solution for https://adventofcode.com/2022/day/5

import zio.*
import zio.stream.*

import scala.annotation.tailrec
import scala.collection.mutable

object Day5 extends ZIOAppDefault {

// Do NOT NOT NOT use a mutable data structure inside a FiberRef
type SafeStack[A] = List[A]
extension[A](safeStack: SafeStack[A]) {
def push(a: A): SafeStack[A] = a +: safeStack
def pop: (A, SafeStack[A]) = (safeStack.head, safeStack.tail)
def peek: A = safeStack.head
def popN(n: Int): (List[A], SafeStack[A]) =
(safeStack.take(n), safeStack.drop(n))
def pushN(l: List[A]): SafeStack[A] = l ++ safeStack
}

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

// Moves one item at a time, multiple times.
def craneOperation[A](
a: FiberRef[SafeStack[A]],
b: FiberRef[SafeStack[A]],
amount: Int = 1
): UIO[Unit] = {
(
for {
stackA <- a.get
stackB <- b.get
_ <- b.set(stackB.push(stackA.pop._1))
_ <- a.set(stackA.pop._2)
} yield ()
).repeatN(Math.max(0, amount - 1)).when(amount > 0).unit
}

// Move many items all at once
def craneOperation9001[A](
a: FiberRef[SafeStack[A]],
b: FiberRef[SafeStack[A]],
amount: Int = 1
): UIO[Unit] = {
(
for {
stackA <- a.get
stackB <- b.get
_ <- b.set(stackB.pushN(stackA.popN(amount)._1))
_ <- a.set(stackA.popN(amount)._2)
} yield ()
).when(amount > 0).unit
}

// Scala is the best!
def parseOperation(line: String): (Int, Int, Int) = line match {
case s"move $amount from $stackA to $stackB" =>
(amount.toInt, stackA.toInt, stackB.toInt)
}

// Parse out letters from a string located at multiple indices
@tailrec
def charsAt(
str: String,
indexes: Array[Int],
accum: Seq[String] = Seq.empty
): Seq[String] = {
if (indexes.nonEmpty) {
charsAt(str, indexes.tail, accum :+ str.charAt(indexes.head).toString)
} else {
accum
}
}

// Who doesn't love parsing an ascii diagram to an initial computational state?
def parseInit(data: Chunk[String]): (Int, Chunk[Seq[String]]) = {
val bottomsUp: Chunk[String] = data.reverse
val nStacks: Int =
bottomsUp.head.trim.split("""\s+""").map(_.toInt).max
val stackIndexes: Array[Int] = bottomsUp.head.toCharArray.zipWithIndex
.map { case (c, i) => if (c == ' ') -1 else i }
.filter(_ >= 0)
val stackData: Chunk[Seq[String]] =
bottomsUp.tail.map(line => charsAt(line, stackIndexes))
(nStacks, stackData)
}

val data = "day-5.data"
override def run: ZIO[Scope, Any, Any] = for {
// Load/parse the initial state
initData: (Int, Chunk[Seq[String]]) <- source(data)
.takeWhile(_.nonEmpty)
.run(
ZSink.collectAll
.map(data => parseInit(data))
)
// Stage a map of empty FiberRef[SafeStack[String]]
refMap <- ZIO
.foreach(1 to initData._1)(i =>
FiberRef.make[SafeStack[String]](List.empty[String]).map(s => (i -> s))
)
.map(_.toMap)
// Load our initial state into our FiberRefs
ziosToRun <- ZIO.attempt {
initData._2.flatMap { rowToLoad =>
rowToLoad.zipWithIndex.map { case (s, i) =>
val stackRef: FiberRef[SafeStack[String]] =
refMap.getOrElse(i + 1, throw new Exception(""))
stackRef.get
.flatMap(stack => stackRef.set(stack.push(s)))
.when(s != " ")
}
}
}
_ <- ZIO.foreach(ziosToRun)(identity)
// DO IT TO IT
_ <- source(data)
.dropUntil(_.isEmpty)
.map(parseOperation)
.tap(cmd =>
// use craneOperation for part1
craneOperation9001(
refMap.getOrElse(cmd._2, throw new Exception("")),
refMap.getOrElse(cmd._3, throw new Exception("")),
cmd._1
)
)
.runDrain
results <- ZIO.foreach(refMap.toSeq.sortBy(_._1)) { case (_, ref) =>
ref.get.map(_.peek)
}
_ <- ZIO.attempt(results.mkString).debug("Top Boxes")
} yield ExitCode.success

}

· One min read

My solution for https://adventofcode.com/2022/day/4

import zio.*
import zio.stream.*

object Day4 extends ZIOAppDefault {

val source: String => ZStream[Any, Throwable, String] =
fileName =>
ZStream
.fromFileName(fileName)
.via(ZPipeline.utfDecode >>> ZPipeline.splitLines)

trait ElfJanitor {
val assignedTo: Range
}

object ElfJanitor {

def apply(line: String): (ElfJanitor, ElfJanitor) = {
val elves: Array[ElfJanitor] = line
.split(",")
.map { rngStr =>
val bounds = rngStr.split("-").map(_.toInt).take(2)
new ElfJanitor {
override val assignedTo: Range = bounds.head to bounds.last
}
}
.take(2)
(elves.head, elves.last)
}

extension(workers: (ElfJanitor, ElfJanitor)) {
// Indicates if an Elf is redundant, and the index of which one is, if any.
def redundant: Option[Int] =
(workers._1.assignedTo.toSet, workers._2.assignedTo.toSet) match {
case (a, b) if a.subsetOf(b) => Some(1)
case (a, b) if b.subsetOf(a) => Some(2)
case _ => None
}
// Indicates if there is any overlap to assigned work
def overlap: Boolean =
(workers._1.assignedTo.toSet, workers._2.assignedTo.toSet) match {
case (a, b) if a.union(b).size < a.size + b.size => true
case _ => false
}
}

}

val data = "day-4-1.data"
override def run: ZIO[Any, Any, Any] = for {
_ <- source(data)
.map(ElfJanitor.apply)
.filter(_.redundant.isDefined)
.run(ZSink.count)
.debug("Answer Pt.1")
_ <- source(data)
.map(ElfJanitor.apply)
.filter(_.overlap)
.run(ZSink.count)
.debug("Answer Pt.2")
} yield ExitCode.success

}