Packages
ra
3.0.2
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
3.0.0-beta.1
2.17.3
2.17.2
2.17.1
2.17.0
2.16.13
2.16.12
2.16.11
2.16.10
2.16.9
2.16.8
2.16.7
2.16.6
2.16.5
2.16.4
2.16.3
2.16.2
2.16.1
2.16.0
2.16.0-pre.12
2.16.0-pre.11
2.16.0-pre.10
2.16.0-pre.9
2.16.0-pre.8
2.16.0-pre.7
2.16.0-pre.6
2.16.0-pre.5
2.16.0-pre.4
2.16.0-pre.3
2.16.0-pre.2
2.16.0-pre.1
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.0
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.13.0-pre.1
2.12.0
2.11.0
2.11.0-pre.1
2.10.2-pre.2
2.10.2-pre.1
2.10.1
2.10.0
2.10.0-pre.3
2.10.0-pre.2
2.10.0-pre.1
2.9.10-pre.1
2.9.1
2.9.1-pre.2
2.9.1-pre.1
2.9.0
2.8.0
retired
2.7.3
2.7.2
2.7.1
2.7.0
2.7.0-pre.3
2.7.0-pre.2
2.7.0-pre.1
2.6.3
2.6.2
2.6.1
2.6.0-pre.1
2.5.1
2.5.1-pre.1
2.5.0
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
retired
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.9.6
0.9.5
0.9.4
0.9.2
0.3.3
retired
0.3.2
retired
0.3.1
retired
Raft library
Current section
Files
Jump to
Current section
Files
src/ra_seq.erl
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2017-2025 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries.
-module(ra_seq).
%% open type
%% sequences are ordered high -> low but ranges are ordered
%% {low, high} so a typical sequence could look like
%% [55, {20, 52}, 3]
-type state() :: [ra:index() | ra:range()].
-record(i, {seq :: state()}).
-opaque iter() :: #i{}.
-export_type([state/0,
iter/0]).
-export([
append/2,
from_list/1,
floor/2,
limit/2,
add/2,
fold/3,
expand/1,
subtract/2,
remove_prefix/2,
first/1,
last/1,
iterator/1,
next/1,
list_chunk/2,
length/1,
in/2,
range/1,
in_range/2,
has_overlap/2
]).
-spec append(ra:index(), state()) -> state().
append(Idx, [IdxN1, IdxN2 | Rem])
when Idx == IdxN1 + 1 andalso
Idx == IdxN2 + 2 ->
%% we can compact into a range
[{IdxN2, Idx} | Rem];
append(Idx, [{IdxN, IdxN1} | Rem])
when Idx == IdxN1 + 1 ->
%% Extend the raage
[{IdxN, Idx} | Rem];
append(Idx, [])
when is_integer(Idx) ->
[Idx];
append(Idx, [Prev | _] = Seq)
when is_integer(Idx) andalso
((is_tuple(Prev) andalso
Idx > element(2, Prev)) orelse
Idx > Prev) ->
[Idx | Seq].
-spec from_list([ra:index()]) -> state().
from_list(L) ->
lists:foldl(fun append/2, [], lists:usort(L)).
%% @doc This operation is O(n) + a list:reverse/1
-spec floor(ra:index(), state()) -> state().
floor(FloorIdxIncl, Seq) when is_list(Seq) ->
%% TODO: assert appendable
%% for now assume appendable
floor0(FloorIdxIncl, Seq, []).
-spec limit(ra:index(), state()) -> state().
limit(CeilIdxIncl, [Last | Rem])
when is_integer(Last) andalso
Last > CeilIdxIncl ->
limit(CeilIdxIncl, Rem);
limit(CeilIdxIncl, [{_, _} = T | Rem])
when is_integer(CeilIdxIncl) ->
case ra_range:limit(CeilIdxIncl + 1, T) of
undefined ->
limit(CeilIdxIncl, Rem);
{I, I} ->
[I | Rem];
{I, I2} when I == I2 - 1 ->
[I2, I | Rem];
NewRange ->
[NewRange | Rem]
end;
limit(_CeilIdxIncl, Seq) ->
Seq.
%% @doc adds two sequences together where To is
%% the "lower" sequence
%% TODO: optimise to avoid the fold which could be expensive
%% for very large sequences containing large ranges
-spec add(Add :: state(), To :: state()) -> state().
add([], To) ->
To;
add(Add, []) ->
Add;
add(Add, To) ->
Fst = first(Add),
fold(fun append/2, limit(Fst - 1, To), Add).
-spec fold(fun ((ra:index(), Acc) -> Acc), Acc, state()) ->
Acc when Acc :: term().
fold(Fun, Acc0, Seq) ->
lists:foldr(
fun ({_, _} = Range, Acc) ->
ra_range:fold(Range, Fun, Acc);
(Idx, Acc) ->
Fun(Idx, Acc)
end, Acc0, Seq).
-spec expand(state()) -> [ra:index()].
expand(Seq) ->
fold(fun (I, Acc) -> [I | Acc] end, [], Seq).
-spec subtract(Min :: state(), Sub :: state()) -> Diff :: state().
subtract(SeqA, SeqB) ->
%% TODO: not efficient at all but good enough for now
%% optimise if we end up using this in critical path
A = expand(SeqA),
B = expand(SeqB),
from_list(A -- B).
-spec first(state()) -> undefined | ra:index().
first([]) ->
undefined;
first(Seq) ->
case lists:last(Seq) of
{I, _} ->
I;
I ->
I
end.
-spec last(state()) -> undefined | ra:index().
last([]) ->
undefined;
last(Seq) ->
case hd(Seq) of
{_, I} ->
I;
I ->
I
end.
-spec remove_prefix(state(), state()) ->
{ok, state()} | {error, not_prefix}.
remove_prefix(Prefix, Seq) ->
P = iterator(Prefix),
S = iterator(Seq),
drop_prefix(next(P), next(S)).
-spec iterator(state()) -> iter() | end_of_seq.
iterator(Seq) when is_list(Seq) ->
#i{seq = lists:reverse(Seq)}.
-spec next(iter()) -> {ra:index(), iter()} | end_of_seq.
next(#i{seq = []}) ->
end_of_seq;
next(#i{seq = [Next | Rem]})
when is_integer(Next) ->
{Next, #i{seq = Rem}};
next(#i{seq = [{Next, End} | Rem]}) ->
case ra_range:new(Next + 1, End) of
undefined ->
{Next, #i{seq = Rem}};
NextRange ->
{Next, #i{seq = [NextRange | Rem]}}
end.
%% @doc Returns a chunk of up to ChunkSize expanded indices from the sequence
%% without eagerly expanding the entire sequence. On first call, pass a state().
%% On subsequent calls, pass the returned iterator.
%% Returns `{Chunk, NewIterator}' or `end_of_seq' when exhausted.
%% Indices are returned in ascending order.
-spec list_chunk(ChunkSize :: pos_integer(), state() | iter()) ->
{[ra:index()], iter()} | end_of_seq.
list_chunk(ChunkSize, Seq) when is_list(Seq) ->
list_chunk(ChunkSize, iterator(Seq));
list_chunk(ChunkSize, Iter) when is_record(Iter, i) ->
list_chunk(ChunkSize, Iter, []).
list_chunk(0, Iter, Acc) ->
{lists:reverse(Acc), Iter};
list_chunk(N, Iter, Acc) ->
case next(Iter) of
end_of_seq when Acc =:= [] ->
end_of_seq;
end_of_seq ->
{lists:reverse(Acc), Iter};
{Idx, NextIter} ->
list_chunk(N - 1, NextIter, [Idx | Acc])
end.
length(Seq) ->
lists:foldl(
fun (Idx, Acc) when is_integer(Idx) ->
Acc + 1;
(Range, Acc) when is_tuple(Range) ->
Acc + ra_range:size(Range)
end, 0, Seq).
in(_Idx, []) ->
false;
in(Idx, [Idx | _]) ->
true;
in(Idx, [Next | Rem])
when is_integer(Next) ->
in(Idx, Rem);
in(Idx, [Range | Rem]) ->
case ra_range:in(Idx, Range) of
true ->
true;
false ->
in(Idx, Rem)
end.
-spec range(state()) -> ra:range().
range([]) ->
undefined;
range(Seq) ->
ra_range:new(first(Seq), last(Seq)).
-spec in_range(ra:range(), state()) ->
state().
in_range(_Range, []) ->
[];
in_range(undefined, _) ->
[];
in_range({Start, End}, Seq0) ->
%% TODO: optimise
floor(Start, limit(End, Seq0)).
%% @doc Check if any element in the sequence overlaps with the given range.
%% This is a pure query that does not modify the sequence and can terminate
%% early as soon as an overlap is found.
%% Sequences are ordered high -> low, so we traverse from highest to lowest.
%% @end
-spec has_overlap(ra:range(), state()) -> boolean().
has_overlap(_Range, []) ->
false;
has_overlap(undefined, _Seq) ->
false;
has_overlap({Start, End}, Seq) ->
has_overlap0(Start, End, Seq).
%% Internal functions
%% Traverse the sequence (ordered high -> low) checking for overlap.
%% - Skip elements entirely above End
%% - Return true if any element overlaps [Start, End]
%% - Return false once we pass below Start (no need to check further)
has_overlap0(_Start, _End, []) ->
false;
has_overlap0(Start, End, [Idx | Rem]) when is_integer(Idx) ->
if Idx > End ->
%% Element is above the range, skip it
has_overlap0(Start, End, Rem);
Idx >= Start ->
%% Element is within [Start, End], overlap found
true;
true ->
%% Idx < Start, and since sequence is ordered high->low,
%% all remaining elements are also < Start, no overlap possible
false
end;
has_overlap0(Start, End, [{RStart, REnd} | Rem]) ->
%% Range element: check if it overlaps with [Start, End]
if RStart > End ->
%% Entire range is above End, skip it
has_overlap0(Start, End, Rem);
REnd < Start ->
%% Entire range is below Start, and since sequence is ordered
%% high->low, all remaining elements are also below Start
false;
true ->
%% Ranges overlap: RStart =< End and REnd >= Start
true
end.
drop_prefix({IDX, PI}, {IDX, SI}) ->
drop_prefix(next(PI), next(SI));
drop_prefix(_, end_of_seq) ->
%% TODO: is this always right as it includes the case where there is
%% more prefex left to drop but nothing in the target?
{ok, []};
drop_prefix(end_of_seq, {Idx, #i{seq = RevSeq}}) ->
{ok, add(lists:reverse(RevSeq), [Idx])};
drop_prefix({PrefIdx, PI}, {Idx, _SI} = I)
when PrefIdx < Idx ->
drop_prefix(next(PI), I);
drop_prefix({PrefIdx, _PI}, {Idx, _SI})
when Idx < PrefIdx ->
{error, not_prefix}.
floor0(FloorIdx, [Last | Rem], Acc)
when is_integer(Last) andalso
Last >= FloorIdx ->
floor0(FloorIdx, Rem, [Last | Acc]);
floor0(FloorIdx, [{_, _} = T | Rem], Acc) ->
case ra_range:truncate(FloorIdx - 1, T) of
undefined ->
lists:reverse(Acc);
{I, I} ->
floor0(FloorIdx, Rem, [I | Acc]);
{I, I2} when I == I2 - 1 ->
floor0(FloorIdx, Rem, [I, I2 | Acc]);
NewRange ->
floor0(FloorIdx, Rem, [NewRange | Acc])
end;
floor0(_FloorIdx, _Seq, Acc) ->
lists:reverse(Acc).