Koyomi.Rules (Koyomi v0.2.1)

Copy Markdown View Source

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.

Summary

Types

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

t()

Functions

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

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

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

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.

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.

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

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

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

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

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

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

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.

Today in the time zone of the rules.

Types

bound()

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

error()

@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()

@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()
}

Functions

allowed?(rules, date)

@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(rules, date)

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

Checks a day. Returns :ok or {:error, error} (see 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(rules, dates)

@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(rules, value)

@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(rules, arg2)

@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(rules, time)

@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?(rules, date)

@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(rules)

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

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

merge(rules, opts)

@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(rules)

@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(rules)

@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(rules)

@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(rules)

@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