Skip to content

Commit

Permalink
refactor: initial full cleanup (#157)
Browse files Browse the repository at this point in the history
* refactor: initial full cleanup

* fix: fix time merge typo

* chore: minor cleanup

* chore: minor code cleanup
  • Loading branch information
cirex-web authored Sep 22, 2024
1 parent 9b1ca1a commit c14762c
Show file tree
Hide file tree
Showing 17 changed files with 511 additions and 635 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ package-lock.json
.venv
*.log
__pycache__
dist/
dist/
coverage/
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
transform: {
"^.+.tsx?$": ["ts-jest", { diagnostics: { warnOnly: true } }],
},
moduleDirectories: ['node_modules', 'src']
};
163 changes: 64 additions & 99 deletions src/containers/locationBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
import { DayOfTheWeek } from "../utils/timeUtils";
import { ISpecial } from "./specials/specialsBuilder";
import { Element, load } from "cheerio";
import { getHTMLResponse } from "utils/requestUtils";
import { LocationOverwrites } from "overwrites/locationOverwrites";
import { getTimeRangesFromString } from "./timeBuilder";
import { ICoordinate, ILocation, ISpecial, ITimeRange } from "../types";
import { sortAndMergeTimeRanges } from "utils/timeUtils";

export interface ILocation {
conceptId: number;
name?: string;
shortDescription?: string;
description: string;
url: string;
menu?: string;
location: string;
coordinates?: ICoordinate;
acceptsOnlineOrders: boolean;
times: ITime[];
todaysSpecials?: ISpecial[];
todaysSoups?: ISpecial[];
}

interface IMomentTime {
day: DayOfTheWeek;
hour: number;
minute: number;
}

