Skip to content

Instantly share code, notes, and snippets.

@davidkayce
Last active February 8, 2022 06:38
Show Gist options
  • Save davidkayce/f9b3018dbca09d7bc92da1495f95595c to your computer and use it in GitHub Desktop.
Save davidkayce/f9b3018dbca09d7bc92da1495f95595c to your computer and use it in GitHub Desktop.
Elevator Engineering Test. Full test description can be found here: https://docs.google.com/document/d/1Vx2iVzvoB_wQ-fG5RC-tQviseJvs-mzofL_rAaz4t_A/edit?usp=sharing
interface IElevator {
on: (events: string, callback: Function) => void;
getCurrentFloor: () => number;
getCurrentDirection: () => DIRECTIONS;
moveUp: () => void;
moveDown: () => void;
}
interface IElevatorDoor {
open: () => void;
close: () => void;
}
interface IElevatorButton {
onPress: () => void;
type: BUTTON_TYPES;
floor: number;
directions?: DIRECTIONS[];
}
enum BUTTON_TYPES {
CABIN = "cabin",
FLOOR = "floor",
}
enum ElevatorEvents {
DOORS_CLOSED = "doorsClosed",
BEFORE_FLOOR = "beforeFloor",
FLOOR_BUTTON_PRESSED = "floorButtonPressed",
CABIN_BUTTON_PRESSED = "cabinButtonPressed",
}
enum DIRECTIONS {
DOWN = -1,
NONE = 0,
UP = 1,
}
type Nullable<T> = T | undefined;
/* This implements the hardware elevator according to the specification in the gist's description
* It is assumed that the functions for the elevator doors and buttons, how they are controlled or pressed to emit events are handled
* and just passed into the class constructor. It is also assumed that logic on managing the elevator doors state is handled */
class HardwareElevator extends EventTarget implements IElevator {
static DIRECTIONS = {
DOWN: -1,
NONE: 0,
UP: 1,
};
// Floor information
private minFloor: number = 0;
private maxFloor: number = 0;
private currentFloor: number;
private nextFloor: number;
private currentDirection: DIRECTIONS;
private destinationFloorQueue: [number, Nullable<DIRECTIONS>][] = [];
private elevatorMoving: boolean;
private elevatorTimestep: number = 5000; // arbitrary number
private elevatorInterval: Nullable<number>;
private recheckInterval: Nullable<number>;
constructor(
elevatorButtons: IElevatorButton[],
public ElevatorDoor: IElevatorDoor
) {
super();
// On initializing the elevator, we assume it starts at rest and on the ground floor.
this.currentFloor = 0;
this.nextFloor = 0;
this.currentDirection = DIRECTIONS.NONE;
this.elevatorMoving = false;
// Find the top and last floors based on the configuration of the floor buttons
const [minFloor, maxFloor] = elevatorButtons.reduce(
(acc: number[], curr: IElevatorButton) => {
if (curr.type !== BUTTON_TYPES.FLOOR || curr.directions?.length !== 1)
return acc;
if (curr.floor < acc[0]) {
acc[1] = acc[0];
acc[0] = curr.floor;
return acc;
}
return acc;
},
[0, 0]
);
this.minFloor = minFloor;
this.maxFloor = maxFloor;
/* Register event listener. It is assumed that the event params are not passed through 'event.details'
* but only as callback arguments in order to maintain the structure given for the 'on' method */
[
ElevatorEvents.DOORS_CLOSED,
ElevatorEvents.BEFORE_FLOOR,
ElevatorEvents.CABIN_BUTTON_PRESSED,
ElevatorEvents.FLOOR_BUTTON_PRESSED,
].forEach((e) =>
this.addEventListener(
e,
this.on(e, (event: any) => event.callback())
)
);
}
/* Event handler for elevator hardware, registering the event listeners is not
* handled but can be done by having the class inherit the 'EventTarget' class and
* registering all events in the constructor */
on(event: string, callback: Function) {
switch (event) {
case ElevatorEvents.BEFORE_FLOOR:
// If there are no more destinations on the queue, stop
if (!this.destinationFloorQueue.length) {
clearInterval(this.elevatorInterval);
this.elevatorMoving = false;
return (this.currentDirection = DIRECTIONS.NONE);
}
// Otherwise check if the next floor is the next destination
const stopNextFloor = this.destinationFloorQueue.find(
(floorinfo) => floorinfo[0] === this.nextFloor
);
this.currentFloor = this.nextFloor;
/* If the elevator is not meant to stop in the next floor,
* allow the existing elevator interval to run it's course */
return stopNextFloor ? this.stopAndOpenDoors() : null;
case ElevatorEvents.FLOOR_BUTTON_PRESSED:
const [floorNumber, direction]: [number, DIRECTIONS] =
callback.arguments;
/* Account for cases when another customer on the cyrrent floor clicks on the
* floor button when the elevator is closing or while it is resting.
* This would trigger the elevator to open again */
if (!this.elevatorMoving && this.currentFloor === floorNumber) {
this.nextFloor = floorNumber;
callback(floorNumber, direction);
return this.stopAndOpenDoors();
}
this.destinationFloorQueue.push([floorNumber, direction]);
// If the elevator is not being used, call it to current floor
if (!this.elevatorMoving && this.destinationFloorQueue.length === 1) {
callback(floorNumber, direction);
return floorNumber > this.currentFloor
? this.moveUp()
: this.moveDown();
}
return callback(floorNumber, direction);
case ElevatorEvents.CABIN_BUTTON_PRESSED:
const [cabinfloorNumber]: [number] = callback.arguments;
this.destinationFloorQueue.push([cabinfloorNumber, DIRECTIONS.NONE]);
callback(cabinfloorNumber);
return setTimeout(() => {
this.ElevatorDoor.close();
this.dispatchEvent(new Event(ElevatorEvents.DOORS_CLOSED));
}, this.elevatorTimestep);
case ElevatorEvents.DOORS_CLOSED:
clearInterval(this.elevatorInterval);
const continueInCurrentDirection = this.destinationFloorQueue.find(
(floorInfo) => {
const checkFloor =
this.currentDirection === DIRECTIONS.UP
? floorInfo[0] > this.currentFloor
: floorInfo[0] < this.currentFloor;
return checkFloor && this.currentDirection === floorInfo[1];
}
);
// Otherwise we would need to reverse the direction of the elevator
if (!continueInCurrentDirection) {
return this.currentDirection === DIRECTIONS.UP
? this.moveDown()
: this.moveUp();
}
return this.currentDirection === DIRECTIONS.UP
? this.moveUp()
: this.moveDown();
default:
break;
}
}
getCurrentFloor(): number {
// The current floor is the last floor that has been passed,
// until it is updated by setting it to 'this.nextFloor'
return this.currentFloor;
}
getCurrentDirection(): DIRECTIONS {
return this.currentDirection;
}
stopAndOpenDoors() {
/* Check if the elevator should be in motion
* in the event that this method needs to be called from other methods or APIs
* if it is check again after some time. */
if (this.currentFloor !== this.nextFloor) {
if (!this.recheckInterval) {
this.recheckInterval = setInterval(() => {
this.stopAndOpenDoors();
}, 100);
return;
}
return;
}
this.elevatorMoving = false;
this.currentDirection = DIRECTIONS.NONE;
if (this.recheckInterval) {
clearInterval(this.recheckInterval);
}
this.ElevatorDoor.open();
// Remove the current floor from the destination queue
this.destinationFloorQueue = this.destinationFloorQueue.filter(
(floorInfo) => floorInfo[0] !== this.currentFloor
);
this.ElevatorDoor.close();
this.dispatchEvent(new Event(ElevatorEvents.DOORS_CLOSED));
}
/* Setting the elevator to move upwards or downwards sets up an interval so there
* is no need to explicitly call the functions for movement if all conditions in the event handlers
* event are met */
moveUp() {
if (
this.currentFloor >= this.maxFloor ||
this.currentDirection !== DIRECTIONS.NONE
)
return;
this.nextFloor++;
this.elevatorMoving = true;
this.dispatchEvent(new Event(ElevatorEvents.BEFORE_FLOOR));
return (this.elevatorInterval = setInterval(() => {
this.moveUp();
}, this.elevatorTimestep));
}
moveDown() {
if (
this.currentFloor <= this.minFloor ||
this.currentDirection !== DIRECTIONS.NONE
)
return;
this.nextFloor--;
this.elevatorMoving = true;
this.dispatchEvent(new Event(ElevatorEvents.BEFORE_FLOOR));
return (this.elevatorInterval = setInterval(() => {
this.moveDown();
}, this.elevatorTimestep));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment