mirror of
https://github.com/valitydev/genlib.git
synced 2024-11-06 01:05:20 +00:00
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:
parent
3e17765368
commit
2bbc54d4ab
72
src/genlib_range.erl
Normal file
72
src/genlib_range.erl
Normal 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.
|
15
test/genlib_range_test.erl
Normal file
15
test/genlib_range_test.erl
Normal 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})
|
||||
).
|
57
test/prop_genlib_range.erl
Normal file
57
test/prop_genlib_range.erl
Normal 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()
|
||||
]).
|
Loading…
Reference in New Issue
Block a user