Skip to content

Commit 0746c34

Browse files
Add nullable/2 generator (#221)
Closes #220. Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com>
1 parent 3320d84 commit 0746c34

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

lib/stream_data.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,6 +2325,47 @@ defmodule StreamData do
23252325
scale(data, fn size -> trunc(:math.pow(size, exponent)) end)
23262326
end
23272327

2328+
@doc """
2329+
Generates either `nil` or the value generated by the given `data`.
2330+
2331+
The frequency distribution can be specified using the `ratio` option.
2332+
2333+
## Options
2334+
2335+
* `:ratio` - (float in between `0.0` and `1.0`, not included) specifies the
2336+
frequency with which `nil` should be selected. Higher means that `nil` is
2337+
more likely to be selected. Defaults to `0.5`.
2338+
2339+
## Examples
2340+
2341+
Enum.take(StreamData.nullable(StreamData.integer()), 10)
2342+
#=> [1, -1, nil, nil, 5, 4, -3, nil, nil, 1]
2343+
2344+
Enum.take(StreamData.nullable(StreamData.integer(), ratio: 0.25), 10)
2345+
#=> [1, nil, -3, nil, 3, -3, 0, 3, nil, nil]
2346+
2347+
Enum.take(StreamData.nullable(StreamData.integer(), ratio: 0.25), 10)
2348+
#=> [0, -2, 1, nil, -2, 0, -4, 4, 6, nil]
2349+
2350+
Enum.take(StreamData.nullable(StreamData.boolean(), ratio: 0.25), 10)
2351+
#=> [false, false, false, nil, true, true, true, false, false, true]
2352+
2353+
"""
2354+
@spec nullable(t(a), keyword()) :: t(nil | a) when a: term()
2355+
def nullable(generator, options \\ []) when is_list(options) do
2356+
ratio = Keyword.get(options, :ratio, 0.5)
2357+
2358+
{numerator, denominator} =
2359+
if 0 <= ratio and ratio <= 1.0 do
2360+
Float.ratio(ratio)
2361+
else
2362+
raise ArgumentError,
2363+
"expected :ratio to be greater than or equal to 0.0 and less than or equal to 1.0, got: #{inspect(ratio)}"
2364+
end
2365+
2366+
StreamData.frequency([{numerator, nil}, {denominator - numerator, generator}])
2367+
end
2368+
23282369
@doc """
23292370
Checks the behaviour of a given function on values generated by `data`.
23302371

test/stream_data_test.exs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,44 @@ defmodule StreamDataTest do
792792
end
793793
end
794794

795+
describe "nullable/2" do
796+
test "with invalid options" do
797+
data = constant(:term)
798+
799+
message =
800+
"expected :ratio to be greater than or equal to 0.0 and less than or equal to 1.0, got: 1.1"
801+
802+
assert_raise ArgumentError, message, fn -> nullable(data, ratio: 1.1) end
803+
804+
message =
805+
"expected :ratio to be greater than or equal to 0.0 and less than or equal to 1.0, got: -0.1"
806+
807+
assert_raise ArgumentError, message, fn -> nullable(data, ratio: -0.1) end
808+
end
809+
810+
property "returns nil or the value returned by data" do
811+
check all maybe_nil <- nullable(constant(:term)) do
812+
assert is_nil(maybe_nil) or maybe_nil == :term
813+
end
814+
end
815+
816+
test "with very large chance of nils" do
817+
values = Enum.take(nullable(:small_chance, ratio: 0.99), 1000)
818+
819+
assert :small_chance in values
820+
assert nil in values
821+
assert Enum.count(values, &(&1 == :small_chance)) < Enum.count(values, &is_nil(&1))
822+
end
823+
824+
test "with very small chance of nils" do
825+
values = Enum.take(nullable(:big_chance, ratio: 0.01), 1000)
826+
827+
assert :big_chance in values
828+
assert nil in values
829+
assert Enum.count(values, &is_nil(&1)) < Enum.count(values, &(&1 == :big_chance))
830+
end
831+
end
832+
795833
test "check_all/3 with :os.timestamp" do
796834
options = [initial_seed: :os.timestamp()]
797835

0 commit comments

Comments
 (0)