Skip to content

Lift immutable cleanup #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 7, 2025
22 changes: 22 additions & 0 deletions src/main/scala/ScalaPlayground/Lift/Immutable/LiftExtensions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ScalaPlayground.Lift.Immutable

extension (state: LiftSystem) {
def toPrintable: String = {
import state.{building, lift, stops}

val sb = new StringBuilder()
sb.append(s"${stops.length} stops: ${stops.mkString(", ")}\n")

building.floors.zipWithIndex.reverse.foreach { (queue, floor) =>
sb.append(s"| $floor | ${queue.reverse.map(_.destination).mkString(", ").padTo(20, ' ')} |")

// draw the lift if it is on the current level
if lift.position == floor
then sb.append(s" | ${lift.people.map(_.destination).mkString(", ").padTo(15, ' ')} |")

sb.append('\n')
}

sb.toString()
}
}
174 changes: 65 additions & 109 deletions src/main/scala/ScalaPlayground/Lift/Immutable/LiftImmutable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package ScalaPlayground.Lift.Immutable
import ScalaPlayground.Lift.Immutable.Direction.{Down, Up}

import scala.annotation.tailrec
import scala.collection.immutable.{ListMap, Queue}
import scala.collection.immutable.Queue

type Floor = Int
enum Direction { case Up, Down }
Expand Down Expand Up @@ -38,14 +38,48 @@ case class Lift(

def nearestPassengerTarget: Option[Floor] =
people.filter(_.matchesDirection(this)).map(_.destination).minByOption(floor => Math.abs(floor - position))

@tailrec
final def pickup(building: Building): (Lift, Building) =
val queue = building.floors(position)
queue.filter(accepts).dequeueOption match
case None => (this, building)
case Some(person, _) =>
val fullerLift = copy(people = people.enqueue(person))
val emptierQueue = queue.diff(Seq(person))
val emptierBuilding = building.copy(floors = building.floors.updated(position, emptierQueue))
fullerLift.pickup(emptierBuilding)

def fixDirection(building: Building): Lift = position match
case 0 => copy(direction = Up)
case p if p == building.floors.length - 1 => copy(direction = Down)
case _ => this

def dropOff: Lift = copy(people = people.filter(_.destination != position))

def align(building: Building): Lift =
List(nearestPassengerTarget, building.nearestRequestInSameDirection(this)).flatten
.minByOption(floor => Math.abs(floor - position))
.match
case Some(floor) => copy(position = floor, direction = direction)
case None => // switch direction
direction match
case Up =>
building.highestFloorGoingDown(this) match
case Some(highest) => copy(highest, Down)
case None => copy(building.lowestFloorGoingUp(this).getOrElse(0), Up)
case Down =>
building.lowestFloorGoingUp(this) match
case Some(lowest) => copy(lowest, Up)
case None => copy(building.highestFloorGoingDown(this).getOrElse(0), Down)
}

