oystercard

The Oyster Card Problem: Javascript Solution

What is Oyster Card Problem?

You are required to model the following fare card system which is a limited version of London’s
Oyster card system. At the end of the test, you should be able to demonstrate a user loading a card
with £30, and taking the following trips, and then viewing the balance.

  1. Tube Holborn to Earl’s Court
  2. 328 bus from Earl’s Court to Chelsea
  3. Tube from Earl’s court to Hammersmith

Operations:

  • When the user passes through the inward barrier at the station, their oyster card is charged the maximum fare.
  • When they pass out of the barrier at the exit station, the fare is calculated and the maximum fare transaction is removed and replaced with the real transaction (in this way, if the user doesn’t swipe out, they are charged the maximum fare).
  • All bus journeys are charged at the same price.
  • The system should favour the customer where more than one fare is possible for a given journey. E.g. Holburn to Earl’s Court is charged at £2.50

Stations and Zones are as follows:

STATIONSZONE(S)
Holborn1
Aldgate1
Earl’s Court1, 2
Hammersmith2
Arsenal2
Wimbledon3

Fares are as follows:

JOURNEYFAREEXAMPLE
Anywhere in Zone 1£2.50From Holborn to Aldgate
Any one zone outside zone 1£2.00From Arsenal to Hammersmith
Any two zones including zone 1£3.00From Hammersmith to Holborn
Any two zones excluding zone 1£2.25From Arsenal to Wimbledon
More than two zones (3+)£3.20From Wimbledon to Aldgate
Any bus journey£1.80Earl’s Court to Chelsea
  • Any bus journey costs a flat rate of £1.80 regardless of the journey stations.
  • The maximum possible fare is therefore £3.20.

Solution:

First, we will create an OysterCard.js file as follows-


const BUS_COST = 1.80,
      MAX_FARE = 3.20,
      COST_ONLY_ZONE_ONE = 2.50,
      COST_ONE_ZONE_NOT_INCLUDING_ZONE_ONE = 2.00,
      COST_TWO_ZONES_INCLUDING_ZONE_ONE = 3.00,
      COST_TWO_ZONES_EXCLUDING_ZONE_ONE = 2.25;
      

const STATIONS = {
    Holborn: [1],
    Aldgate: [1],
    EarlsCourt: [1, 2],
    Hammersmith: [2],
    Arsenal: [2],
    Wimbledon: [3]
}


class OysterCard {
    constructor(credit = 0) {
        this.credit = credit;
        this.fare = 0;
        this.points = [];
    }
    
    getCredit() {
        return this.credit;
    }

    // Sets Credit in the Card
    setCredit(amt) {
        const totalCredit = this.credit += amt;
        return totalCredit;
    }

    // Sets Debit in the Card
    setDebit() {
        (this.credit >= this.fare ? this.credit -= this.fare : process.exit())
    }

    // Select the correct cost according to zones of the trip
    getCostByZone(zonesCrossed, isZoneOneCrossed) {
        if(zonesCrossed === 1 && isZoneOneCrossed) {
            return COST_ONLY_ZONE_ONE
        }
        if(zonesCrossed === 1 && !isZoneOneCrossed) {
            return COST_ONE_ZONE_NOT_INCLUDING_ZONE_ONE
        }
        if(zonesCrossed === 2 && isZoneOneCrossed) {
            return COST_TWO_ZONES_INCLUDING_ZONE_ONE
        }
        if(zonesCrossed === 2 && !isZoneOneCrossed) {
            return COST_TWO_ZONES_EXCLUDING_ZONE_ONE
        }
        if(zonesCrossed === 3) {
            return MAX_FARE
        }
        return MAX_FARE
    }

    // When the user passes through the inward barrier at the station, their oyster card is charged the maximum fare.
    swipeIn(station) {
        this.points.push(station)
        this.fare = MAX_FARE
        this.setDebit()
    }

    // When they pass out of the barrier at the exit station, the fare is calculated
    // The maximum fare transaction removed and replaced with the real transaction
    swipeOut() {
        this.getFinalCost()
        this.setDebit()
    }

    // Method used to keep the station of departure and the arriving station
    setNewJourney(nextStation) {
        this.points.push(nextStation);
    }

    // Method used to account a new bus trip
    setBusJourney() {
        this.fare = BUS_COST
        this.setDebit()
    }

