UNPKG

@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
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>;