package org.threadly.concurrent;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
/**
* Class for helping calculate the offset for scheduling tasks. For example if you want a task to
* run at 10 minutes after the hour, every hour, you can use {@link #getDelayTillMinute(int)} to
* calculate the initial delay needed when scheduling with
* {@link SubmitterScheduler#scheduleAtFixedRate(Runnable, long, long)}, and then provide 1 hour
* in milliseconds for the "period".
*
* @since 3.5.0
*/
public class SchedulingUtils {
protected static volatile int cachedHourShift = Integer.MIN_VALUE;
/**
* Call to calculate how many milliseconds until the provided minute. If we are past the
* provided minute, it will be the milliseconds until we reach that minute with the NEXT hour.
* <p>
* Because of use of {@link Clock#lastKnownTimeMillis()}, this calculation will only be accurate
* within about 100 milliseconds. Of course if provided to a scheduler, depending on it's work
* load that variation may be greater.
*
* @param minute Minute to calculate too, can not be negative and must be less than 60
* @return Time in milliseconds till that minute is reached
*/
public static long getDelayTillMinute(int minute) {
ArgumentVerifier.assertLessThan(minute, TimeUnit.HOURS.toMinutes(1), "minute");
ArgumentVerifier.assertNotNegative(minute, "minute");
return getDelayTillMinute(Clock.lastKnownTimeMillis(), minute);
}
/**
* Call to calculate how many milliseconds until the provided minute, If we are past the
* provided minute, it will be the milliseconds until we reach that minute with the NEXT hour.
* <p>
* Because of use of {@link Clock#lastKnownTimeMillis()}, this calculation will only be accurate
* within about 100 milliseconds. Of course if provided to a scheduler, depending on it's work
* load that variation may be greater.
*
* @param now Current time in milliseconds since epoc
* @param minute Minute to calculate too, can not be negative and must be less than 60
* @return Time in milliseconds till that minute is reached
*/
protected static long getDelayTillMinute(long now, int minute) {
long currentMin = TimeUnit.MILLISECONDS.toMinutes(now % TimeUnit.HOURS.toMillis(1));
long minutesToWait = 0;
if (minute > currentMin) {
minutesToWait = minute - currentMin;
} else if (minute <= currentMin) {
minutesToWait = TimeUnit.HOURS.toMinutes(1) - currentMin + minute;
}
// subtract seconds that have passed in current minute
long offset = now % TimeUnit.MINUTES.toMillis(1);
return TimeUnit.MINUTES.toMillis(minutesToWait) - offset;
}
/**
* Call to calculate how many milliseconds until the provided time. If we are past the
* provided hour/minute, it will be the milliseconds until we reach that time with the NEXT day.
* <p>
* It is important to note that the time zone for this hour is UTC. If you want to use this for
* local time, just pass the hour through {@link #shiftLocalHourToUTC(int)}. This will convert
* a local time's hour to UTC so that it can be used in this invocation.
* <p>
* Because of use of {@link Clock#lastKnownTimeMillis()}, this calculation will only be accurate
* within about 100 milliseconds. Of course if provided to a scheduler, depending on it's work
* load that variation may be greater.
*
* @param hour Hour in the 24 hour format, can not be negative and must be less than 24
* @param minute Minute to calculate too, can not be negative and must be less than 60
* @return Time in milliseconds till provided time is reached
*/
public static long getDelayTillHour(int hour, int minute) {
ArgumentVerifier.assertLessThan(hour, TimeUnit.DAYS.toHours(1), "hour");
ArgumentVerifier.assertNotNegative(hour, "hour");
ArgumentVerifier.assertLessThan(minute, TimeUnit.HOURS.toMinutes(1), "minute");
ArgumentVerifier.assertNotNegative(minute, "minute");
return getDelayTillHour(Clock.lastKnownTimeMillis(), hour, minute);
}
/**
* Call to calculate how many milliseconds until the provided time. If we are past the
* provided hour/minute, it will be the milliseconds until we reach that time with the NEXT day.
* <p>
* It is important to note that the time zone for this hour is UTC. If you want to use this for
* local time, just pass the hour through {@link #shiftLocalHourToUTC(int)}. This will convert
* a local time's hour to UTC so that it can be used in this invocation.
* <p>
* Because of use of {@link Clock#lastKnownTimeMillis()}, this calculation will only be accurate
* within about 100 milliseconds. Of course if provided to a scheduler, depending on it's work
* load that variation may be greater.
*
* @param now Current time in milliseconds since epoc
* @param hour Hour in the 24 hour format, can not be negative and must be less than 24
* @param minute Minute to calculate too, can not be negative and must be less than 60
* @return Time in milliseconds till provided time is reached
*/
protected static long getDelayTillHour(long now, int hour, int minute) {
long delayInMillis = TimeUnit.MINUTES.toMillis(minute);
long currentHour = TimeUnit.MILLISECONDS.toHours(now % TimeUnit.DAYS.toMillis(1));
if (hour > currentHour) {
delayInMillis += TimeUnit.HOURS.toMillis(hour - currentHour);
} else if (hour < currentHour) {
delayInMillis += TimeUnit.HOURS.toMillis(TimeUnit.DAYS.toHours(1) - currentHour + hour);
} else {
long result = getDelayTillMinute(Clock.lastKnownTimeMillis(), minute);
if (TimeUnit.MILLISECONDS.toMinutes(result) <= minute) {
return result;
} else {
// here we have to add the time to forward us to the next day
return result + TimeUnit.HOURS.toMillis(TimeUnit.DAYS.toHours(1) - 1);
}
}
// subtract minutes, seconds, and milliseconds that have passed
long offset = now % TimeUnit.HOURS.toMillis(1);
return delayInMillis - offset;
}
/**
* This will shift an hour from the local time zone to UTC. This shift will take into account
* the current local state of daylight savings time. The primary usage of this is so that
* {@link #getDelayTillHour(int, int)} can be used with a local time zone hour.
*
* @param hour Hour to be shifted in the local time zone in 24 hour format
* @return Hour shifted to the UTC time zone in 24 hour format
*/
public static int shiftLocalHourToUTC(int hour) {
ArgumentVerifier.assertLessThan(hour, TimeUnit.DAYS.toHours(1), "hour");
ArgumentVerifier.assertNotNegative(hour, "hour");
if (cachedHourShift == Integer.MIN_VALUE) {
Calendar calendar = Calendar.getInstance();
int shiftInMillis = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
cachedHourShift = (int)(shiftInMillis / TimeUnit.HOURS.toMillis(1));
}
hour -= cachedHourShift;
if (hour > TimeUnit.DAYS.toHours(1) - 1) {
hour %= TimeUnit.DAYS.toHours(1);
} else if (hour < 0) {
hour += TimeUnit.DAYS.toHours(1);
}
return hour;
}
}