Time range with reversed bounds

Hi,

I have:

using Dates
lower_bound = Dates.Time(23,59,00)
upper_bound = Dates.Time(00,01,00)

I do:

julia> collect(upper_bound:Dates.Minute(1):lower_bound)
1439-element Vector{Time}:
 00:01:00
 00:02:00
 00:03:00
 ⋮
 23:57:00
 23:58:00
 23:59:00

But if I do:

julia> collect(lower_bound:Dates.Minute(1):upper_bound)
Time[]

I expected to have this result:

23:59:00
00:00:00
00:01:00

How could I get this result nicely (e.g. without having to manipulate days)?

This is actually interesting.

I think your third code block is wrong (it’s the same as the second), but I think what you’re getting at is the fact that we have:

julia> collect(3:1:5)
3-element Vector{Int64}:
 3
 4
 5

julia> collect(5:-1:3)
3-element Vector{Int64}:
 5
 4
 3

so one would expect

julia> collect(Time(0,1):Minute(-1):Time(23, 59))
Time[]

to work given that we have

julia> [Time(0,1) + i*Minute(-1) for i ∈ 0:2]
3-element Vector{Time}:
 00:01:00
 00:00:00
 23:59:00

Someone who’s more intimately familiar with the workings of the Dates std lib might comment, but this seems issue-worthy to me?

1 Like

I am not intimately familiar with Dates, but the problem is that Time arithmetic, on one hand, works modulo 24 hours,

julia> Time("23:59", "HH:MM") + Minute(2)
00:01:00 # one minute after midnight

but the ordering that is defined for Time is valid only within one day. So

x + Minute(1) > x

does not necessarily hold for x::Time. For example,

julia> Time("23:59", "HH:MM") + Minute(1) > Time("23:59", "HH:MM")
false

Therefore, if you create a range r such that the start time, first(r), is before midnight and the end time, last(r), is after midnight, that range is considered empty, because first(r) > last(r).

2 Likes

Ah sure, that makes sense, same as

julia> [typemax(Int)-1 + i for i ∈ 0:3]
4-element Vector{Int64}:
  9223372036854775806
  9223372036854775807
 -9223372036854775808
 -9223372036854775807

julia> collect(typemax(Int)-1:(typemin(Int)+1))
Int64[]

So OP would need a proper ordering of those times like:

julia> Time.(DateTime(0, 1, 1, 23,59):Minute(1):DateTime(0,1,2,0,1))
3-element Vector{Time}:
 23:59:00
 00:00:00
 00:01:00

julia> Time.(DateTime(0, 1, 2, 0, 1):Minute(-1):DateTime(0,1,1,23,59))
3-element Vector{Time}:
 00:01:00
 00:00:00
 23:59:00
1 Like

This is tricky. It seems like we could use a nice utility for this. The best I could do with builtins was

julia> Iterators.takewhile(!=(Dates.Time(00,01,00) + Dates.Minute(1)),
           Iterators.countfrom(Dates.Time(23,59,00), Dates.Minute(1))) |> collect
3-element Vector{Time}:
 23:59:00
 00:00:00
 00:01:00

But this is obviously very brittle. It relies on the endpoint being hit exactly and has to add 1 to it so that it doesn’t stop one too early.

If you knew how many steps you wanted, rather than the stop point, this would be easier

julia> Iterators.take(Iterators.countfrom(Dates.Time(23,59,00), Dates.Minute(1)), 3) |> collect
3-element Vector{Time}:
 23:59:00
 00:00:00
 00:01:00

One can always implement their own iterator for this. Here’s what I threw together. It covers this basic case but perhaps not everything you need. It probably has some strange behavior in some corner cases that you may need to fix (for example, increments larger than 1 Day will probably cause trouble).

struct TimeIterator{I<:Dates.TimePeriod}
    start::Dates.Time
    stop::Dates.Time
    step::I
end

Base.IteratorSize(::Type{<:TimeIterator}) = Base.SizeUnknown()
Base.eltype(::Type{<:TimeIterator}) = Dates.Time

function Base.iterate(t::TimeIterator, state=(t.start, t.start <= t.stop, false))
    t.step > zero(t.step) || throw(DomainError(t.step, "needs extra work to support nonpositive steps")) # TODO: should check in constructor instead
    tnow, homestretch, stopnow = state
    if stopnow || (homestretch && !(tnow <= t.stop)) # time to stop
        return nothing
    end
    tnext = tnow + t.step
    stopnext = false
    if tnext < tnow # detect when the time wraps
        if homestretch # we were already on the home stretch -- don't let us wrap again!
            stopnext = true
        end
        homestretch = true # use inequality-based stopping condition now
    end
    return (tnow, (tnext, homestretch, stopnext))
end
julia> TimeIterator(Dates.Time(23,57,00), Dates.Time(23,59,00), Dates.Minute(1)) |> collect
3-element Vector{Time}:
 23:57:00
 23:58:00
 23:59:00

julia> TimeIterator(Dates.Time(23,57,00), Dates.Time(00,01,00), Dates.Minute(1)) |> collect
5-element Vector{Time}:
 23:57:00
 23:58:00
 23:59:00
 00:00:00
 00:01:00

Using the various Iterator tools:

using Iterators
using IterTools
using Dates

maprepeateduntil(mapfn, condfn, stepfn, initval) = 
  Iterators.map(mapfn, 
    Iterators.takewhile(
      let done=false
        x->(t = done; done = condfn(x); !t)
      end, 
      IterTools.iterated(stepfn, initval)
    )
  )

iteratetimes(lb, ub, step) = 
  maprepeateduntil(
    Time, ==(ub)∘Time, 
    Base.Fix1(+, step), 
    DateTime(Date(now())+lb)
  )

with these in place:

julia> iteratetimes(lower_bound, upper_bound, Minute(1)) |> collect
3-element Vector{Time}:
 23:59:00
 00:00:00
 00:01:00

and

julia> iteratetimes(upper_bound, lower_bound, Minute(1)) |> collect |> 
  x->(display.(extrema(x)); length(x))
00:01:00
23:59:00
1439