diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..9613e05 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +style = "yas" \ No newline at end of file diff --git a/Project.toml b/Project.toml index a8283c2..c183e50 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,22 @@ name = "TimeSpans" uuid = "bb34ddd2-327f-4c4a-bfb0-c98fc494ece1" authors = ["Beacon Biosignals, Inc."] -version = "0.3.1" +version = "0.3.2" [deps] +ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] julia = "1.6" +Arrow = "1.6" +ArrowTypes = "1.1.0" [extras] +Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Arrow", "Test", "Tables"] diff --git a/README.md b/README.md index ad0cb80..442cf15 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,4 @@ [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://beacon-biosignals.github.io/TimeSpans.jl/stable) [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://beacon-biosignals.github.io/TimeSpans.jl/dev) -TimeSpans.jl provides a simple `TimeSpan` type for representing a continuous span between two points in time, along with generic utility functions for common operations on `TimeSpan`-like types. Importantly, the package exposes a minimal interface (`TimeSpans.start` and `TimeSpans.stop`) that any type can implement to enable support for the TimeSpans API. +TimeSpans.jl provides a simple `TimeSpan` type for representing a continuous span between two points in time, along with generic utility functions for common operations on `TimeSpan`-like types. Importantly, the package exposes a minimal interface (`TimeSpans.start` and `TimeSpans.stop`) that any type can implement to enable support for the TimeSpans API. In addition to `TimeSpan`, this package implements the TimeSpans API for `Period`s and `NamedTuple`s that have `:start` and `:stop` keys. diff --git a/src/TimeSpans.jl b/src/TimeSpans.jl index a304f45..4895e12 100644 --- a/src/TimeSpans.jl +++ b/src/TimeSpans.jl @@ -3,12 +3,12 @@ module TimeSpans using Base.Iterators using Dates using Statistics +using ArrowTypes export TimeSpan, start, stop, istimespan, translate, overlaps, shortest_timespan_containing, duration, index_from_time, time_from_index, merge_spans!, merge_spans, invert_spans - const NS_IN_SEC = Dates.value(Nanosecond(Second(1))) # Number of nanoseconds in one second ##### @@ -102,6 +102,11 @@ istimespan(::Any) = false istimespan(::TimeSpan) = true istimespan(::Period) = true +function istimespan(::T) where {T<:NamedTuple} + return hasfield(T, :start) && fieldtype(T, :start) <: Period && + hasfield(T, :stop) && fieldtype(T, :stop) <: Period +end + """ start(span) @@ -110,6 +115,11 @@ Return the inclusive lower bound of `span` as a `Nanosecond` value. start(span::TimeSpan) = span.start start(t::Period) = convert(Nanosecond, t) +function start(x::NamedTuple) + istimespan(x) || throw(ArgumentError("input is not a valid timespan")) + return Nanosecond(x.start) +end + """ stop(span) @@ -118,6 +128,11 @@ Return the exclusive upper bound of `span` as a `Nanosecond` value. stop(span::TimeSpan) = span.stop stop(t::Period) = convert(Nanosecond, t) + Nanosecond(1) +function stop(x::NamedTuple) + istimespan(x) || throw(ArgumentError("input is not a valid timespan")) + return Nanosecond(x.stop) +end + ##### ##### generic utilities ##### @@ -388,4 +403,11 @@ function invert_spans(spans, parent_span) return gaps end +# Support arrow serialization of timespans + +const TIME_SPAN_ARROW_NAME = Symbol("JuliaLang.TimeSpan") + +ArrowTypes.arrowname(::Type{TimeSpan}) = TIME_SPAN_ARROW_NAME +ArrowTypes.JuliaType(::Val{TIME_SPAN_ARROW_NAME}) = TimeSpan + end # module diff --git a/test/runtests.jl b/test/runtests.jl index 199835a..4c354c7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -using Test, TimeSpans, Dates +using Test, TimeSpans, Dates, Arrow, Tables using TimeSpans: contains, nanoseconds_per_sample using Statistics @@ -221,3 +221,45 @@ end @test length(i_spans) == 6 @test all(duration.(i_spans) .== Second(8)) end + +ntspan(a, b) = (; start=Nanosecond(a), other=1.0, stop=Nanosecond(b)) +@testset "support named tuples" begin + @test index_from_time(100, (;start=Second(3), stop=Second(6))) == 301:600 + @test_throws ArgumentError stop((;stop = 0)) + @test_throws ArgumentError start((;start = 0)) + @test !istimespan((;stop=0, start=1)) + @test !istimespan((;stop=Nanosecond(0))) + + for t in [(; start=Nanosecond(1), stop=Nanosecond(2)), + (; stop=Second(1), start=Second(0))] + @test istimespan(t) + @test start(t) == Nanosecond(t.start) + @test stop(t) == Nanosecond(t.stop) + @test contains(t, t) + @test overlaps(t, t) + @test shortest_timespan_containing([t]) == TimeSpan(t) + @test duration(t) == Nanosecond(t.stop - t.start) + by = Second(rand(1:10)) + @test translate(t, by) === TimeSpan(start(t) + Nanosecond(by), + stop(t) + Nanosecond(by)) + end + + spans = [ntspan(0, 10), ntspan(6, 12), ntspan(15, 20), + ntspan(21, 30), ntspan(29, 31)] + # NOTE: this could be fixed by defining + # Base.convert(::Type{<:NamedTuple}, x::TimeSpan) = (; start=start(x), stop=stop(x)) + @test_broken merge_spans!(overlaps, spans) == [ntspan(0, 12), ntspan(15, 20), ntspan(21, 31)] + + spans = [ntspan(Second(x), Second(x + 1)) for x in 0:10:59] + parent_span = ntspan(Second(0), Second(60)) + i_spans = invert_spans(spans, parent_span) + @test length(i_spans) == 6 +end + +@testset "Arrow serialization" begin + cols = (; span=TimeSpan(1, 2)) + io = Arrow.tobuffer([cols]) + spancol = first(Tables.columns(Arrow.Table(io))) + @test spancol isa AbstractVector{<:TimeSpan} + @test spancol == [cols.span] +end