case class Building(floors: ListMap[Floor, Queue[Person]]) {
def isEmpty: Boolean = floors.values.forall(_.isEmpty)
case class Building(floors: Array[Queue[Person]]) {
def isEmpty: Boolean = floors.forall(_.isEmpty)
def hasPeople: Boolean = !isEmpty

private def peopleGoing(direction: Direction): List[Person] =
floors.values.flatMap(queue => queue.filter(_.desiredDirection == direction)).toList
floors.flatMap(queue => queue.filter(_.desiredDirection == direction)).toList

def lowestFloorGoingUp(lift: Lift): Option[Floor] =
peopleGoing(Up).filter(_.isBelow(lift)).map(_.position).minOption
Expand All @@ -59,127 +93,49 @@ case class Building(floors: ListMap[Floor, Queue[Person]]) {
case Down => peopleGoing(Down).filter(_.isBelow(lift)).map(_.position).maxOption
}

case class State(building: Building, lift: Lift, stops: List[Floor])
case class LiftSystem(building: Building, lift: Lift, stops: List[Floor]) {
private def fixDirection: LiftSystem =
copy(lift = lift.fixDirection(building))

extension (state: State) {
def toPrintable: String = {
import state.{building, lift, stops}
private def dropOff: LiftSystem =
copy(lift = lift.dropOff)

val sb = new StringBuilder()
sb.append(s"${stops.length} stops: ${stops.mkString(", ")}\n")
private def pickup: LiftSystem =
val (lift2, building2) = lift.pickup(building)
copy(lift = lift2, building = building2)

building.floors.toSeq.reverse.foreach { case (floor, queue) =>
sb.append(s"| $floor | ${queue.reverse.map(_.destination).mkString(", ").padTo(20, ' ')} |")
private def align: LiftSystem =
copy(lift = lift.align(building))

// draw the lift if it is on the current level
if lift.position == floor
then sb.append(s" | ${lift.people.map(_.destination).mkString(", ").padTo(15, ' ')} |")
def registerStop: LiftSystem =
stops.lastOption match
case Some(lastStop) if lastStop == lift.position => this
case _ => copy(stops = stops :+ lift.position)

sb.append('\n')
}
def isDone: Boolean =
building.isEmpty && lift.isEmpty && lift.position == 0

sb.toString()
}
def step: LiftSystem =
registerStop.fixDirection.dropOff.pickup.align
}

// Excuse the name. Dinglemouse.theLift() is how the function is called in the Codewars test suite
object Dinglemouse {
def theLift(queues: Array[Array[Int]], capacity: Int): Array[Int] = {
val floors: ListMap[Int, Queue[Person]] =
queues.zipWithIndex
.map { case (queue, index) =>
(index, queue.map(destination => Person(position = index, destination = destination)).to(Queue))
}
.to(ListMap)
val floors: Array[Queue[Person]] =
queues.zipWithIndex.map { case (queue, index) =>
queue.map(destination => Person(position = index, destination = destination)).to(Queue)
}

val lift = Lift(position = 0, Direction.Up, people = Queue.empty, capacity)
val building = Building(floors)

val initialState = State(building = building, lift = lift, stops = List.empty)
val finalState = LiftLogic.simulate(initialState)

finalState.stops.toArray
}
}

object LiftLogic {
def simulate(initialState: State): State = {
val state = initialState.copy(stops = initialState.stops :+ initialState.lift.position)

@tailrec
def resolve(state: State): State =
val newState = step(state)
val State(building, lift, _) = newState
if building.isEmpty && lift.isEmpty && lift.position == 0 then newState
else resolve(newState)

resolve(state)
}

private def step(state: State): State = {
import state.{building, lift, stops}

val validDirection = lift.position match
case 0 => Up
case p if p == building.floors.size - 1 => Down
case _ => lift.direction

// Off-board people who reached their destination
val lift2 = lift.copy(
direction = validDirection,
people = lift.people.filter(_.destination != lift.position)
)

@tailrec
def pickup(lift: Lift, queue: Queue[Person]): (Lift, Queue[Person]) =
queue.filter(lift.accepts).dequeueOption match
case None => (lift, queue)
case Some(person, _) =>
val fullerLift = lift.copy(people = lift.people.enqueue(person))
val emptierQueue = queue.diff(Seq(person))
pickup(fullerLift, emptierQueue)

// pick up people from the current floor
val (lift3, floorQueue) = pickup(lift = lift2, queue = building.floors(lift2.position))
@tailrec def resolve(state: LiftSystem): LiftSystem =
if state.isDone then state else resolve(state.step)

// update the building to reflect the updated floor
val building2 = building.copy(floors = building.floors.updated(lift3.position, floorQueue))
val initialState = LiftSystem(building = building, lift = lift, stops = List.empty)
val finalState = resolve(initialState)

// core task: find the new target and direction
val (nextPosition, nextDirection) = getNextPositionAndDirection(building2, lift3)

// update lift parameters
val lift4 = lift3.copy(nextPosition, nextDirection)

// Register the stop. I added the extra condition because of a bug
// by which the lift sometimes takes two turns for the very last move 🤔
val stops2 = true match
case _ if lift3.position != lift4.position => stops :+ lift4.position
case _ => stops

state.copy(building2, lift4, stops2)
finalState.registerStop.stops.toArray
}

private def getNextPositionAndDirection(building: Building, lift: Lift): (Floor, Direction) =
List( // Build a list of primary targets
lift.nearestPassengerTarget, // request from passenger already on the lift
building.nearestRequestInSameDirection(lift) // request from people [waiting in AND going to] the same direction
).flatten // turn list of options into list of Integers
.minByOption(floor => Math.abs(floor - lift.position)) // get Some floor with the lowest distance, or None
.match
case Some(floor) => (floor, lift.direction) // return requested floor, keep direction
case None => // otherwise choose a new target
lift.direction match
case Up => upwardsNewTarget(building, lift) // look for people above going downwards
case Down => downwardsNewTarget(building, lift) // look for people below going upwards

private def downwardsNewTarget(building: Building, lift: Lift): (Floor, Direction) =
building.lowestFloorGoingUp(lift) match
case Some(lowest) => (lowest, Up)
case None => (building.highestFloorGoingDown(lift).getOrElse(0), Down)

private def upwardsNewTarget(building: Building, lift: Lift): (Floor, Direction) =
building.highestFloorGoingDown(lift) match
case Some(highest) => (highest, Down)
case None => (building.lowestFloorGoingUp(lift).getOrElse(0), Up)
}