Skip to content

Commit f46ff8c

Browse files
authored
Merge pull request #1 from epic-64/lift-kata
Dinglemouse Lift Kata
2 parents 7bdd4c6 + 47e59a0 commit f46ff8c

File tree

5 files changed

+323
-4
lines changed

5 files changed

+323
-4
lines changed

build.sbt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@ libraryDependencies ++= Seq(
1414
)
1515

1616
coverageEnabled := true
17+
18+
wartremoverWarnings ++= Seq(
19+
// Wart.Any
20+
// Wart.Null,
21+
)
22+
23+
wartremoverErrors ++= Seq(
24+
// Wart.IterableOps,
25+
)

project/plugins.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2")
22
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
3-
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2")
3+
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2")
4+
addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.3.1")

src/main/scala/ScalaPlayground/BinaryTree/BinaryTree.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ case class Tree[A](value: A, left: BinaryTree[A], right: BinaryTree[A]) extends
9494
val pathFromLocal = findPathToRoot(from).dropWhile(_ != sharedAncestor).reverse :+ sharedAncestor
9595
val pathFromTarget = findPathToRoot(to).dropWhile(_ != sharedAncestor)
9696

97-
pathFromLocal.dropRight(1) ++ pathFromTarget.tail
97+
pathFromLocal.dropRight(1) ++ pathFromTarget.drop(1)
9898
}
9999

