feat: Add genlib_range (#35)

* feat: Add genlib_range

* test: Add proptests for simple ranges

* feat: Add genlib_range:to_list

* fmt: Fix formatting

* Update src/genlib_range.erl

Co-authored-by: ndiezel0 <ndiezel0@gmail.com>

* Update src/genlib_range.erl

Co-authored-by: ndiezel0 <ndiezel0@gmail.com>
This commit is contained in:
Yaroslav Rogov 2021-08-24 21:22:02 +07:00 committed by GitHub
parent 3e17765368
commit 2bbc54d4ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 144 additions and 0 deletions

72
src/genlib_range.erl Normal file
View File

@ -0,0 +1,72 @@
-module(genlib_range).
%% @doc Module for working with number sequences (like lists:seq/2,3),
%% but more efficiently (i.e. without generating a list of numbers)
%%
%% Supports both forward- and backward-ranges (increasing and decreasing respectively)
-export([map/2]).
-export([foldl/3]).
-export([to_list/1]).
-type bound() :: integer().
-type step() :: neg_integer() | pos_integer().
-type t() :: {bound(), bound()} | {bound(), bound(), step()}.
-define(IS_RANGE(R),
((is_integer(element(1, R))) andalso
(is_integer(element(2, R))) andalso
(?IS_SIMPLE_RANGE(R) orelse ?IS_RANGE_WITH_STEP(R)))
).
-define(IS_SIMPLE_RANGE(R),
(tuple_size(R) == 2)
).
-define(IS_RANGE_WITH_STEP(R),
(tuple_size(R) == 3 andalso
is_integer(element(3, R)) andalso
element(3, R) /= 0)
).
%% @doc Map over range
-spec map(fun((integer()) -> T), t()) -> [T].
map(Fun0, Range) when is_function(Fun0, 1) ->
Fun1 = fun(Idx, Acc) ->
[Fun0(Idx) | Acc]
end,
lists:reverse(foldl(Fun1, [], Range));
map(_, _) ->
error(badarg).
%% @doc Fold over range from starting from the first boundary
-spec foldl(fun((integer(), T) -> T), T, t()) -> T.
foldl(Fun, Acc, Range) when is_function(Fun, 2), ?IS_RANGE(Range) ->
{From, To, Step} = to_extended_range(Range),
do_foldl(Fun, Acc, From, To, Step);
foldl(_, _, _) ->
error(badarg).
%% @doc Convert range to list
%% Somewhat similar to lists:seq/2,3, but covers all possible valid variations of arguments
-spec to_list(t()) -> [integer()].
to_list(Range) ->
{From, To, Step} = to_extended_range(Range),
if
From < To, Step < 0 -> [];
From > To, Step > 0 -> [];
true -> lists:seq(From, To, Step)
end.
%%
%% Internals
%%
do_foldl(_Fun, Acc, From, To, Step) when (From > To andalso Step > 0) -> Acc;
do_foldl(_Fun, Acc, From, To, Step) when (From < To andalso Step < 0) -> Acc;
do_foldl(Fun, Acc, From, To, Step) -> do_foldl(Fun, Fun(From, Acc), From + Step, To, Step).
to_extended_range({From, To}) ->
{From, To, 1};
to_extended_range({_From, _To, _Step} = Range) ->
Range.

View File

@ -0,0 +1,15 @@
-module(genlib_range_test).
-export([range_ops_fail_with_zero_step/0]).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec range_ops_fail_with_zero_step() -> _.
range_ops_fail_with_zero_step() ->
?assertError(
badarg,
genlib_range:map(fun(_) -> throw(blow) end, {0, 0, 0})
).

View File

@ -0,0 +1,57 @@
-module(prop_genlib_range).
-include_lib("proper/include/proper.hrl").
-spec prop_map() -> proper:test().
prop_map() ->
?FORALL(
Range,
range(),
lists_seq(Range) =:= genlib_range:map(fun identity/1, Range)
).
-spec prop_foldl() -> proper:test().
prop_foldl() ->
?FORALL(
Range,
range(),
lists:foldl(fun sum/2, 0, lists_seq(Range)) =:= genlib_range:foldl(fun sum/2, 0, Range)
).
-spec prop_to_list() -> proper:test().
prop_to_list() ->
?FORALL(
Range,
range(),
lists_seq(Range) =:= genlib_range:to_list(Range)
).
identity(X) ->
X.
sum(A, B) ->
A + B.
%% Workaround missing if statements in implementation
lists_seq({From, To}) when From =< To ->
lists:seq(From, To);
lists_seq({From, To}) when From > To ->
[];
lists_seq({From, To, Step}) when From < To, Step < 0 ->
[];
lists_seq({From, To, Step}) when From > To, Step > 0 ->
[];
lists_seq({From, To, Step}) ->
lists:seq(From, To, Step).
range() ->
oneof([
{integer(), integer()},
{integer(), integer(), non_zero_integer()}
]).
non_zero_integer() ->
oneof([
neg_integer(),
pos_integer()
]).