Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Date & time generators #161

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
117 changes: 117 additions & 0 deletions lib/stream_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ defmodule StreamData do
require Integer
lower_stepless = Integer.floor_div(left, step)
upper_stepless = Integer.floor_div(right, step)

if lower_stepless > upper_stepless do
raise "cannot generate elements from an empty range"
end
Expand Down Expand Up @@ -1915,6 +1916,122 @@ defmodule StreamData do
end)
end

@doc """
Generates arbitrary (past and future) dates.

Generated values shrink towards `Date.utc_today/0`.
"""
def date do
whatyouhide marked this conversation as resolved.
Show resolved Hide resolved
date([])
end

@doc """
Generates dates according to the given `options` or `date_range`.

## Options

* `:origin` - (`Date`) if present, generated values will shrink towards this date. Cannot be combined with `:min` or `:max`.

* `:min` - (`Date`) if present, only dates _after_ this date will be generated. Values will shrink towards this date.

* `:max` - (`Date`) if present, only dates _before_ this date will be generated. Values will shrink towards this date.

If both `:min` and `:max` are provided, dates between the two mentioned dates will be generated.
Values will shrink towards `:min`.

If no options are provided, will work just like `StreamData.date/0`.
whatyouhide marked this conversation as resolved.
Show resolved Hide resolved

## Date.Range
whatyouhide marked this conversation as resolved.
Show resolved Hide resolved

Alternatively a `Date.Range` can be given. This will generate dates in the given range,
and with the supplied `date_range.step`.

Values will shrink towards `date_range.first`.

## Calendar support

This generator works with `Calendar.ISO` and any other calendar
which implements the callbacks
`c:naive_datetime_to_iso_days/7` and `c:naive_datetime_from_iso_days/2`.
whatyouhide marked this conversation as resolved.
Show resolved Hide resolved
"""
@spec date(Date.Range.t() | keyword()) :: t(Date.t())
def date(options_or_date_range)

def date(date_range = %Date.Range{}) do
member_of(date_range)
end

def date(options) when is_list(options) do
min = Keyword.get(options, :min, nil)
max = Keyword.get(options, :max, nil)
origin = Keyword.get(options, :origin, Date.utc_today())

case {min, max} do
{nil, nil} ->
any_date(origin, origin.calendar)

{nil, max = %Date{}} ->
past_date(max, max.calendar)

{min = %Date{}, nil} ->
future_date(min, min.calendar)

{min = %Date{}, max = %Date{}} ->
if min.calendar != max.calendar do
raise ArgumentError,
"Two dates with different calendars were passed to `StreamData.date/1`"
whatyouhide marked this conversation as resolved.
Show resolved Hide resolved
end

date_between_bounds(min, max, min.calendar)
end
end

defp any_date(origin, calendar) do
{iso_days, day_fraction} = extract_date(origin, calendar)

map(integer(), fn offset ->
construct_date!(iso_days + offset, day_fraction, calendar)
end)
end

defp past_date(origin, calendar) do
{iso_days, day_fraction} = extract_date(origin, calendar)

map(positive_integer(), fn offset ->
construct_date!(iso_days - offset, day_fraction, calendar)
end)
end

defp future_date(origin, calendar) do
{iso_days, day_fraction} = extract_date(origin, calendar)

map(positive_integer(), fn offset ->
construct_date!(iso_days + offset, day_fraction, calendar)
end)
end

defp date_between_bounds(min, max, calendar) do
{min_iso_days, min_day_fraction} = extract_date(min, calendar)
{max_iso_days, _max_day_fraction} = extract_date(max, calendar)

map(integer(min_iso_days..max_iso_days), fn iso_days ->
construct_date!(iso_days, min_day_fraction, calendar)
end)
end

@compile {:inline, extract_date: 2}
defp extract_date(date, calendar) do
calendar.naive_datetime_to_iso_days(date.year, date.month, date.day, 0, 0, 0, {0, 0})
end

@compile {:inline, construct_date!: 3}
defp construct_date!(iso_days, day_fraction, calendar) do
{year, month, day, _hour, _minute, _second, _second_fraction} =
calendar.naive_datetime_from_iso_days({iso_days, day_fraction})

Date.new!(year, month, day, calendar)
end

@doc """
Generates iolists.

Expand Down
47 changes: 47 additions & 0 deletions test/stream_data_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,53 @@ defmodule StreamDataTest do
end
end

describe "date/0" do
property "generates any dates" do
check all date <- date() do
assert %Date{} = date
end
end
end

describe "date/1" do
property "without options, generates any dates" do
check all date <- date([]) do
assert %Date{} = date
end
end

property "with a :min option, generates dates after it" do
check all minimum <- date(),
date <- date(min: minimum) do
assert Date.compare(date, minimum) in [:eq, :gt]
end
end

property "with a :max option, generates dates before it" do
check all maximum <- date(),
date <- date(max: maximum) do
assert Date.compare(date, maximum) in [:lt, :eq]
end
end

property "with both a :min and a :max option, generates dates in-between the bounds" do
check all minimum <- date(),
maximum <- date(min: minimum),
date <- date(min: minimum, max: maximum) do
assert Enum.member?(Date.range(minimum, maximum), date)
end
end

property "with a Date.Range, generates dates in-between the bounds" do
check all minimum <- date(),
maximum <- date(min: minimum),
range = Date.range(minimum, maximum),
date <- date(range) do
assert Enum.member?(range, date)
end
end
end

property "byte/0" do
check all value <- byte() do
assert value in 0..255
Expand Down