From 2e23f489ade0ed8394c5f094ff77c516c243f8cf Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 28 Dec 2025 21:54:50 +0530 Subject: [PATCH 1/7] Added docstrings for all exported methods! --- src/TimeSeries.jl | 42 +++++++++++ src/apply.jl | 176 +++++++++++++++++++++++++++++++++++++++++-- src/deprecated.jl | 8 ++ src/readwrite.jl | 44 +++++++++++ src/retime.jl | 187 +++++++++++++++++++++++++++++++++++++++++++++- src/split.jl | 75 +++++++++++++++++++ src/timearray.jl | 96 ++++++++++++++++++++++++ 7 files changed, 621 insertions(+), 7 deletions(-) diff --git a/src/TimeSeries.jl b/src/TimeSeries.jl index 254cd04d..62b5bcf9 100644 --- a/src/TimeSeries.jl +++ b/src/TimeSeries.jl @@ -1,3 +1,45 @@ +""" + TimeSeries + +A Julia package for working with time series data. + +The `TimeSeries` package provides the `TimeArray` type and associated methods for handling +time-indexed data. It supports operations like filtering, transforming, combining, and +resampling time series data. + +# Main Types + + - `TimeArray`: The primary time series container + - `AbstractTimeSeries`: Abstract supertype for time series types + +# Key Functions + + - Indexing and filtering: `when`, `from`, `to`, `findwhen`, `head`, `tail` + - Transformations: `lag`, `lead`, `diff`, `percentchange`, `moving`, `upto` + - Combining: `merge`, `collapse`, `vcat`, `hcat` + - Resampling: `retime` with various interpolation and aggregation methods + - I/O: `readtimearray`, `writetimearray` + - Utilities: `timestamp`, `values`, `colnames`, `meta` + +# Examples + +```julia +using TimeSeries, Dates + +# Create a TimeArray +dates = Date(2020,1,1):Day(1):Date(2020,1,10) +ta = TimeArray(dates, rand(10), [:Value]) + +# Filter by date range +ta_subset = from(ta, Date(2020,1,5)) + +# Calculate moving average +ta_ma = moving(mean, ta, 3) + +# Resample to weekly +ta_weekly = retime(ta, Week(1)) +``` +""" module TimeSeries # stdlib diff --git a/src/apply.jl b/src/apply.jl index 2acffa52..28bc1b10 100644 --- a/src/apply.jl +++ b/src/apply.jl @@ -1,11 +1,46 @@ import Base: +, - import Base.diff +""" + +(ta::TimeArray) + +Unary plus operator for `TimeArray`. + +Returns a broadcasted copy with the unary plus operator applied elementwise. +""" (+)(ta::TimeArray) = .+ta + +""" + -(ta::TimeArray) + +Unary minus operator for `TimeArray`. + +Returns a broadcasted copy with elementwise negation applied to all values. +""" (-)(ta::TimeArray) = .-ta ###### lag, lead ################ +""" + lag(ta::TimeArray, n::Int=1; padding::Bool=false) + +Shift values backward by `n` periods, removing the first `n` rows. + +# Arguments + + - `n::Int`: Number of periods to lag (default: 1) + - `padding::Bool`: If `true`, pad with `NaN` to maintain length (default: `false`) + +Note: The deprecated `period` keyword argument is also accepted but should not be used. + +# Examples + +```julia +lag(ta) # Lag by 1 period +lag(ta, 5) # Lag by 5 periods +lag(ta, padding=true) # Lag with NaN padding +``` +""" function lag(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) where {T,N} if period != 0 @warn("the period kwarg is deprecated, use lag(ta::TimeArray, period::Int) instead") @@ -26,6 +61,26 @@ function lag(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) w return ta end # lag +""" + lead(ta::TimeArray, n::Int=1; padding::Bool=false) + +Shift values forward by `n` periods, removing the last `n` rows. + +# Arguments + + - `n::Int`: Number of periods to lead (default: 1) + - `padding::Bool`: If `true`, pad with `NaN` to maintain length (default: `false`) + +Note: The deprecated `period` keyword argument is also accepted but should not be used. + +# Examples + +```julia +lead(ta) # Lead by 1 period +lead(ta, 5) # Lead by 5 periods +lead(ta, padding=true) # Lead with NaN padding +``` +""" function lead(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) where {T,N} if period != 0 @warn( @@ -49,6 +104,27 @@ end # lead ###### diff ##################### +""" + diff(ta::TimeArray, n::Int=1; padding::Bool=false, differences::Int=1) + +Calculate the `n`-period difference of a `TimeArray`. + +This is a `TimeArray` specialization of `Base.diff`. + +# Arguments + + - `n::Int`: Number of periods for differencing (default: 1) + - `padding::Bool`: If `true`, pad with `NaN` to maintain length (default: `false`) + - `differences::Int`: Number of times to apply differencing (default: 1) + +# Examples + +```julia +diff(ta) # First difference +diff(ta, 2) # 2-period difference +diff(ta, differences=2) # Second-order difference +``` +""" function diff(ta::TimeArray, n::Int=1; padding::Bool=false, differences::Int=1) cols = colnames(ta) for d in 1:differences @@ -60,6 +136,24 @@ end # diff ###### percentchange ############ +""" + percentchange(ta::TimeArray, returns::Symbol=:simple; padding::Bool=false) + +Calculate percentage change between consecutive periods. + +# Arguments + + - `returns::Symbol`: Type of returns calculation - `:simple` or `:log` (default: `:simple`) + - `padding::Bool`: If `true`, pad with `NaN` to maintain length (default: `false`) + +# Examples + +```julia +percentchange(ta) # Simple returns +percentchange(ta, :log) # Log returns +percentchange(ta, padding=true) # With NaN padding +``` +""" function percentchange( ta::TimeArray, returns::Symbol=:simple; padding::Bool=false, method::AbstractString="" ) @@ -108,15 +202,26 @@ end """ moving(f, ta::TimeArray{T,2}, w::Integer; padding = false, dims = 1, colnames = [...]) -## Example +Apply user-defined function `f` to a 2D `TimeArray` with window size `w`. -In case of `dims = 2`, the user-defined function `f` will get a 2D `Array` as input. +# Arguments + + - `f`: Function to apply to each window + - `ta::TimeArray{T,2}`: 2D time array + - `w::Integer`: Window size + - `padding::Bool`: If `true`, pad with `NaN` to maintain length (default: `false`) + - `dims::Integer`: Dimension to apply function - `1` for column-wise, `2` for row-wise (default: 1) + - `colnames::Vector{Symbol}`: Column names for result (default: original column names) + +# Examples ```julia -moving(ohlc, 10; dims=2, colnames=[:A, ...]) do - # given that `ohlc` is a 500x4 `TimeArray`, - # size(A) is (10, 4) - ... +moving(mean, ta, 10) # 10-period moving average + +# For dims=2, function receives a 2D window +moving(ta, 10; dims=2) do window + # window is a 10×ncol matrix + sum(window) end ``` """ @@ -158,6 +263,18 @@ end ###### upto ##################### +""" + upto(f, ta::TimeArray) + +Apply function `f` cumulatively from the start up to each row. + +# Examples + +```julia +upto(sum, ta) # Cumulative sum +upto(mean, ta) # Expanding mean +``` +""" function upto(f, ta::TimeArray{T,1}) where {T} vals = zero(values(ta)) for i in 1:length(vals) @@ -176,12 +293,36 @@ end ###### basecall ################# +""" + basecall(ta::TimeArray, f::Function; cnames=colnames(ta)) + +Apply a function `f` to the values array and return a new `TimeArray`. + +# Arguments + + - `f::Function`: Function to apply to the values array + - `cnames`: Column names for the result (default: original column names) + +# Examples + +```julia +basecall(ta, transpose) # Transpose the values +basecall(ta, sort; cnames=[:Sorted]) # Sort values +``` +""" function basecall(ta::TimeArray, f::Function; cnames=colnames(ta)) return TimeArray(timestamp(ta), f(values(ta)), cnames, meta(ta)) end ###### uniform observations ##### +""" + uniformspaced(ta::TimeArray) + +Check if timestamps in a `TimeArray` are uniformly spaced. + +Returns `true` if all consecutive timestamp differences are equal, `false` otherwise. +""" function uniformspaced(ta::TimeArray) gap1 = timestamp(ta)[2] - timestamp(ta)[1] i, n, is_uniform = 2, length(ta), true @@ -192,6 +333,13 @@ function uniformspaced(ta::TimeArray) return is_uniform end # uniformspaced +""" + uniformspace(ta::TimeArray) + +Convert a `TimeArray` to uniform spacing using the minimum gap between timestamps. + +Missing timestamps are filled with rows containing zeros. +""" function uniformspace(ta::TimeArray{T,N}) where {T,N} min_gap = minimum(timestamp(ta)[2:end] - timestamp(ta)[1:(end - 1)]) newtimestamp = timestamp(ta)[1]:min_gap:timestamp(ta)[end] @@ -205,6 +353,22 @@ end # uniformspace ###### dropnan #################### +""" + dropnan(ta::TimeArray, method::Symbol=:all) + +Remove rows containing `NaN` values from a `TimeArray`. + +# Arguments + + - `method::Symbol`: Removal strategy - `:all` removes rows where all values are `NaN`, `:any` removes rows with any `NaN` (default: `:all`) + +# Examples + +```julia +dropnan(ta) # Remove rows where all values are NaN +dropnan(ta, :any) # Remove rows with any NaN +``` +""" function dropnan(ta::TimeArray, method::Symbol=:all) return if method == :all ta[findall(reshape(values(any(.!isnan.(ta); dims=2)), :))] diff --git a/src/deprecated.jl b/src/deprecated.jl index bb680615..95f553ca 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -31,6 +31,14 @@ # 0.20.0 ############################################################################### +""" + update(ta::TimeArray, tstamp, val) + +!!! warning "Deprecated" + This function is deprecated. Use `vcat(ta, TimeArray(tstamp, val))` instead. +""" +function update end + export update @deprecate( diff --git a/src/readwrite.jl b/src/readwrite.jl index 4e1a7a94..cafbce11 100644 --- a/src/readwrite.jl +++ b/src/readwrite.jl @@ -1,5 +1,29 @@ ###### readtimearray ############ +""" + readtimearray(source; delim=',', meta=nothing, format="", header=true) + +Read a delimited file into a `TimeArray`. + +The first column is treated as timestamps. Empty or non-numeric cells in data columns +are converted to `NaN`. Date vs DateTime is inferred from timestamp length (< 11 characters +assumes Date, otherwise DateTime). + +# Arguments + + - `source`: File path or IO stream + - `delim::Char`: Delimiter character (default: `','`) + - `meta`: Metadata to attach to the `TimeArray` (default: `nothing`) + - `format::AbstractString`: Date format string (default: `""` for automatic detection) + - `header::Bool`: Whether the file has a header row (default: `true`) + +# Examples + +```julia +readtimearray("data.csv") +readtimearray("data.csv"; delim='\t', format="yyyy-mm-dd") +``` +""" function readtimearray( source; delim::Char=',', meta=nothing, format::AbstractString="", header::Bool=true ) @@ -44,6 +68,26 @@ function insertNaN(aa::Array{Any,N}) where {N} return convert(Array{Float64,N}, aa) end +""" + writetimearray(ta::TimeArray, fname::AbstractString; delim=',', format="", header=true) + +Write a `TimeArray` to a delimited file. + +# Arguments + + - `ta::TimeArray`: Time array to write + - `fname::AbstractString`: Output file path + - `delim::Char`: Delimiter character (default: `','`) + - `format::AbstractString`: Date format string (default: `""` for default formatting) + - `header::Bool`: Whether to write a header row (default: `true`) + +# Examples + +```julia +writetimearray(ta, "output.csv") +writetimearray(ta, "output.tsv"; delim='\t', format="yyyy-mm-dd") +``` +""" function writetimearray( ta::TimeArray, fname::AbstractString; diff --git a/src/retime.jl b/src/retime.jl index 1d0a972f..8844a2ec 100644 --- a/src/retime.jl +++ b/src/retime.jl @@ -1,33 +1,188 @@ # Abstract types for interpolation, aggregation, and extrapolation methods +""" + InterpolationMethod + +Abstract supertype for interpolation strategies used by `retime`. +""" abstract type InterpolationMethod end + +""" + AggregationMethod + +Abstract supertype for aggregation strategies used by `retime`. +""" abstract type AggregationMethod end + +""" + ExtrapolationMethod + +Abstract supertype for extrapolation strategies used by `retime`. +""" abstract type ExtrapolationMethod end # Interpolation methods +""" + Linear <: InterpolationMethod + +Linear interpolation method for `retime`. + +Interpolates values linearly between known points. +""" struct Linear <: InterpolationMethod end + +""" + Previous <: InterpolationMethod + +Previous value interpolation method for `retime`. + +Uses the most recent previous value for interpolation. +""" struct Previous <: InterpolationMethod end + +""" + Next <: InterpolationMethod + +Next value interpolation method for `retime`. + +Uses the next available value for interpolation. +""" struct Next <: InterpolationMethod end + +""" + Nearest <: InterpolationMethod + +Nearest value interpolation method for `retime`. + +Uses the nearest value in time for interpolation. +""" struct Nearest <: InterpolationMethod end # Aggregation methods +""" + Mean <: AggregationMethod + +Mean aggregation method for `retime`. + +Computes the mean of values in each time period. +""" struct Mean <: AggregationMethod end + +""" + Min <: AggregationMethod + +Minimum aggregation method for `retime`. + +Computes the minimum of values in each time period. +""" struct Min <: AggregationMethod end + +""" + Max <: AggregationMethod + +Maximum aggregation method for `retime`. + +Computes the maximum of values in each time period. +""" struct Max <: AggregationMethod end + +""" + Count <: AggregationMethod + +Count aggregation method for `retime`. + +Counts non-missing values in each time period. +""" struct Count <: AggregationMethod end + +""" + Sum <: AggregationMethod + +Sum aggregation method for `retime`. + +Computes the sum of values in each time period. +""" struct Sum <: AggregationMethod end + +""" + Median <: AggregationMethod + +Median aggregation method for `retime`. + +Computes the median of values in each time period. +""" struct Median <: AggregationMethod end + +""" + First <: AggregationMethod + +First value aggregation method for `retime`. + +Uses the first value in each time period. +""" struct First <: AggregationMethod end + +""" + Last <: AggregationMethod + +Last value aggregation method for `retime`. + +Uses the last value in each time period. +""" struct Last <: AggregationMethod end + +""" + AggregationFunction{F<:Function} <: AggregationMethod + +Wraps a user-provided function as an `AggregationMethod` for `retime`. +""" struct AggregationFunction{F<:Function} <: AggregationMethod func::F end # Extrapolation methods +""" + FillConstant{V} <: ExtrapolationMethod + +Constant fill extrapolation method for `retime`. + +Fills extrapolated values with a constant value. + +# Examples + +```julia +FillConstant(0.0) # Fill with zeros +FillConstant(NaN) # Fill with NaN +``` +""" struct FillConstant{V} <: ExtrapolationMethod value::V end + +""" + NearestExtrapolate <: ExtrapolationMethod + +Nearest value extrapolation method for `retime`. + +Uses the nearest edge value for extrapolation. +""" struct NearestExtrapolate <: ExtrapolationMethod end + +""" + MissingExtrapolate <: ExtrapolationMethod + +Missing value extrapolation method for `retime`. + +Fills extrapolated values with `missing`. +""" struct MissingExtrapolate <: ExtrapolationMethod end + +""" + NaNExtrapolate <: ExtrapolationMethod + +NaN extrapolation method for `retime`. + +Fills extrapolated values with `NaN`. +""" struct NaNExtrapolate <: ExtrapolationMethod end _toInterpolationMethod(x::Symbol) = _toInterpolationMethod(Val(x)) @@ -46,7 +201,7 @@ _toAggregationMethod(::Val{:sum}) = Sum() _toAggregationMethod(::Val{:median}) = Median() _toAggregationMethod(::Val{:first}) = First() _toAggregationMethod(::Val{:last}) = Last() -_toAggregationMethof(f::Function) = AggregationFunction(f) +_toAggregationMethod(f::Function) = AggregationFunction(f) _toAggregationMethod(x::AggregationMethod) = x _toExtrapolationMethod(x::Symbol) = _toExtrapolationMethod(Val(x)) @@ -56,6 +211,36 @@ _toExtrapolationMethod(::Val{:missing}) = MissingExtrapolate() _toExtrapolationMethod(::Val{:nan}) = NaNExtrapolate() _toExtrapolationMethod(x::ExtrapolationMethod) = x +""" + retime(ta::TimeArray, new_timestamps; upsample=Previous(), downsample=Mean(), extrapolate=NearestExtrapolate(), skip_missing=true) + retime(ta::TimeArray, period::Dates.Period; kwargs...) + retime(ta::TimeArray, period::Function; kwargs...) + +Resample a `TimeArray` to new timestamps with specified interpolation, aggregation, and extrapolation methods. + +# Arguments + + - `ta::TimeArray`: Time array to resample + - `new_timestamps`: Vector of new timestamps, a `Dates.Period`, or a period function + - `upsample`: Interpolation method for upsampling (default: `Previous()`) + - `downsample`: Aggregation method for downsampling (default: `Mean()`) + - `extrapolate`: Extrapolation method for out-of-range timestamps (default: `NearestExtrapolate()`) + - `skip_missing`: Skip missing values in calculations (default: `true`) + +# Examples + +```julia +# Resample to hourly using linear interpolation +retime(ta, Hour(1); upsample=Linear()) + +# Resample to daily using last value and sum aggregation +retime(ta, Day(1); upsample=Previous(), downsample=Sum()) + +# Resample to specific timestamps +new_times = [DateTime(2020,1,1), DateTime(2020,1,2)] +retime(ta, new_times) +``` +""" function retime(ta, new_dt::Dates.Period; kwargs...) new_timestamps = floor(timestamp(ta)[1], new_dt):new_dt:floor(timestamp(ta)[end], new_dt) diff --git a/src/split.jl b/src/split.jl index a8d3eb17..e5cb1be9 100644 --- a/src/split.jl +++ b/src/split.jl @@ -1,5 +1,23 @@ # when ############################ +""" + when(ta::TimeArray, period::Function, t) + +Filter a `TimeArray` to rows where the `period` function applied to timestamps equals `t`. + +# Arguments + + - `ta::TimeArray`: The time array to filter + - `period::Function`: A function that extracts a period component (e.g., `Dates.hour`, `Dates.dayofweek`) + - `t`: The target value to match (Integer or String) + +# Examples + +```julia +when(ta, Dates.hour, 12) # Get all rows at noon +when(ta, Dates.dayofweek, 1) # Get all Monday rows +``` +""" function when(ta::TimeArray, period::Function, t::Integer) return ta[findall(period.(timestamp(ta)) .== t)] end @@ -7,6 +25,17 @@ when(ta::TimeArray, period::Function, t::String) = ta[findall(period.(timestamp( # from, to ###################### +""" + from(ta::TimeArray, d::TimeType) + +Select all rows from a `TimeArray` starting from date `d` onwards. + +# Examples + +```julia +from(ta, Date(2020, 1, 1)) # All rows from 2020-01-01 onwards +``` +""" function from(ta::TimeArray{T,N,D}, d::D) where {T,N,D} return if length(ta) == 0 ta @@ -19,6 +48,17 @@ function from(ta::TimeArray{T,N,D}, d::D) where {T,N,D} end end +""" + to(ta::TimeArray, d::TimeType) + +Select all rows from a `TimeArray` up to and including date `d`. + +# Examples + +```julia +to(ta, Date(2020, 12, 31)) # All rows up to 2020-12-31 +``` +""" function to(ta::TimeArray{T,N,D}, d::D) where {T,N,D} return if length(ta) == 0 ta @@ -42,10 +82,33 @@ end ###### findwhen ################# +""" + findwhen(ta::TimeArray{Bool,1}) + +Return timestamps where the boolean `TimeArray` has `true` values. + +# Examples + +```julia +findwhen(ta .> 100) # Get timestamps where values exceed 100 +``` +""" findwhen(ta::TimeArray{Bool,1}) = timestamp(ta)[findall(values(ta))] ###### head, tail ########### +""" + head(ta::TimeArray, n::Int=6) + +Return the first `n` rows of a `TimeArray`. + +# Examples + +```julia +head(ta) # First 6 rows +head(ta, 10) # First 10 rows +``` +""" @generated function head(ta::TimeArray{T,N}, n::Int=6) where {T,N} new_values = (N == 1) ? :(values(ta)[1:n]) : :(values(ta)[1:n, :]) @@ -55,6 +118,18 @@ findwhen(ta::TimeArray{Bool,1}) = timestamp(ta)[findall(values(ta))] end end +""" + tail(ta::TimeArray, n::Int=6) + +Return the last `n` rows of a `TimeArray`. + +# Examples + +```julia +tail(ta) # Last 6 rows +tail(ta, 10) # Last 10 rows +``` +""" @generated function tail(ta::TimeArray{T,N}, n::Int=6) where {T,N} new_values = (N == 1) ? :(values(ta)[start:end]) : :(values(ta)[start:end, :]) diff --git a/src/timearray.jl b/src/timearray.jl index 351b44ca..080a14bd 100644 --- a/src/timearray.jl +++ b/src/timearray.jl @@ -20,6 +20,16 @@ import Base: propertynames, values +""" + AbstractTimeSeries{T,N,D} + +Abstract supertype for time series types. + +Type parameters: + - `T`: Element type of the values + - `N`: Number of dimensions (1 or 2) + - `D`: TimeType subtype for timestamps (Date or DateTime) +""" abstract type AbstractTimeSeries{T,N,D} end """ @@ -135,6 +145,11 @@ end ###### conversion ############### +""" + convert(::Type{TimeArray{Float64,N}}, x::TimeArray{Bool,N}) + +Convert a `TimeArray` with `Bool` values to one with `Float64` values. +""" function convert(::Type{TimeArray{Float64,N}}, x::TimeArray{Bool,N}) where {N} return TimeArray( timestamp(x), Float64.(values(x)), colnames(x), meta(x); unchecked=true @@ -147,25 +162,52 @@ end ###### copy ############### +""" + copy(ta::TimeArray) + +Create a shallow copy of a `TimeArray`. Returns a new `TimeArray` with the same data. +""" function copy(ta::TimeArray) return TimeArray(timestamp(ta), values(ta), colnames(ta), meta(ta); unchecked=true) end ###### length ################### +""" + length(ta::AbstractTimeSeries) + +Return the number of timestamps (rows) in a `TimeArray`. +""" length(ata::AbstractTimeSeries) = length(timestamp(ata)) ###### size ##################### +""" + size(ta::TimeArray) + size(ta::TimeArray, dim) + +Return the dimensions of the `TimeArray` values as `(nrows, ncols)`, or the size along dimension `dim`. +""" size(ta::TimeArray) = size(values(ta)) size(ta::TimeArray, dim) = size(values(ta), dim) ###### ndims ##################### +""" + ndims(ta::AbstractTimeSeries) + +Return the number of dimensions of the `TimeArray` (1 or 2). +""" ndims(ta::AbstractTimeSeries{T,N}) where {T,N} = N ###### iteration protocol ######## +""" + iterate(ta::AbstractTimeSeries[, state]) + +Iterate over a `TimeArray`, yielding `(timestamp, value)` tuples for each row. +For 1D arrays, `value` is a scalar; for 2D arrays, `value` is a vector. +""" @generated function iterate(ta::AbstractTimeSeries{T,N}, i=1) where {T,N} val = (N == 1) ? :(values(ta)[i]) : :(values(ta)[i, :]) @@ -204,11 +246,22 @@ function ==(x::TimeArray{T,N}, y::TimeArray{S,N}) where {T,S,N} return all(f -> getfield(x, f) == getfield(y, f), fieldnames(TimeArray)) end +""" + isequal(x::TimeArray, y::TimeArray) + +Test whether two `TimeArray`s are equal in all fields, including handling of `missing` values. +Similar to `==` but uses `isequal` semantics for comparisons. +""" isequal(x::TimeArray{T,N}, y::TimeArray{S,M}) where {T,S,N,M} = false function isequal(x::TimeArray{T,N}, y::TimeArray{S,N}) where {T,S,N} return all(f -> isequal(getfield(x, f), getfield(y, f)), fieldnames(TimeArray)) end +""" + hash(ta::TimeArray, h::UInt) + +Compute a hash value for a `TimeArray` for use in hash-based collections like `Dict` and `Set`. +""" # support for Dict hash(x::TimeArray, h::UInt) = sum(f -> hash(getfield(x, f), h), fieldnames(TimeArray)) @@ -218,6 +271,12 @@ Base.eltype(::AbstractTimeSeries{T,1,D}) where {T,D} = Tuple{D,T} Base.eltype(::AbstractTimeSeries{T,2,D}) where {T,D} = Tuple{D,Vector{T}} ###### show ##################### + +""" + show(io::IO, ta::TimeArray) + +Display a compact summary of a `TimeArray` showing its dimensions, type, and time range. +""" Base.summary(io::IO, ta::TimeArray) = show(io, ta) function Base.show(io::IO, ta::TimeArray) @@ -313,6 +372,21 @@ end # the getindex function should return a new TimeArray, and copy data from # the source, includes `timestamp`, `values` and `colnames`. +""" + getindex(ta::TimeArray, ...) + +Index into a `TimeArray` by rows, columns, timestamps, or combinations thereof. + +Supports indexing by: +- Integer indices for rows +- Ranges and vectors of integers for multiple rows +- Column names (`:colname` or `[:col1, :col2]`) +- Timestamps (`Date` or `DateTime` values) +- Boolean `TimeArray` for filtering +- Combinations like `ta[rows, cols]` + +Returns a new `TimeArray` preserving metadata. +""" getindex(ta::TimeArray) = throw(BoundsError(typeof(ta), [])) # single row @@ -397,6 +471,12 @@ end # getindex{T,N}(ta::TimeArray{T,N}, d::DAYOFWEEK) = ta[dayofweek(timestamp(ta)) .== d] # Define end keyword +""" + lastindex(ta::TimeArray[, dim]) + +Return the last valid index for a `TimeArray`. For dimension 1 (rows), returns the number of timestamps. +For dimension 2 (columns), returns the number of columns. +""" function lastindex(ta::TimeArray, d::Integer=1) return if (d == 1) length(timestamp(ta)) @@ -407,12 +487,28 @@ function lastindex(ta::TimeArray, d::Integer=1) end end +""" + eachindex(ta::TimeArray) + +Return an iterator over the valid row indices of a `TimeArray`. +""" eachindex(ta::TimeArray) = Base.OneTo(length(timestamp(ta))) ###### getproperty/propertynames ################# +""" + getproperty(ta::AbstractTimeSeries, col::Symbol) + +Access a column of a `TimeArray` using dot notation (e.g., `ta.colname`). +Returns a new `TimeArray` containing only the specified column. +""" getproperty(ta::AbstractTimeSeries, c::Symbol) = ta[c] +""" + propertynames(ta::TimeArray) + +Return the column names of a `TimeArray` for use with tab completion and dot notation. +""" propertynames(ta::TimeArray) = colnames(ta) ###### element wrappers ########### From 539c0e36bc50c6dc9dbd2597f01cbb3724e79687 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 00:09:00 +0530 Subject: [PATCH 2/7] Resolved formatting issues --- src/basemisc.jl | 10 +++----- src/broadcast.jl | 2 +- src/combine.jl | 6 +++-- src/modify.jl | 15 +++++++----- src/split.jl | 3 ++- src/timearray.jl | 64 ++++++++++++++++++++++++++---------------------- 6 files changed, 55 insertions(+), 45 deletions(-) diff --git a/src/basemisc.jl b/src/basemisc.jl index 595199e9..18fa2029 100644 --- a/src/basemisc.jl +++ b/src/basemisc.jl @@ -37,12 +37,10 @@ macro _mapbase(sig::Expr, imp::Expr) doc = " $sig" fdef = Expr(:function, sig, fbody) - return esc( - quote - $doc - $fdef - end, - ) + return esc(quote + $doc + $fdef + end) end # Cumulative functions diff --git a/src/broadcast.jl b/src/broadcast.jl index 0a7bd59e..2152aec5 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -12,7 +12,7 @@ Base.BroadcastStyle(::Type{<:TimeArray{T,N}}) where {T,N} = TimeArrayStyle{N}() Base.broadcastable(x::AbstractTimeSeries) = x Base.Broadcast.instantiate(bc::Broadcasted{<:TimeArrayStyle}) = -# skip the default axes checking + # skip the default axes checking Broadcast.flatten(bc) function Base.copy(bc′::Broadcasted{<:TimeArrayStyle}) diff --git a/src/combine.jl b/src/combine.jl index beec8692..04a6711e 100644 --- a/src/combine.jl +++ b/src/combine.jl @@ -206,9 +206,11 @@ collapse(ta, month, last) collapse(ta, month, last, mean) ``` """ -collapse( +function collapse( ta::TimeArray, period::Period, timestamp::Function, value::Function=timestamp; kw... -) = collapse(ta, x -> floor(x, period), timestamp, value; kw...) +) + collapse(ta, x -> floor(x, period), timestamp, value; kw...) +end # vcat ###################### diff --git a/src/modify.jl b/src/modify.jl index 6088adcb..3d0585f4 100644 --- a/src/modify.jl +++ b/src/modify.jl @@ -49,12 +49,15 @@ for f in [:rename, :rename!] @eval begin $f(ta::TimeArray, colnames::Symbol) = $f(ta, [colnames]) $f(ta::TimeArray) = throw(MethodError($f, (ta,))) - $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = - $f(ta, _mapcol(colnames(ta), pairs)) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = - $f(ta, map(f, colnames(ta))) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = - $f(Symbol ∘ f ∘ string, ta) + $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = $f( + ta, _mapcol(colnames(ta), pairs) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = $f( + ta, map(f, colnames(ta)) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = $f( + Symbol ∘ f ∘ string, ta + ) end end diff --git a/src/split.jl b/src/split.jl index e5cb1be9..b02138ef 100644 --- a/src/split.jl +++ b/src/split.jl @@ -156,8 +156,9 @@ Split `data` by `period` function, returns a vector of `TimeSeries.TimeArray`. - `data::TimeSeries.TimeArray`: Data to split - `period::Function`: Function, e.g. `Dates.day` that is used to split the `data`. """ -Base.split(data::TimeSeries.TimeArray, period::Function) = +function Base.split(data::TimeSeries.TimeArray, period::Function) Iterators.map(i -> data[i], _split(TimeSeries.timestamp(data), period)) +end function _split(ts::AbstractVector{D}, period::Function) where {D<:TimeType} m = length(ts) diff --git a/src/timearray.jl b/src/timearray.jl index 080a14bd..784c9e6f 100644 --- a/src/timearray.jl +++ b/src/timearray.jl @@ -296,8 +296,8 @@ end io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -337,8 +337,8 @@ else io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -360,8 +360,8 @@ else row_label_column_alignment=:r, column_label_alignment=:l, continuation_row_alignment=:c, - fit_table_in_display_horizontally=!allcols, # replaced crop keyword for v3 of PrettyTables - fit_table_in_display_vertically=!allrows, # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_horizontally=(!allcols), # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_vertically=(!allrows), # replaced crop keyword for v3 of PrettyTables vertical_crop_mode=:middle, ) end @@ -391,28 +391,35 @@ getindex(ta::TimeArray) = throw(BoundsError(typeof(ta), [])) # single row @propagate_inbounds getindex(ta::TimeArray, n::Integer) = -# avoid conversion to column vector - TimeArray(timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta)) + # avoid conversion to column vector + TimeArray( + timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta) + ) # single row 1d -@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = - TimeArray(timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = TimeArray( + timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta) +) # range of rows -@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = - TimeArray(timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = TimeArray( + timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta) +) # range of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = - TimeArray(timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = TimeArray( + timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta) +) # array of rows -@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = - TimeArray(timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = TimeArray( + timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta) +) # array of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = - TimeArray(timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = TimeArray( + timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta) +) # single column by name @propagate_inbounds function getindex(ta::TimeArray, s::Symbol) @@ -422,15 +429,12 @@ end # array of columns by name @propagate_inbounds getindex(ta::TimeArray, ss::Symbol...) = getindex(ta, collect(ss)) -@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = - TimeArray(ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss) +@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = TimeArray( + ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss +) # ta[rows, cols] -@propagate_inbounds getindex( - ta::TimeArray, - rows::Union{AbstractVector{<:Integer},Colon}, - cols::AbstractVector{Symbol}, -) = TimeArray( +@propagate_inbounds getindex(ta::TimeArray, rows::Union{AbstractVector{<:Integer},Colon}, cols::AbstractVector{Symbol}) = TimeArray( ta; timestamp=timestamp(ta)[rows], values=values(ta)[rows, map(s -> findcol(ta, s), cols)], @@ -445,8 +449,9 @@ end @propagate_inbounds getindex(ta::TimeArray, rows, col::Symbol) = getindex(ta, rows, [col]) # ta[n, col] -@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = - getindex(ta, [n], [col]) +@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = getindex( + ta, [n], [col] +) # single date @propagate_inbounds function getindex(ta::TimeArray{T,N,D}, d::D) where {T,N,D} @@ -462,8 +467,9 @@ end end # StepRange{Date,...} -@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = - ta[collect(r)] +@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = ta[collect( + r +)] @propagate_inbounds getindex(ta::TimeArray, k::TimeArray{Bool,1}) = ta[findwhen(k)] From 7d9d4c4e7f53034c92c5cbdf626a80cda64fd777 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 00:13:28 +0530 Subject: [PATCH 3/7] Apply JuliaFormatter formatting fixes --- src/broadcast.jl | 4 ++-- src/timearray.jl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/broadcast.jl b/src/broadcast.jl index 2152aec5..7b597210 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -12,8 +12,8 @@ Base.BroadcastStyle(::Type{<:TimeArray{T,N}}) where {T,N} = TimeArrayStyle{N}() Base.broadcastable(x::AbstractTimeSeries) = x Base.Broadcast.instantiate(bc::Broadcasted{<:TimeArrayStyle}) = - # skip the default axes checking - Broadcast.flatten(bc) +# skip the default axes checking +Broadcast.flatten(bc) function Base.copy(bc′::Broadcasted{<:TimeArrayStyle}) tas = find_ta(bc′) diff --git a/src/timearray.jl b/src/timearray.jl index 784c9e6f..63df516f 100644 --- a/src/timearray.jl +++ b/src/timearray.jl @@ -391,10 +391,10 @@ getindex(ta::TimeArray) = throw(BoundsError(typeof(ta), [])) # single row @propagate_inbounds getindex(ta::TimeArray, n::Integer) = - # avoid conversion to column vector - TimeArray( - timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta) - ) +# avoid conversion to column vector +TimeArray( + timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta) +) # single row 1d @propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = TimeArray( From 531745d8c324c7d4d18756e8a9ad33341f602ccc Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 09:40:15 +0530 Subject: [PATCH 4/7] Minor error fixes! --- src/retime.jl | 5 +++++ test/retime.jl | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/retime.jl b/src/retime.jl index 8844a2ec..5e607c94 100644 --- a/src/retime.jl +++ b/src/retime.jl @@ -306,6 +306,11 @@ function _retime!( downsample::AggregationMethod, extrapolate::ExtrapolationMethod, ) where {D,AN,A} + # Handle empty input arrays gracefully + if isempty(old_timestamps) || isempty(old_values) + return nothing + end + x = Dates.value.(old_timestamps) x_min, x_max = x[1], x[end] # assume that the timestamps are sorted x_new = Dates.value.(new_timestamps) diff --git a/test/retime.jl b/test/retime.jl index 1914a120..ad1b3b5c 100644 --- a/test/retime.jl +++ b/test/retime.jl @@ -75,6 +75,14 @@ using Statistics idx = new_timestamps[2] .<= timestamp(cl) .< new_timestamps[3] @test func(values(cl[:Close][idx])) == values(cl_new[:Close][2])[1] end + + @testset "empty TimeArray coverage" begin + ta_empty = TimeArray(Date[], Int[], [:val]) + new_times = Date[] + ta_new = retime(ta_empty, new_times) + @test length(ta_new) == 0 + @test isa(ta_new, TimeArray) + end end @testset "single column interpolation" begin From e3f492312923a074b4c1dbfa5619e66984016178 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 09:53:04 +0530 Subject: [PATCH 5/7] Formatting issues fixed --- test/retime.jl | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/test/retime.jl b/test/retime.jl index ad1b3b5c..72e07698 100644 --- a/test/retime.jl +++ b/test/retime.jl @@ -76,13 +76,27 @@ using Statistics @test func(values(cl[:Close][idx])) == values(cl_new[:Close][2])[1] end - @testset "empty TimeArray coverage" begin - ta_empty = TimeArray(Date[], Int[], [:val]) - new_times = Date[] - ta_new = retime(ta_empty, new_times) - @test length(ta_new) == 0 - @test isa(ta_new, TimeArray) - end + @testset "empty TimeArray coverage" begin + ta_empty = TimeArray(Date[], Int[], [:val]) + new_times = Date[] + ta_new = retime(ta_empty, new_times) + @test length(ta_new) == 0 + @test isa(ta_new, TimeArray) + end + + @testset "empty input coverage" begin + # Non-empty TimeArray, empty new_timestamps + ta = TimeArray([Date(2020,1,1)], [1], [:val]) + ta_new = retime(ta, Date[]) + @test length(ta_new) == 0 + @test isa(ta_new, TimeArray) + + # Empty TimeArray, non-empty new_timestamps + ta_empty = TimeArray(Date[], Int[], [:val]) + ta_new2 = retime(ta_empty, [Date(2020,1,1)]) + @test length(ta_new2) == 1 + @test isa(ta_new2, TimeArray) + end end @testset "single column interpolation" begin From 00799447fcbfac399f04096b427509a06c29de07 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 10:42:54 +0530 Subject: [PATCH 6/7] Format fix --- test/retime.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/retime.jl b/test/retime.jl index 72e07698..0d8251db 100644 --- a/test/retime.jl +++ b/test/retime.jl @@ -86,14 +86,14 @@ using Statistics @testset "empty input coverage" begin # Non-empty TimeArray, empty new_timestamps - ta = TimeArray([Date(2020,1,1)], [1], [:val]) + ta = TimeArray([Date(2020, 1, 1)], [1], [:val]) ta_new = retime(ta, Date[]) @test length(ta_new) == 0 @test isa(ta_new, TimeArray) # Empty TimeArray, non-empty new_timestamps ta_empty = TimeArray(Date[], Int[], [:val]) - ta_new2 = retime(ta_empty, [Date(2020,1,1)]) + ta_new2 = retime(ta_empty, [Date(2020, 1, 1)]) @test length(ta_new2) == 1 @test isa(ta_new2, TimeArray) end From 453dafd6f5c52db6eafd803ff838d3155211721a Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Dec 2025 11:00:38 +0530 Subject: [PATCH 7/7] Codecov patch fixing --- test/retime.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/retime.jl b/test/retime.jl index 0d8251db..a509110c 100644 --- a/test/retime.jl +++ b/test/retime.jl @@ -96,6 +96,19 @@ using Statistics ta_new2 = retime(ta_empty, [Date(2020, 1, 1)]) @test length(ta_new2) == 1 @test isa(ta_new2, TimeArray) + end + + @testset "internal _retime! coverage" begin + # Directly test _retime! with empty arrays + new_values = zeros(Int, 0) + old_timestamps = Date[] + old_values = Int[] + new_timestamps = Date[] + upsample = TimeSeries.Previous() + downsample = TimeSeries.Mean() + extrapolate = TimeSeries.NearestExtrapolate() + @test TimeSeries._retime!(new_values, old_timestamps, old_values, new_timestamps, upsample, downsample, extrapolate) === nothing + end end end