From 891a767d6f198f09da4892c72cae11419b030e42 Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 24 Feb 2023 23:32:55 -0600 Subject: [PATCH] Parse ^& transform timing data inside TimeConfigSchema, add DayEnum type --- src/timing.ts | 138 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 32 deletions(-) diff --git a/src/timing.ts b/src/timing.ts index 27b4314..f0b3f29 100644 --- a/src/timing.ts +++ b/src/timing.ts @@ -15,15 +15,34 @@ const DayEnumSchema = z.enum([ 'saturday', 'sunday' ]); +type DayEnum = z.infer; const TimeConfigSchema = z.object({ // A short name to be included in notifications name: z.string(), // The time this notification is intended for. e.g. "12:00", 24 hour time - time: z.string().refine((time) => { - const { hours, minutes } = parseTime(time); - return isValidTime({ hours, minutes }); - }, 'Invalid time format'), + time: z.string().transform((time, ctx) => { + if (!time.match(/^[0-9]{2}:[0-9]{2}$/)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Time must be in the format "HH:MM" e.g. "12:00"' + }); + return z.NEVER; + } + + const parsed = parseTime(time); + + if (!isValidTime(parsed)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Invalid time format. Max 23 hours, 59 minutes. Positives only.' + }); + return z.NEVER; + } + + return parsed; + }), // The days this configuration is active on. days: z.preprocess((v) => { const parsedArray = z.any().array().parse(v); @@ -33,11 +52,30 @@ const TimeConfigSchema = z.object({ maxLate: z .string() .optional() - .refine((duration) => { - if (duration == undefined) return true; - const { hours, minutes } = parseTime(duration); - return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; - }, 'Invalid duration format') + .transform((duration, ctx) => { + if (duration == undefined) return undefined; + + if (!duration.match(/^[0-9]{2}:[0-9]{2}$/)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Duration must be in the format "HH:MM" e.g. "12:00"' + }); + return z.NEVER; + } + + const parsed = parseTime(duration); + + if (!isValidTime(parsed)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Invalid duration format. Max 23 hours, 59 minutes. Positives only.' + }); + return z.NEVER; + } + + return parsed; + }) }); type TimeConfig = z.infer; @@ -56,16 +94,19 @@ const parseTime = (time: string): ParsedTime => { /** * A predicate function to compare two times. - * @param {ParsedTime} a A time to compare - * @param {ParsedTime} b A time to compare - * @returns {-1 | 0 | 1} The relative order of the two times in terms of -1, 0, or 1. + * If {primary} is before {secondary}, returns -1. + * If {primary} is after {secondary}, returns 1. + * If {primary} is equal to {secondary}, returns 0. + * @param {ParsedTime} primary A time to compare. The return value will be relative to this time. + * @param {ParsedTime} secondary A time to compare. + * @returns {-1 | 0 | 1} The relative order of {primary} in relation to {secondary} as a value of -1, 0, or 1. */ -function compareTime(a: ParsedTime, b: ParsedTime): -1 | 0 | 1 { - if (a.hours < b.hours) return -1; - if (a.hours > b.hours) return 1; - if (a.minutes < b.minutes) return -1; - if (a.minutes > b.minutes) return 1; - return 0; +function compareTime(primary: ParsedTime, secondary: ParsedTime): -1 | 0 | 1 { + if (primary.hours < secondary.hours) return -1; + if (primary.hours > secondary.hours) return 1; + if (primary.minutes < secondary.minutes) return -1; + if (primary.minutes > secondary.minutes) return 1; + return 0; // Identical } /** @@ -106,10 +147,7 @@ export const ConfigurationSchema = z.object({ if (time.maxLate == undefined) return; // Get the computed maxLate time. - const maxLateTime = addTime( - parseTime(time.time), - parseTime(time.maxLate!) - ); + const maxLateTime = addTime(time.time, time.maxLate!); // If the computed maxLate time is invalid, add an issue. if (!isValidTime(maxLateTime)) @@ -132,27 +170,22 @@ export const ConfigurationSchema = z.object({ times.map((time, index) => ({ ...time, originalIndex: index })); // Sort the values to make validation easier. - rememberedTimes.sort((a, b) => - compareTime(parseTime(a.time), parseTime(b.time)) - ); + rememberedTimes.sort((a, b) => compareTime(a.time, b.time)); // Validate that no rules are overlapping from maxLate. rememberedTimes.forEach((time, index) => { if (index === 0) return false; - const previous = times[index - 1]; + const previous = rememberedTimes[index - 1]; if (previous.maxLate == undefined) return false; // If the days don't overlap, there's no need to check the time. if (intersection(time.days, previous.days).size < 1) return false; - const previousEndTime = addTime( - parseTime(previous.time), - parseTime(previous.maxLate) - ); + const previousEndTime = addTime(previous.time, previous.maxLate); // If the previous rule's end time is greater than the current rule's start time, add an issue. - const startTime = parseTime(time.time); - if (compareTime(startTime, previousEndTime) < 1) { + // The times cannot overlap at all as multiple rules cannot be active at the same time. + if (compareTime(time.time, previousEndTime) <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['time', time.originalIndex], @@ -165,3 +198,44 @@ export const ConfigurationSchema = z.object({ }) }); export type Configuration = z.infer; + +const dayAsNumber: Record = { + 1: 'monday', + 2: 'tuesday', + 3: 'wednesday', + 4: 'thursday', + 5: 'friday', + 6: 'saturday', + 0: 'sunday' +}; + +export async function getMatchingTime( + config: Configuration, + now = new Date() +): Promise { + const times = config.times.filter((time) => { + // If the day doesn't match, skip. + console.log(dayAsNumber[now.getDay().toString()]); + if (!time.days.has(dayAsNumber[now.getDay().toString()])) return false; + + const startTime = time.time; + const endTime = addTime( + time.time, + time.maxLate ?? { hours: 0, minutes: 0 } + ); + + const nowTime = { hours: now.getHours(), minutes: now.getMinutes() }; + + return ( + compareTime(nowTime, startTime) >= 0 && compareTime(nowTime, endTime) <= 0 + ); + }); + + // This shouldn't be thrown, if I did my job right. + if (times.length > 1) + throw new Error( + "Multiple rules matched the current time. This shouldn't happen." + ); + + return times[0]; +}