tutorbook
Version:
Web app connecting students with expert mentors and tutors.
189 lines (174 loc) • 7.7 kB
text/typescript
import {
Timeslot,
TimeslotJSON,
TimeslotInterface,
TimeslotFirestoreInterface,
TimeslotSearchHitInterface,
} from './timeslot';
/**
* One's schedule contains all your booked timeslots (the inverse of one's
* availability).
* @deprecated We have no use of this for now (though we might in the future
* when we implement a dashboard view).
*/
export type ScheduleAlias = TimeslotInterface[];
/**
* One's availability contains all your open timeslots (the inverse of one's
* schedule).
*/
export type AvailabilityAlias = TimeslotInterface[];
export type AvailabilityJSON = TimeslotJSON[];
export type AvailabilityFirestoreAlias = TimeslotFirestoreInterface[];
export type AvailabilitySearchHitAlias = TimeslotSearchHitInterface[];
/**
* Class that contains a bunch of time slots or openings that represents a
* user's availability (inverse of their schedule, which contains a bunch of
* booked time slots or appointments). This provides some useful methods for
* finding time slots and a better `toString` representation than
* `[Object object]`.
*/
export class Availability extends Array<Timeslot> implements AvailabilityAlias {
/**
* Note that this method (`Availability.prototype.contains`) is **very**
* different from the `Availability.prototype.hasTimeslot` method; this method
* checks to see if any `Timeslot` contains the given `Timeslot` whereas the
* `hasTimeslot` methods checks to see if this availability contains the exact
* given `Timeslot`.
*/
public contains(other: Timeslot): boolean {
const contains = (a: boolean, t: Timeslot) => a || t.contains(other);
return this.reduce(contains, false);
}
/**
* Helper function to remove a given `Timeslot` from this `Availability`. Note
* that this **does not** just remove that exact `Timeslot` but rather ensures
* that there are no `Timeslot`s remaining that overlap with the given
* `Timeslot` by (where A is the given `Timeslot` and B is a `Timeslot` in
* `this`):
* 1. If (they overlap; B's close time is contained w/in A):
* - B's open time is before A's open time AND;
* - B's close time is before A's close time AND;
* - B's close time is after A's open time.
* Then we'll adjust B such that it's close time is equal to A's open time.
* 2. If (B's open time is contained w/in A; opposite of scenario #1):
* - B's close time is after A's close time AND;
* - B's open time is before A's close time AND;
* - B's open time is after A's open time.
* Then we'll adjust B's open time to be equal to A's close time.
* 3. If (A contains B):
* - B's open time is after A's open time AND;
* - B's close time is before A's close time.
* Then we'll remove B altogether.
* 4. If (B contains A; opposite of scenario #2):
* - B's open time is before A's open time AND;
* - B's close time is after A's close time.
* Then we'll split B into two timeslots (i.e. essentically cutting out A):
* - One timeslot will be `{ from: B.from, to: A.from }`
* - The other timeslot will be `{ from: A.to, to: B.to }`
* 5. If B and A are equal, we just remove B altogether.
* 6. Otherwise, we keep B and continue to the next check.
*/
public remove(a: Timeslot): void {
const temp: Availability = new Availability();
const aFrom = a.from.valueOf();
const aTo = a.to.valueOf();
this.forEach((b: Timeslot) => {
/* eslint-disable no-param-reassign */
const bFrom = b.from.valueOf();
const bTo = b.to.valueOf();
if (bFrom < aFrom && bTo < aTo && bTo > aFrom) {
// Adjust `b` such that it's close time is equal to `a`'s open time.
b.to = new Date(aFrom);
temp.push(b);
} else if (bTo > aTo && bFrom < aTo && bFrom > aFrom) {
// Adjust `b` such that it's open time is equal to `a`'s close time.
b.from = new Date(aTo);
temp.push(b);
} else if (a.contains(b)) {
// Remove `b` altogether (i.e. don't add to `temp`).
} else if (b.contains(a)) {
// Split `b` into two timeslots (i.e. essentially cutting out `a`).
temp.push(new Timeslot(new Date(bFrom), new Date(aFrom)));
temp.push(new Timeslot(new Date(aTo), new Date(bTo)));
} else if (a.equalTo(b)) {
// Remove `b` altogether (i.e. don't add to `temp`).
} else {
temp.push(b);
}
/* eslint-enable no-param-reassign */
});
this.length = 0;
temp.forEach((timeslot: Timeslot) => this.push(timeslot));
}
/**
* Converts this `Availability` into a comma-separated string of all of it's
* constituent timeslots.
* @deprecated We're going to put these into strings within the React tree so
* that we can use `react-intl` for better i18n support (e.g. we'll set the
* localization in the `pages/_app.tsx` top-level component and all children
* components will render their `Date`s properly for that locale).
*/
public toString(showDay = true): string {
return this.length > 0
? this.map((timeslot: Timeslot) => timeslot.toString(showDay)).join(', ')
: '';
}
public hasTimeslot(timeslot: TimeslotInterface): boolean {
return !!this.filter((t) => t.equalTo(timeslot)).length;
}
public toFirestore(): AvailabilityFirestoreAlias {
return Array.from(this.map((timeslot: Timeslot) => timeslot.toFirestore()));
}
/**
* Takes in an array of `Timeslot` objects (but w/ Firestore `Timestamp`
* objects in the `from` and `to` fields instead of `Date` objects) and
* returns an `Availability` object.
*/
public static fromFirestore(data: AvailabilityFirestoreAlias): Availability {
const availability: Availability = new Availability();
data.forEach((t) => availability.push(Timeslot.fromFirestore(t)));
return availability;
}
/**
* Returns a basic `Array` object containing `TimeslotJSON`s. Note
* that we **must** wrap the `this.map` statement with an `Array.from` call
* because otherwise, we'd just return an invalid `Availability` object (which
* would cause subsequent `toJSON` calls to fail because the new array
* wouldn't contain valid `Timeslot` objects).
*/
public toJSON(): AvailabilityJSON {
return Array.from(this.map((timeslot: Timeslot) => timeslot.toJSON()));
}
public static fromJSON(json: AvailabilityJSON): Availability {
const availability: Availability = new Availability();
json.forEach((t) => availability.push(Timeslot.fromJSON(t)));
return availability;
}
public static fromSearchHit(hit: AvailabilitySearchHitAlias): Availability {
const availability: Availability = new Availability();
hit.forEach((t) => availability.push(Timeslot.fromSearchHit(t)));
return availability;
}
public toURLParam(): string {
return encodeURIComponent(JSON.stringify(this));
}
public static fromURLParam(param: string): Availability {
const availability: Availability = new Availability();
const params: string[] = JSON.parse(decodeURIComponent(param)) as string[];
params.forEach((timeslotParam: string) => {
availability.push(Timeslot.fromURLParam(timeslotParam));
});
return availability;
}
/**
* Checks if two availabilities contain all the same timeslots by ensuring
* that:
* 1. This availability contains all the timeslots of the other availability.
* 2. The other availability contains all the timeslots of this availability.
*/
public equalTo(other: Availability): boolean {
if (!other.every((t: Timeslot) => this.hasTimeslot(t))) return false;
if (!this.every((t: Timeslot) => other.hasTimeslot(t))) return false;
return true;
}
}