tick icon indicating copy to clipboard operation
tick copied to clipboard

`tick.alpha.interval/divide-by` gives incorrect results for some intervals and divisors

Open dgr opened this issue 7 months ago • 1 comments

When the divisor doesn't divide nicely into the interval period, divide-by will sometimes return an extra interval that consists of just a few nanoseconds.

For instance:

=> (ti/divide-by 17 (ti/new-interval #time/zoned-date-time "2025-07-09T09:33-04:00" #time/zoned-date-time "2025-07-09T14:30-04:00"))

;;; result
([#time/zoned-date-time "2025-07-09T09:33-04:00"
  #time/zoned-date-time "2025-07-09T09:50:28.235294117-04:00"]
 [#time/zoned-date-time "2025-07-09T09:50:28.235294117-04:00"
  #time/zoned-date-time "2025-07-09T10:07:56.470588234-04:00"]
 [#time/zoned-date-time "2025-07-09T10:07:56.470588234-04:00"
  #time/zoned-date-time "2025-07-09T10:25:24.705882351-04:00"]
 [#time/zoned-date-time "2025-07-09T10:25:24.705882351-04:00"
  #time/zoned-date-time "2025-07-09T10:42:52.941176468-04:00"]
 [#time/zoned-date-time "2025-07-09T10:42:52.941176468-04:00"
  #time/zoned-date-time "2025-07-09T11:00:21.176470585-04:00"]
 [#time/zoned-date-time "2025-07-09T11:00:21.176470585-04:00"
  #time/zoned-date-time "2025-07-09T11:17:49.411764702-04:00"]
 [#time/zoned-date-time "2025-07-09T11:17:49.411764702-04:00"
  #time/zoned-date-time "2025-07-09T11:35:17.647058819-04:00"]
 [#time/zoned-date-time "2025-07-09T11:35:17.647058819-04:00"
  #time/zoned-date-time "2025-07-09T11:52:45.882352936-04:00"]
 [#time/zoned-date-time "2025-07-09T11:52:45.882352936-04:00"
  #time/zoned-date-time "2025-07-09T12:10:14.117647053-04:00"]
 [#time/zoned-date-time "2025-07-09T12:10:14.117647053-04:00"
  #time/zoned-date-time "2025-07-09T12:27:42.352941170-04:00"]
 [#time/zoned-date-time "2025-07-09T12:27:42.352941170-04:00"
  #time/zoned-date-time "2025-07-09T12:45:10.588235287-04:00"]
 [#time/zoned-date-time "2025-07-09T12:45:10.588235287-04:00"
  #time/zoned-date-time "2025-07-09T13:02:38.823529404-04:00"]
 [#time/zoned-date-time "2025-07-09T13:02:38.823529404-04:00"
  #time/zoned-date-time "2025-07-09T13:20:07.058823521-04:00"]
 [#time/zoned-date-time "2025-07-09T13:20:07.058823521-04:00"
  #time/zoned-date-time "2025-07-09T13:37:35.294117638-04:00"]
 [#time/zoned-date-time "2025-07-09T13:37:35.294117638-04:00"
  #time/zoned-date-time "2025-07-09T13:55:03.529411755-04:00"]
 [#time/zoned-date-time "2025-07-09T13:55:03.529411755-04:00"
  #time/zoned-date-time "2025-07-09T14:12:31.764705872-04:00"]
 [#time/zoned-date-time "2025-07-09T14:12:31.764705872-04:00"
  #time/zoned-date-time "2025-07-09T14:29:59.999999989-04:00"]
 [#time/zoned-date-time "2025-07-09T14:29:59.999999989-04:00"
  #time/zoned-date-time "2025-07-09T14:30-04:00"])

=> (count *1)
18

Note how we asked for 17 intervals and we got 18. The last is just a fraction of a second.

I believe this is because divide-by ultimately reduces to p/range which uses a non-integral step, where step * n is less than the ending value. Thus, p/range checks the termination condition of being greater-than or equal-to the ending time and finds that it wants to keep going for one more step.

dgr avatar Jul 09 '25 20:07 dgr

I suspect, though haven't investigated thoroughly enough to know for sure, that this same bug infects the other versions of divide-*. After a quick look at the code, they all seem to call p/range, which I think is where the bug is located. Basically, this is an issue of finite fractional precision for dates and times.

Note that clojure.core/range has a similar issue when using floating point steps, but it correctly stops before adding the last:

=> (range 0 1 0.1)
(0
 0.1
 0.2
 0.30000000000000004
 0.4
 0.5
 0.6
 0.7
 0.7999999999999999
 0.8999999999999999
 0.9999999999999999)

It stops at 0.9999999999999999.

dgr avatar Jul 09 '25 20:07 dgr