ahbeng/NUSMods

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

Summary

Maintainability
A
3 hrs
Test Coverage
import {
  format,
  getHours,
  getISODay,
  getMinutes,
  getSeconds,
  parseISO,
  setHours,
  setMinutes,
  setSeconds,
  startOfDay,
} from 'date-fns';
import { DayText, LessonTime } from 'types/modules';
import { TimePeriod } from 'types/venues';
import { Lesson } from 'types/timetables';

const SGT_OFFSET = -8 * 60;

export function getLessonTimeHours(time: LessonTime): number {
  return parseInt(time.substring(0, 2), 10);
}

export function getLessonTimeMinutes(time: LessonTime): number {
  return parseInt(time.substring(2), 10);
}

// Converts a 24-hour format time string to an index.
// Each index corresponds to one cell of each timetable row.
// Each row may not start from index 0, it depends on the config's starting time.
// 0000 -> 0, 0030 -> 1, 0100 -> 2, ...
export function convertTimeToIndex(time: LessonTime): number {
  const hour = getLessonTimeHours(time);
  const minute = getLessonTimeMinutes(time);

  // TODO: Expose incorrect offsets to user via UI
  // Currently we round up in half hour blocks, but the actual time is not shown
  let minuteOffset;
  if (minute === 0) {
    minuteOffset = 0;
  } else if (minute <= 30) {
    minuteOffset = 1;
  } else {
    minuteOffset = 2;
  }

  return hour * 2 + minuteOffset;
}

// Reverse of convertTimeToIndex.
// 0 -> 0000, 1 -> 0030, 2 -> 0100, ... , 48 -> 2400
export function convertIndexToTime(index: number): LessonTime {
  const timeIndex = Math.min(index, 48);
  const hour: number = Math.floor(timeIndex / 2);
  const minute: string = timeIndex % 2 === 0 ? '00' : '30';
  return (hour < 10 ? `0${hour}` : hour.toString()) + minute;
}

export function formatHour(hour: number): string {
  if (hour === 12) return '12 noon';
  if (hour === 0 || hour === 24) return '12 midnight';
  if (hour < 12) return `${hour}am`;
  return `${hour - 12}pm`;
}

export function getTimeAsDate(time: string | number, date: Date = new Date()): Date {
  const dateNumber = typeof time === 'string' ? parseInt(time, 10) : time;
  return setHours(setMinutes(startOfDay(date), dateNumber % 100), Math.floor(dateNumber / 100));
}

export function formatTime(time: string | number): string {
  const timeNumber = typeof time === 'string' ? parseInt(time, 10) : time;

  if (timeNumber === 0) return '12 midnight';
  if (timeNumber === 1200) return '12 noon';

  return format(getTimeAsDate(timeNumber), 'h:mm a').toLowerCase();
}

// Create a new date object with time from the second date object
export function setTime(date: Date, time: Date): Date {
  return setHours(setMinutes(setSeconds(date, getSeconds(time)), getMinutes(time)), getHours(time));
}

export const SCHOOLDAYS: DayText[] = [
  'Monday',
  'Tuesday',
  'Wednesday',
  'Thursday',
  'Friday',
  'Saturday',
];
export const DEFAULT_EARLIEST_TIME: LessonTime = '1000';
export const DEFAULT_LATEST_TIME: LessonTime = '1800';

// Given an array of lessons, we calculate the earliest and latest timings based on the lesson timings.
// This bounds will then be used to decide the starting and ending hours of the timetable.
export function calculateBorderTimings(
  lessons: Lesson[],
  period?: TimePeriod,
): { startingIndex: number; endingIndex: number } {
  let earliestTime: number = convertTimeToIndex(DEFAULT_EARLIEST_TIME);
  let latestTime: number = convertTimeToIndex(DEFAULT_LATEST_TIME);
  lessons.forEach((lesson) => {
    earliestTime = Math.min(earliestTime, convertTimeToIndex(lesson.startTime));
    latestTime = Math.max(latestTime, convertTimeToIndex(lesson.endTime));
  });

  // Consider time range of period, if applicable
  if (period != null) {
    earliestTime = Math.min(earliestTime, convertTimeToIndex(period.startTime));
    latestTime = Math.max(latestTime, convertTimeToIndex(period.endTime));
  }

  return {
    startingIndex: earliestTime % 2 === 0 ? earliestTime : earliestTime - 1, // floor to earliest hour.
    endingIndex: latestTime % 2 === 0 ? latestTime : latestTime + 1, // ceil to latest hour.
  };
}

/**
 * Gets the current time in hours, 0915 -> 9, 1315 -> 13
 * @deprecated Use date injected by withTimer instead
 */
export function getCurrentHours(
  now: Date = new Date(), // Used for tests only
): number {
  return now.getHours();
}

/**
 * Gets the current time in hours, 0915 -> 15, 1345 -> 45
 * Current time to always match Singapore's
 *
 * @deprecated Use date injected by withTimer instead
 */
export function getCurrentMinutes(
  now: Date = new Date(), // Used for tests only
): number {
  return now.getMinutes();
}

// Monday = 0, Friday = 4, Sunday = 6
export function getDayIndex(date: Date = new Date()): number {
  return getISODay(date) - 1;
}

/**
 * Return a copy of the original Date incremented by the given number of days
 *
 * @deprecated Use addDays from date-fns
 */
export function daysAfter(startDate: Date, days: number): Date {
  const d = new Date(startDate.valueOf());
  d.setUTCDate(d.getUTCDate() + days);
  return d;
}

/**
 * Converts a Date object representing an event happening in Singapore time
 * and outputs a new Date object with the local time in SGT. This is useful
 * in conjunction with format from date-fns since it always use local time when
 * formatting output.
 *
 * @example
 *     // Exam is at 9AM 23rd of October 2016
 *     const examDate = new Date('2016-11-23T01:00:00.000Z');
 *     format(examDate, 'dd-MM-yyyy p');
 *     // => "23-11-2016 9:00 AM", no matter where the user machine's TZ is
 */
export function toSingaporeTime(date: string | number | Date): Date {
  const localDate = new Date(date);
  return new Date(localDate.getTime() + (localDate.getTimezoneOffset() - SGT_OFFSET) * 60 * 1000);
}

/**
 * Convert an ISO date string, eg. 2018-10-12 to a Date object with the
 * given date and time set to midnight SGT (UTC+8)
 */
export function parseDate(string: string): Date {
  return parseISO(`${string}T00:00+0800`);
}