Skip to content
This repository was archived by the owner on Mar 22, 2025. It is now read-only.

Commit 0c8c50a

Browse files
authored
Merge pull request #14 from tpgillam/tg/drop_trailing
Refactor general case into `push!` and `popfirst!`
2 parents 6c2f0d3 + 247110a commit 0c8c50a

File tree

8 files changed

+81
-48
lines changed

8 files changed

+81
-48
lines changed

.JuliaFormatter.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
style = "blue"

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "AssociativeWindowAggregation"
22
uuid = "444271a7-5434-4a02-b82b-0e30a9223c60"
3-
authors = ["Thomas Gillam <tpgillam@googlemail.com>"]
4-
version = "0.3.2"
3+
authors = ["Tom Gillam <tpgillam@googlemail.com>"]
4+
version = "0.4.0"
55

66
[deps]
77
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"

docs/src/reference/base.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# General window
22

3-
A state to represent a window of aribtrarily variable capacity.
3+
A state to represent a window of arbitrarily variable capacity.
44

5-
Every time a new value is pushed onto the end of the window, we must specify how many values are removed from the front of the window.
5+
We push new values onto the window with [`Base.push!`](@ref).
6+
We can remove old values from the window with [`Base.popfirst!`](@ref).
67

78
```@autodocs
8-
Modules = [AssociativeWindowAggregation]
9+
Modules = [AssociativeWindowAggregation, Base]
910
Pages = ["base.jl"]
1011
```

src/base.jl

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -72,47 +72,54 @@ end
7272
WindowedAssociativeOp{T,Op}() where {T,Op} = WindowedAssociativeOp{T,Op,Op}()
7373

7474
"""
75-
update_state!(
76-
state::WindowedAssociativeOp,
77-
value,
78-
num_dropped_from_window::Integer
79-
) -> state
75+
push!(state::WindowedAssociativeOp{T}, value) -> state
8076
81-
Add the specified value to the state, drop some number of elements from the start of the
82-
window, and return `state` (which will have been mutated).
77+
Push `value` onto the end of `state`.
8378
8479
# Arguments
8580
- `state::WindowedAssociativeOp`: The state to update (will be mutated).
8681
- `value`: The value to add to the end of the window - must be convertible to a `T`.
87-
- `num_dropped_from_window::Integer`: The number of elements to remove from the front of
88-
the window.
82+
"""
83+
function Base.push!(state::WindowedAssociativeOp{T,Op,Op!}, value) where {T,Op,Op!}
84+
# We want to make sure that we never actually mutate any value object that we pass to
85+
# `push!`.
86+
# Note that when initialising `state.sum`, we should take a copy if our mutating `Op!`
87+
# is different to `Op`. This is because we know that `Op` is necessarily non-mutating.
88+
_copy(x) = (Op === Op!) ? x : deepcopy(x)
89+
90+
# Include the new value in sum and values.
91+
state.sum = isempty(state.values) ? _copy(value) : Op!(state.sum, value)
92+
push!(state.values, value)
93+
return state
94+
end
8995

90-
# Returns
91-
- The instance `state` that was passed in.
9296
"""
93-
Base.@propagate_inbounds function update_state!(
94-
state::WindowedAssociativeOp{T,Op,Op!}, value, num_dropped_from_window::Integer
95-
) where {T,Op,Op!}
96-
# Our index into previous_cumsum is advanced by the number of values we drop from the
97-
# window.
98-
state.ri_previous_cumsum += num_dropped_from_window
97+
popfirst!(state::WindowedAssociativeOp, n::Integer=1) -> state
98+
99+
Drop `n` values from the start of `state`.
100+
"""
101+
Base.@propagate_inbounds function Base.popfirst!(
102+
state::WindowedAssociativeOp{T,Op}, n::Integer=1
103+
) where {T,Op}
104+
n == 0 && return state
105+
n >= 0 || throw(ArgumentError("n must be non-negative, got n=$n"))
106+
107+
# Our index into previous_cumsum is advanced by the number of values we drop.
108+
# Note that we do not assign to `ri_previous_cumsum` immediately, since this would make
109+
# `state` invalid were we to throw an exception before returning.
110+
new_ri_previous_cumsum = state.ri_previous_cumsum + n
99111

100112
# If this has taken us out-of-range of the _previous_cumsum values, we must recompute
101113
# them.
102-
elements_remaining = length(state.previous_cumsum) - state.ri_previous_cumsum
114+
elements_remaining = length(state.previous_cumsum) - new_ri_previous_cumsum
103115

104116
if elements_remaining < 0
105117
# We have overshot the end of previous_cumsum.
106118

107-
# We may also need to discard elements from values. This could happen if
108-
# num_dropped_from_window > 1.
119+
# We may also need to discard elements from values. This could happen if n > 1.
109120
num_values_to_remove = -elements_remaining
110121
@boundscheck if num_values_to_remove > length(state.values)
111-
throw(
112-
ArgumentError(
113-
"num_dropped_from_window = $num_dropped_from_window is out of range"
114-
),
115-
)
122+
throw(ArgumentError("n = $n is out of range"))
116123
end
117124

