Skip to content

Commit

Permalink
Merge pull request #8 from Teamweek/regions
Browse files Browse the repository at this point in the history
Support to regions
  • Loading branch information
kelvinst authored Feb 28, 2018
2 parents 878a722 + 22afcde commit 6ded694
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 119 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.3.0

### Added

- Support for multiple regions on the same locale

## 0.2.1

### Added
Expand Down
62 changes: 38 additions & 24 deletions lib/holidefs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,28 @@ defmodule Holidefs do
Gettext.put_locale(Holidefs.Gettext, locale)
end

@doc """
Returns the list of regions from the given locale.
If succeed returns a `{:ok, regions}` tuple, otherwise
returns a `{:error, reason}` tuple.
"""
@spec get_regions(locale_code) :: {:ok, [String.t()]} | {:error, error_reasons}
def get_regions(locale) do
case Store.get_definition(locale) do
nil -> {:error, :no_def}
definition -> {:ok, Definition.get_regions(definition)}
end
end

@doc """
Returns all the holidays for the given locale on the given date.
If succeed returns a `{:ok, holidays}` tuple, otherwise
returns a `{:error, reason}` tuple.
"""
@spec on(locale_code, Date.t()) :: {:ok, [Holidefs.Holiday.t()]} | {:error, String.t()}
@spec on(locale_code, Date.t(), Holidefs.Options.t()) ::
@spec on(locale_code, Date.t()) :: {:ok, [Holidefs.Holiday.t()]} | {:error, error_reasons}
@spec on(locale_code, Date.t(), Holidefs.Options.attrs()) ::
{:ok, [Holidefs.Holiday.t()]} | {:error, error_reasons}
def on(locale, date, opts \\ []) do
locale
Expand All @@ -100,43 +114,43 @@ defmodule Holidefs do
returns a `{:error, reason}` tuple
"""
@spec year(locale_code, integer) :: {:ok, [Holidefs.Holiday.t()]} | {:error, String.t()}
@spec year(locale_code, integer, Holidefs.Options.t()) ::
@spec year(locale_code, integer, Holidefs.Options.attrs()) ::
{:ok, [Holidefs.Holiday.t()]} | {:error, error_reasons}
def year(locale, year, opts \\ []) do
def year(locale, year, opts \\ [])

def year(locale, year, opts) when is_integer(year) do
locale
|> Store.get_definition()
|> case do
nil ->
{:error, :no_def}

%Definition{rules: rules, code: locale} when is_integer(year) ->
{:ok, all_year_holidays(rules, year, locale, opts)}

_ ->
{:error, :invalid_date}
%Definition{} = definition ->
{:ok, all_year_holidays(definition, year, opts)}
end
end

@spec all_year_holidays(
[Holidefs.Definition.Rule.t()],
integer,
locale_code,
Holidefs.Options.t() | list
) :: [Holidefs.Holiday.t()]
def year(_, _, _) do
{:error, :invalid_date}
end

@spec all_year_holidays(Holidefs.Definition.t(), integer, Holidefs.Options.attrs()) :: [
Holidefs.Holiday.t()
]
defp all_year_holidays(
rules,
%Definition{code: code, rules: rules},
year,
locale,
%Options{include_informal?: include_informal?} = opts
%Options{include_informal?: include_informal?, regions: regions} = opts
) do
rules
|> Stream.filter(&(include_informal? or not &1.informal?))
|> Stream.flat_map(&Holiday.from_rule(locale, &1, year, opts))
|> Stream.filter(&(regions -- &1.regions != regions))
|> Stream.flat_map(&Holiday.from_rule(code, &1, year, opts))
|> Enum.sort_by(&Date.to_erl(&1.date))
end

defp all_year_holidays(rules, year, locale, opts) when is_list(opts) or is_map(opts) do
all_year_holidays(rules, year, locale, struct(Options, opts))
defp all_year_holidays(definition, year, opts) when is_list(opts) or is_map(opts) do
all_year_holidays(definition, year, Options.build(opts, definition))
end

@doc """
Expand All @@ -146,7 +160,7 @@ defmodule Holidefs do
If succeed returns a `{:ok, holidays}` tuple, otherwise
returns a `{:error, reason}` tuple
"""
@spec between(locale_code, Date.t(), Date.t(), Holidefs.Options.t()) ::
@spec between(locale_code, Date.t(), Date.t(), Holidefs.Options.attrs()) ::
{:ok, [Holidefs.Holiday.t()]} | {:error, error_reasons}
def between(locale, start, finish, opts \\ []) do
locale
Expand All @@ -159,14 +173,14 @@ defmodule Holidefs do
end

defp find_between(
%Definition{rules: rules, code: locale},
definition,
%Date{} = start,
%Date{} = finish,
opts
) do
holidays =
start.year..finish.year
|> Stream.flat_map(&all_year_holidays(rules, &1, locale, opts))
|> Stream.flat_map(&all_year_holidays(definition, &1, opts))
|> Stream.drop_while(&(Date.compare(&1.date, start) == :lt))
|> Enum.take_while(&(Date.compare(&1.date, finish) != :gt))

Expand Down
16 changes: 14 additions & 2 deletions lib/holidefs/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule Holidefs.Definition do
@doc """
Loads the definition for a locale code and name.
If any definition rule is invalid, a RuntimeError will be raised
If any definition rule is invalid, a `RuntimeError` will be raised
"""
@spec load!(atom, String.t()) :: t
def load!(code, name) do
Expand All @@ -43,7 +43,7 @@ defmodule Holidefs.Definition do
|> YamlElixir.read_from_file()
|> Map.get("months")
|> Enum.flat_map(fn {month, rules} ->
for rule <- rules, do: Rule.build(month, rule)
for rule <- rules, do: Rule.build(code, month, rule)
end)

%Definition{
Expand All @@ -52,4 +52,16 @@ defmodule Holidefs.Definition do
rules: rules
}
end

@doc """
Returns the list of regions from the definition.
"""
@spec get_regions(t) :: [String.t()]
def get_regions(%Definition{} = definition) do
definition
|> Map.get(:rules)
|> Stream.flat_map(&Map.get(&1, :regions))
|> Stream.uniq()
|> Enum.sort()
end
end
10 changes: 5 additions & 5 deletions lib/holidefs/definition/custom_functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ defmodule Holidefs.Definition.CustomFunctions do

@doc false
def qld_labour_day_october(year, _) do
if year in 2014..2015, do: DateCalculator.nth_day_of_week(year, 10, 1, 1)
if year in 2013..2015, do: DateCalculator.nth_day_of_week(year, 10, 1, 1)
end

@doc false
Expand All @@ -207,10 +207,10 @@ defmodule Holidefs.Definition.CustomFunctions do

@doc false
def to_weekday_if_boxing_weekend(%Date{} = date, _) do
if Date.day_of_week(date) in 6..7 do
Date.add(date, 2)
else
date
case Date.day_of_week(date) do
day when day in 6..7 -> Date.add(date, 2)
1 -> Date.add(date, 1)
_ -> date
end
end

Expand Down
25 changes: 19 additions & 6 deletions lib/holidefs/definition/rule.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Holidefs.Definition.Rule do
:week,
:weekday,
:function,
:regions,
:observed,
:year_ranges,
informal?: false
Expand All @@ -26,6 +27,7 @@ defmodule Holidefs.Definition.Rule do
week: integer,
weekday: integer,
function: function,
regions: [String.t()],
observed: function,
year_ranges: map | nil,
informal?: boolean
Expand All @@ -37,8 +39,8 @@ defmodule Holidefs.Definition.Rule do
@doc """
Builds a new rule from its month and definition map
"""
@spec build(integer, map) :: t
def build(month, %{"name" => name, "function" => func} = map) do
@spec build(atom, integer, map) :: t
def build(code, month, %{"name" => name, "function" => func} = map) do
%Rule{
name: name,
month: month,
Expand All @@ -48,14 +50,15 @@ defmodule Holidefs.Definition.Rule do
year_ranges: map["year_ranges"],
informal?: map["type"] == "informal",
observed: observed_from_name(map["observed"]),
regions: load_regions(map, code),
function:
func
|> function_from_name()
|> load_function(map["function_modifier"])
}
end

def build(month, %{"name" => name, "week" => week, "wday" => wday} = map)
def build(code, month, %{"name" => name, "week" => week, "wday" => wday} = map)
when week in @valid_weeks and wday in @valid_weekdays do
%Rule{
name: name,
Expand All @@ -64,21 +67,31 @@ defmodule Holidefs.Definition.Rule do
weekday: wday,
year_ranges: map["year_ranges"],
informal?: map["type"] == "informal",
observed: observed_from_name(map["observed"])
observed: observed_from_name(map["observed"]),
regions: load_regions(map, code)
}
end

def build(month, %{"name" => name, "mday" => day} = map) do
def build(code, month, %{"name" => name, "mday" => day} = map) do
%Rule{
name: name,
month: month,
day: day,
year_ranges: map["year_ranges"],
informal?: map["type"] == "informal",
observed: observed_from_name(map["observed"])
observed: observed_from_name(map["observed"]),
regions: load_regions(map, code)
}
end

defp load_regions(%{"regions" => regions}, code) do
Enum.map(regions, &String.replace(&1, "#{code}_", ""))
end

defp load_regions(_, _) do
[]
end

defp load_function(function, nil) do
function
end
Expand Down
9 changes: 2 additions & 7 deletions lib/holidefs/holiday.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Holidefs.Holiday do

defstruct [:name, :raw_date, :observed_date, :date, informal?: false]

@type t :: %Holiday{
@type t :: %Holidefs.Holiday{
name: String.t(),
raw_date: Date.t(),
observed_date: Date.t(),
Expand All @@ -22,7 +22,7 @@ defmodule Holidefs.Holiday do
Returns a list of holidays for the definition rule on the given year
"""
@spec from_rule(atom, Holidefs.Definition.Rule.t(), integer, Holidefs.Options.t()) :: [t]
def from_rule(code, %Rule{year_ranges: year_ranges} = rule, year, opts \\ []) do
def from_rule(code, %Rule{year_ranges: year_ranges} = rule, year, opts \\ %Options{}) do
if in_year_ranges?(year_ranges, year) do
build_from_rule(code, rule, year, opts)
else
Expand All @@ -43,7 +43,6 @@ defmodule Holidefs.Holiday do
defp in_year_range?(%{"limited" => years}, year), do: year in years
defp in_year_range?(%{"between" => years}, year), do: year in years

