diff --git a/src/LookupArrays/LookupArrays.jl b/src/LookupArrays/LookupArrays.jl index d76447235..02f6c6871 100644 --- a/src/LookupArrays/LookupArrays.jl +++ b/src/LookupArrays/LookupArrays.jl @@ -44,7 +44,7 @@ export AutoStep, AutoBounds, AutoIndex export LookupArray export AutoLookup, NoLookup -export Aligned, AbstractSampled, Sampled, AbstractCategorical, Categorical +export Aligned, AbstractSampled, Sampled, AbstractCyclic, Cyclic, AbstractCategorical, Categorical export Unaligned, Transformed const StandardIndices = Union{AbstractArray{<:Integer},Colon,Integer,CartesianIndex,CartesianIndices} diff --git a/src/LookupArrays/lookup_arrays.jl b/src/LookupArrays/lookup_arrays.jl index bce283374..32d28cf44 100644 --- a/src/LookupArrays/lookup_arrays.jl +++ b/src/LookupArrays/lookup_arrays.jl @@ -284,8 +284,7 @@ struct Sampled{T,A<:AbstractVector{T},O,Sp,Sa,M} <: AbstractSampled{T,O,Sp,Sa} sampling::Sa metadata::M end -function Sampled( - data=AutoIndex(); +function Sampled(data=AutoIndex(); order=AutoOrder(), span=AutoSpan(), sampling=AutoSampling(), metadata=NoMetadata() ) @@ -298,6 +297,80 @@ function rebuild(l::Sampled; Sampled(data, order, span, sampling, metadata) end +abstract type CycleStatus end +struct Cycling <: CycleStatus end +struct NotCycling <: CycleStatus end + +abstract type AbstractCyclic{X,T,O,Sp,Sa} <: AbstractSampled{T,O,Sp,Sa} end + +cycle(l::AbstractCyclic) = l.cycle +cycle_status(l::AbstractCyclic) = l.cycle_status + +no_cycling(l::AbstractCyclic) = rebuild(l; cycle_status=NotCycling()) + +function cycle_val(l::AbstractCyclic, val) + cycle_start = ordered_first(l) + # This formulation is necessary for dates + ncycles = (val - cycle_start) รท (cycle_start + cycle(l) - cycle_start) + res = val - ncycles * cycle(l) + # Catch precision errors + @show val res ncycles cycle(l) + if (cycle_start + (ncycles + 1) * cycle(l)) <= val + @show "higher" + i = 1 + while i < 10000 + if (cycle_start + (ncycles + i) * cycle(l)) > val + res = val - (ncycles + i - 1) * cycle(l) + @show res i + return res + end + i += 1 + end + elseif res < cycle_start + @show "lower" + i = 1 + while i < 10000 + res = val - (ncycles - i + 1) * cycle(l) + if res >= cycle_start + res = val - (ncycles - i + 1) * cycle(l) + @show res i val + return res + end + i += 1 + end + else + @show "no change" + return res + end + error("`Cyclic` lookup too innacurate, value not found") +end + + +struct Cyclic{X,T,A<:AbstractVector{T},O,Sp,Sa,M,C} <: AbstractCyclic{X,T,O,Sp,Sa} + data::A + order::O + span::Sp + sampling::Sa + metadata::M + cycle::C + cycle_status::X +end +function Cyclic(data=AutoIndex(); + order=AutoOrder(), span=AutoSpan(), + sampling=AutoSampling(), metadata=NoMetadata(), + cycle, # Mandatory keyword, there are too many possible bugs with auto detection +) + cycle_status = Cycling() # Not use-facing + Cyclic(data, order, span, sampling, metadata, cycle, cycle_status) +end + +function rebuild(l::Cyclic; + data=parent(l), order=order(l), span=span(l), sampling=sampling(l), metadata=metadata(l), + cycle=cycle(l), _cycle_status=cycle_status(l), kw... +) + Cyclic(data, order, span, sampling, metadata, cycle, cycle_status) +end + """ AbstractCategorical <: Aligned diff --git a/src/LookupArrays/lookup_traits.jl b/src/LookupArrays/lookup_traits.jl index 8778a44f4..a2f7cf1f2 100644 --- a/src/LookupArrays/lookup_traits.jl +++ b/src/LookupArrays/lookup_traits.jl @@ -269,4 +269,3 @@ change the `LookupArray` type without changing the index values. struct AutoIndex <: AbstractVector{Int} end Base.size(::AutoIndex) = (0,) - diff --git a/src/LookupArrays/selector.jl b/src/LookupArrays/selector.jl index 2440a1a6a..72a78f7ee 100644 --- a/src/LookupArrays/selector.jl +++ b/src/LookupArrays/selector.jl @@ -122,6 +122,10 @@ selectindices(l::LookupArray, sel::At{<:AbstractVector}) = _selectvec(l, sel) _selectvec(l, sel) = [selectindices(l, rebuild(sel; val=v)) for v in val(sel)] +function at(lookup::AbstractCyclic{Cycling}, sel::At; kw...) + cycled_sel = rebuild(sel; val=cycle_val(lookup, val(sel))) + return at(no_cycling(lookup), cycled_sel; kw...) +end function at(lookup::NoLookup, sel::At; kw...) v = val(sel) r = round(Int, v) @@ -226,6 +230,10 @@ end selectindices(l::LookupArray, sel::Near) = near(l, sel) selectindices(l::LookupArray, sel::Near{<:AbstractVector}) = _selectvec(l, sel) +function near(lookup::AbstractCyclic{Cycling}, sel::Near) + cycled_sel = rebuild(sel; val=cycle_val(lookup, val(sel))) + near(no_cycling(lookup), cycled_sel) +end near(lookup::NoLookup, sel::Near{<:Real}) = max(1, min(round(Int, val(sel)), lastindex(lookup))) function near(lookup::LookupArray, sel::Near) span(lookup) isa Union{Irregular,Explicit} && locus(lookup) isa Union{Start,End} && @@ -306,6 +314,10 @@ end selectindices(l::LookupArray, sel::Contains; kw...) = contains(l, sel) selectindices(l::LookupArray, sel::Contains{<:AbstractVector}) = _selectvec(l, sel) +function contains(lookup::AbstractCyclic{Cycling}, sel::Contains; kw...) + cycled_sel = rebuild(sel; val=cycle_val(lookup, val(sel))) + return contains(no_cycling(lookup), cycled_sel; kw...) +end function contains(l::NoLookup, sel::Contains; kw...) i = Int(val(sel)) i in l || throw(SelectorError(l, i)) @@ -484,6 +496,11 @@ function between(l::NoLookup, sel::Interval) x = intersect(sel, first(axes(l, 1))..last(axes(l, 1))) return ceil(Int, x.left):floor(Int, x.right) end +# function between(l::AbstractCyclic{Cycling}, sel::Interval) +# cycle_val(l, sel.x)..cycle_val(l, sel.x) +# cycled_sel = rebuild(sel; val=) +# near(no_cycling(lookup), cycled_sel; kw...) +# end between(l::LookupArray, interval::Interval) = between(sampling(l), l, interval) # This is the main method called above function between(sampling::Sampling, l::LookupArray, interval::Interval) diff --git a/test/selector.jl b/test/selector.jl index e1d5ce50a..3cba8f144 100644 --- a/test/selector.jl +++ b/test/selector.jl @@ -1,6 +1,6 @@ using DimensionalData, Test, Unitful, Combinatorics, Dates, IntervalSets, Extents using DimensionalData.LookupArrays, DimensionalData.Dimensions -using .LookupArrays: between, touches, at, near, contains, bounds, SelectorError +using .LookupArrays: between, touches, at, near, contains, bounds, SelectorError, cycle_val a = [1 2 3 4 5 6 7 8 @@ -1326,7 +1326,61 @@ end end -@testset "NoIndex" begin +@testset "Cyclic lookup" begin + lookups = ( + day=Cyclic(DateTime(2001):Day(1):DateTime(2002, 12, 31); cycle=Year(1), order=ForwardOrdered(), span=Regular(Day(1)), sampling=Intervals(Start())), + week=Cyclic(DateTime(2001):Week(1):DateTime(2002, 12, 31); cycle=Year(1), order=ForwardOrdered(), span=Regular(Week(1)), sampling=Intervals(Start())), + month=Cyclic(DateTime(2001):Month(1):DateTime(2002, 12, 31); cycle=Year(1), order=ForwardOrdered(), span=Regular(Month(1)), sampling=Intervals(Start())), + month_month=Cyclic(DateTime(2001):Month(1):DateTime(2002, 1, 31); cycle=Month(1), order=ForwardOrdered(), span=Regular(Month(1)), sampling=Intervals(Start())), + ) + lookup = lookups[1] + + for lookup in lookups + # Test exact cycles + @test at(lookup, At(DateTime(1))) == 1 + @test at(lookup, At(DateTime(1999))) == 1 + @test at(lookup, At(DateTime(2000))) == 1 + @test at(lookup, At(DateTime(2001))) == 1 + @test at(lookup, At(DateTime(4000))) == 1 + @test near(lookup, Near(DateTime(1))) == 1 + @test near(lookup, Near(DateTime(1999))) == 1 + @test near(lookup, Near(DateTime(2000))) == 1 + @test near(lookup, Near(DateTime(2001))) == 1 + @test near(lookup, Near(DateTime(4000))) == 1 + @test contains(lookup, Contains(DateTime(1))) == 1 + @test contains(lookup, Contains(DateTime(1999))) == 1 + @test contains(lookup, Contains(DateTime(2000))) == 1 + @test contains(lookup, Contains(DateTime(2001))) == 1 + @test contains(lookup, Contains(DateTime(4000))) == 1 + end + + lookup = lookups.month + @test at(lookup, At(DateTime(1, 12))) == 12 + @test at(lookup, At(DateTime(1999, 12))) == 12 + @test at(lookup, At(DateTime(2000, 12))) == 12 + @test at(lookup, At(DateTime(2001, 12))) == 12 + @test at(lookup, At(DateTime(3000, 12))) == 12 + lookup = lookups.day + @test at(lookup, At(DateTime(1, 12, 31))) == 365 + @test at(lookup, At(DateTime(1999, 12, 31))) == 365 + # This is kinda wrong, as there are 366 days in 2000 + # But our lookup has 365. Leap years would be handled + # properly with a four year cycle + @test at(lookup, At(DateTime(2000, 12, 31))) == 365 + @test at(lookup, At(DateTime(2001, 12, 31))) == 365 + @test at(lookup, At(DateTime(3000, 12, 31))) == 365 + + @testset "Leap years are correct with four year cycles" begin + lookup = Cyclic(DateTime(2000):Day(1):DateTime(2003, 12, 31); cycle=Year(4), order=ForwardOrdered(), span=Regular(Day(1)), sampling=Intervals(Start())) + @test at(lookup, At(DateTime(1, 12, 31))) == findfirst(==(DateTime(2001, 12, 31)), lookup) + @test at(lookup, At(DateTime(1999, 12, 31))) == findfirst(==(DateTime(1999 + 4, 12, 31)), lookup) + @test at(lookup, At(DateTime(2000, 12, 31))) == 366 == findfirst(==(DateTime(2000, 12, 31)), lookup) + @test at(lookup, At(DateTime(2007, 12, 31))) == findfirst(==(DateTime(2007 - 4, 12, 31)), lookup) + @test at(lookup, At(DateTime(3000, 12, 31))) == 366 == findfirst(==(DateTime(3000 - 250 * 4, 12, 31)), lookup) + end +end + +@testset "NoLookup" begin l = NoLookup(1:100) @test_throws SelectorError selectindices(l, At(0)) @test_throws SelectorError selectindices(l, At(200))