Design and implement a Parking Lot
Author: Mohammad J Iqbal
Follow Mohammad J Iqbal on LinkedIn
If watching the topic on video more convenient to you, you can watch it on YouTube below:
Constraints and assumptions
-
What types of vehicles should we support?
Motorcycle, Car, Bus
-
Does the parking lot have multiple levels?
Yes
-
How you park cars in each Level? Each Level will have multiple rows, rows can have different number of parking spots of different types.
public static final char [][] level1Layout = {
{'L','L','L','L','L','L','C','C','C','C'},
{'M','M','M','M','M','M','M','M','M','C'},
{'C','C','C','C','C','C','C','L','L','L','L','L','L'}
};
-
Does each vehicle type take up a different amount of parking spots?
Yes
-
Spot usages:
Motorcycle spot can fit Motorcycle
Compact spot can fit Motorcycle, Car
Large spot can fit Motorcycle, Car, Bus
Bus can park if we have 5 consecutive “large” spots in a Row
Uses Cases:
-
We should be able to configure spot layout for all the parking levels in config file
-
We should be able to park a vehicle and get ticket if spot available.
-
During checkout, spots should be freed, amount due should be printed along with other info in the Ticket.
In the class diagram above we can see an abstract class Vehicle and the class MotorCycle, Car and Bus extended class Vehicle.
Do we need the inheritance from Vehicle? For some other parking lot requirements it might not be needed but for our requirements we have functionality like a vehicle can take multiple spots or fit in two or more different spots.
Motor Cycle fits in all Motorcycle, Compact and Large spots. So canFitInSpot function has different implementation for different vehicle and is a property to a Vehicle.
A Parking Lot can have multiple Levels and Each levels has Rows of Spots.
To keep track of the startTime, endTime etc. and for calculating amount we are using the Ticket class.
Let’s look at the classes below:
I have used a Configuration class to provide details for number of levels in a parking lot, level layout etc.
The Configuration Class is provided below:
package com.spsoft.parkinglot;
import java.util.concurrent.atomic.AtomicInteger;
public class Configuration {
public static final int Number_OF_LEVELS = 3;
public static AtomicInteger ticketNumber = new AtomicInteger(1);
public static final char [][] level1Layout = {
{'L','L','L','L','L','L','C','C','C','C'},
{'M','M','M','M','M','M','M','M','M','C'},
{'C','C','C','C','C','C','C','L','L','L','L','L','L'}
};
public static final char [][] level2Layout = {
{'M','M','M','L','L','L','L','L','L','C'},
{'M','M','M','M','M','M','M','M','M','C'},
{'C','C','C','C','C','C','C','L','L','L','L','L','L'},
{'C','C','C','C','C','C','C','L','L','L','L','L','L'}
};
public static final char [][] level3Layout = {
{'M','M','M','L','L','L','L','L','L','C'},
{'C','C','C','C','C','C','C','C','C','M'},
{'C','C','C','C','C','C','C','L','L','L','L','L','L'},
{'L','L','L','L','L','L','L','L','L'},
{'L','L','L','L','L','L','L','L','L'}
};
public static double getParkingRate(Spot.SpotType spotType){
double rate = switch (spotType){
case MOTORCYCLE -> 5.0;
case COMPACT -> 8.0;
case LARGE -> 10.0;
};
return rate;
}
}
Abstract class Vehicle:
package com.spsoft.parkinglot;
import java.util.ArrayList;
import java.util.List;
public abstract class Vehicle {
String licensePlate;
int spotRequired;
List<Spot> spots = new ArrayList<>();
public Vehicle(String licensePlate, int spotRequired){
this.licensePlate = licensePlate;
this.spotRequired = spotRequired;
}
void clearSpots(){
spots.clear();
System.out.println("Size of spotTakens after clearing spots");
}
void assignSpot(Spot spot){
spots.add(spot);
System.out.println("Spot: "+spot.spotID+" has been assigned to Vehicle: "+ licensePlate);
}
public abstract boolean canFitInSpot(Spot spot);
}
Class Car extends Vehicle
package com.spsoft.parkinglot;
public class Car extends Vehicle {
public Car(String licensePlate,int spotRequired){
super(licensePlate,spotRequired);
}
public boolean canFitInSpot(Spot spot){
return (spot.spotType == Spot.SpotType.COMPACT || spot.spotType == Spot.SpotType.LARGE);
}
}
Class MotorCycle extends Vehicle
package com.spsoft.parkinglot;
public class MotorCycle extends Vehicle {
public MotorCycle(String licensePlate, int spotRequired) {
super(licensePlate, 1);
}
public boolean canFitInSpot(Spot spot) {
return true;
}
}
Class Bus extends Vehicle
package com.spsoft.parkinglot;
public class Bus extends Vehicle{
public Bus(String licensePlate, int spotRequired){
super(licensePlate,spotRequired);
}
public boolean canFitInSpot(Spot spot){
return (spot.spotType == Spot.SpotType.LARGE);
}
}
Class Spot
package com.spsoft.parkinglot;
import java.time.LocalDateTime;
public class Spot {
enum SpotType{
MOTORCYCLE,COMPACT,LARGE
}
int spotID;
public SpotType spotType;
Vehicle vehicle;
boolean isOccupied;
LocalDateTime startTime;
public Spot(int spotID,SpotType spotType){
this.spotID = spotID;
this.spotType = spotType;
}
public void reserveSpot(Vehicle vehicle, LocalDateTime startTime){
this.isOccupied = true;
this.vehicle = vehicle;
this.startTime = startTime;
}
public void freeSpot(){
this.isOccupied = false;
this.vehicle = null;
}
public Spot getSpotById(int spotID){
if(this.spotID == spotID)
return this;
else
return null;
}
public com.spsoft.parkinglot.Spot.SpotType getSpotType() {
return this.spotType;
}
public static Spot createSpot(int spotId, char type){
Spot.SpotType currentSpot = switch (type) {
case 'M' -> Spot.SpotType.MOTORCYCLE;
case 'C' -> Spot.SpotType.COMPACT;
case 'L' -> Spot.SpotType.LARGE;
default -> null;
};
if(currentSpot == null)
throw new IllegalArgumentException("Invalid Spot Type");
return new Spot(spotId,currentSpot);
}
}
Class Level
package com.spsoft.parkinglot;
import java.time.LocalDateTime;
import java.util.*;
public class Level {
int levelId;
List<List<Spot>> rowList;
int numRows;
public Level(int levelId, int numRows){
this.levelId = levelId;
this.numRows = numRows;
rowList = new ArrayList<>(numRows);
for(int i=0; i < numRows; i++){
rowList.add(new ArrayList<Spot>());
}
}
public void addSpot(int rowNum, Spot spot){
if(rowNum >= rowList.size())
throw new IllegalArgumentException("Invalid Row Number: "+ rowNum);
this.rowList.get(rowNum).add(spot); //since rowList is 0 based array and row number is starting from 1
}
public List<Spot> assignSpots(Vehicle vehicle){
List<Spot> spots = findAvailableSpots(vehicle);
if(spots == null)
return null;
LocalDateTime startTime = LocalDateTime.now();
for(Spot sp : spots){
sp.reserveSpot(vehicle,startTime);
}
return spots;
}
public List<Spot> findAvailableSpots(Vehicle vehicle){
for(List<Spot> spotList : rowList){
for(int spotIndex =0; spotIndex < spotList.size(); spotIndex++){
Spot spot = spotList.get(spotIndex);
if(!spot.isOccupied && vehicle.canFitInSpot(spot)){
if(vehicle.spotRequired == 1)
return Collections.singletonList(spot);
else{ // For Bus it will require more than 1 spots
int endIndex = spotIndex + vehicle.spotRequired -1;
if(isConsecutiveFreeSpotsExists(spotList,spotIndex,endIndex,spot)){
return spotList.subList(spotIndex,endIndex+1);
}
}
}
}
}
return null;
}
private boolean isConsecutiveFreeSpotsExists(List<Spot> spotList, int spotIndex,int endIndex, Spot spot){
if(endIndex >= spotList.size())
return false;
for(int i= spotIndex; i <= endIndex; i++){
if(spotList.get(i).isOccupied)
return false;
if(spotList.get(i).spotType != spot.spotType)
return false;
}
return true;
}
public void printLayoutForLevel(){
for(List<Spot> row : rowList){
for(Spot spot : row){
System.out.print(spot.spotType.toString().charAt(0)+" ");
}
System.out.println();
}
}
public void createSpotsForLevel(char [][] levelLayout){
int numRows = levelLayout.length;
int spotId=1;
for(int row =0; row < numRows; row++){
for(int col=0; col < levelLayout[row].length; col++){
addSpot(row, Spot.createSpot(spotId++,levelLayout[row][col]));
}
}
}
}
Class Ticket
package com.spsoft.parkinglot;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
public class Ticket {
int ticketNo;
Vehicle vehicle;
List<Spot> spots;
LocalDateTime startTime;
LocalDateTime endTime;
boolean isPaid;
double amount;
public Ticket(Vehicle vehicle, List<Spot> spots, LocalDateTime startTime){
this.vehicle = vehicle;
this.spots = spots;
this.startTime = startTime;
ticketNo = Configuration.ticketNumber.getAndIncrement();
}
public long parkingDurationInMins(){
if(endTime == null)
return 0;
return Duration.between(startTime,endTime).toMinutes();
}
public double calculateAmount(){
double duration = parkingDurationInMins() * 1.0 /60;
double amount=0;
for(Spot spot : spots){
amount += Configuration.getParkingRate(spot.spotType) * duration;
}
this.amount = amount;
return amount;
}
public void printTicket(){
System.out.println("Ticket Number: "+ ticketNo);
System.out.println("Vehicle License Plate: "+vehicle.licensePlate);
System.out.println("Start Time: "+ startTime);
System.out.println("End Time: "+ endTime);
System.out.println("Is Paid: "+ isPaid);
System.out.println("Spots :"+ this.spots.stream().map(spot -> String.valueOf(spot.spotID)).collect(Collectors.joining(", ")));
System.out.println("Amount: "+ amount);
System.out.println();
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
}
Class ParkingLot We can create Multiple objects of this parking lot in case we have our parking garages like South Garage, North Garage etc.
package com.spsoft.parkinglot;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class ParkingLot {
int parkingLotId;
int numLevels;
private List<Level> levels;
ConcurrentHashMap<Integer,Ticket> ticketMap = new ConcurrentHashMap<>();
public ParkingLot(int parkingLotId,int numLevels){
this.numLevels = numLevels;
levels = new ArrayList<>(numLevels);
}
public void addLevel(Level level){
this.levels.add(level);
}
public Ticket assignTicket(Vehicle vehicle){
List<Spot> spots = parkVehicle(vehicle);
if(spots == null)
return null;
LocalDateTime startTime = spots.get(0).startTime;
Ticket ticket = new Ticket(vehicle,spots,startTime);
ticketMap.put(ticket.ticketNo,ticket);
return ticket;
}
public List<Spot> parkVehicle(Vehicle vehicle){
for(Level level: levels){
List<Spot> spots = level.assignSpots(vehicle);
if(spots != null)
return spots;
}
return null;
}
public Level createLevel(int levelId,int numRows){
return new Level(levelId,numRows);
}
public boolean checkoutVehicle(Ticket ticket){
List<Spot> spots = ticket.spots;
spots.forEach(Spot::freeSpot);
ticket.vehicle.clearSpots();
ticket.setEndTime(LocalDateTime.now());
ticket.calculateAmount();
return true;
}
public void printLayout(){
for(Level level : levels){
System.out.println("Level :"+level.levelId);
level.printLayoutForLevel();
}
}
}
Class ParkingLotTest to test our Parking Lot
package com.spsoft.parkinglot;
public class ParkingLotTest {
public static void main(String args[]) {
ParkingLot parkingLot = new ParkingLot(1,Configuration.Number_OF_LEVELS);
Level level1 = parkingLot.createLevel(1,Configuration.level1Layout.length);
level1.createSpotsForLevel(Configuration.level1Layout);
parkingLot.addLevel(level1);
Level level2 = parkingLot.createLevel(2,Configuration.level2Layout.length);
level2.createSpotsForLevel(Configuration.level2Layout);
parkingLot.addLevel(level2);
Level level3 = parkingLot.createLevel(3,Configuration.level3Layout.length);
level3.createSpotsForLevel(Configuration.level3Layout);
parkingLot.addLevel(level3);
parkingLot.printLayout();
//create an object of Car and call the parkVehicle
Vehicle car = new Car("C1",1);
Ticket ticket1 = parkingLot.assignTicket(car);
ticket1.printTicket();
//create an object or MotorCycle and call parkVehicle
Vehicle motorCycle = new MotorCycle("M1",1);
Ticket ticket2 = parkingLot.assignTicket(motorCycle);
ticket2.printTicket();
Vehicle bus = new Bus("B1",5);
Ticket ticket3 = parkingLot.assignTicket(bus);
ticket3.printTicket();
System.out.println();
//Delaying 1 min before we checkout a Vehicle.
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Checkout a vehicle
Ticket ticket4 = parkingLot.ticketMap.get(3);
System.out.println("Check out a vehicle with ticketNo : "+ticket4.ticketNo);
parkingLot.checkoutVehicle(ticket4);
ticket4.printTicket();
}
}
Output
Level :1
L L L L L L C C C C
M M M M M M M M M C
C C C C C C C L L L L L L
Level :2
M M M L L L L L L C
M M M M M M M M M C
C C C C C C C L L L L L L
C C C C C C C L L L L L L
Level :3
M M M L L L L L L C
C C C C C C C C C M
C C C C C C C L L L L L L
L L L L L L L L L
L L L L L L L L L
Ticket Number: 1
Vehicle License Plate: C1
Start Time: 2024-12-01T19:42:30.275873500
End Time: null
Is Paid: false
Spots :1
Amount: 0.0
Ticket Number: 2
Vehicle License Plate: M1
Start Time: 2024-12-01T19:42:30.302873
End Time: null
Is Paid: false
Spots :2
Amount: 0.0
Ticket Number: 3
Vehicle License Plate: B1
Start Time: 2024-12-01T19:42:30.305873900
End Time: null
Is Paid: false
Spots :28, 29, 30, 31, 32
Amount: 0.0
Check out a vehicle with ticketNo : 3
Size of spotTakens after clearing spots
Ticket Number: 3
Vehicle License Plate: B1
Start Time: 2024-12-01T19:42:30.305873900
End Time: 2024-12-01T19:43:30.323344
Is Paid: false
Spots :28, 29, 30, 31, 32
Amount: 0.8333333333333333