ahbeng/NUSMods

View on GitHub
website/src/utils/venues.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { range, entries, padStart, clamp } from 'lodash';
import produce from 'immer';
import {
  VenueInfo,
  VenueSearchOptions,
  VenueDetailList,
  VenueLocationMap,
  OCCUPIED,
} from 'types/venues';

import excludedRooms from 'data/excludedFreeRooms';
import { tokenize } from './moduleSearch';
import { SCHOOLDAYS } from './timify';

// The first and last starting time of lessons
export const FIRST_CLASS_HOUR = 8;
export const LAST_CLASS_HOUR = 22;
export const SCHOOL_CLOSE_HOUR = 24;

// Array of [0, 30, 100, 130, 200, 230, ...], used to create time strings at half hour intervals
// eg. 900 + hourDifference[2] // (9am + 2 * 30 minutes = 10am)
const hourDifference = range(48).map((i) => Math.floor(i / 2) * 100 + (i % 2) * 30);

const stringCompare =
  // Feature detect Intl API
  window.Intl && typeof window.Intl === 'object'
    ? new window.Intl.Collator('en', { sensitivity: 'base', numeric: true }).compare
    : (a: string, b: string) => a.localeCompare(b);

export function sortVenues(venues: VenueInfo): VenueDetailList {
  return entries(venues).sort(([a], [b]) => stringCompare(a, b));
}

export function searchVenue(
  venues: VenueDetailList,
  search: string,
  venueLocations?: VenueLocationMap,
): VenueDetailList {
  // Search by venue or room name
  const tokens = tokenize(search.toLowerCase());

  return venues.filter(([venue]) => {
    const lowercaseVenueName = venue.toLowerCase();
    const lowercaseRoomName =
      venueLocations && venueLocations[venue] ? venueLocations[venue].roomName.toLowerCase() : '';
    return tokens.every(
      (token) => lowercaseVenueName.includes(token) || lowercaseRoomName.includes(token),
    );
  });
}

export function filterAvailability(
  venues: VenueDetailList,
  options: VenueSearchOptions,
): VenueDetailList {
  const { day, time, duration } = options;

  return venues.filter(([venueName, venue]) => {
    if (excludedRooms.has(venueName)) {
      // Locations with occasional timetabled lessons where their main purpose
      // isn't for lessons, and should not be identified as a "Free Room".
      return false;
    }

    const start = time * 100;
    const dayAvailability = venue.find((availability) => availability.day === SCHOOLDAYS[day]);
    if (!dayAvailability) return true;

    // Check that all half-hour slots within the time requested are vacant
    for (let i = 0; i < duration * 2; i++) {
      const timeString = padStart(String(start + hourDifference[i]), 4, '0');
      if (dayAvailability.availability[timeString] === OCCUPIED) {
        return false;
      }
    }

    return true;
  });
}

/**
 * Clamp the duration of the search option to within school hours
 */
export function clampClassDuration(searchOptions: VenueSearchOptions): VenueSearchOptions {
  return produce(searchOptions, (draft) => {
    const newEndTime = clamp(draft.time + draft.duration, FIRST_CLASS_HOUR, SCHOOL_CLOSE_HOUR);
    draft.duration = newEndTime - draft.time;
  });
}

export function floorName(floor: number | string): string {
  if (typeof floor === 'string') {
    return `${floor.toLowerCase()} floor`;
  }

  if (floor === 0) {
    return 'the ground floor';
  }

  const floorNumber = floor < 0 ? `B${-floor}` : floor;
  return `floor ${floorNumber}`;
}