118125
# We now generate the partial sum, and set our index back to zero. values is also
@@ -152,18 +159,12 @@ Base.@propagate_inbounds function update_state!(
152159

153160
state.ri_previous_cumsum = 0
154161
empty!(state.values)
155-
# state.sum is now garbage, but we are not going to use it before we recompute it.
162+
# WARNING! `state.sum` is now garbage.
163+
# This means that we have to be careful to not use it if `state.values` is empty.
164+
else
165+
state.ri_previous_cumsum = new_ri_previous_cumsum
156166
end
157167

158-
# We want to make sure that we never actually mutate any value object that we pass to
159-
# `update_state!`.
160-
# Note that when initialising `state.sum`, we should take a copy if our mutating `Op!`
161-
# is different to `Op`. This is because we know that `Op` is necessarily non-mutating.
162-
_copy(x) = (Op == Op!) ? x : deepcopy(x)
163-
164-
# Include the new value in sum and values.
165-
state.sum = isempty(state.values) ? _copy(value) : Op!(state.sum, value)
166-
push!(state.values, value)
167168
return state
168169
end
169170

@@ -188,6 +189,10 @@ function window_value(state::WindowedAssociativeOp{T,Op})::T where {T,Op}
188189
# We aren't using the A buffer, either because values is full or the A buffer has
189190
# not yet been populated.
190191
state.sum
192+
elseif isempty(state.values)
193+
# We are using the A buffer, but not the B buffer.
194+
# `state.sum` has an uninitialised value.
195+
@inbounds(state.previous_cumsum[index])
191196
else
192197
Op(@inbounds(state.previous_cumsum[index]), state.sum)
193198
end

src/fixed.jl

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ mutable struct FixedWindowAssociativeOp{T,Op,Op!}
1616
window_state::WindowedAssociativeOp{T,Op,Op!,CircularBuffer{T}}
1717
remaining_window::Int
1818

19-
function FixedWindowAssociativeOp{T,Op,Op!}(
20-
window::Integer
21-
) where {T,Op,Op!}
19+
function FixedWindowAssociativeOp{T,Op,Op!}(window::Integer) where {T,Op,Op!}
2220
window < 1 && throw(ArgumentError("Got window $window, but it must be positive."))
2321
window_state = WindowedAssociativeOp{T,Op,Op!,CircularBuffer{T}}(
2422
CircularBuffer{T}(window - 1), CircularBuffer{T}(window)
@@ -50,7 +48,8 @@ function update_state!(state::FixedWindowAssociativeOp, value)
5048

5149
# With @inbounds, we assert that num_dropped_from_window will never exceed the size of
5250
# the window.
53-
@inbounds update_state!(state.window_state, value, num_dropped_from_window)
51+
@inbounds popfirst!(state.window_state, num_dropped_from_window)
52+
push!(state.window_state, value)
5453
return state
5554
end
5655

src/time.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ function update_state!(state::TimeWindowAssociativeOp, time, value)
8484
num_dropped_from_window += 1
8585
end
8686

87-
update_state!(state.window_state, value, num_dropped_from_window)
87+
@inbounds popfirst!(state.window_state, num_dropped_from_window)
88+
push!(state.window_state, value)
8889

8990
if !state.window_full && num_dropped_from_window > 0
9091
# The window has now filled; record this for use in `window_full` below.

test/base.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,32 @@
99
end
1010
end
1111
end
12+
13+
@testset "simple" begin
14+
state = WindowedAssociativeOp{Int64,+}()
15+
@test window_size(state) == 0
16+
17+
push!(state, 1)
18+
@test window_size(state) == 1
19+
@test window_value(state) == 1
20+
21+
push!(state, 2)
22+
push!(state, 3, 4)
23+
@test window_size(state) == 4
24+
@test window_value(state) == 10
25+
26+
popfirst!(state)
27+
@test window_size(state) == 3
28+
@test window_value(state) == 9
29+
30+
popfirst!(state, 2)
31+
@test window_size(state) == 1
32+
@test window_value(state) == 4
33+
@test_throws ArgumentError popfirst!(state, 2)
34+
@test window_size(state) == 1
35+
@test window_value(state) == 4
36+
37+
popfirst!(state, 1)
38+
@test window_size(state) == 0
39+
end
1240
end

test/fixed.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
function test_fixed_window(
2-
values, op; op_bang=nothing, approximate_equality::Bool=false
3-
)
1+
function test_fixed_window(values, op; op_bang=nothing, approximate_equality::Bool=false)
42
# For later comparison, to ensure that we don't mutate any of these
53
values_copy = deepcopy(values)
64

0 commit comments

Comments
 (0)