# Discrete event simulation of a prioritized lunch queue in Java (Data structures)

Post details

The program I am putting for being reviewed does not fit entirely in a single post, so I have to resort to the equation by Niklaus Wirth:

Algorithms + Data Structures = Programs

This part is about data structures: a set of classes that do not do anything fancy. Of course, you could consider PrioritizedQueue (to be posted in the Algorithms part) to be a data structure, yet I have to place it to the Algorithms part after all.

Simulation

I am considering a (hopefully fictional) academic setting, where there is a lunch cafeteria in a campus. The academic society recognizes four privilege levels (listed from most privileged to least privileged):

1. doctors (PhD),
2. masters (MSc),
3. bachelors (BSc),

Now, for example, when a master goes for a lunch, he skips all undergraduates and bachelors in the queue, and appends him-/herself to the end of queue consisting of masters.

(If a user want to consider a fair queue, he/she could just create a population with each member with the same academic degree.)

As often, we do this for gathering statistics: we gather minimum, average and maximum waiting times for each privilege level; also, we compute the standard deviation of the waiting times within each privilege level. Apart from that, we also compute the same set of statistical variables for the idle time of the cafeteria cashier.

Code

package net.coderodde.simulation.lunch;

/**
* This class implements an enumeration over academic degrees.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015)
*/

// The order denotes priority from highest to lowest.
DOCTOR       ("PhD"),
MASTER       ("MSc"),
BACHELOR     ("BSc"),

private final String description;

@Override
public String toString() {
return description;
}

this.description = description;
}
}


Cashier.java:

package net.coderodde.simulation.lunch;

import java.util.Objects;
import java.util.Random;
import static net.coderodde.simulation.lunch.Utils.checkMean;
import static net.coderodde.simulation.lunch.Utils.checkStandardDeviation;

/**
* This class models the action of a cashier.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 4, 2015)
*/
public final class Cashier {

private final double meanServiceTime;
private final double standardDeviationOfServiceTime;
private final Random random;

/**
* Initiates a strong fluent API for creating a {@code Cashier}.
*
* @param  random the random number generator to use.
* @return the mean service time selector.
*/
public static MeanServiceTimeSelector withRandom(Random random) {
Objects.requireNonNull(random, "The input Random is null.");
Configuration configuration = new Configuration();
configuration.random = random;
return new MeanServiceTimeSelector(configuration);
}

/**
* Initiates a strong fluent API for creating a {@code Cashier} using a
* default random number generator.
*
* @return the mean service time selector.
*/
public static MeanServiceTimeSelector withDefaultRandom() {
return withRandom(new Random());
}

public final static class MeanServiceTimeSelector {

private final Configuration configuration;

private MeanServiceTimeSelector(Configuration configuration) {
this.configuration = configuration;
}

/**
* Selects the mean service time and returns a standard deviation
* selector.
*
* @param  meanServiceTime the mean service time in seconds.
* @return a standard deviation selector.
*/
public StandardDeviationSelector
withMeanServiceTime(double meanServiceTime) {
checkMean(meanServiceTime);
configuration.meanServiceTime = meanServiceTime;
return new StandardDeviationSelector(configuration);
}
}

public final static class StandardDeviationSelector {

private final Configuration configuration;

private StandardDeviationSelector(Configuration configuration) {
this.configuration = configuration;
}

/**
* Selects a standard deviation for the service time and returns the
* {@code Cashier} using the gathered parameters.
*
* @param  standardDeviationOfServiceTime the standard deviation of the
*                                        service time in seconds.
* @return a {@code Cashier} object.
*/
public Cashier withStandardDeviationOfServiceTime(
double standardDeviationOfServiceTime) {
checkStandardDeviation(standardDeviationOfServiceTime);
return new Cashier(configuration.meanServiceTime,
standardDeviationOfServiceTime,
configuration.random);
}
}

private Cashier(double meanServiceTime,
double standardDeviationOfServiceTime,
Random random) {
this.meanServiceTime = meanServiceTime;
this.standardDeviationOfServiceTime = standardDeviationOfServiceTime;
this.random = random;
}

public int getServiceTime() {
return (int)(Math.round(meanServiceTime +
standardDeviationOfServiceTime *
random.nextGaussian()));
}

private static final class Configuration {
private Random random;
private double meanServiceTime;
}
}


