如何将时间段划分为相等的时间间隔并找到当前时间段?

时间:2015-08-05 02:18:09

标签: java jodatime java-time

我需要为很多用户安排定期工作。此作业将以固定速率运行,间隔时间。我希望在该时间间隔内统一分配每个用户的作业执行。例如,如果间隔是4天,我会使用带有每个用户标识符的consistent hashing function来同时安排作业,例如。每隔4天,第3天。

间隔是相对于所有用户都相同的原点瞬间。给定这样的原点瞬间,如Instant#EPOCH或其他一些常数值,如何找到当前间隔的开始日期?

我能做到

Instant now = Instant.now();
Instant origin = Instant.EPOCH;
Duration interval = Duration.ofDays(4);

Duration duration = Duration.between(origin, now);
long sinceOrigin = duration.toMillis();
long millisPerInterval = interval.toMillis();

long intervalsSince = sinceOrigin / millisPerInterval;
Instant startNext = origin.plus(interval.multipliedBy(intervalsSince));

int cursor = distributionStrategy.distribute(hashCode, millisPerInterval);

然后,我可以使用cursor以相对于当前间隔开始的Instant安排作业。

这里有很多数学,我不确定在任何地方转换到毫秒都会维持实际日期。是否有更精确的方法来划分两个时刻之间的时间并找到我们目前所处的那个(细分)?

5 个答案:

答案 0 :(得分:12)

如果你只想减少数学,你可以使用余数而不是除法和乘法。

long millisSinceIntervalStart = sinceOrigin % millisPerInterval;
Instant startNext = now.minusMillis(millisSinceIntervalStart);

此处您不必计算自原点以来经过的间隔数。只需从intervalStart开始计算时间并从当前时间中减去它。

此外,您的startNext似乎表示当前间隔的开始,而不是下一个间隔。正确的吗?

答案 1 :(得分:5)

假设您实际上对时间和持续时间感兴趣(即与句点,日期,时区等无关),那么您的代码应该没问题。在这种情况下,我实际上已经进入了之前的 ...... <数学在这里很简单。

now

这假设您在epoch之前不必担心new Instant(start) - 此时您需要做一些工作,因为您需要分区操作的底层,而不是截断为0。

(如果您只想要开始,则可以返回api。)

答案 2 :(得分:5)

我认为你的事情过于复杂。您几乎不需要了解代码建议的内容。

你只需要回答“这个对象下次运行的时间是什么时候?”,这样答案在统计上均匀分布在一个区间内并且是一致的(不依赖于“现在”,除了下一次运行总是在“现在”之后) )。

这种方法可以做到:

public static long nextRun(long origin, long interval, Object obj) {
    long nextRunTime = origin + (System.currentTimeMillis() - origin)
       / interval * interval + Math.abs(obj.hashCode() % interval);
    return nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + interval;
}

此方法在下次使用其hashCode()运行对象时返回,以确定应在何时安排它,然后返回将要发生的下一个实际时间。

小实施说明:使用Math.abs(obj.hashCode() % interval)代替Math.abs(obj.hashCode()) % interval来防范hashCode()返回Integer.MIN_VALUE并知道Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE

如果您需要在API中使用java.time个类,这里​​的代码相同,但java.time参数和返回类型:

public static Instant nextRun(Instant origin, Duration interval, Object target) {
    long start = origin.toEpochMilli();
    long width = interval.toMillis();
    long nextRunTime = start + (System.currentTimeMillis() - start)
       / width * width + Math.abs(target.hashCode() % width);
    nextRunTime = nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + width;
    return Instant.ofEpochMilli(nextRunTime);
}

为了帮助理解数学,这里是一个较长的版本,其中组件计算被细分并分配给有意义的变量名称:

public static Instant nextRun(Instant origin, Duration duration, Object target) {
    long now = System.currentTimeMillis();
    long start = origin.toEpochMilli();
    long intervalWidth = duration.toMillis();
    long ageSinceOrigin = now - start;
    long totalCompleteDurations = ageSinceOrigin / intervalWidth * intervalWidth;
    long mostRecentIntervalStart = start + totalCompleteDurations;
    long offsetInDuration = Math.abs(target.hashCode() % intervalWidth);
    long nextRun = mostRecentIntervalStart + offsetInDuration;
    // schedule for next duration if this duration's time has already passed
    if (nextRun < now) { 
        nextRun += intervalWidth;
    }
    return Instant.ofEpochMilli(nextRun);
}

答案 3 :(得分:2)

我会尝试将每个时间段定义为具有开始和结束日期的对象。然后使用RB树存储Period对象。然后,您可以在树中导航特定日期:

如果日期在第一个期间内,您就找到了它。 如果日期在期间的开始日期之前,请导航到左侧节点并检查该期间 如果日期在句点的结束日期之后,请导航到右侧节点并检查该期间

答案 4 :(得分:2)

好吧,正如已经说过的那样,找到包含Duration间隔的内容正如您已经在做或直接使用millis就足够了这个用例,所涉及的数学就很简单了。但是,如果您确实有一个保证Period间隔涉及小时的用例,请按以下步骤操作:

  1. Period翻译成大约几小时的持续时间,然后用它来估算目标离原点的距离。
  2. 根据您的估算来缩放Period。将累计的24小时视为额外的天数。
  3. 将原点间隔移动缩放的时间段。如果移动的间隔包含您的目标,则表示您已完成。如果没有,请根据您错过的金额重新估算。
  4. 在查找包含间隔时需要注意的一点是,所有Period添加必须直接从原点进行,而不是从中间间隔进行,以保持一致性。例如,如果您的Period为一个月,而原点为1月31日,则原点间隔之后的时间间隔应从2月28日和3月31日开始。增加到1月31日的两个月将正确地产生3月31日,但是到2月28日增加一个月将错误地产生3月28日。

    以下是上述方法的代码。请注意,这类事情存在大量异常情况,我只测试了其中的一些情况,因此请不要将此代码视为经过严格测试。

    public static final int NUM_HOURS_IN_DAY = 24;
    public static final int NUM_HOURS_IN_MONTH = 730;  // approximate
    
    public ZonedDateTime startOfContainingInterval(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
        return intervalStart(origin, period, hours, containingIntervalNum(origin, period, hours, target));
    }
    
    public int containingIntervalNum(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
        int intervalNum = 0;
        ZonedDateTime intervalStart = origin, intervalFinish;
        long approximatePeriodHours = period.toTotalMonths() * NUM_HOURS_IN_MONTH + period.getDays() * NUM_HOURS_IN_DAY + hours;
        do {
            long gap = ChronoUnit.HOURS.between(intervalStart, target);
            long estimatedIntervalsAway = Math.floorDiv(gap, approximatePeriodHours);
            intervalNum += estimatedIntervalsAway;
            intervalStart = intervalStart(origin, period, hours, intervalNum);
            intervalFinish = intervalStart(origin, period, hours, intervalNum + 1);
        } while (!(target.isAfter(intervalStart) && target.isBefore(intervalFinish) || target.equals(intervalStart)));
        return intervalNum;
    }
    
    public ZonedDateTime intervalStart(ZonedDateTime origin, Period period, int hours, int intervalNum) {
        Period scaledPeriod = period.multipliedBy(intervalNum).plusDays(hours * intervalNum / NUM_HOURS_IN_DAY);
        long leftoverHours = hours * intervalNum % NUM_HOURS_IN_DAY;
        return origin.plus(scaledPeriod).plusHours(leftoverHours);
    }