    // Method that calculates the number of Zones Crossed according to two points
    getZonesCrossed(from, to) {
        var minZoneVisited = 0;
        from.forEach(fromZone => {
            to.forEach(toZone => {
                // console.log(fromZone, toZone)
                var zonesVisited = Math.abs(fromZone - toZone)
                if(zonesVisited < minZoneVisited) {
                    minZoneVisited = zonesVisited
                }
                if(minZoneVisited === 1) {
                    return;
                }
            })
        })
        return minZoneVisited
    }

    //  Method to verify if element exists in array
    in_array(needle, haystack) {
        for(let i = 0; i < haystack.length; i++) {
            if(haystack[i] === needle){
                return true
            }
            return false
        }
    }

    //  Method that verify if the range of stations cross over the zone one
    didZoneOneCrossed(from, to) {
        return ( from.length === 1 && this.in_array(1, from)) || (to.length === 1 && this.in_array(1, to))
    }

    //   Method that calculates the real transaction amount that will be charged according to the journey's stations
    getFinalCost() {
        if(this.points.length === 2) {
            this.setCredit(MAX_FARE);
            var zonesCrossed = this.getZonesCrossed(this.points[0], this.points[1]);
            var isZoneOneCrossed = this.didZoneOneCrossed(this.points[0], this.points[1]);
            var fare = this.getCostByZone(zonesCrossed, isZoneOneCrossed)
            this.fare = fare
        } else {
            this.fare = MAX_FARE
        }
    }
}

Object.defineProperty(OysterCard, 'STATIONS', {
    value: STATIONS,
    writable: false
});


export default OysterCard;

Now we will create a file Main.js that calls all functions and load the card with £30. And take the following trips

  1. 328 bus from Earl’s Court to Chelsea.
  2. 328 bus from Earl’s Court to Chelsea.
  3. Tube from Earl’s court to Hammersmith.
import OysterCard from './OysterCard'

// Instantiate User
let card = new OysterCard();

// Card charged with £30
card.setCredit(30);

// enter in the subway
card.swipeIn(OysterCard.STATIONS.Holborn);
// set new trip from Tube Holborn to Earl’s Court
card.setNewJourney(OysterCard.STATIONS.Aldgate);
// exit station
card.swipeOut();

// set bus trip from 328 bus from Earl’s Court to Chelsea
card.setBusJourney();

// enter in the subway
card.swipeIn(OysterCard.STATIONS.EarlsCourt);
// set new trip
card.setNewJourney(OysterCard.STATIONS.Hammersmith);
// exit station
card.swipeOut();

var credit = card.getCredit().toFixed(2);
console.log('Remaning Credit: £', credit);

We have created OysterCard.js with all functionality and Main.js for calling them. Now we will create a test file OysterCard.test.js for testing our code.

import OysterCard from './OysterCard'

let card = new OysterCard();

// tests methods getCostByZone()

test('returns 2.5 of fare for a trip between 1 zones crossing zone 1', () => {
  expect(card.getCostByZone(1, true)).toBe(2.5);
});

test('returns 3 of fare for a trip between 2 zones crossing zone 1', () => {
  expect(card.getCostByZone(2, true)).toBe(3);
});

test('returns 3.2 of fare for a trip between 3 zones crossing zone 1', () => {
  expect(card.getCostByZone(3, true)).toBe(3.2);
});

test('returns 2 of fare for a trip between 1 zone NOT crossing zone 1', () => {
  expect(card.getCostByZone(1, false)).toBe(2);
});

test('returns 2.25 of fare for a trip between 2 zones NOT crossing zone 1', () => {
  expect(card.getCostByZone(2, false)).toBe(2.25);
});

test('returns 3.2 of fare for a trip between 3 zones NOT crossing zone 1', () => {
  expect(card.getCostByZone(3, false)).toBe(3.2);
});

// tests methods setCredit()

test('returns 10 for integer 10', () => {
  expect(card.setCredit(10)).toBe(10);
});

test('returns 0 for a negative number', () => {
  expect(card.setCredit(-10)).toBe(0);
});

test('returns 30.5 for a float number', () => {
  expect(card.setCredit(30.5)).toBe(30.5);
});

//  test if user hump the station
test("didn't swipe the card previously and exit the station", () => {
	var card = new OysterCard(30);
	card.setNewJourney(OysterCard.STATIONS.EarlsCourt);
	card.swipeOut();
	expect(card.getCredit()).toBe(26.8);
});

976 thoughts on “The Oyster Card Problem: Javascript Solution

Comments are closed.