LunchQueueEvent.java:

package net.coderodde.simulation.lunch;

/**
* This class describes a lunch queue event.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015).
*/
final class LunchQueueEvent implements Comparable<LunchQueueEvent> {

private final Person person;
private final int timeStamp;

LunchQueueEvent(Person person, int timeStamp) {
this.person = person;
this.timeStamp = timeStamp;
}

Person getPerson() {
return person;
}

int getTimestamp() {
return timeStamp;
}

@Override
public int compareTo(LunchQueueEvent anotherEvent) {
return Double.compare(timeStamp, anotherEvent.timeStamp);
}
}


Person.java:

package net.coderodde.simulation.lunch;

import java.util.Objects;

/**
* This class implements a record for a person.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015)
*/
public final class Person implements Comparable<Person> {

private final String firstName;
private final String lastName;
private final String stringRepresentation;
private final String identity;

public static LastNameSelector withFirstName(String firstName) {
Objects.requireNonNull(firstName,
"The first name of a person is null.");
Configuration configuration = new Configuration();
configuration.firstName = firstName;
return new LastNameSelector(configuration);
}

public static final class LastNameSelector {

private final Configuration configuration;

private LastNameSelector(Configuration configuration) {
this.configuration = configuration;
}

Objects.requireNonNull(lastName,
"The last name of a person is null.");
configuration.lastName = lastName;
}
}

public static final class AcademicDegreeSelector {

private final Configuration configuration;

this.configuration = configuration;
}

Objects.requireNonNull(degree, "The academic degree is null.");
return new Person(configuration.firstName,
configuration.lastName,
degree);
}
}

private Person(String firstName,
String lastName,
this.firstName      = firstName;
this.lastName       = lastName;
this.stringRepresentation = "[" + firstName + " " + lastName + ", " +
this.identity = firstName + " " + lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

}

@Override
public String toString() {
return stringRepresentation;
}

@Override
public int compareTo(Person o) {
}

@Override
public int hashCode() {
return identity.hashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}

if (getClass() != obj.getClass()) {
return false;
}

Person other = (Person) obj;
return Objects.equals(identity, other.identity);
}

private static final class Configuration {
private String firstName;
private String lastName;
}
}


Population.java:

package net.coderodde.simulation.lunch;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import static net.coderodde.simulation.lunch.Utils.checkTime;

/**
* This class represents simulated population.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 3, 2015)
*/
public final class Population {

private final Map<Person, Integer> arrivalTimeMap = new HashMap<>();

public final class ArrivalTimeSelector {
private final Person person;

ArrivalTimeSelector(Person person) {
this.person = Objects.requireNonNull(person,
"The input person is null.");
}

public boolean withArrivalTime(int arrivalTime) {
checkTime(arrivalTime);

if (arrivalTimeMap.containsKey(person)) {
return false;
}

arrivalTimeMap.put(person, arrivalTime);
return true;
}
}

return new ArrivalTimeSelector(person);
}

public int size() {
return arrivalTimeMap.size();
}

Set<Person> getPersonSet() {
return Collections.<Person>unmodifiableSet(arrivalTimeMap.keySet());
}

Queue<LunchQueueEvent> toEventQueue() {
List<LunchQueueEvent> eventList = new ArrayList<>(size());

getPersonSet().stream().forEach((person) -> {
arrivalTimeMap.get(person)));
});

Collections.sort(eventList,
(event1, event2) -> {
// Try to compare by the time stamps of the events.
int cmp = event1.compareTo(event2);

if (cmp != 0) {
return cmp;
}

// The two input events have same time stamp, break ties by person
// priority.
return event1.getPerson().compareTo(event2.getPerson());
});

