@wyteco/berkeley-ical
Version:
A command line tool to easily export your classes from the Berkeley Academic Guide to your calendar in iCal (.ics) format. Without having a student account lol!
135 lines (109 loc) • 3.84 kB
text/typescript
import { z } from 'zod';
import { addDays, parse } from 'date-fns';
// ----------------------------------------------------------------------
/**
* Parse a time-only string (e.g. `02:00 pm`) as a local JS Date pinned to 1970-01-01.
*/
export function parseTimeString(timeString: string): Date {
const parsed = parse(timeString.trim(), 'hh:mm aa', new Date(0));
if (isNaN(parsed.getTime())) {
console.log('');
throw new Error(`Invalid time string: "${timeString}"`);
}
return parsed;
}
// ----------------------------------------------------------------------
/**
* Parse a date-only string (e.g. `Jan 21, 2025`) as a local JS date.
*/
export function parseDateString(dateString: string): Date {
const parsed = parse(dateString.trim(), 'MMM d, yyyy', new Date());
if (isNaN(parsed.getTime())) {
console.log('');
throw new Error(`Invalid date string: "${dateString}"`);
}
return parsed;
}
// ----------------------------------------------------------------------
/**
* Combine a date and a time JS Date into a single JS Date.
*
* @example
* dateOnly => 2025-01-21T00:00 local
* timeOnly => 1970-01-01T14:00 local
* We want 2025-01-21T14:00 local
*/
export function combineDateAndTime(dateOnly: Date, timeOnly: Date): Date {
/**
* Extract the year, month, and day from dateOnly.
*/
const year = dateOnly.getFullYear();
const month = dateOnly.getMonth();
const day = dateOnly.getDate();
/**
* Extract the hours and minutes from timeOnly.
*/
const hours = timeOnly.getHours();
const minutes = timeOnly.getMinutes();
return new Date(year, month, day, hours, minutes);
}
// ----------------------------------------------------------------------
/**
* We store the weekdays in a constant array to avoid
* typos and to make it easier to update the code
* if the website changes.
* Note that `Date.getDay` assumes Sunday is 0, Monday is 1, etc.
*/
export const weekdays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] as const;
export type Weekday = (typeof weekdays)[number];
export const weekdayLookup: Record<Weekday, string[]> = {
MO: ['mo', 'mon', 'monday'],
TU: ['tu', 'tue', 'tuesday'],
WE: ['we', 'wed', 'wednesday'],
TH: ['th', 'thu', 'thursday'],
FR: ['fr', 'fri', 'friday'],
SA: ['sa', 'sat', 'saturday'],
SU: ['su', 'sun', 'sunday'],
};
// ----------------------------------------------------------------------
/**
* Given a date (e.g. Jan 21, 2025) and a list of valid ICS day codes
* (e.g. ["TU","TH"]), find the earliest date that matches one of the days.
* For example, if the date is Jan 21, 2025 (a Tuesday) and the days are
* ["TU","TH"], the function will return Jan 21, 2025.
*/
export function findFirstMeeting(
startDate: Date,
meetingDays: Weekday[]
): Date {
// Turn the meetingDays into a set of numeric weekdays:
const meetingDayIndexes = meetingDays.map((day) => weekdays.indexOf(day));
// We'll loop from 'start' forward until we find a weekday that matches:
let current = new Date(startDate);
while (true) {
const dayOfWeek = current.getDay();
if (meetingDayIndexes.includes(dayOfWeek)) {
return current;
}
current = addDays(current, 1);
}
}
// ----------------------------------------------------------------------
/**
* Define the schema for the course data.
* This schema is used to validate the extracted data from the website.
*/
export const CourseSchema = z.object({
title: z.string(),
description: z.string(),
instructors: z.array(z.string()),
startDate: z.date(),
endDate: z.date().nullable(),
meetingDays: z.array(z.enum(weekdays)),
meetingStartTime: z.date(),
meetingEndTime: z.date().nullable(),
location: z.string(),
numberOfEnrollments: z.number(),
capacity: z.number(),
});
export type CourseData = z.infer<typeof CourseSchema>;