@spec build_from_rule(atom, Holidefs.Definition.Rule.t(), integer, Holidefs.Options.t()) :: [t]
defp build_from_rule(
code,
%Rule{name: name, function: fun, informal?: informal?} = rule,
Expand Down Expand Up @@ -130,10 +129,6 @@ defmodule Holidefs.Holiday do
load_date(rule, date, observed?)
end

defp load_date(rule, date, opts) when is_list(opts) or is_map(opts) do
load_date(rule, date, struct(Options, opts))
end

defp load_date(rule, date, true) do
load_observed(rule, date)
end
Expand Down
50 changes: 42 additions & 8 deletions lib/holidefs/options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,54 @@ defmodule Holidefs.Options do
@moduledoc """
Here is the list of options you can send to `Holidefs` functions:
* `regions` - a list of strings to define what region will be loaded. When
empty it fallbacks to the basic region of the locale, which is the region
with the same code of the locale. Defaults to `[]`
* `include_informal?` - flag to include the informal holidays on the
return list. Defaults to `false`
* `observed?` - flag to consider the `observed_date` of the holidays as
the `date`. Defaults to `false`
"""

defstruct include_informal?: false, observed?: false
alias Holidefs.Definition
alias Holidefs.Options

@type t ::
Keyword.t()
| map
| %Holidefs.Options{
include_informal?: boolean,
observed?: boolean
}
defstruct regions: [], include_informal?: false, observed?: false

@type attrs :: [] | Keyword.t() | map
@type t :: %Holidefs.Options{
regions: [String.t()],
include_informal?: boolean,
observed?: boolean
}

@doc """
Builds a new `Options` struct with normalized fields.
The `definition` is used to get the all the regions and code.
"""
@spec build(attrs, Holidefs.Definition.t()) :: Holidefs.Options.t()
def build(attrs, %Definition{} = definition) do
opts = struct(Options, attrs)
%{opts | regions: get_regions(opts.regions, definition)}
end

defp get_regions([""], definition) do
Definition.get_regions(definition)
end

defp get_regions(regions, definition) when is_list(regions) do
[main_region(definition) | regions]
end

defp get_regions(region, definition) when is_bitstring(region) do
[main_region(definition), region]
end

defp get_regions(regions, definition) when regions in [[], nil] do
[main_region(definition)]
end

defp main_region(%Definition{code: code}), do: Atom.to_string(code)
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Holidefs.Mixfile do
def project do
[
app: :holidefs,
version: "0.2.1",
version: "0.3.0",
elixir: "~> 1.5",
description: "Definition-based national holidays",
source_url: @github_url,
Expand Down
Loading

0 comments on commit 6ded694

Please sign in to comment.