export interface ITime {
start: IMomentTime;
end: IMomentTime;
}
export interface ICoordinate {
lat: number;
lng: number;
}
/**
* For building the location data structure
*/
export default class LocationBuilder {
static readonly CONCEPT_BASE_LINK =
"https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/";

private conceptId: number;
private conceptId?: number;
private name?: string;
private shortDescription?: string;
private description?: string;
Expand All @@ -46,94 +21,84 @@ export default class LocationBuilder {
private menu?: string;
private coordinates?: ICoordinate;
private acceptsOnlineOrders?: boolean;
private times?: ITime[];
private times?: ITimeRange[];
private specials?: ISpecial[];
private soups?: ISpecial[];
private valid: boolean = true;

constructor(conceptId: number) {
this.conceptId = conceptId;
}

setName(name: string): LocationBuilder {
this.name = name;
return this;
}

setShortDesc(shortDesc: string): LocationBuilder {
this.shortDescription = shortDesc;
return this;
}

setDesc(desc: string): LocationBuilder {
this.description = desc;
return this;
}

setCoordinates(coordinates: ICoordinate): LocationBuilder {
this.coordinates = coordinates;
return this;
}
constructor(card: Element) {
const link = load(card)("h3.name.detailsLink");
this.name = link.text().trim();

setLocation(location: string): LocationBuilder {
this.location = location;
return this;
}
const conceptId = link.attr("onclick")?.match(/Concept\/(\d+)/)?.[1];
this.conceptId = conceptId !== undefined ? parseInt(conceptId) : undefined;

setAcceptsOnlineOrders(acceptsOnlineOrders: boolean) {
this.acceptsOnlineOrders = acceptsOnlineOrders;
return this;
this.shortDescription = load(card)("div.description").text().trim();
}

setURL(url: string) {
this.url = url;
return this;
}

setMenu(menuLink: string) {
this.menu = menuLink;
return this;
overwriteLocation(locationOverwrites: LocationOverwrites) {
if (
this.name !== undefined &&
locationOverwrites[this.name] !== undefined
) {
this.coordinates = locationOverwrites[this.name];
}
}

setTimes(times: ITime[]) {
this.times = times;
return this;
setSoup(soupList: Record<string, ISpecial[]>) {
if (this.name && soupList[this.name] !== undefined) {
this.soups = soupList[this.name];
}
}

setSpecials(specials: ISpecial[]) {
this.specials = specials;
return this;
setSpecials(specialList: Record<string, ISpecial[]>) {
if (this.name && specialList[this.name] !== undefined) {
this.specials = specialList[this.name];
}
}
convertMapsLinkToCoordinates(link: string) {
const atIndex = link.indexOf("@");
const locationUrl = link.slice(atIndex + 1, link.length);
const commaIndex = locationUrl.indexOf(",");
const latitude = locationUrl.slice(0, commaIndex);
const longitude = locationUrl.slice(commaIndex + 1, locationUrl.length);
return { lat: parseFloat(latitude), lng: parseFloat(longitude) };
}

async populateDetailedInfo() {
const conceptURL = this.getConceptLink();
if (!conceptURL) return;

const $ = load(await getHTMLResponse(conceptURL));
this.url = conceptURL.toString();
this.description = $("div.description p").text().trim();
this.menu = $("div.navItems > a#getMenu").attr("href");
this.location = $("div.location a").text().trim();
this.acceptsOnlineOrders =
$("div.navItems.orderOnline").toArray().length > 0;

const locationHref = $("div.location a").attr("href");
if (locationHref !== undefined) {
this.coordinates = this.convertMapsLinkToCoordinates(locationHref);
}

setSoups(soups: ISpecial[]) {
this.soups = soups;
return this;
const nextSevenDays = $("ul.schedule").find("li").toArray();
this.times = sortAndMergeTimeRanges(
nextSevenDays.flatMap((rowHTML) => getTimeRangesFromString(rowHTML))
);
}

getConceptLink(): string {
return LocationBuilder.CONCEPT_BASE_LINK + this.conceptId;
getConceptLink() {
if (this.conceptId === undefined) return undefined;
return new URL(LocationBuilder.CONCEPT_BASE_LINK + this.conceptId);
}

getName(): string | undefined {
return this.name;
}
invalidate() {
this.valid = false;
}
isValid() {
return this.valid;
}
build(): ILocation {
if (!this.valid) throw Error("Location has been invalidated!");
if (
this.times === undefined ||
this.acceptsOnlineOrders === undefined ||
this.description === undefined ||
this.url === undefined ||
this.location === undefined
this.location === undefined ||
this.conceptId === undefined
) {
throw Error(
"Didn't finish configuring restaurant before building metadata!"
"Didn't finish configuring location before building metadata!"
);
// All fetches were good - yet we have missing data. This is a problem.
}
Expand Down
31 changes: 27 additions & 4 deletions src/containers/specials/specialsBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export interface ISpecial {
title: string;
description?: string;
}
import { load } from "cheerio";
import { ISpecial } from "types";

/**
* For building the specials/soups data structure
Expand All @@ -24,3 +22,28 @@ export default class SpecialsBuilder {
return this.specials;
}
}

export async function retrieveSpecials(htmlContent: string) {
const $ = load(htmlContent);
const cards = $("div.card").toArray();

const locationSpecialMap: Record<string, ISpecial[]> = {};

for (const card of cards) {
const name = load(card)("h3.name").text().trim();
const specialsBuilder = new SpecialsBuilder();

const specialsText = load(card)("div.specialDetails").text().trim();
const specialsArray = specialsText.split(/(?<=\n)\s*(?=\S)/);

for (let i = 0; i < specialsArray.length; i += 2) {
const title = specialsArray[i].trim();
const description = specialsArray[i + 1]?.trim() || "";
specialsBuilder.addSpecial(title, description);
}

locationSpecialMap[name] = specialsBuilder.build();
}

return locationSpecialMap;
}
4 changes: 2 additions & 2 deletions src/containers/time/parsedTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface Time {
minute: number;
}

export interface ParsedTimeRange {
export interface IParsedTimeRange {
start: Time;
end: Time;
}
Expand All @@ -15,7 +15,7 @@ export interface ParsedTimeRange {
* structure
*/
export default class ParsedTime extends ParsedTimeBase {
declare value: ParsedTimeRange;
declare value: IParsedTimeRange;

private parseTime(timeStr: string): Time {
const normalizedStr = timeStr.trim().toLowerCase();
Expand Down
2 changes: 0 additions & 2 deletions src/containers/time/parsedTimeBase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { TimeInfoType } from "../../utils/timeUtils";

/**
* Base class for parsing time from a string
*/
Expand Down
55 changes: 48 additions & 7 deletions src/containers/time/parsedTimeForDate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import {
convertMonthStringToEnum,
isValidDate,
MonthOfTheYear,
} from "../../utils/timeUtils";
import { MonthOfTheYear } from "types";
import { isValidDate } from "utils/parseTimeToken";
import ParsedTimeBase from "./parsedTimeBase";

export interface ParsedTimeDate {
export interface IParsedTimeDate {
month: MonthOfTheYear;
date: number;
}
Expand All @@ -14,7 +11,7 @@ export interface ParsedTimeDate {
* For parsing a string representing a date to a date data structure
*/
export default class ParsedTimeForDate extends ParsedTimeBase {
declare value: ParsedTimeDate;
declare value: IParsedTimeDate;

parse() {
const tokens = this.input.trim().split(/\s/);
Expand All @@ -39,3 +36,47 @@ export default class ParsedTimeForDate extends ParsedTimeBase {
return this;
}
}

export function convertMonthStringToEnum(monthStr: string): MonthOfTheYear {
const normalizedMonth = monthStr.trim().toLowerCase();
switch (normalizedMonth) {
case "january":
case "jan":
return MonthOfTheYear.JANUARY;
case "february":
case "feb":
return MonthOfTheYear.FEBRUARY;
case "march":
case "mar":
return MonthOfTheYear.MARCH;
case "april":
case "apr":
return MonthOfTheYear.APRIL;
case "may":
return MonthOfTheYear.MAY;
case "june":
case "jun":
return MonthOfTheYear.JUNE;
case "july":
case "jul":
return MonthOfTheYear.JULY;
case "august":
case "aug":
return MonthOfTheYear.AUGUST;
case "september":
case "sept":
case "sep":
return MonthOfTheYear.SEPTEMBER;
case "october":
case "oct":
return MonthOfTheYear.OCTOBER;
case "november":
case "nov":
return MonthOfTheYear.NOVEMBER;
case "december":
case "dec":
return MonthOfTheYear.DECEMBER;
default:
throw new Error(`Invalid Month: ${monthStr}`);
}
}
32 changes: 31 additions & 1 deletion src/containers/time/parsedTimeForDay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { convertDayStringToEnum, DayOfTheWeek } from "../../utils/timeUtils";
import { DayOfTheWeek } from "types";
import ParsedTimeBase from "./parsedTimeBase";

/**
Expand All @@ -12,3 +12,33 @@ export default class ParsedTimeForDay extends ParsedTimeBase {
return this;
}
}

export function convertDayStringToEnum(dayStr: string): DayOfTheWeek {
const normalizedDay = dayStr.trim().toLowerCase();
switch (normalizedDay) {
case "sunday":
case "sun":
return DayOfTheWeek.SUNDAY;
case "monday":
case "mon":
return DayOfTheWeek.MONDAY;
case "tuesday":
case "tue":
return DayOfTheWeek.TUESDAY;
case "wednesday":
case "wed":
return DayOfTheWeek.WEDNESDAY;
case "thursday":
case "thu":
case "thurs":
return DayOfTheWeek.THURSDAY;
case "friday":
case "fri":
return DayOfTheWeek.FRIDAY;
case "saturday":
case "sat":
return DayOfTheWeek.SATURDAY;
default:
throw new Error(`Invalid Day: ${dayStr}`);
}
}
Loading

0 comments on commit c14762c

Please sign in to comment.