Skip to content

Commit 761f9a2

Browse files
authored
Merge pull request #4 from epic-64/immutable-lift-2
Immutable lift and hybrid lift
2 parents f46ff8c + d2734a6 commit 761f9a2

File tree

4 files changed

+337
-57
lines changed

4 files changed

+337
-57
lines changed

src/main/scala/ScalaPlayground/Lift/LiftLogic.scala renamed to src/main/scala/ScalaPlayground/Lift/Immutable/LiftImmutable.scala

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
package ScalaPlayground.Lift
1+
package ScalaPlayground.Lift.Immutable
22

33
// https://www.codewars.com/kata/58905bfa1decb981da00009e
44

5-
import ScalaPlayground.Lift.Direction.{Down, Up}
5+
import Direction.{Down, Up}
66

7-
import scala.collection.immutable.ListMap
7+
import scala.annotation.tailrec
8+
import scala.collection.immutable.{ListMap, Queue}
89
import scala.collection.mutable
910

1011
type Floor = Int
@@ -23,26 +24,24 @@ case class Person(position: Floor, destination: Floor) {
2324
}
2425

2526
case class Lift(
26-
var position: Floor,
27-
var direction: Direction,
28-
people: mutable.Queue[Person],
27+
position: Floor,
28+
direction: Direction,
29+
people: Queue[Person],
2930
capacity: Int
3031
) {
31-
private def isFull: Boolean = people.size == capacity
32-
def hasRoom: Boolean = !isFull
33-
def hasPeople: Boolean = people.nonEmpty
34-
def isEmpty: Boolean = people.isEmpty
35-
def accepts(person: Person): Boolean = hasRoom && person.desiredDirection == direction
32+
def isFull: Boolean = people.size == capacity
33+
def hasRoom: Boolean = people.size != capacity
34+
def hasPeople: Boolean = people.nonEmpty
35+
def isEmpty: Boolean = people.isEmpty
36+
37+
def accepts(person: Person): Boolean =
38+
hasRoom && person.desiredDirection == direction
3639

3740
def nearestPassengerTarget: Option[Floor] =
3841
people.filter(_.matchesDirection(this)).map(_.destination).minByOption(floor => Math.abs(floor - position))
39-
40-
def turn(): Unit = direction = direction match
41-
case Up => Down
42-
case Down => Up
4342
}
4443

45-
case class Building(floors: ListMap[Floor, mutable.Queue[Person]]) {
44+
case class Building(floors: ListMap[Floor, Queue[Person]]) {
4645
def isEmpty: Boolean = floors.values.forall(_.isEmpty)
4746
def hasPeople: Boolean = !isEmpty
4847

@@ -61,10 +60,13 @@ case class Building(floors: ListMap[Floor, mutable.Queue[Person]]) {
6160
case Down => peopleGoing(Down).filter(_.isBelow(lift)).map(_.position).maxOption
6261
}
6362

64-
case class State(building: Building, lift: Lift, stops: mutable.ListBuffer[Floor]) {
63+
case class State(building: Building, lift: Lift, stops: List[Floor])
64+
65+
extension (state: State) {
6566
def toPrintable: String = {
66-
val sb = new StringBuilder()
67+
import state.{building, stops, lift}
6768

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

7072
building.floors.toSeq.reverse.foreach { case (floor, queue) =>
@@ -84,17 +86,17 @@ case class State(building: Building, lift: Lift, stops: mutable.ListBuffer[Floor
8486
// Excuse the name. Dinglemouse.theLift() is how the function is called in the Codewars test suite
8587
object Dinglemouse {
8688
def theLift(queues: Array[Array[Int]], capacity: Int): Array[Int] = {
87-
val floors: ListMap[Int, mutable.Queue[Person]] =
89+
val floors: ListMap[Int, Queue[Person]] =
8890
queues.zipWithIndex
8991
.map { case (queue, index) =>
90-
(index, queue.map(destination => Person(position = index, destination = destination)).to(mutable.Queue))
92+
(index, queue.map(destination => Person(position = index, destination = destination)).to(Queue))
9193
}
9294
.to(ListMap)
9395

94-
val lift = Lift(position = 0, Direction.Up, people = mutable.Queue.empty, capacity)
96+
val lift = Lift(position = 0, Direction.Up, people = Queue.empty, capacity)
9597
val building = Building(floors)
9698

97-
val initialState = State(building = building, lift = lift, stops = mutable.ListBuffer.empty)
99+
val initialState = State(building = building, lift = lift, stops = List.empty)
98100
val finalState = LiftLogic.simulate(initialState)
99101

100102
finalState.stops.toArray
@@ -103,55 +105,60 @@ object Dinglemouse {
103105

104106
object LiftLogic {
105107
def simulate(initialState: State): State = {
106-
var state = initialState
108+
val state = initialState.copy(stops = initialState.stops :+ initialState.lift.position)
107109

108-
state.stops += state.lift.position // register initial position as the first stop
109-
println(state.toPrintable) // draw the initial state of the lift
110+
@tailrec
111+
def resolve(state: State): State =
112+
val newState = step(state)
113+
val State(building, lift, _) = newState
114+
if building.isEmpty && lift.isEmpty && lift.position == 0 then newState
115+
else resolve(newState)
110116

111-
val State(building, lift, _) = state
112-
113-
while building.hasPeople || lift.hasPeople || lift.position > 0 do
114-
state = step(state)
115-
println(state.toPrintable)
116-
117-
state
117+
resolve(state)
118118
}
119119

120120
private def step(state: State): State = {
121-
// Destructure state into convenient variables
122-
val State(building, lift, stops) = state
121+
import state.{building, lift, stops}
123122

124-
val maxFloor = building.floors.keys.maxOption.getOrElse(0)
125-
126-
// Always force the lift into a valid direction
127-
lift.direction = lift.position match
128-
case 0 => Up
129-
case p if p == maxFloor => Down
130-
case _ => lift.direction
123+
val validDirection = lift.position match
124+
case 0 => Up
125+
case p if p == building.floors.size - 1 => Down
126+
case _ => lift.direction
131127

132128
// Off-board people who reached their destination
133-
lift.people.dequeueAll(_.destination == lift.position)
129+
val lift2 = lift.copy(
130+
direction = validDirection,
131+
people = lift.people.filter(_.destination != lift.position)
132+
)
133+
134+
@tailrec
135+
def pickup(lift: Lift, queue: Queue[Person]): (Lift, Queue[Person]) =
136+
queue.filter(lift.accepts).dequeueOption match
137+
case None => (lift, queue)
138+
case Some(person, _) =>
139+
val fullerLift = lift.copy(people = lift.people.enqueue(person))
140+
val emptierQueue = queue.diff(Seq(person))
141+
pickup(fullerLift, emptierQueue)
134142

135-
// get current floor queue
136-
val queue = building.floors(lift.position)
143+
// pick up people from the current floor
144+
val (lift3, floorQueue) = pickup(lift = lift2, queue = building.floors(lift2.position))
137145

138-
// Transfer people from floor queue into lift
139-
while lift.hasRoom && queue.exists(lift.accepts) do
140-
val person = queue.dequeueFirst(lift.accepts).get
141-
lift.people.enqueue(person)
146+
// update the building to reflect the updated floor
147+
val building2 = building.copy(floors = building.floors.updated(lift3.position, floorQueue))
142148

143-
val oldPosition = lift.position
144-
val (nextPosition, nextDirection) = getNextPositionAndDirection(building, lift)
149+
// core task: find the new target and direction
150+
val (nextPosition, nextDirection) = getNextPositionAndDirection(building2, lift3)
145151

146-
// Set the new values
147-
lift.direction = nextDirection
148-
lift.position = nextPosition
152+
// update lift parameters
153+
val lift4 = lift3.copy(nextPosition, nextDirection)
149154

150155
// Register the stop. I added the extra condition because of a bug
151156
// by which the lift sometimes takes two turns for the very last move 🤔
152-
if oldPosition != nextPosition then stops += nextPosition
157+
val stops2 = true match
158+
case _ if lift3.position != lift4.position => stops :+ lift4.position
159+
case _ => stops
153160

154-
state
161+
state.copy(building2, lift4, stops2)
155162
}
156163

157164
private def getNextPositionAndDirection(building: Building, lift: Lift): (Floor, Direction) =
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package ScalaPlayground.Lift.Mutable
2+
3+
// https://www.codewars.com/kata/58905bfa1decb981da00009e
4+
5+
import Direction.{Down, Up}
6+
7+
import scala.annotation.tailrec
8+
import scala.collection.mutable
9+
10+
type Floor = Int
11+
enum Direction { case Up, Down }
12+
13+
case class Person(position: Floor, destination: Floor) {
14+
require(position != destination, "source and destination floor cannot be the same")
15+
16+
val desiredDirection: Direction = (position, destination) match
17+
case _ if destination > position => Up
18+
case _ if destination < position => Down
19+
20+
def matchesDirection(lift: Lift): Boolean = desiredDirection == lift.direction
21+
def isBelow(lift: Lift): Boolean = position < lift.position
22+
def isAbove(lift: Lift): Boolean = position > lift.position
23+
}
24+
25+
case class Lift(
26+
var position: Floor,
27+
var direction: Direction,
28+
people: mutable.Queue[Person],
29+
capacity: Int
30+
) {
31+
private def isFull: Boolean = people.size == capacity
32+
def hasRoom: Boolean = !isFull
33+
def hasPeople: Boolean = people.nonEmpty
34+
def isEmpty: Boolean = people.isEmpty
35+
def accepts(person: Person): Boolean = hasRoom && person.desiredDirection == direction
36+
37+
def forceValidDirection(maxFloor: Floor): Unit =
38+
direction = position match
39+
case 0 => Up
40+
case p if p == maxFloor => Down
41+
case _ => direction
42+
43+
@tailrec
44+
final def pickup(queue: mutable.Queue[Person]): Unit =
45+
queue.dequeueFirst(accepts) match
46+
case None => ()
47+
case Some(person) => people.enqueue(person); pickup(queue)
48+
49+
def getNewPositionAndDirection(building: Building): (Floor, Direction) =
50+
List( // Build a list of primary targets
51+
nearestPassengerTarget, // request from passenger already on the lift
52+
building.nearestRequestInSameDirection(this) // request from people [waiting in AND going to] the same direction
53+
).flatten // turn list of options into list of Integers
54+
.minByOption(floor => Math.abs(floor - position)) // get Some floor with the lowest distance, or None
55+
.match
56+
case Some(floor) => (floor, direction) // return requested floor, keep direction
57+
case None =>
58+
direction match // otherwise choose a new target:
59+
case Up => upwardsNewTarget(building) // look for people above going downwards
60+
case Down => downwardsNewTarget(building) // look for people below going upwards
61+
62+
private def nearestPassengerTarget: Option[Floor] =
63+
people.filter(_.matchesDirection(this)).map(_.destination).minByOption(floor => Math.abs(floor - position))
64+
65+
private def downwardsNewTarget(building: Building): (Floor, Direction) =
66+
building.lowestFloorGoingUp(this) match
67+
case Some(lowest) => (lowest, Up)
68+
case None => (building.highestFloorGoingDown(this).getOrElse(0), Down)
69+
70+
private def upwardsNewTarget(building: Building): (Floor, Direction) =
71+
building.highestFloorGoingDown(this) match
72+
case Some(highest) => (highest, Down)
73+
case None => (building.lowestFloorGoingUp(this).getOrElse(0), Up)
74+
}
75+
76+
case class Building(floors: List[mutable.Queue[Person]]) {
77+
def isEmpty: Boolean = floors.forall(_.isEmpty)
78+
def hasPeople: Boolean = !isEmpty
79+
80+
private def peopleGoing(direction: Direction): List[Person] =
81+
floors.flatMap(queue => queue.filter(_.desiredDirection == direction)).toList
82+
83+
def lowestFloorGoingUp(lift: Lift): Option[Floor] =
84+
peopleGoing(Up).filter(_.isBelow(lift)).map(_.position).minOption
85+
86+
def highestFloorGoingDown(lift: Lift): Option[Floor] =
87+
peopleGoing(Down).filter(_.isAbove(lift)).map(_.position).maxOption
88+
89+
def nearestRequestInSameDirection(lift: Lift): Option[Floor] =
90+
lift.direction match
91+
case Up => peopleGoing(Up).filter(_.isAbove(lift)).map(_.position).minOption
92+
case Down => peopleGoing(Down).filter(_.isBelow(lift)).map(_.position).maxOption
93+
}
94+
95+
case class State(building: Building, lift: Lift, stops: mutable.ListBuffer[Floor])
96+
97+
object Dinglemouse {
98+
def theLift(queues: Array[Array[Int]], capacity: Int): Array[Int] = {
99+
def toPersonQueue(queue: Array[Int], floor: Floor): mutable.Queue[Person] =
100+
queue.map(destination => Person(position = floor, destination = destination)).to(mutable.Queue)
101+
102+
val floors: List[mutable.Queue[Person]] =
103+
queues.zipWithIndex.map((queue, index) => toPersonQueue(queue, index)).toList
104+
105+
val lift = Lift(position = 0, Direction.Up, people = mutable.Queue.empty, capacity)
106+
val building = Building(floors)
107+
108+
val initialState = State(building = building, lift = lift, stops = mutable.ListBuffer.empty)
109+
val finalState = LiftLogic.simulate(initialState)
110+
111+
finalState.stops.toArray
112+
}
113+
}
114+
115+
object LiftLogic {
116+
def simulate(initialState: State): State = {
117+
var state = initialState
118+
119+
val State(building, lift, stops) = state
120+
stops += lift.position // register initial stop
121+
122+
while building.hasPeople || lift.hasPeople || lift.position > 0
123+
do state = step(state)
124+
125+
state
126+
}
127+
128+
private def step(state: State): State = {
129+
import state.{building, lift, stops}
130+
131+
lift.forceValidDirection(maxFloor = building.floors.size - 1)
132+
lift.people.dequeueAll(_.destination == lift.position)
133+
lift.pickup(queue = building.floors(lift.position))
134+
135+
val (newPosition, newDirection) = lift.getNewPositionAndDirection(building)
136+
if (lift.position != newPosition) stops += newPosition
137+
138+
lift.direction = newDirection
139+
lift.position = newPosition
140+
141+
state
142+
}
143+
}

0 commit comments

Comments
 (0)