This is an exercise to implement a waiting room for an hospital with the following requirements:
- Add (and remove) a patient to the queue of the waiting room
- Move a patient up and down the queue
- Show all patients in the queue
- The queue needs to be persisted
The WaitingRoom
interface:
public interface WaitingRoom {
Patient addPatient(Patient patient);
Optional<Patient> removePatient();
// Move a patient by delta positions in the queue.
// E.g. if p1 is second in the queue, the operation
// move(p1,1) moves p1 to the first position.
// Delta can be negative.
void move(Patient patient, int delta);
List<Patient> peekPatients();
int size();
void clear();
}
The Patient
class:
@Getter
@Setter
@NoArgsConstructor
@Entity
@ToString
public class Patient {
@Id
@GeneratedValue
private Long id;
private String name;
public Patient(String name) {
this.name = name;
}
}
This is the WaitingRoomService
:
@Service
public class WaitingRoomService implements WaitingRoom {
private EditableQueue<Patient> queue;
public WaitingRoomService(PersistentQueue<Patient> queue) {
this.queue = queue;
}
@Override
public List<Patient> getAllPatients() {
return queue.getAll();
}
@Override
public Patient addPatient(Patient patient) {
return queue.enqueue(patient);
}
@Override
public Optional<Patient> removePatient() {
return queue.dequeue();
}
@Override
public void move(Patient patient, int delta) {
queue.move(patient, delta);
}
@Override
public int size() {
return queue.size();
}
@Override
public void clear() {
queue.clear();
}
}
EditableQueue
is an interface implemented by PersistentQueue
:
@Component
public class PersistentQueue<T> implements EditableQueue<T> {
private NodeRepository<T> nodeRepo;
public PersistentQueue(NodeRepository<T> nodeRepo) {
this.nodeRepo = nodeRepo;
}
@Override
public List<T> getAll() {
return nodeRepo.findByOrderByPosition()
.stream()
.map(node -> node.getValue())
.collect(Collectors.toList());
}
@Override
@Transactional
public T enqueue(T element) {
Node<T> node = new Node<>(element);
Optional<Node<T>> lastNode = nodeRepo.findByLastTrue();
int newPosition = 0;
if (lastNode.isPresent()) {
newPosition = lastNode.get().getPosition() + 1;
lastNode.get().setLast(false);
} else {
node.setFirst(true);
}
node.setPosition(newPosition);
node.setLast(true);
return nodeRepo.save(node).getValue();
}
@Override
@Transactional
public Optional<T> dequeue() {
if (size() == 0) {
return Optional.empty();
}
Optional<Node<T>> first = nodeRepo.findByFirstTrue();
Optional<Node<T>> second = nodeRepo.findByPosition(first.get().getPosition() + 1);
if (second.isPresent()) {
second.get().setFirst(true);
}
nodeRepo.deleteById(first.get().getId());
return Optional.of(first.get().getValue());
}
@Override
@Transactional
public void move(T element, int delta) {
Node<T> toMove = nodeRepo.findByValue(element).get();
int newPosition = toMove.getPosition() - delta;
int next = delta > 0 ? toMove.getPosition() - 1 : toMove.getPosition() + 1;
int start, end = 0;
if (delta > 0) {
start = newPosition;
end = next;
} else {
start = next;
end = newPosition;
}
for (Node<T> n : nodeRepo.findAllByPositionBetween(start, end)) {
if (delta > 0) {
// swap head with node to move
if (n.isFirst()) {
n.setFirst(false);
toMove.setFirst(true);
}
// swap tail with node to move
if (toMove.isLast() && n.getPosition() == next) {
toMove.setLast(false);
n.setLast(true);
}
} else {
if (n.isLast()) {
n.setLast(false);
toMove.setLast(true);
}
if (toMove.isFirst() && n.getPosition() == next) {
toMove.setFirst(false);
n.setFirst(true);
}
}
n.setPosition(delta > 0 ? n.getPosition() + 1 : n.getPosition() - 1);
}
toMove.setPosition(newPosition);
}
@Override
public int size() {
return (int) nodeRepo.count();
}
@Override
public void clear() {
nodeRepo.deleteAll();
}
}
NodeRepository
:
interface NodeRepository<T> extends JpaRepository<Node<T>, Long> {
List<Node<T>> findByOrderByPosition();
Optional<Node<T>> findByPosition(int position);
Optional<Node<T>> findByValue(T value);
Optional<Node<T>> findByFirstTrue();
Optional<Node<T>> findByLastTrue();
List<Node<T>> findAllByPositionBetween(int start, int end);
}
A Node
is an element of EditableQueue
and a wrapper for Patient
:
@Getter
@Setter
@NoArgsConstructor
@Entity
@ToString
public class Node<T> {
@Id
@GeneratedValue
private Long id;
@OneToOne(cascade = CascadeType.PERSIST, targetEntity = Patient.class)
private T value;
private int position;
private boolean first;
private boolean last;
public Node(T value) {
this.value = value;
this.position = -1;
}
}
Finally, the tests:
@SpringBootTest
public class WaitingRoomServiceTest {
@Autowired
private WaitingRoom waitingRoom;
@BeforeEach
public void clearRoom() {
waitingRoom.clear();
}
@Test
public void enqueueDequeueTest() {
waitingRoom.addPatient(new Patient("Marc"));
waitingRoom.addPatient(new Patient("Anna"));
assertTrue(waitingRoom.size() == 2);
assertEquals("Marc", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 1);
assertEquals("Anna", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 0);
}
@Test
public void moveTest() {
waitingRoom.addPatient(new Patient("p1"));
waitingRoom.addPatient(new Patient("p2"));
Patient p3 = waitingRoom.addPatient(new Patient("p3"));
waitingRoom.addPatient(new Patient("p4"));
// Move p3 up by 1 position
waitingRoom.move(p3, 1);
assertEquals("p1", waitingRoom.removePatient().get().getName());
assertEquals("p3", waitingRoom.removePatient().get().getName());
assertEquals("p2", waitingRoom.removePatient().get().getName());
assertEquals("p4", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 0);
}
@Test
public void moveTest2() {
waitingRoom.addPatient(new Patient("p1"));
waitingRoom.addPatient(new Patient("p2"));
Patient p3 = waitingRoom.addPatient(new Patient("p3"));
Patient p4 = waitingRoom.addPatient(new Patient("p4"));
waitingRoom.addPatient(new Patient("p5"));
// Move p3 up by 2 position
waitingRoom.move(p3, 2);
// Move p4 down by 1 position
waitingRoom.move(p4, -1);
assertEquals("p3", waitingRoom.removePatient().get().getName());
assertEquals("p1", waitingRoom.removePatient().get().getName());
assertEquals("p2", waitingRoom.removePatient().get().getName());
assertEquals("p5", waitingRoom.removePatient().get().getName());
assertEquals("p4", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 0);
}
@Test
public void moveTest3() {
waitingRoom.addPatient(new Patient("p1"));
Patient p2 = waitingRoom.addPatient(new Patient("p2"));
waitingRoom.move(p2, 1);
waitingRoom.move(p2, -1);
assertEquals("p1", waitingRoom.removePatient().get().getName());
assertEquals("p2", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 0);
}
@Test
public void moveTest4() {
waitingRoom.addPatient(new Patient("p1"));
Patient p2 = waitingRoom.addPatient(new Patient("p2"));
waitingRoom.addPatient(new Patient("p3"));
waitingRoom.addPatient(new Patient("p4"));
waitingRoom.removePatient();
waitingRoom.move(p2, -1);
assertEquals("p3", waitingRoom.removePatient().get().getName());
assertEquals("p2", waitingRoom.removePatient().get().getName());
assertEquals("p4", waitingRoom.removePatient().get().getName());
assertTrue(waitingRoom.size() == 0);
}
@Test
public void stressTest() {
int n = 1000;
long startTime = System.nanoTime();
for (long i = 1; i < n; i++) {
waitingRoom.addPatient(new Patient("p" + i));
}
long endTime = System.nanoTime();
System.out.format("Enqueue %d time: %d ms%n", n, (endTime - startTime) / 1000000);
startTime = System.nanoTime();
for (long i = 1; i < n; i++) {
waitingRoom.removePatient();
}
endTime = System.nanoTime();
System.out.format("Dequeue %d time: %d ms%n", n, (endTime - startTime) / 1000000);
assertTrue(waitingRoom.size() == 0);
}
@Test
public void getAllPatientsTest() {
waitingRoom.addPatient(new Patient("p1"));
waitingRoom.addPatient(new Patient("p2"));
waitingRoom.addPatient(new Patient("p3"));
List<Patient> patients = waitingRoom.getAllPatients();
assertEquals("p1", patients.get(0).getName());
assertEquals("p2", patients.get(1).getName());
assertEquals("p3", patients.get(2).getName());
}
}
The GitHub repo is here for further details.
This is how the method move
works. Every node in the queue has a position:
n1=0
: headn2=1
n3=2
n4=3
n5=4
: tail
Calling the method move(n4,2)
means moving the node n4
up by 2 positions. It does it in two actions:
- Increment by 1 the nodes
n3
andn2
:n1=0
,n2=2
,n3=3
,n4=3
,n5=4
- Set the position of
n4
ton4.position - delta
(that is3-2=1
):n1=0
,n2=2
,n3=3
,n4=1
,n5=4
Sorting by position, the nodes are now: n1
, n4
, n2
, n3
, n5
.
Every time the method move
needs to update the positions of the nodes in the database, can it be improved? Or can EditableQueue
be implemented more efficiently?
Any feedbacks, improvements or alternative solutions are welcome. Thanks.