lib/langtagmatcher.ex

defmodule LangTagMatcher do
  @moduledoc """
  """

  @type language :: String.t()
  @type languages :: list()
  @type subtag_rep :: list(map())

  @spec match(languages(), languages(), languages()) :: languages()
  @doc """
  Return a list of all languages in available that matches criteria,
  in the ascending order by the index of the matching criteria. The fallback
  languages are added at the back if they are not already in the list.
  """
  def match(criteria, available, fallback) do
    criteria_subtags = make_subtags_with_lang(criteria)
    available_subtags = make_subtags_with_lang(available)

    criteria_subtags
    |> Enum.flat_map(fn c -> languages_matched(available_subtags, c) end)
    |> Enum.map(fn {lang, _subtags} -> lang end)
    |> Enum.concat(fallback)
    |> Enum.uniq()
  end

  defp languages_matched(available_subtags, {_criterion, criterion_subtags}) do
    available_subtags
    |> Enum.filter(fn {_lang, lang_subtags} ->
      meets_criterion?(criterion_subtags, lang_subtags)
    end)
  end

  defp language_cmp(a, b) do
    cond do
      a["Subtag"] == b["Subtag"] ->
        :equal

      a["Record"]["Macrolanguage"] == b["Subtag"] ->
        :subset

      a["Subtag"] == b["Record"]["Macrolanguage"] ->
        :superset

      true ->
        :unrelated
    end
  end

  defp tag_reducer(idx) do
    fn cur, acc ->
      key = cur["Record"]["Type"]
      Map.put(acc, key, (acc[key] || [nil, nil]) |> List.replace_at(idx, cur["Subtag"]))
    end
  end

  defp subtags_cmp(as, bs) do
    subtags_map = Enum.reduce(as, %{}, tag_reducer(0))
    subtags_map = Enum.reduce(bs, subtags_map, tag_reducer(1))

    Enum.map(subtags_map, fn {_, [a, b]} ->
      cond do
        a == b -> :equal
        is_nil(a) and not is_nil(b) -> :superset
        not is_nil(a) and is_nil(b) -> :subset
        true -> :unrelated
      end
    end)
  end

  defp meets_criterion?([c_lang | c_rest] = _criterion, [a_lang | a_rest] = _available_lang) do
    with r when r in [:equal, :superset] <- language_cmp(c_lang, a_lang),
         rs <- subtags_cmp(c_rest, a_rest),
         true <- Enum.all?(rs, fn r -> r in [:equal, :superset] end) do
      true
    else
      _ -> false
    end
  end

  defp make_subtags_with_lang(langs) when is_list(langs) do
    Enum.map(langs, &make_subtags_with_lang/1)
  end

  defp make_subtags_with_lang(lang) do
    {lang, make_subtags(lang)}
  end

  defp make_subtags(lang) do
    LangTags.Tag.new(lang)
    |> make_preferred()
    |> LangTags.Tag.subtags()
  end

  defp make_preferred(tag) do
    pf = LangTags.Tag.preferred(tag)
    if pf, do: pf, else: tag
  end
end