Last active
February 8, 2022 06:38
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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