100100
def findPathToRoot[B >: A](target: B)(using ord: Ordering[B]): List[B] =
@@ -149,13 +149,13 @@ class TreeFormatter[A](padding: Int = 4) {
149149
var i = half
150150

151151
if (lines.nonEmpty) {
152-
i = lines.head.indexOf('*') // Marker position
152+
i = lines.headOption.getOrElse("").indexOf('*') // Marker position
153153
val line = (left, right) match {
154154
case (EmptyTree, EmptyTree) => " " * i + "┌─┘"
155155
case (_, EmptyTree) => " " * i + "┌─┘"
156156
case (EmptyTree, _) => " " * indent(lines, i - 2) + "└─┐"
157157
case (_, _) =>
158-
val dist = lines.head.length - 1 - i // Calculate distance between roots
158+
val dist = lines.headOption.getOrElse("").length - 1 - i // Calculate distance between roots
159159
s"${" " * i}${"" * (dist / 2 - 1)}${"" * ((dist - 1) / 2)}"
160160
}
161161
lines(0) = line
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package ScalaPlayground.Lift
2+
3+
// https://www.codewars.com/kata/58905bfa1decb981da00009e
4+
5+
import ScalaPlayground.Lift.Direction.{Down, Up}
6+
7+
import scala.collection.immutable.ListMap
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 nearestPassengerTarget: Option[Floor] =
38+
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
43+
}
44+
45+
case class Building(floors: ListMap[Floor, mutable.Queue[Person]]) {
46+
def isEmpty: Boolean = floors.values.forall(_.isEmpty)
47+
def hasPeople: Boolean = !isEmpty
48+
49+
private def peopleGoing(direction: Direction): List[Person] =
50+
floors.values.flatMap(queue => queue.filter(_.desiredDirection == direction)).toList
51+
52+
def lowestFloorGoingUp(lift: Lift): Option[Floor] =
53+
peopleGoing(Up).filter(_.isBelow(lift)).map(_.position).minOption
54+
55+
def highestFloorGoingDown(lift: Lift): Option[Floor] =
56+
peopleGoing(Down).filter(_.isAbove(lift)).map(_.position).maxOption
57+
58+
def nearestRequestInSameDirection(lift: Lift): Option[Floor] =
59+
lift.direction match
60+
case Up => peopleGoing(Up).filter(_.isAbove(lift)).map(_.position).minOption
61+
case Down => peopleGoing(Down).filter(_.isBelow(lift)).map(_.position).maxOption
62+
}
63+
64+
case class State(building: Building, lift: Lift, stops: mutable.ListBuffer[Floor]) {
65+
def toPrintable: String = {
66+
val sb = new StringBuilder()
67+
68+
sb.append(s"${stops.length} stops: ${stops.mkString(", ")}\n")
69+
70+
building.floors.toSeq.reverse.foreach { case (floor, queue) =>
71+
sb.append(s"| $floor | ${queue.reverse.map(_.destination).mkString(", ").padTo(20, ' ')} |")
72+
73+
// draw the lift if it is on the current level
74+
if lift.position == floor
75+
then sb.append(s" | ${lift.people.map(_.destination).mkString(", ").padTo(15, ' ')} |")
76+
77+
sb.append('\n')
78+
}
79+
80+
sb.toString()
81+
}
82+
}
83+
84+
// Excuse the name. Dinglemouse.theLift() is how the function is called in the Codewars test suite
85+
object Dinglemouse {
86+
def theLift(queues: Array[Array[Int]], capacity: Int): Array[Int] = {
87+
val floors: ListMap[Int, mutable.Queue[Person]] =
88+
queues.zipWithIndex
89+
.map { case (queue, index) =>
90+
(index, queue.map(destination => Person(position = index, destination = destination)).to(mutable.Queue))
91+
}
92+
.to(ListMap)
93+
94+
val lift = Lift(position = 0, Direction.Up, people = mutable.Queue.empty, capacity)
95+
val building = Building(floors)
96+
97+
val initialState = State(building = building, lift = lift, stops = mutable.ListBuffer.empty)
98+
val finalState = LiftLogic.simulate(initialState)
99+
100+
finalState.stops.toArray
101+
}
102+
}
103+
104+
object LiftLogic {
105+
def simulate(initialState: State): State = {
106+
var state = initialState
107+
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+
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
118+
}
119+
120+
private def step(state: State): State = {
121+
// Destructure state into convenient variables
122+
val State(building, lift, stops) = state
123+
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
131+
132+
// Off-board people who reached their destination
133+
lift.people.dequeueAll(_.destination == lift.position)
134+
135+
// get current floor queue
136+
val queue = building.floors(lift.position)
137+
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)
142+
143+
val oldPosition = lift.position
144+
val (nextPosition, nextDirection) = getNextPositionAndDirection(building, lift)
145+
146+
// Set the new values
147+
lift.direction = nextDirection
148+
lift.position = nextPosition
149+
150+
// Register the stop. I added the extra condition because of a bug
151+
// by which the lift sometimes takes two turns for the very last move 🤔
152+
if oldPosition != nextPosition then stops += nextPosition
153+
154+
state
155+
}
156+
157+
private def getNextPositionAndDirection(building: Building, lift: Lift): (Floor, Direction) =
158+
List( // Build a list of primary targets
159+
lift.nearestPassengerTarget, // request from passenger already on the lift
160+
building.nearestRequestInSameDirection(lift) // request from people [waiting in AND going to] the same direction
161+
).flatten // turn list of options into list of Integers
162+
.minByOption(floor => Math.abs(floor - lift.position)) // get Some floor with the lowest distance, or None
163+
.match
164+
case Some(floor) => (floor, lift.direction) // return requested floor, keep direction
165+
case None => // otherwise choose a new target
166+
lift.direction match
167+
case Up => upwardsNewTarget(building, lift) // look for people above going downwards
168+
case Down => downwardsNewTarget(building, lift) // look for people below going upwards
169+
170+
private def downwardsNewTarget(building: Building, lift: Lift): (Floor, Direction) =
171+
building.lowestFloorGoingUp(lift) match
172+
case Some(lowest) => (lowest, Up)
173+
case None => (building.highestFloorGoingDown(lift).getOrElse(0), Down)
174+
175+
private def upwardsNewTarget(building: Building, lift: Lift): (Floor, Direction) =
176+
building.highestFloorGoingDown(lift) match
177+
case Some(highest) => (highest, Down)
178+
case None => (building.lowestFloorGoingUp(lift).getOrElse(0), Up)
179+
}

