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

Commit eda83b9

Browse files
committed
Refactor general case into push! and popfirst!
1 parent bc0cb3b commit eda83b9

File tree

5 files changed

+62
-124
lines changed

5 files changed

+62
-124
lines changed

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: 25 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -72,96 +72,20 @@ 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.
89-
90-
# Returns
91-
- The instance `state` that was passed in.
9282
"""
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-
# TODO This could be dangerous, since if we get an error later the state will be
99-
# invalid.
100-
state.ri_previous_cumsum += num_dropped_from_window
101-
102-
# If this has taken us out-of-range of the _previous_cumsum values, we must recompute
103-
# them.
104-
elements_remaining = length(state.previous_cumsum) - state.ri_previous_cumsum
105-
106-
if elements_remaining < 0
107-
# We have overshot the end of previous_cumsum.
108-
109-
# We may also need to discard elements from values. This could happen if
110-
# num_dropped_from_window > 1.
111-
num_values_to_remove = -elements_remaining
112-
@boundscheck if num_values_to_remove > length(state.values)
113-
throw(
114-
ArgumentError(
115-
"num_dropped_from_window = $num_dropped_from_window is out of range"
116-
),
117-
)
118-
end
119-
120-
# We now generate the partial sum, and set our index back to zero. values is also
121-
# emptied, since its information is now reflected in previous_cumsum.
122-
# NOTE: We need to take care here in the case of non-commutation. In accumulate, we
123-
# will be getting:
124-
#
125-
# (x0, op(x0, x1), op(op(x0, x1), x2), ...)
126-
#
127-
# but we actually want:
128-
#
129-
# (x0, op(x1, x0), op(x2, op(x1, x0)), ...)
130-
131-
# Conceptually the following code is equivalent to the following, however avoids
132-
# unnecessary allocations:
133-
# trimmed_reversed_values = state.values[end:-1:1 + num_values_to_remove]
134-
# state.previous_cumsum = accumulate(
135-
# (x, y) -> Op(y, x),
136-
# trimmed_reversed_values
137-
# )
138-
139-
empty!(state.previous_cumsum)
140-
upper = length(state.values)
141-
lower = 1 + num_values_to_remove
142-
@inbounds if (upper - lower) >= 0 # i.e. we have a non-zero range
143-
i = upper
144-
accumulation = state.values[i]
145-
while true
146-
push!(state.previous_cumsum, accumulation)
147-
i -= 1
148-
i >= lower || break
149-
# If we were to use a mutating operation here, then we'd have to introduce
150-
# a copy above. So there is no drawback to using the non-mutating Op.
151-
accumulation = Op(state.values[i], accumulation)
152-
end
153-
end
154-
155-
state.ri_previous_cumsum = 0
156-
empty!(state.values)
157-
# state.sum is now garbage, but we are not going to use it before we recompute it.
158-
end
159-
83+
function Base.push!(state::WindowedAssociativeOp{T,Op,Op!}, value) where {T,Op,Op!}
16084
# We want to make sure that we never actually mutate any value object that we pass to
161-
# `update_state!`.
85+
# `push!`.
16286
# Note that when initialising `state.sum`, we should take a copy if our mutating `Op!`
16387
# is different to `Op`. This is because we know that `Op` is necessarily non-mutating.
164-
_copy(x) = (Op == Op!) ? x : deepcopy(x)
88+
_copy(x) = (Op === Op!) ? x : deepcopy(x)
16589

16690
# Include the new value in sum and values.
16791
state.sum = isempty(state.values) ? _copy(value) : Op!(state.sum, value)
@@ -170,29 +94,24 @@ Base.@propagate_inbounds function update_state!(
17094
end
17195

17296
"""
173-
drop_values!(
174-
state::WindowedAssociativeOp{T,Op,Op!}, n::Integer
175-
) where {T,Op,Op!}
97+
popfirst!(state::WindowedAssociativeOp, n::Integer=1) -> state
17698
177-
Drop `n` values from the leading edge of the window.
99+
Drop `n` values from the start of `state`.
178100
"""
179-
function drop_values!(state::WindowedAssociativeOp{T,Op,Op!}, n::Integer) where {T,Op,Op!}
180-
n > 0 || throw(ArgumentError("n must be positive, got n=$n"))
181-
182-
# FIXME At best this will be hacky
183-
# FIXME At best this will be hacky
184-
# FIXME At best this will be hacky
185-
# FIXME At best this will be hacky
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"))
186106

187-
# Our index into previous_cumsum is advanced by the number of values we drop from the
188-
# window.
189-
# TODO This could be dangerous, since if we get an error later the state will be
190-
# invalid.
191-
state.ri_previous_cumsum += n
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
192111

193112
# If this has taken us out-of-range of the _previous_cumsum values, we must recompute
194113
# them.
195-
elements_remaining = length(state.previous_cumsum) - state.ri_previous_cumsum
114+
elements_remaining = length(state.previous_cumsum) - new_ri_previous_cumsum
196115

197116
if elements_remaining < 0
198117
# We have overshot the end of previous_cumsum.
@@ -225,37 +144,27 @@ function drop_values!(state::WindowedAssociativeOp{T,Op,Op!}, n::Integer) where
225144
empty!(state.previous_cumsum)
226145
upper = length(state.values)
227146
lower = 1 + num_values_to_remove
228-
if (upper - lower) >= 0 # i.e. we have a non-zero range
147+
@inbounds if (upper - lower) >= 0 # i.e. we have a non-zero range
229148
i = upper
230-
accumulation = @inbounds(state.values[i])
149+
accumulation = state.values[i]
231150
while true
232151
push!(state.previous_cumsum, accumulation)
233152
i -= 1
234153
i >= lower || break
235154
# If we were to use a mutating operation here, then we'd have to introduce
236155
# a copy above. So there is no drawback to using the non-mutating Op.
237-
accumulation = Op(@inbounds(state.values[i]), accumulation)
156+
accumulation = Op(state.values[i], accumulation)
238157
end
239158
end
240159

241160
state.ri_previous_cumsum = 0
242161
empty!(state.values)
243-
# FIXME state.sum is now garbage.
244-
# This matters...
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
245166
end
246167

247-
248-
# # We want to make sure that we never actually mutate any value object that we pass to
249-
# # `update_state!`.
250-
# # Note that when initialising `state.sum`, we should take a copy if our mutating `Op!`
251-
# # is different to `Op`. This is because we know that `Op` is necessarily non-mutating.
252-
# _copy(x) = (Op == Op!) ? x : deepcopy(x)
253-
254-
# # Include the new value in sum and values.
255-
# state.sum = isempty(state.values) ? _copy(value) : Op!(state.sum, value)
256-
# state.sum =
257-
# push!(state.values, value)
258-
259168
return state
260169
end
261170

@@ -280,7 +189,7 @@ function window_value(state::WindowedAssociativeOp{T,Op})::T where {T,Op}
280189
# We aren't using the A buffer, either because values is full or the A buffer has
281190
# not yet been populated.
282191
state.sum
283-
elseif length(state.values) == 0
192+
elseif isempty(state.values)
284193
# We are using the A buffer, but not the B buffer.
285194
# `state.sum` has an uninitialised value.
286195
@inbounds(state.previous_cumsum[index])

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

0 commit comments

Comments
 (0)