mirror of
https://github.com/Xevion/bus-reminder.git
synced 2025-12-06 05:14:35 -06:00
Parse ^& transform timing data inside TimeConfigSchema, add DayEnum type
This commit is contained in:
138
src/timing.ts
138
src/timing.ts
@@ -15,15 +15,34 @@ const DayEnumSchema = z.enum([
|
||||
'saturday',
|
||||
'sunday'
|
||||
]);
|
||||
type DayEnum = z.infer<typeof DayEnumSchema>;
|
||||
|
||||
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<typeof TimeConfigSchema>;
|
||||
|
||||
@@ -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<typeof ConfigurationSchema>;
|
||||
|
||||
const dayAsNumber: Record<string, DayEnum> = {
|
||||
1: 'monday',
|
||||
2: 'tuesday',
|
||||
3: 'wednesday',
|
||||
4: 'thursday',
|
||||
5: 'friday',
|
||||
6: 'saturday',
|
||||
0: 'sunday'
|
||||
};
|
||||
|
||||
export async function getMatchingTime(
|
||||
config: Configuration,
|
||||
now = new Date()
|
||||
): Promise<TimeConfig | null> {
|
||||
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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user