Skip to content

Commit

Permalink
feat: next occurance utils
Browse files Browse the repository at this point in the history
  • Loading branch information
abp6318 committed Sep 25, 2024
1 parent 0acc123 commit d7b64c7
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/common/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./shareEntityWithGroups";
export * from "./unshareEntityWithGroups";
export * from "./getEntityGroups";
export * from "./getEntityThumbnailUrl";
export * from "./nextOccuranceUtils";

// For sme reason, if these are exported here,
// they are not actually exported in the final package.
Expand Down
220 changes: 220 additions & 0 deletions packages/common/src/core/nextOccuranceUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { IHubSchedule } from "./types";

// IHubSchedule utils

/**
* Get the next daily occurance of the specified hour in the specified timezone
* @param hour takes an hour of the day between 0 and 23
* @param timezone takes a timezone string
* @returns a Date object representing the next daily occurance of the specified hour
*/
const getNextDailyOccurance = (hour: number, timezone: string): Date => {
validateHour(hour);

// Get the time in the specified timezone
const targetTime = getInitialTargetTime(timezone, "en-US");

if (targetTime.getHours() < hour) {
targetTime.setHours(hour, 0, 0, 0);
} else {
targetTime.setDate(targetTime.getDate() + 1);
targetTime.setHours(hour, 0, 0, 0);
}

return targetTime;
};

/**
* Get the next weekly occurance of the specified hour in the specified timezone
* @param hour takes an hour of the day between 0 and 23
* @param timezone takes a timezone string
* @param dayOfWeek takes a day of the week between 0 (Sunday) and 6 (Saturday)
* @returns a Date object representing the next weekly occurance of the specified hour
*/
const getNextWeeklyOccurance = (
hour: number,
timezone: string,
dayOfWeek: number
): Date => {
validateHour(hour);
validateDayOfWeek(dayOfWeek);

// Get the time in the specified timezone
const targetTime = getInitialTargetTime(timezone, "en-US");

// Calculate the difference in days between today and the next occurrence of the specified day of the week
const currentDayOfWeek = targetTime.getDay();
let daysUntilNextEvent = (dayOfWeek - currentDayOfWeek + 7) % 7;

// If the event is today but the hour has already passed, schedule it for next week
if (daysUntilNextEvent === 0 && targetTime.getHours() >= hour) {
daysUntilNextEvent = 7;
}

// Set the event time to the next occurrence of the specified day at the given hour
targetTime.setDate(targetTime.getDate() + daysUntilNextEvent);
targetTime.setHours(hour, 0, 0, 0);

// Convert the event time to an ISO string
return targetTime;
};

/**
* Get the next monthly occurance of the specified hour in the specified timezone
* @param hour takes an hour of the day between 0 and 23
* @param timezone takes a timezone string
* @param dayOfMonth takes a day of the week between 1 and 28
* @returns a Date object representing the next monthly occurance of the specified hour
*/
const getNextMonthlyOccurance = (
hour: number,
timezone: string,
dayOfMonth: number
): Date => {
validateHour(hour);
validateDayOfMonth(dayOfMonth);

// Get the time in the specified timezone
const targetTime = getInitialTargetTime(timezone, "en-US");

// Calculate the difference in days between today and the next occurrence of the specified day of the month
const currentDayOfMonth = targetTime.getDate();
let daysUntilNextEvent = dayOfMonth - currentDayOfMonth;

// If the event is today but the hour has already passed, schedule it for next month
if (daysUntilNextEvent === 0 && targetTime.getHours() >= hour) {
daysUntilNextEvent = 0;
targetTime.setMonth(targetTime.getMonth() + 1);
} else if (daysUntilNextEvent < 0) {
// If the day of the month has already passed, schedule it for next month
targetTime.setMonth(targetTime.getMonth() + 1);
targetTime.setDate(dayOfMonth);
} else {
// Set the event time to the next occurrence of the specified day at the given hour
targetTime.setDate(dayOfMonth);
}

targetTime.setHours(hour, 0, 0, 0);

// Convert the event time to an ISO string
return targetTime;
};

/**
* Get the next yearly occurance of the specified hour in the specified timezone
* @param hour takes an hour of the day between 0 and 23
* @param timezone takes a timezone string
* @param dayOfMonth takes a day of the week between 1 and 28
* @param month takes a month of the year between 0 and 11
* @returns a Date object representing the next yearly occurance of the specified hour
*/
const getNextYearlyOccurance = (
hour: number,
timezone: string,
dayOfMonth: number,
month: number
): Date => {
validateHour(hour);
validateDayOfMonth(dayOfMonth);
validateMonth(month);

// Get the time in the specified timezone
const targetTime = getInitialTargetTime(timezone, "en-US");

// Calculate the next occurrence of the specified month and day
const currentMonth = targetTime.getMonth();
const currentDayOfMonth = targetTime.getDate();
const currentYear = targetTime.getFullYear();

if (
currentMonth > month ||
(currentMonth === month &&
(currentDayOfMonth > dayOfMonth ||
(currentDayOfMonth === dayOfMonth && targetTime.getHours() >= hour)))
) {
// If the specified month and day have already passed this year, schedule it for next year
targetTime.setFullYear(currentYear + 1);
}
targetTime.setMonth(month);
targetTime.setDate(dayOfMonth);
targetTime.setHours(hour, 0, 0, 0);

// Convert the event time to an ISO string
return targetTime;
};

/**
* Get the next occurance of the specified schedule
* @param schedule takes an IHubSchedule object
* @returns a Date object representing the next occurance of the specified schedule
*/
export const getNextOccurance = (schedule: IHubSchedule): Date => {
if (schedule.mode === "scheduled") {
switch (schedule.cadence) {
case "daily":
return getNextDailyOccurance(schedule.hour, schedule.timezone);
case "weekly":
return getNextWeeklyOccurance(
schedule.hour,
schedule.timezone,
schedule.day
);
case "monthly":
return getNextMonthlyOccurance(
schedule.hour,
schedule.timezone,
schedule.date
);
case "yearly":
return getNextYearlyOccurance(
schedule.hour,
schedule.timezone,
schedule.date,
schedule.month
);
default:
throw new Error("Invalid cadence");
}
} else {
// If the schedule mode is not "scheduled", return undefined because it is either "automatic" or "manual"
return undefined;
}
};

/****** REPEATED CODE ******/
const getInitialTargetTime = (timezone: string, locale: string): Date => {
const now = new Date();
const utcOffset = now.getTimezoneOffset() * 60000;
const localTime = new Date(now.getTime() + utcOffset);
const targetTime = new Date(
localTime.toLocaleString(locale, { timeZone: timezone })
);
return targetTime;
};

/****** VALIDATION ******/
const validateHour = (hour: number): void => {
if (hour < 0 || hour > 23) {
throw new Error("Hour must be between 0 and 23");
}
};

const validateDayOfWeek = (dayOfWeek: number): void => {
if (dayOfWeek < 0 || dayOfWeek > 6) {
throw new Error(
"Day of the week must be between 0 (Sunday) and 6 (Saturday)"
);
}
};

const validateMonth = (month: number): void => {
if (month < 0 || month > 11) {
throw new Error("Month must be between 0 and 11");
}
};

const validateDayOfMonth = (dayOfMonth: number): void => {
if (dayOfMonth < 1 || dayOfMonth > 28) {
throw new Error("Day of the month must be between 1 and 28");
}
};

0 comments on commit d7b64c7

Please sign in to comment.