return new ArrayDeque<>(eventList);
}
}


SimulationResult.java:

package net.coderodde.simulation.lunch;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* This class holds the statistics of a simulation.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015)
*/
public final class SimulationResult {

private static final String NL = "\n";
private static final String SKIP = "    ";
private static final int NO_DATA = -1;

private final Map<AcademicDegree, Integer> waitAverageMap = new HashMap<>();
private final Map<AcademicDegree, Integer> waitStandardDeviationMap =
new HashMap<>();

private final Map<AcademicDegree, Integer> waitMinMap = new HashMap<>();
private final Map<AcademicDegree, Integer> waitMaxMap = new HashMap<>();

private final Map<Person, LunchQueueEvent> arrivalEventMap;
private final Map<Person, LunchQueueEvent> servedEventMap;

private int cashierMinimumIdleTime = NO_DATA;
private int cashierAverageIdleTime = NO_DATA;
private int cashierMaximumIdleTime = NO_DATA;
private int cashierStandardDeviation = NO_DATA;

return waitMinMap.getOrDefault(degree, NO_DATA);
}

return waitAverageMap.getOrDefault(degree, NO_DATA);
}

return waitMaxMap.getOrDefault(degree, NO_DATA);
}

return waitStandardDeviationMap.getOrDefault(degree, NO_DATA);
}

public int getCashierMinimumIdleTime() {
return cashierMinimumIdleTime;
}

public int getCashierAverageIdleTime() {
return cashierAverageIdleTime;
}

public int getCashierMaximumIdleTime() {
return cashierMaximumIdleTime;
}

public int getCashierStandardDeviation() {
return cashierStandardDeviation;
}

SimulationResult(Map<Person, LunchQueueEvent> arrivalEventMap,
Map<Person, LunchQueueEvent> servedEventMap) {
this.arrivalEventMap = arrivalEventMap;
this.servedEventMap = servedEventMap;
}

void putWaitMinimumTime(AcademicDegree degree, int minimumWaitTime) {
waitMinMap.put(degree, minimumWaitTime);
}

void putAverageWaitTime(AcademicDegree degree, int averageTime) {
waitAverageMap.put(degree, averageTime);
}

void putWaitMaximumTime(AcademicDegree degree, int maximumWaitTime) {
waitMaxMap.put(degree, maximumWaitTime);
}

int timeStandardDeviation) {
waitStandardDeviationMap.put(degree, timeStandardDeviation);
}

void putCashierMinimumIdleTime(int cashierMinimumIdleTime) {
this.cashierMinimumIdleTime = cashierMinimumIdleTime;
}

void putCashierAverageIdleTime(int cashierAverageIdleTime) {
this.cashierAverageIdleTime = cashierAverageIdleTime;
}

void putCashierMaximumIdleTime(int cashierMaximumIdleTime) {
this.cashierMaximumIdleTime = cashierMaximumIdleTime;
}

void putCashierStandardDeviation(int cashierStandardDeviation) {
this.cashierStandardDeviation = cashierStandardDeviation;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
List<Person> personList = new ArrayList<>(arrivalEventMap.keySet());

Collections.<Person>sort(personList,
(p1, p2) -> {
double arrivalTime1 = arrivalEventMap.get(p1).getTimestamp();
double servedTime1 = servedEventMap.get(p1).getTimestamp();

double arrivalTime2 = arrivalEventMap.get(p2).getTimestamp();
double servedTime2 = servedEventMap.get(p2).getTimestamp();

return Double.compare(servedTime1 - arrivalTime1,
servedTime2 - arrivalTime2);
});

for (Person person : personList) {
sb.append(person.toString())
.append(", wait time: ")
.append((int)(servedEventMap.get(person).getTimestamp() -
arrivalEventMap.get(person).getTimestamp()))
.append(" seconds.")
.append(NL);
}

sb.append("Cashier:")
.append(NL)
.append(SKIP)
.append("Minimum idle time:  ")
.append(getCashierMinimumIdleTime())
.append(" seconds.")
.append(NL)
.append(SKIP)
.append("Average idle time:  ")
.append(getCashierAverageIdleTime())
.append(" seconds.")
.append(NL)
.append(SKIP)
.append("Maximum idle time:  ")
.append(getCashierMaximumIdleTime())
.append(" seconds.")
.append(NL)
.append(SKIP)
.append("Standard deviation: ")
.append(getCashierStandardDeviation())
.append(" seconds.");

return sb.toString();
}