src/test/java/test/LiftTest.java

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package test;
2+
3+
import ScalaPlayground.Lift.Dinglemouse;
4+
import org.junit.Test;
5+
6+
import static org.junit.Assert.*;
7+
8+
public class LiftTest {
9+
@Test
10+
public void testUp() {
11+
final int[][] queues = {
12+
new int[0], // 0
13+
new int[0], // 1
14+
new int[]{5, 5, 5}, // 2
15+
new int[0], // 3
16+
new int[0], // 4
17+
new int[0], // 5
18+
new int[0], // 6
19+
};
20+
21+
final int[] result = Dinglemouse.theLift(queues, 5);
22+
23+
assertArrayEquals(new int[]{0, 2, 5, 0}, result);
24+
}
25+
26+
@Test
27+
public void testDown() {
28+
final int[][] queues = {
29+
new int[0], // 0
30+
new int[0], // 1
31+
new int[]{1, 1}, // 2
32+
new int[0], // 3
33+
new int[0], // 4
34+
new int[0], // 5
35+
new int[0], // 6
36+
};
37+
38+
final int[] result = Dinglemouse.theLift(queues, 5);
39+
40+
assertArrayEquals(new int[]{0, 2, 1, 0}, result);
41+
}
42+
43+
@Test
44+
public void testUpAndUp() {
45+
final int[][] queues = {
46+
new int[0], // 0
47+
new int[]{3}, // 1
48+
new int[]{4}, // 2
49+
new int[0], // 3
50+
new int[]{5}, // 4
51+
new int[0], // 5
52+
new int[0], // 6
53+
};
54+
55+
final int[] result = Dinglemouse.theLift(queues, 5);
56+
57+
assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5, 0}, result);
58+
}
59+
60+
@Test
61+
public void testDownAndDown() {
62+
final int[][] queues = {
63+
new int[0], // G
64+
new int[]{0}, // 1
65+
new int[0], // 2
66+
new int[0], // 3
67+
new int[]{2}, // 4
68+
new int[]{3}, // 5
69+
new int[0], // 6
70+
};
71+
72+
final int[] result = Dinglemouse.theLift(queues, 5);
73+
74+
assertArrayEquals(new int[]{0, 5, 4, 3, 2, 1, 0}, result);
75+
}
76+
77+
@Test
78+
public void MyExample1() {
79+
final int[][] queues = {
80+
new int[]{2}, // G
81+
new int[0], // 1
82+
new int[0], // 2
83+
};
84+
85+
final int[] result = Dinglemouse.theLift(queues, 5);
86+
87+
assertArrayEquals(new int[]{0, 2, 0}, result);
88+
}
89+
90+
@Test
91+
public void MyExample2() {
92+
final int[][] queues = {
93+
new int[]{2, 2, 2, 2, 1, 1, 1, 1}, // G
94+
new int[0], // 1
95+
new int[0], // 2
96+
};
97+
98+
final int[] result = Dinglemouse.theLift(queues, 3);
99+
100+
assertArrayEquals(new int[]{0, 2, 0, 1, 2, 0, 1, 0}, result);
101+
}
102+
103+
@Test
104+
public void MyExample3() {
105+
final int[][] queues = {
106+
new int[]{3, 3, 3, 1, 1, 1}, // 0
107+
new int[0], // 1
108+
new int[0], // 2
109+
new int[]{0, 0, 0, 0, 0, 0}, // 3
110+
};
111+
112+
final int[] result = Dinglemouse.theLift(queues, 3);
113+
114+
assertArrayEquals(new int[]{0, 3, 0, 1, 3, 0}, result);
115+
}
116+
117+
@Test
118+
public void MyExample4() {
119+
final int[][] queues = {
120+
new int[0], // 0
121+
new int[]{0, 2, 2}, // 1
122+
new int[0], // 2
123+
new int[]{2}, // 3
124+
};
125+
126+
final int[] result = Dinglemouse.theLift(queues, 1);
127+
128+
assertArrayEquals(new int[]{0, 1, 2, 3, 2, 1, 0, 1, 2, 0}, result);
129+
}
130+
}

0 commit comments

Comments
 (0)