# `Koyomi.Rules`
[🔗](https://github.com/hattoriai/koyomi/blob/v0.2.1/lib/koyomi/rules.ex#L1)

The rules of a picker: which days, ranges and times a person may choose.

The same rules go to the component, which disables what they refuse in
the browser, and to `Koyomi.Changeset`, which refuses it on the server.
So the two never disagree:

    @rules Koyomi.Rules.new(min: :today, disabled_weekdays: [6, 7], max_days: 14)

    <.date_range_picker field={@form[:stay]} rules={@rules} />

    changeset
    |> cast(params, [:stay])
    |> Koyomi.Changeset.validate_range(:stay, @rules)

## Options

  * `:min`, `:max` - the first and the last day that may be chosen: a
    `Date`, `:today`, or `{:today, days}` for a day relative to today
    (`{:today, 1}` is tomorrow). For a date and time picker, also a
    `DateTime` or a `NaiveDateTime`.
  * `:disabled_dates` - days that may not be chosen (a list of `Date`).
  * `:disabled_ranges` - spans of days that may not be chosen (a list of
    `Date.Range` with a step of 1, or `{first, last}` tuples).
  * `:disabled_weekdays` - weekdays that may not be chosen, ISO numbers
    from 1 (Monday) to 7 (Sunday).
  * `:min_days`, `:max_days` - the shortest and the longest range, in
    days, both ends counted: a range from Monday to Wednesday has 3 days.
  * `:allow_disabled_in_range` - when `true`, a range may span disabled
    days (a stay that may cross a closed day). `false` by default: a
    range that holds a disabled day is refused.
  * `:min_count`, `:max_count` - the fewest and the most days of a
    multiple-date picker.
  * `:min_time`, `:max_time` - the earliest and the latest time of day
    (`Time`), for the time and date and time pickers.
  * `:step` - the minutes between the times that may be chosen, counted
    from midnight: with `15`, 09:15 may be chosen and 09:20 may not.
  * `:time_zone` - the IANA time zone of `:today` (`"Etc/UTC"` by
    default). A zone other than UTC needs a time zone database (see
    `Calendar`).

`:today` is read again on each check and each render, so rules built
once (in a module attribute) stay right the next day.

# `bound`

```elixir
@type bound() ::
  Date.t()
  | DateTime.t()
  | NaiveDateTime.t()
  | :today
  | {:today, integer()}
  | nil
```

# `error`

```elixir
@type error() :: {atom(), keyword()}
```

Why a value breaks the rules, with the values that a message needs:

  * `{:before_min, min: Date | DateTime}`, `{:after_max, max: ...}`
  * `{:disabled, date: Date}` - a disabled day.
  * `{:disabled_in_range, date: Date}` - a disabled day inside a range.
  * `{:too_short, min_days: n}`, `{:too_long, max_days: n}`
  * `{:too_few, min_count: n}`, `{:too_many, max_count: n}`
  * `{:time_before_min, min_time: Time}`, `{:time_after_max, max_time: Time}`
  * `{:time_step, step: minutes}`
  * `{:reversed, []}` - a range that ends before it starts.

# `t`

```elixir
@type t() :: %Koyomi.Rules{
  allow_disabled_in_range: boolean(),
  disabled_dates: [Date.t()],
  disabled_ranges: [{Date.t(), Date.t()}],
  disabled_weekdays: [1..7],
  max: bound(),
  max_count: pos_integer() | nil,
  max_days: pos_integer() | nil,
  max_time: Time.t() | nil,
  min: bound(),
  min_count: non_neg_integer() | nil,
  min_days: pos_integer() | nil,
  min_time: Time.t() | nil,
  step: pos_integer() | nil,
  time_zone: String.t()
}
```

# `allowed?`

```elixir
@spec allowed?(t() | keyword(), Date.t()) :: boolean()
```

Returns `true` when the day itself may be chosen: on or after `:min`,
on or before `:max`, and not disabled.

## Examples

    iex> rules = Koyomi.Rules.new(disabled_weekdays: [6, 7])
    iex> Koyomi.Rules.allowed?(rules, ~D[2027-03-15])
    true
    iex> Koyomi.Rules.allowed?(rules, ~D[2027-03-13])
    false

# `check_date`

```elixir
@spec check_date(t() | keyword(), Date.t()) :: :ok | {:error, error()}
```

Checks a day. Returns `:ok` or `{:error, error}` (see `t:error/0`).

## Examples

    iex> rules = Koyomi.Rules.new(min: ~D[2027-03-01])
    iex> Koyomi.Rules.check_date(rules, ~D[2027-02-28])
    {:error, {:before_min, min: ~D[2027-03-01]}}

# `check_dates`

```elixir
@spec check_dates(t() | keyword(), [Date.t()]) :: :ok | {:error, error()}
```

Checks the days of a multiple-date picker: each day, and the count.

## Examples

    iex> Koyomi.Rules.check_dates([max_count: 2], [~D[2027-03-01], ~D[2027-03-02], ~D[2027-03-03]])
    {:error, {:too_many, max_count: 2}}

# `check_datetime`

```elixir
@spec check_datetime(t() | keyword(), DateTime.t() | NaiveDateTime.t()) ::
  :ok | {:error, error()}
```

Checks a date and time: its day as `check_date/2` does, its time as
`check_time/2` does, and the bounds as instants when `:min` or `:max`
is a `DateTime` or a `NaiveDateTime`.

A `DateTime` is read in the time zone of the rules: its day and its
time of day are those of the wall clock there.

# `check_range`

```elixir
@spec check_range(t() | keyword(), Date.Range.t() | {Date.t(), Date.t()}) ::
  :ok | {:error, error()}
```

Checks a range of days, both ends included: each end must be allowed,
the length must be within `:min_days` and `:max_days`, and no day inside
may be disabled unless `:allow_disabled_in_range`.

## Examples

    iex> rules = Koyomi.Rules.new(max_days: 7)
    iex> Koyomi.Rules.check_range(rules, Date.range(~D[2027-03-01], ~D[2027-03-07]))
    :ok
    iex> Koyomi.Rules.check_range(rules, Date.range(~D[2027-03-01], ~D[2027-03-08]))
    {:error, {:too_long, max_days: 7}}

# `check_time`

```elixir
@spec check_time(t() | keyword(), Time.t()) :: :ok | {:error, error()}
```

Checks a time of day against `:min_time`, `:max_time` and `:step`.

## Examples

    iex> Koyomi.Rules.check_time([step: 15], ~T[09:20:00])
    {:error, {:time_step, step: 15}}

# `disabled?`

```elixir
@spec disabled?(t(), Date.t()) :: boolean()
```

Returns `true` when the day is disabled by `:disabled_dates`,
`:disabled_ranges` or `:disabled_weekdays` (the bounds aside).

# `max_date`

```elixir
@spec max_date(t()) :: Date.t() | nil
```

The last day that may be chosen, with `:today` resolved, or `nil`.

# `merge`

```elixir
@spec merge(t() | keyword() | nil, keyword()) :: t()
```

Merges more rules into rules: each option given replaces the one in
`rules`.

## Examples

    iex> rules = Koyomi.Rules.new(min: ~D[2027-03-01])
    iex> Koyomi.Rules.merge(rules, max: ~D[2027-03-31]).max
    ~D[2027-03-31]

# `min_date`

```elixir
@spec min_date(t()) :: Date.t() | nil
```

The first day that may be chosen, with `:today` resolved, or `nil`.

## Examples

    iex> Koyomi.Rules.min_date(Koyomi.Rules.new(min: ~D[2027-03-01]))
    ~D[2027-03-01]

# `new`

```elixir
@spec new(t() | keyword() | nil) :: t()
```

Builds rules from a keyword list (see the options above), or returns
rules as they are. `nil` gives rules that allow everything.

Raises `ArgumentError` for an unknown option or a value of the wrong
kind, so that a mistake shows when the rules are built.

## Examples

    iex> rules = Koyomi.Rules.new(min: ~D[2027-03-01], disabled_weekdays: [7])
    iex> rules.disabled_weekdays
    [7]

    iex> Koyomi.Rules.new(max_days: 0)
    ** (ArgumentError) Koyomi.Rules: :max_days must be a positive integer, got: 0

# `to_client`

```elixir
@spec to_client(t() | keyword() | nil) :: map()
```

The rules as the browser reads them: a map with JSON values, `:today`
resolved and the camelCase keys of the picker. Empty rules are left out.

## Examples

    iex> Koyomi.Rules.to_client(Koyomi.Rules.new(min: ~D[2027-03-01], disabled_weekdays: [7]))
    %{"min" => "2027-03-01", "disabledWeekdays" => [7]}

# `today`

```elixir
@spec today(t()) :: Date.t()
```

Today in the time zone of the rules.

## Examples

    iex> Koyomi.Rules.today(Koyomi.Rules.new([])) == Date.utc_today()
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
