defmodule LibJudge.Rule do
@moduledoc """
Defines the `Rule` structure and provides methods for generating
and working with them
"""
import LibJudge.Tokenizer.Guards
alias LibJudge.Rule.InvalidPartError
@type rule_type :: :category | :subcategory | :rule | :subrule
@type t :: %__MODULE__{
category: String.t(),
subcategory: String.t(),
rule: String.t(),
subrule: String.t(),
type: rule_type()
}
defstruct [:category, :subcategory, :rule, :subrule, :type]
@rule_regex ~r/\b[1-9](?:\d{2}(?:\.\d{1,3}(?:\-\d{1,3}|[a-z](?:\-[b-z])?)?\b|\.)?|\.)/
@doc """
Creates a `Rule` struct from a string
## Examples
iex> LibJudge.Rule.from_string("702.21j")
{:ok, %LibJudge.Rule{type: :subrule, category: "7", subcategory: "02", rule: "21", subrule: "j"}}
"""
@spec from_string(String.t()) :: {:ok, t} | {:error, String.t()}
def from_string(str) when is_binary(str) do
opts =
try do
split!(str)
rescue
err in InvalidPartError -> {:error, err}
end
case opts do
{:error, reason} -> {:error, reason}
_ -> {:ok, struct(__MODULE__, opts)}
end
end
def from_string(_not_a_str) do
{:error, "input is not a string"}
end
@doc """
Creates a list of `Rule`s referenced in a string
## Examples
iex> LibJudge.Rule.all_from_string("See rules 702.21j and 702.108.")
{
:ok,
[
%LibJudge.Rule{type: :subrule, category: "7", subcategory: "02", rule: "21", subrule: "j"},
%LibJudge.Rule{type: :rule, category: "7", subcategory: "02", rule: "108", subrule: nil}
]
}
"""
@spec all_from_string(String.t()) :: {:ok, [t]} | {:error, String.t()}
def all_from_string(str) when is_binary(str) do
# what the fuck wizards
clean_str = String.replace(str, "–", "-")
rules =
@rule_regex
|> Regex.scan(clean_str)
|> List.flatten()
|> Stream.map(&from_string/1)
|> Stream.filter(fn
{:ok, _} -> true
_ -> false
end)
|> Enum.map(fn {:ok, x} -> x end)
{:ok, rules}
end
def all_from_string(_not_a_str) do
{:error, "input is not a string"}
end
@doc """
Turns a `Rule` back into a string
## Examples
iex> LibJudge.Rule.to_string!(%LibJudge.Rule{type: :subrule, category: "7", subcategory: "02", rule: "21", subrule: "j"})
"702.21j"
"""
@spec to_string!(t()) :: String.t() | no_return
def to_string!(rule = %{__struct__: kind}) when kind == __MODULE__ do
case rule do
%__MODULE__{
type: :subrule,
category: cat,
subcategory: subcat,
rule: rule,
subrule: subrule
} ->
validate_cat!(cat)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(subrule)
cat <> subcat <> "." <> rule <> subrule
%__MODULE__{type: :rule, category: cat, subcategory: subcat, rule: rule} ->
validate_cat!(cat)
validate_subcat!(subcat)
validate_rule!(rule)
cat <> subcat <> "." <> rule <> "."
%__MODULE__{type: :subcategory, category: cat, subcategory: subcat} ->
validate_cat!(cat)
validate_subcat!(subcat)
cat <> subcat <> "."
%__MODULE__{type: :category, category: cat} ->
validate_cat!(cat)
cat <> "."
end
end
@doc """
Turns a `Rule` back into a string
Non-bang variant
## Examples
iex> LibJudge.Rule.to_string(%LibJudge.Rule{type: :category, category: "1"})
{:ok, "1."}
"""
@spec to_string(t()) :: {:ok, String.t()} | {:error, reason :: any}
def to_string(rule) do
{:ok, to_string!(rule)}
rescue
ArgumentError ->
{:error, {:invalid_rule, "missing properties for type"}}
err in FunctionClauseError ->
case err.function do
:to_string! -> {:error, {:invalid_rule, "not a %Rule{}"}}
_ -> {:error, err}
end
err ->
{:error, err}
end
defp split!(rule = <<cat::utf8, subcat_1::utf8, subcat_2::utf8>>)
when cat in 48..57 and subcat_1 in 48..57 and subcat_2 in 48..57,
do: split!(rule <> ".")
defp split!(<<cat::utf8, ".">>) when cat in 48..57 do
validate_cat!(<<cat>>)
[category: <<cat>>, type: :category]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".">>) when cat in 48..57 do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
[category: <<cat>>, subcategory: subcat, type: :subcategory]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(1), ".">>)
when cat in 48..57 and is_rule_1(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(2), ".">>)
when cat in 48..57 and is_rule_2(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(3), ".">>)
when cat in 48..57 and is_rule_3(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(1), subrule::utf8>>)
when cat in 48..57 and subrule in 97..122 and is_rule_1(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(2), subrule::utf8>>)
when cat in 48..57 and subrule in 97..122 and is_rule_2(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(3), subrule::utf8>>)
when cat in 48..57 and subrule in 97..122 and is_rule_3(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
# these are a hack to make not-strictly-correct rule ids like
# '205.1' (should be '205.1.') work to make this more friendly
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(1)>>)
when cat in 48..57 and is_rule_1(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(2)>>)
when cat in 48..57 and is_rule_2(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
defp split!(<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(3)>>)
when cat in 48..57 and is_rule_3(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
type: :rule
]
end
# these are a hack to make wotc's typo'd rule ids like
# '119.1d.' (should be '119.1d') work to make this more friendly
defp split!(
<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(1), subrule::utf8, ".">>
)
when cat in 48..57 and subrule in 97..122 and is_rule_1(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
defp split!(
<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(2), subrule::utf8, ".">>
)
when cat in 48..57 and subrule in 97..122 and is_rule_2(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
defp split!(
<<cat::utf8, subcat::binary-size(2), ".", rule::binary-size(3), subrule::utf8, ".">>
)
when cat in 48..57 and subrule in 97..122 and is_rule_3(rule) do
validate_cat!(<<cat>>)
validate_subcat!(subcat)
validate_rule!(rule)
validate_subrule!(<<subrule>>)
[
category: <<cat>>,
subcategory: subcat,
rule: rule,
subrule: <<subrule>>,
type: :subrule
]
end
defp split!(str) do
{:error, "invalid rule: #{inspect(str)}"}
end
defp validate_cat!(cat) when is_binary(cat) do
unless String.match?(cat, ~r/^\d$/) do
raise InvalidPartError, {:category, cat}
end
end
defp validate_subcat!(subcat) do
unless String.match?(subcat, ~r/^\d\d$/) do
raise InvalidPartError, {:subcategory, subcat}
end
end
defp validate_rule!(rule) do
unless String.match?(rule, ~r/^\d\d?\d?$/) do
raise InvalidPartError, {:rule, rule}
end
end
defp validate_subrule!(subrule) do
unless String.match?(subrule, ~r/^[a-z]$/) do
raise InvalidPartError, {:subrule, subrule}
end
end
end
defmodule LibJudge.Rule.InvalidPartError do
@moduledoc """
An exception raised when validating `LibJudge.Rule` structs.
"""
alias __MODULE__
defexception [:message, :part, :value]
@doc false
@impl Exception
def exception({part, value}) do
msg = "invalid part:\n\tPart:\t#{inspect(part)}\n\tValue:\t#{inspect(value)}"
%InvalidPartError{message: msg, part: part, value: value}
end
def exception([]) do
msg = "invalid part"
%InvalidPartError{message: msg}
end
def exception(part) do
msg = "invalid part:\n\tPart:\t#{inspect(part)}"
%InvalidPartError{message: msg, part: part}
end
end