private void toString(StringBuilder sb, AcademicDegree degree) {
sb.append(degree.toString()).append(":").append(NL);

sb.append(SKIP)
.append("Minimum wait time:  ")
.append(getMinimumWaitTime(degree))
.append(" seconds.")
.append(NL);

sb.append(SKIP)
.append("Average wait time:  ")
.append(getWaitAverage(degree))
.append(" seconds.")
.append(NL);

sb.append(SKIP)
.append("Maximum wait time:  ")
.append(getMaximumWaitTime(degree))
.append(" seconds.")
.append(NL);

sb.append(SKIP)
.append("Standard deviation: ")
.append(getWaitStandardDeviation(degree))
.append(" seconds.")
.append(NL);
}
}


Utils.java:

package net.coderodde.simulation.lunch;

/**
* This class contains miscellaneous utilities.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015)
*/
final class Utils {

public static void checkMean(double mean) {
if (Double.isNaN(mean)) {
throw new IllegalArgumentException(
"The mean is NaN (not-a-number):");
}

if (Double.isInfinite(mean)) {
throw new IllegalArgumentException("The mean is infinite: " + mean);
}
}

public static void checkStandardDeviation(double deviation) {
if (Double.isNaN(deviation)) {
throw new IllegalArgumentException(
"The standard deviation is NaN (not-a-number):");
}

if (Double.isInfinite(deviation)) {
throw new IllegalArgumentException(
"The standard deviation is infinite: " + deviation);
}

if (deviation < 0.0) {
throw new IllegalArgumentException(
"The standard deviation is negative: " + deviation);
}
}

public static void checkTime(double time) {
if (Double.isNaN(time)) {
throw new IllegalArgumentException(
"The input time is NaN (not-a-number).");
}

if (Double.isInfinite(time)) {
throw new IllegalArgumentException(
"The input time is infinite: " + time);
}
}
}


(I will post the algorithms part shortly.)

Is my coding style, naming conventions, API design reasonable? Any critique much appreciated!

Your Utils class has a lot of duplicate code. Separate components into methods:

package net.coderodde.simulation.lunch;

/**
* This class contains miscellaneous utilities.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 2, 2015)
*/
final class Utils {

public static void checkMean(double mean) {
final String errorStart = "The mean is ";
checkNaN(errorStart, mean);
checkInfinity(errorStart, mean);
}

public static void checkStandardDeviation(double deviation) {
final String errorStart = "The standard deviation is ";
checkNaN(errorStart, mean);
checkInfinity(errorStart, mean);
checkNegative(errorStart, mean);
}

public static void checkTime(double time) {
final String errorStart = "The input time is ";
checkNaN(errorStart, time);
checkInfinity(errorStart, time);
}

private static void checkNaN(String errorStart, double num) {
if (Double.isNaN(num)) {
throw new IllegalArgumentException(
errorStart + "NaN (not-a-number).");
}
}

private static void checkInfinite(double num) {
if (Double.isInfinite(num)) {
throw new IllegalArgumentException(
errorStart + "infinite: " + num);
}
}

private static void checkNegative(double num) {
if (num < 0.0) {
throw new IllegalArgumentException(
errorStart + "negative: " + num);
}
}

}


This way, you have individual method for each value you have to check for, and it is easier to add another method if you need to.

You have a NL constant, which is pretty much a \n. You use it to append. Instead of doing that, you can easily append the \n char itself, which saves a String creation.