Use Elixir stdlib File.chmod instead of shelling out

defmodule ReleaseManager.Utils do
  @moduledoc """
  This module provides helper functions for the `mix release` and
  `mix release.clean` tasks.
  """
  import Mix.Shell,    only: [cmd: 2]

  # Relx constants
  @relx_output_path      "rel"

  @doc """
  Perform some actions within the context of a specific mix environment
  """
  def with_env(env, fun) do
    old_env = Mix.env
    try do
      # Change env
      Mix.env(env)
      fun.()
    after
      # Change back
      Mix.env(old_env)
    end
  end

  @doc """
  Load the current project's configuration
  """
  def load_config(env) do
    with_env env, fn ->
      if File.regular?("config/config.exs") do
        Mix.Config.read! "config/config.exs"
      else
        []
      end
    end
  end

  @doc """
  Call the _elixir mix binary with the given arguments
  """
  def mix(command, :quiet),        do: mix(command, :dev, :quiet)
  def mix(command, :verbose),      do: mix(command, :dev, :verbose)
  def mix(command, env),           do: mix(command, env, :quiet)
  def mix(command, env, :quiet),   do: do_cmd("MIX_ENV=#{env} mix #{command}", &ignore/1)
  def mix(command, env, :verbose), do: do_cmd("MIX_ENV=#{env} mix #{command}", &IO.write/1)
  @doc """
  Change user permissions for a target file or directory
  """
  def chmod(target, flags) do
    case File.chmod(target, flags) do
      :ok         -> :ok
      {:error, _} -> :ok
    end
  end
  @doc """
  Execute `relx`
  """
  def relx(name, version, verbosity, upgrade?, dev_mode?) do
    # Setup paths
    config     = rel_file_dest_path "relx.config"
    output_dir = @relx_output_path |> Path.expand
    # Convert friendly verbosity names to relx values
    v = case verbosity do
      :silent  -> 0
      :quiet   -> 1
      :normal  -> 2
      :verbose -> 3
      _        -> 2 # Normal if we get an odd value
    end
    # Let relx do the heavy lifting
    relx_args = [
        log_level: v,
        root_dir: '#{File.cwd!}',
        config: '#{config}',
        relname: '#{name}',
        relvsn: '#{version}',
        output_dir: '#{output_dir}',
        dev_mode: dev_mode?
      ]
    result = case upgrade? do
      false -> :relx.do relx_args, ['release', 'tar']
      true  ->
        last_release = get_last_release(name)
        :relx.do [{:upfrom, '#{last_release}'} | relx_args], ['release', 'relup', 'tar']
    end
    case result do
      {:ok, _state} -> :ok
      {:error, _e}  -> {:error, "Failed to build release. Please fix any errors and try again."}
    end
  end

  @doc "Print an informational message without color"
  def debug(message), do: IO.puts "==> #{message}"
  @doc "Print an informational message in green"
  def info(message),  do: IO.puts "==> #{IO.ANSI.green}#{message}#{IO.ANSI.reset}"
  @doc "Print a warning message in yellow"
  def warn(message),  do: IO.puts "==> #{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}"
  @doc "Print a notice in yellow"
  def notice(message), do: IO.puts "#{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}"
  @doc "Print an error message in red"
  def error(message), do: IO.puts "==> #{IO.ANSI.red}#{message}#{IO.ANSI.reset}"

  @doc "Exits with exit status 1"
  def abort!, do: exit({:shutdown, 1})

  @doc """
  Get a list of tuples representing the previous releases:

  ## Examples

      get_releases #=> [{"test", "0.0.1"}, {"test", "0.0.2"}]

  """
  def get_releases(project) do
    release_path = Path.join([File.cwd!, "rel", project, "releases"])
    case release_path |> File.exists? do
      false -> []
      true  ->
        release_path
        |> File.ls!
        |> Enum.reject(fn entry -> entry in ["RELEASES", "start_erl.data"] end)
        |> Enum.map(fn version -> {project, version} end)
    end
  end

  @doc """
  Get the most recent release prior to the current one
  """
  def get_last_release(project) do
    hd(project |> get_releases |> Enum.map(fn {_, v} -> v end) |> sort_versions)
  end

  @doc """
  Sort a list of versions, latest one first. Tries to use semver version
  compare, but can fall back to regular string compare.
  """
  def sort_versions(versions) do
    versions |> Enum.sort(
      fn v1, v2 ->
        case { parse_version(v1), parse_version(v2) } do
          {{:semantic, v1}, {:semantic, v2}} ->
            case Version.compare(v1, v2) do
              :gt -> true
              _   -> false
            end;
          {{_, v1}, {_, v2}} ->
            v1 >  v2
        end
      end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} -> {:semantic, semver}
      :error        -> {:nonsemantic, ver}
    end
  end

  @doc """
  Get the local path of the current elixir executable
  """
  def get_elixir_lib_path() do
    [elixir_lib_path, _ ] = "#{:code.which(Mix)}"
    |> String.split "mix/ebin/Elixir.Mix.beam"

    elixir_lib_path
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  def write_term(path, term) do
    :file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term]))
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  def write_terms(path, terms) when is_list(terms) do
    format_str = String.duplicate("~p.\n\n", Enum.count(terms)) |> String.to_char_list
    :file.write_file('#{path}', :io_lib.fwrite(format_str, terms |> Enum.reverse))
  end

  @doc """
  Reads a file as Erlang terms
  """
  def read_terms(path) do
    result = case '#{path}' |> :file.consult do
      {:ok, terms} ->
        terms
      {:error, {line, type, msg}} ->
        error "Unable to parse #{path}: Line #{line}, #{type}, - #{msg}"
        abort!
      {:error, reason} ->
        error "Unable to access #{path}: #{reason}"
        abort!
    end
    result
  end

  @doc """
  Convert a string to Erlang terms
  """
  def string_to_terms(str) do
    str
    |> String.split("}.")
    |> Stream.map(&(String.strip(&1, ?\n)))
    |> Stream.map(&String.strip/1)
    |> Stream.map(&('#{&1}}.'))
    |> Stream.map(&(:erl_scan.string(&1)))
    |> Stream.map(fn {:ok, tokens, _} -> :erl_parse.parse_term(tokens) end)
    |> Stream.filter(fn {:ok, _} -> true; {:error, _} -> false end)
    |> Enum.reduce([], fn {:ok, term}, acc -> [term|acc] end)
    |> Enum.reverse
  end

  @doc """
  Merges two sets of Elixir/Erlang terms, where the terms come
  in the form of lists of tuples. For example, such as is found
  in the relx.config file
  """
  def merge(old, new) when is_list(old) and is_list(new) do
    merge(old, new, [])
  end

  defp merge([h|t], new, acc) when is_tuple(h) do
    case :lists.keytake(elem(h, 0), 1, new) do
      {:value, new_value, rest} ->
        # Value is present in new, so merge the value
        merged = merge_term(h, new_value)
        merge(t, rest, [merged|acc])
      false ->
        # Value doesn't exist in new, so add it
        merge(t, new, [h|acc])
    end
  end
  defp merge([], new, acc) do
    Enum.reverse(acc, new)
  end

  defp merge_term(old, new) when is_tuple(old) and is_tuple(new) do
    old
    |> Tuple.to_list
    |> Enum.with_index
    |> Enum.reduce([], fn
        {[], idx}, acc ->
          [elem(new, idx)|acc]
        {val, idx}, acc when is_list(val) ->
          case :io_lib.char_list(val) do
            true ->
              [elem(new, idx)|acc]
            false ->
              merged = val |> Enum.concat(elem(new, idx)) |> Enum.uniq
              [merged|acc]
          end
        {val, idx}, acc when is_tuple(val) ->
          [merge_term(val, elem(new, idx))|acc]
        {_val, idx}, acc ->
          [elem(new, idx)|acc]
       end)
    |> Enum.reverse
    |> List.to_tuple
  end

  @doc "Get the priv path of the exrm dependency"
  def priv_path, do: "#{:code.priv_dir('exrm')}"
  @doc "Get the priv/rel path of the exrm dependency"
  def rel_source_path,       do: Path.join(priv_path, "rel")
  @doc "Get the path to a file located in priv/rel of the exrm dependency"
  def rel_source_path(file), do: Path.join(rel_source_path, file)
  @doc "Get the priv/rel/files path of the exrm dependency"
  def rel_file_source_path,       do: Path.join([priv_path, "rel", "files"])
  @doc "Get the path to a file located in priv/rel/files of the exrm dependency"
  def rel_file_source_path(file), do: Path.join(rel_file_source_path, file)
  @doc """
  Get the path to a file located in the rel directory of the current project.
  You can pass either a file name, or a list of directories to a file, like:

      iex> ReleaseManager.Utils.rel_dest_path "relx.config"
      "path/to/project/rel/relx.config"

      iex> ReleaseManager.Utils.rel_dest_path ["<project>", "lib", "<project>.appup"]
      "path/to/project/rel/<project>/lib/<project>.appup"

  """
  def rel_dest_path(files) when is_list(files), do: Path.join([rel_dest_path] ++ files)
  def rel_dest_path(file),                      do: Path.join(rel_dest_path, file)
  @doc "Get the rel path of the current project."
  def rel_dest_path,                            do: Path.join(File.cwd!, "rel")
  @doc """
  Get the path to a file located in the rel/files directory of the current project.
  You can pass either a file name, or a list of directories to a file, like:

      iex> ReleaseManager.Utils.rel_file_dest_path "sys.config"
      "path/to/project/rel/files/sys.config"

      iex> ReleaseManager.Utils.rel_dest_path ["some", "path", "file.txt"]
      "path/to/project/rel/files/some/path/file.txt"

  """
  def rel_file_dest_path(files) when is_list(files), do: Path.join([rel_file_dest_path] ++ files)
  def rel_file_dest_path(file),                      do: Path.join(rel_file_dest_path, file)
  @doc "Get the rel/files path of the current project."
  def rel_file_dest_path,                            do: Path.join([File.cwd!, "rel", "files"])

  # Ignore a message when used as the callback for Mix.Shell.cmd
  defp ignore(_), do: nil

  defp do_cmd(command, callback) do
    case cmd(command, callback) do
      0 -> :ok
      _ -> {:error, "Release step failed. Please fix any errors and try again."}
    end
  end

end

defmodule ReleaseManager.Utils do
  @moduledoc """
  This module provides helper functions for the `mix release` and
  `mix release.clean` tasks.
  """
  import Mix.Shell,    only: [cmd: 2]

  # Relx constants
  @relx_output_path      "rel"

  @doc """
  Perform some actions within the context of a specific mix environment
  """
  def with_env(env, fun) do
    old_env = Mix.env
    try do
      # Change env
      Mix.env(env)
      fun.()
    after
      # Change back
      Mix.env(old_env)
    end
  end

  @doc """
  Load the current project's configuration
  """
  def load_config(env) do
    with_env env, fn ->
      if File.regular?("config/config.exs") do
        Mix.Config.read! "config/config.exs"
      else
        []
      end
    end
  end

  @doc """
  Call the _elixir mix binary with the given arguments
  """
  def mix(command, :quiet),        do: mix(command, :dev, :quiet)
  def mix(command, :verbose),      do: mix(command, :dev, :verbose)
  def mix(command, env),           do: mix(command, env, :quiet)
  def mix(command, env, :quiet),   do: do_cmd("MIX_ENV=#{env} mix #{command}", &ignore/1)
  def mix(command, env, :verbose), do: do_cmd("MIX_ENV=#{env} mix #{command}", &IO.write/1)
  @doc """
  Change user permissions for a target file or directory
  """
  def chmod(target, flags), do: do_cmd("chmod #{flags} #{target}", &ignore/1)
  @doc """
  Execute `relx`
  """
  def relx(name, version, verbosity, upgrade?, dev_mode?) do
    # Setup paths
    config     = rel_file_dest_path "relx.config"
    output_dir = @relx_output_path |> Path.expand
    # Convert friendly verbosity names to relx values
    v = case verbosity do
      :silent  -> 0
      :quiet   -> 1
      :normal  -> 2
      :verbose -> 3
      _        -> 2 # Normal if we get an odd value
    end
    # Let relx do the heavy lifting
    relx_args = [
        log_level: v,
        root_dir: '#{File.cwd!}',
        config: '#{config}',
        relname: '#{name}',
        relvsn: '#{version}',
        output_dir: '#{output_dir}',
        dev_mode: dev_mode?
      ]
    result = case upgrade? do
      false -> :relx.do relx_args, ['release', 'tar']
      true  ->
        last_release = get_last_release(name)
        :relx.do [{:upfrom, '#{last_release}'} | relx_args], ['release', 'relup', 'tar']
    end
    case result do
      {:ok, _state} -> :ok
      {:error, _e}  -> {:error, "Failed to build release. Please fix any errors and try again."}
    end
  end

  @doc "Print an informational message without color"
  def debug(message), do: IO.puts "==> #{message}"
  @doc "Print an informational message in green"
  def info(message),  do: IO.puts "==> #{IO.ANSI.green}#{message}#{IO.ANSI.reset}"
  @doc "Print a warning message in yellow"
  def warn(message),  do: IO.puts "==> #{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}"
  @doc "Print a notice in yellow"
  def notice(message), do: IO.puts "#{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}"
  @doc "Print an error message in red"
  def error(message), do: IO.puts "==> #{IO.ANSI.red}#{message}#{IO.ANSI.reset}"

  @doc "Exits with exit status 1"
  def abort!, do: exit({:shutdown, 1})

  @doc """
  Get a list of tuples representing the previous releases:

  ## Examples

      get_releases #=> [{"test", "0.0.1"}, {"test", "0.0.2"}]

  """
  def get_releases(project) do
    release_path = Path.join([File.cwd!, "rel", project, "releases"])
    case release_path |> File.exists? do
      false -> []
      true  ->
        release_path
        |> File.ls!
        |> Enum.reject(fn entry -> entry in ["RELEASES", "start_erl.data"] end)
        |> Enum.map(fn version -> {project, version} end)
    end
  end

  @doc """
  Get the most recent release prior to the current one
  """
  def get_last_release(project) do
    hd(project |> get_releases |> Enum.map(fn {_, v} -> v end) |> sort_versions)
  end

  @doc """
  Sort a list of versions, latest one first. Tries to use semver version
  compare, but can fall back to regular string compare.
  """
  def sort_versions(versions) do
    versions |> Enum.sort(
      fn v1, v2 ->
        case { parse_version(v1), parse_version(v2) } do
          {{:semantic, v1}, {:semantic, v2}} ->
            case Version.compare(v1, v2) do
              :gt -> true
              _   -> false
            end;
          {{_, v1}, {_, v2}} ->
            v1 >  v2
        end
      end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} -> {:semantic, semver}
      :error        -> {:nonsemantic, ver}
    end
  end

  @doc """
  Get the local path of the current elixir executable
  """
  def get_elixir_lib_path() do
    [elixir_lib_path, _ ] = "#{:code.which(Mix)}"
    |> String.split "mix/ebin/Elixir.Mix.beam"

    elixir_lib_path
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  def write_term(path, term) do
    :file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term]))
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  def write_terms(path, terms) when is_list(terms) do
    format_str = String.duplicate("~p.\n\n", Enum.count(terms)) |> String.to_char_list
    :file.write_file('#{path}', :io_lib.fwrite(format_str, terms |> Enum.reverse))
  end

  @doc """
  Reads a file as Erlang terms
  """
  def read_terms(path) do
    result = case '#{path}' |> :file.consult do
      {:ok, terms} ->
        terms
      {:error, {line, type, msg}} ->
        error "Unable to parse #{path}: Line #{line}, #{type}, - #{msg}"
        abort!
      {:error, reason} ->
        error "Unable to access #{path}: #{reason}"
        abort!
    end
    result
  end

  @doc """
  Convert a string to Erlang terms
  """
  def string_to_terms(str) do
    str
    |> String.split("}.")
    |> Stream.map(&(String.strip(&1, ?\n)))
    |> Stream.map(&String.strip/1)
    |> Stream.map(&('#{&1}}.'))
    |> Stream.map(&(:erl_scan.string(&1)))
    |> Stream.map(fn {:ok, tokens, _} -> :erl_parse.parse_term(tokens) end)
    |> Stream.filter(fn {:ok, _} -> true; {:error, _} -> false end)
    |> Enum.reduce([], fn {:ok, term}, acc -> [term|acc] end)
    |> Enum.reverse
  end

  @doc """
  Merges two sets of Elixir/Erlang terms, where the terms come
  in the form of lists of tuples. For example, such as is found
  in the relx.config file
  """
  def merge(old, new) when is_list(old) and is_list(new) do
    merge(old, new, [])
  end

  defp merge([h|t], new, acc) when is_tuple(h) do
    case :lists.keytake(elem(h, 0), 1, new) do
      {:value, new_value, rest} ->
        # Value is present in new, so merge the value
        merged = merge_term(h, new_value)
        merge(t, rest, [merged|acc])
      false ->
        # Value doesn't exist in new, so add it
        merge(t, new, [h|acc])
    end
  end
  defp merge([], new, acc) do
    Enum.reverse(acc, new)
  end

  defp merge_term(old, new) when is_tuple(old) and is_tuple(new) do
    old
    |> Tuple.to_list
    |> Enum.with_index
    |> Enum.reduce([], fn
        {[], idx}, acc ->
          [elem(new, idx)|acc]
        {val, idx}, acc when is_list(val) ->
          case :io_lib.char_list(val) do
            true ->
              [elem(new, idx)|acc]
            false ->
              merged = val |> Enum.concat(elem(new, idx)) |> Enum.uniq
              [merged|acc]
          end
        {val, idx}, acc when is_tuple(val) ->
          [merge_term(val, elem(new, idx))|acc]
        {_val, idx}, acc ->
          [elem(new, idx)|acc]
       end)
    |> Enum.reverse
    |> List.to_tuple
  end

  @doc "Get the priv path of the exrm dependency"
  def priv_path, do: "#{:code.priv_dir('exrm')}"
  @doc "Get the priv/rel path of the exrm dependency"
  def rel_source_path,       do: Path.join(priv_path, "rel")
  @doc "Get the path to a file located in priv/rel of the exrm dependency"
  def rel_source_path(file), do: Path.join(rel_source_path, file)
  @doc "Get the priv/rel/files path of the exrm dependency"
  def rel_file_source_path,       do: Path.join([priv_path, "rel", "files"])
  @doc "Get the path to a file located in priv/rel/files of the exrm dependency"
  def rel_file_source_path(file), do: Path.join(rel_file_source_path, file)
  @doc """
  Get the path to a file located in the rel directory of the current project.
  You can pass either a file name, or a list of directories to a file, like:

      iex> ReleaseManager.Utils.rel_dest_path "relx.config"
      "path/to/project/rel/relx.config"

      iex> ReleaseManager.Utils.rel_dest_path ["<project>", "lib", "<project>.appup"]
      "path/to/project/rel/<project>/lib/<project>.appup"

  """
  def rel_dest_path(files) when is_list(files), do: Path.join([rel_dest_path] ++ files)
  def rel_dest_path(file),                      do: Path.join(rel_dest_path, file)
  @doc "Get the rel path of the current project."
  def rel_dest_path,                            do: Path.join(File.cwd!, "rel")
  @doc """
  Get the path to a file located in the rel/files directory of the current project.
  You can pass either a file name, or a list of directories to a file, like:

      iex> ReleaseManager.Utils.rel_file_dest_path "sys.config"
      "path/to/project/rel/files/sys.config"

      iex> ReleaseManager.Utils.rel_dest_path ["some", "path", "file.txt"]
      "path/to/project/rel/files/some/path/file.txt"

  """
  def rel_file_dest_path(files) when is_list(files), do: Path.join([rel_file_dest_path] ++ files)
  def rel_file_dest_path(file),                      do: Path.join(rel_file_dest_path, file)
  @doc "Get the rel/files path of the current project."
  def rel_file_dest_path,                            do: Path.join([File.cwd!, "rel", "files"])

  # Ignore a message when used as the callback for Mix.Shell.cmd
  defp ignore(_), do: nil

  defp do_cmd(command, callback) do
    case cmd(command, callback) do
      0 -> :ok
      _ -> {:error, "Release step failed. Please fix any errors and try again."}
    end
  end

end


Fix typo in elm controller tests.

Fix typo in elm controller tests.

defmodule ParkinsonsAndMe.ElmControllerTest do
  use ParkinsonsAndMe.ConnCase
  alias ParkinsonsAndMe.{DatabaseSeeder, User, AnswerSet, Answer, Quote}

  @user %User{
    name: "Anne Onymous",
    age_range: "fifties",
    postcode: "e82na",
    email: "anne@onymous.com"
  }

  setup do
    Repo.insert!(@user)
    DatabaseSeeder.populate_db()
    submit_answers_for_user("Anne Onymous")
  end

  def submit_answers_for_user(username) do
    user = Repo.get_by(User, name: username)
    quotes = Repo.all(Quote)
    {:ok, answer_set} = Repo.insert(%AnswerSet{user_id: user.id, uuid: "12345678"})
    answers = quotes |> Enum.map(fn(x) -> %{quote_id: x.id, answer_set_id: answer_set.id, answer: true} end)
    Repo.insert_all(Answer, answers)
    :ok
  end

  test "loads resources for the elm app", %{conn: conn} do
    conn = get conn, "/"
    page = html_response(conn, 200)
    assert page =~ "<title>Parkinson's and Me</title>"
    assert page =~ "src=\"/js/elm.js"
  end

  test "loads all quotes services and weightings", %{conn: conn} do
    conn = get conn, "/api/quotes-services-weightings"
    %{"data" =>
      %{"quotes" => quotes,
        "services" => services,
        "weightings" => weightings}} = json_response(conn, 200)
    assert length(quotes) > 0
    assert length(services) > 0
    assert length(weightings) == (length(quotes) * length(services))
  end

  test "loads all the data for the elm app based on an answer uuid", %{conn: conn} do
    conn = get conn, "/api/my-results/12345678"
    %{"data" =>
      %{"quotes" => quotes,
        "services" => _,
        "weightings" => _,
        "user" => user,
        "answers" => answers}} = json_response(conn, 200)
    assert length(answers) == length(quotes)
    assert user["name"] == "Anne Onymous"
  end
end

defmodule ParkinsonsAndMe.ElmControllerTest do
  use ParkinsonsAndMe.ConnCase
  alias ParkinsonsAndMe.{DatabaseSeeder, User, AnswerSet, Answer, Quote}

  @user %User{
    name: "Anne Onymous",
    age_range: "fifties",
    postcode: "e82na",
    email: "anne@onymous.com"
  }

  setup do
    Repo.insert!(@user)
    DatabaseSeeder.populate_db()
    submit_answers_for_user("Anne Onymous")
  end

  def submit_answers_for_user(username) do
    user = Repo.get_by(User, name: username)
    quotes = Repo.all(Quote)
    {:ok, answer_set} = Repo.insert(%AnswerSet{user_id: user.id, uuid: "12345678"})
    answers = quotes |> Enum.map(fn(x) -> %{quote_id: x.id, answer_set_id: answer_set.id, answer: true} end)
    Repo.insert_all(Answer, answers)
    :ok
  end

  test "loads resources for the elm app", %{conn: conn} do
    conn = get conn, "/"
    page = html_response(conn, 200)
    assert page =~ "<title>Parkinsons and Me</title>"
    assert page =~ "src=\"/js/elm.js"
  end

  test "loads all quotes services and weightings", %{conn: conn} do
    conn = get conn, "/api/quotes-services-weightings"
    %{"data" =>
      %{"quotes" => quotes,
        "services" => services,
        "weightings" => weightings}} = json_response(conn, 200)
    assert length(quotes) > 0
    assert length(services) > 0
    assert length(weightings) == (length(quotes) * length(services))
  end

  test "loads all the data for the elm app based on an answer uuid", %{conn: conn} do
    conn = get conn, "/api/my-results/12345678"
    %{"data" =>
      %{"quotes" => quotes,
        "services" => _,
        "weightings" => _,
        "user" => user,
        "answers" => answers}} = json_response(conn, 200)
    assert length(answers) == length(quotes)
    assert user["name"] == "Anne Onymous"
  end
end


Touched up the comments.

Touched up the comments.

#!/usr/bin/env elixir

# PROBLEM:
# Write a routine to calculate the angle between the hands of a clock.
# Also create some test data.
# Use any language you like.
# The C++ function signature follows.
#   // hours: 0 to 23
#   // minutes: 0 to 59
#   // output: 0 to 180 degrees, -1 error on invalid input
#   uint32_t ClockAngle(uint32_t hours, uint32_t minutes)

# MY NOTES:
# The simple solution is:
#   return (30 * hours) - (6 * minutes);
# The less simple solution is:
#   return (30 * hours + minutes / 2) - (6 * minutes);

# OTHER THOUGHTS:
# The problem itself is so simple someone bad at trigonometry should be able to solve it.
# Trigonometry problems should generally be solved in radians.
# Trigonometric functions are periodic, so there are an infinite number of solutions if measuring position.
# When measuring movement in degrees, the number of revolutions has meaning.
# -1 moonlights as an error code in C++, but atoms are a better way of expressing errors in Elixir.

# A single file solution written in Elixir follows.
# Note that flags affect the test data computations and output.
# Run with the following flags to match the C++ spec.
#   ./clock_angle.exs -a -b -+ -n -h HOURS -m MINUTES
#
# To see a list of the command line arguments, run the following.
#   chmod +x clock_angle.exs
#   ./clock_angle.exs --help
# Examples:
#   ./clock_angle.exs -h 3 -m 30
#   ./clock_angle.exs -h 8 -m 10 -r --no-pi -p 4
#   ./clock_angle.exs -h 8 -m 10 -r --no-pi -p 4 -n
#   ./clock_angle.exs -h 2 -m 37 -d -p 0
#   ./clock_angle.exs -h 24 -m 44
#   ./clock_angle.exs -h 24 -m 44 -a
#   ./clock_angle.exs -h 24 -m 44 -n
#   ./clock_angle.exs -h 24 -m 44 -+
#   ./clock_angle.exs -h 24 -m 44 -n -+
#   ./clock_angle.exs -h 24 -m 44 -b
#   ./clock_angle.exs -t -p 2
#   ./clock_angle.exs -t -d -p 0 -a -n -+ -b

defmodule ClockAngle do
  @hours_in_day 24
  @hours_in_circle 12
  @minutes_in_circle 60
  @radians_in_circle 2*:math.pi

  def integerToRadians(pValue, pMax) do
    pValue
    |> Kernel./(pMax)
    |> Kernel.*(@radians_in_circle)
  end

  def hourToRadians(pHour) do
    integerToRadians(pHour, @hours_in_circle)
  end

  def adjustHourInRadians(pMinute) do
    pMinute
    |> Kernel./(@minutes_in_circle) # normalize to a value from [0,1)
    |> Kernel.*(@radians_in_circle) # fraction of circumference of circle
    |> Kernel./(@hours_in_circle) # fraction between hour positions
  end

  def minuteToRadians(pMinute) do
    integerToRadians(pMinute, @minutes_in_circle)
  end

  def normalize(pValue) when
    pValue < 0 or @radians_in_circle <= pValue
  do
    pValue
    |> Kernel./(@radians_in_circle) # number of revolutions
    |> Float.floor # number of complete revolutions
    |> Kernel.*(-@radians_in_circle) # excess complete revolutions
    |> Kernel.+(pValue) # partial revolution remains
  end

  def normalize(pValue) do
    pValue
  end

  def normalize_difference(pValue) when
    pValue < 0 or @radians_in_circle <= pValue
  do
    pValue
    |> normalize
    |> normalize_difference
  end

  def normalize_difference(pValue) when
    @radians_in_circle / 2 <= pValue
  do
    @radians_in_circle - pValue
  end

  def normalize_difference(pValue), do: pValue

  # this has too many flag parameters
  # it should be refactored to take a single options paramerter
  def radians(pHour, pMinute, pAdjustHour \\ false, pNormalize \\ false, pPositiveResult \\ false, pCheckBounds \\ false)

  def radians(pHour, pMinute, _pAdjustHour, _pNormalize, _pPositiveResult, true) when
    pHour < 0 or @hours_in_day <= pHour or
    pMinute < 0 or @minutes_in_circle <= pMinute
  do
    :error
  end

  # do not adjust hour hand
  def radians(pHour, pMinute, false, false, false, _pCheckBounds) do
    minuteToRadians(pMinute) - hourToRadians(pHour)
  end

  # adjust hour hand
  def radians(pHour, pMinute, true, false, false, _pCheckBounds) do
    minuteToRadians(pMinute) - hourToRadians(pHour) - adjustHourInRadians(pMinute)
  end

  # normalize result
  def radians(pHour, pMinute, pAdjustHour, true, false, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> normalize
  end

  # positive result
  def radians(pHour, pMinute, pAdjustHour, false, true, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> Kernel.abs
  end

  # normalized positive result
  def radians(pHour, pMinute, pAdjustHour, true, true, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> normalize_difference
  end
end

defmodule Format do
  @degrees_in_circle 360
  @radians_in_circle 2*:math.pi

  def radiansToDegrees(pRadians) do
    pRadians
    |> Kernel.*(@degrees_in_circle)
    |> Kernel./(@radians_in_circle)
  end

  def angle(:error, _pOptions), do: "ERROR"

  def angle(pAngle, pOptions) do
    # [radians: false, degrees: false] -> [radians: true]
    # [radians: true, degrees: true] -> [radians: true]
    # [radians: true, degrees: false] -> [radians: true]
    # [radians: false, degrees: true] -> [radians: false]
    radians = pOptions[:radians] or not pOptions[:degrees]
    if radians do
      radians(pAngle, pOptions)
    else
      degrees(radiansToDegrees(pAngle), pOptions)
    end
  end

  def radians(pRadians, pOptions) do
    precision = pOptions[:precision]
    pi = pOptions[:pi]
    if pi do
      "#{pRadians |> Kernel./(:math.pi) |> round(precision)} PI radians"
    else
      "#{pRadians |> round(precision)} radians"
    end
  end

  def degrees(pDegrees, pOptions) do
    precision = pOptions[:precision]
    "#{pDegrees |> round(precision)} degrees"
  end

  def twoDigitNumber(pValue) do
    pValue
    |> Integer.to_string
    |> String.rjust(2, ?0)
  end

  def time(pHour, pMinute) do
    "#{pHour |> twoDigitNumber}:#{pMinute |> twoDigitNumber}"
  end

  def round(pValue, 0), do: pValue |> Float.round(0) |> trunc
  def round(pValue, pPrecision), do: pValue |> Float.round(pPrecision)
end

defmodule Test do
  @hours_in_day 24
  @minutes_in_circle 60
  @radians_in_circle 2*:math.pi

  def conditionalNormalize(pValue, true, true), do: ClockAngle.normalize_difference(pValue)
  def conditionalNormalize(pValue, true, false), do: ClockAngle.normalize(pValue)
  def conditionalNormalize(pValue, false, true), do: abs(pValue)
  def conditionalNormalize(pValue, false, false), do: pValue

  def round(pValue, _pPrecision) when is_atom(pValue), do: pValue
  def round(pValue, pPrecision), do: pValue |> Float.round(pPrecision)

  def case(pHour, pMinute, pExpected, pOptions) do
    adjust = pOptions[:adjust]
    normalize = pOptions[:normalize]
    positive = pOptions[:positive]
    bounds = pOptions[:bounds]
    precision = pOptions[:precision]

    actual = ClockAngle.radians(pHour, pMinute, adjust, normalize, positive, bounds)
    actual_rounded = actual |> round(precision)

    expected = pExpected
    |> conditionalAdjustMinutes(pMinute, adjust)
    |> conditionalNormalize(normalize, positive)
    |> conditionalBounds(pHour, pMinute, bounds)
    expected_rounded = expected |> round(precision)

    {result, operator} = if actual_rounded == expected_rounded do
      {"PASS", "=="}
    else
      {"FAIL", "!="}
    end
    IO.puts("#{result} #{Format.time(pHour, pMinute)} : Actual #{Format.angle(actual, pOptions)} #{operator} Expected #{Format.angle(expected, pOptions)}")
  end

  def minutesForward(pMinute) do
    pMinute / @minutes_in_circle * @radians_in_circle
  end

  def conditionalAdjustMinutes(pInitialValue, _pMinute, false), do: pInitialValue

  def conditionalAdjustMinutes(pInitialValue, pMinute, true) do
    pInitialValue - ClockAngle.adjustHourInRadians(pMinute)
  end

  def conditionalBounds(_pValue, pHour, pMinute, true) when
    pHour < 0 or @hours_in_day <= pHour or
    pMinute < 0 or @minutes_in_circle <= pMinute
  do
    :error
  end

  def conditionalBounds(pValue, _pHour, _pMinute, _bounds), do: pValue

  def all(pOptions) do
    case( 3,  0, minutesForward(-15), pOptions)
    case( 3,  5, minutesForward(-10), pOptions)
    case( 3, 10, minutesForward(-5), pOptions)
    case( 3, 15, minutesForward(0), pOptions)
    case( 3, 20, minutesForward(5), pOptions)
    case( 3, 25, minutesForward(10), pOptions)
    case( 3, 30, minutesForward(15), pOptions)
    case( 3, 35, minutesForward(20), pOptions)
    case( 3, 40, minutesForward(25), pOptions)
    case( 3, 45, minutesForward(30), pOptions)
    case( 3, 50, minutesForward(35), pOptions)
    case( 3, 55, minutesForward(40), pOptions)
    case( 3, 60, minutesForward(45), pOptions)
    case( 0, 37, minutesForward(37), pOptions)
    case( 1, 37, minutesForward(32), pOptions)
    case( 2, 37, minutesForward(27), pOptions)
    case( 3, 37, minutesForward(22), pOptions)
    case( 4, 37, minutesForward(17), pOptions)
    case( 5, 37, minutesForward(12), pOptions)
    case( 6, 37, minutesForward(7), pOptions)
    case( 7, 37, minutesForward(2), pOptions)
    case( 8, 37, minutesForward(-3), pOptions)
    case( 9, 37, minutesForward(-8), pOptions)
    case(10, 37, minutesForward(-13), pOptions)
    case(11, 37, minutesForward(-18), pOptions)
    case(12, 37, minutesForward(-23), pOptions)
    case(13, 37, minutesForward(-28), pOptions)
    case(14, 37, minutesForward(-33), pOptions)
    case(15, 37, minutesForward(-38), pOptions)
    case(16, 37, minutesForward(-43), pOptions)
    case(17, 37, minutesForward(-48), pOptions)
    case(18, 37, minutesForward(-53), pOptions)
    case(19, 37, minutesForward(-58), pOptions)
    case(20, 37, minutesForward(-63), pOptions)
    case(21, 37, minutesForward(-68), pOptions)
    case(22, 37, minutesForward(-73), pOptions)
    case(23, 37, minutesForward(-78), pOptions)
    case(24, 11, minutesForward(-109), pOptions)
    case(-2, 99, minutesForward(109), pOptions)
    case(-1, -1, minutesForward(4), pOptions)
    case(-1,  0, minutesForward(5), pOptions)
    case( 0, -1, minutesForward(-1), pOptions)
    case( 0,  0, minutesForward(0), pOptions)
    case(23, 59, minutesForward(-56), pOptions)
    case(23, 60, minutesForward(-55), pOptions)
    case(24, 59, minutesForward(-61), pOptions)
    case(24, 60, minutesForward(-60), pOptions)
  end
end

defmodule Script do
  def main(args) do
    defaults = [
      hour: 0,
      minute: 0,
      precision: 3,
      adjust: false,
      normalize: false,
      positive: false,
      bounds: false,
      radians: false,
      degrees: false,
      pi: true,
      test: false,
      help: false,
    ]
    switches = [
      hour: :integer,
      minute: :integer,
      precision: :integer,
      adjust: :boolean,
      normalize: :boolean,
      positive: :boolean,
      bounds: :boolean,
      radians: :boolean,
      degrees: :boolean,
      pi: :boolean,
      test: :boolean,
      help: :boolean,
    ]
    aliases = [
      h: :hour,
      m: :minute,
      p: :precision,
      a: :adjust,
      n: :normalize,
      "+": :positive,
      b: :bounds,
      r: :radians,
      d: :degrees,
      t: :test,
      P: :pi,
      "?": :help,
    ]
    {parsed, remaining, invalid} = OptionParser.parse(args, strict: switches, aliases: aliases)
    options = defaults
    |> Keyword.merge(parsed)
    cond do
      options[:help] or (0 < length remaining) or (0 < length invalid) ->
        usage
      options[:test] ->
        Test.all(options)
      true ->
        process(options)
    end
  end

  def usage() do
    IO.puts("Calculates the angle between hands of a clock.")
    IO.puts("The angle is measured from the hour hand to the minute hand in the clockwise direction.")
    IO.puts("The caluclation assumes the hour and minute hands point directly at the numbers on the clock.")
    IO.puts("Formatting flags affect test data output.")
    IO.puts("Usage:")
    IO.puts("  clock_angle --hour HOUR --minute MINUTE [--precision DIGITS] [--radians [--pi]|--degrees] [--normalize] [--test|--help]")
    IO.puts("    --hour      HOUR   : clock hour, default 0")
    IO.puts("    -h          HOUR   : clock hour, default 0")
    IO.puts("    --minute    MINUTE : clock minute, default 0")
    IO.puts("    -m          MINUTE : clock minute, default 0")
    IO.puts("    --precision DIGITS : number of digits to round result to, default 3")
    IO.puts("    -p          DIGITS : number of digits to round result to, default 3")
    IO.puts("    --radians          : result in radians, default")
    IO.puts("    -r                 : result in radians, default")
    IO.puts("    --degrees          : result in degrees")
    IO.puts("    -d                 : result in degrees")
    IO.puts("    --pi               : display radians as a multiple of PI, default")
    IO.puts("    -P                 : display radians as a multiple of PI, default")
    IO.puts("    --no-pi            : do not display radians as a multiple of PI")
    IO.puts("    --adjust           : adjust hour hand as minute hand moves")
    IO.puts("    -a                 : adjust hour hand as minute hand moves")
    IO.puts("    --normalize        : clamp result to [0, 2.0 PI) radians or [0, 360) degrees")
    IO.puts("    -n                 : clamp result to [0, 2.0 PI) radians or [0, 360) degrees")
    IO.puts("    --positive         : result is positive difference between clock hands")
    IO.puts("    -+                 : result is positive difference between clock hands")
    IO.puts("                       : use --normalize and --positive clamp result to [0, PI) radians or [0, 180) degrees")
    IO.puts("    --bounds           : error unless 0 <= hours < 24 and 0 <= minutes < 60")
    IO.puts("    -b                 : error unless 0 <= hours < 24 and 0 <= minutes < 60")
    IO.puts("    --test             : run test cases")
    IO.puts("    -t                 : run test cases")
    IO.puts("    --help             : display this usage summary")
    IO.puts("    -?                 : display this usage summary")
    IO.puts("Examples:")
    IO.puts("  clock_angle -h 3 -m 30")
    IO.puts("  clock_angle -h 8 -m 10 -r --no-pi -p 4")
    IO.puts("  clock_angle -h 8 -m 10 -r --no-pi -p 4 -n")
    IO.puts("  clock_angle -h 2 -m 37 -d -p 0")
    IO.puts("  clock_angle -h 24 -m 44")
    IO.puts("  clock_angle -h 24 -m 44 -a")
    IO.puts("  clock_angle -h 24 -m 44 -n")
    IO.puts("  clock_angle -h 24 -m 44 -+")
    IO.puts("  clock_angle -h 24 -m 44 -n -+")
    IO.puts("  clock_angle -h 24 -m 44 -b")
    IO.puts("  clock_angle -t -p 2")
    IO.puts("  clock_angle -t -d -p 0 -a -n -+ -b")
  end

  def process(pOptions) do
    hour = pOptions[:hour]
    minute = pOptions[:minute]
    adjust = pOptions[:adjust]
    normalize = pOptions[:normalize]
    positive = pOptions[:positive]
    bounds = pOptions[:bounds]
    angle = ClockAngle.radians(hour, minute, adjust, normalize, positive, bounds)
    output(Format.time(hour, minute), Format.angle(angle, pOptions))
  end

  def output(pTime, pAngle) do
    IO.puts("#{pTime} -> #{pAngle}")
  end
end

Script.main(System.argv)

#!/usr/bin/env elixir

# PROBLEM:
# Write a routine to calculate the angle between the hands of a clock.
# Also create some test data.
# Use any language you like.
# The C++ function signature follows.
#   // hours: 0 to 23
#   // minutes: 0 to 59
#   // output: 0 to 180 degrees, -1 error on invalid input
#   uint32_t ClockAngle(uint32_t hours, uint32_t minutes)

# MY NOTES:
# The simple solution is:
#   return (30 * hours) - (6 * minutes);
# The less simple solution is:
#   return (30 * hours + minutes / 2) - (6 * minutes);

# OTHER THOUGHTS:
# The problem itself is so simple someone bad at trigonometry should be able to solve it.
# Trigonometry problems should generally be solved in radians.
# Trigonometric functions are periodic, so there are an infinite number of solutions if measuring position.
# When measuring movement in degrees, the number of revolutions has meaning.
# -1 moonlights as an error code in C++, but atoms are a better choice in Elixir.

# A single file solution written in Elixir follows.
# Run with the following flags for the above spec.
#   ./clock_angle.exs -a -b -+ -n -h HOURS -m MINUTES
# To see a list of the command line arguments, run the following.
# Note that formatting flags affect the test data.
#   chmod +x clock_angle.exs
#   ./clock_angle.exs --help
# Examples:
#   ./clock_angle.exs -h 3 -m 30
#   ./clock_angle.exs -h 8 -m 10 -r --no-pi -p 4
#   ./clock_angle.exs -h 8 -m 10 -r --no-pi -p 4 -n
#   ./clock_angle.exs -h 2 -m 37 -d -p 0
#   ./clock_angle.exs -h 24 -m 44
#   ./clock_angle.exs -h 24 -m 44 -a
#   ./clock_angle.exs -h 24 -m 44 -n
#   ./clock_angle.exs -h 24 -m 44 -+
#   ./clock_angle.exs -h 24 -m 44 -n -+
#   ./clock_angle.exs -h 24 -m 44 -b
#   ./clock_angle.exs -t -p 2
#   ./clock_angle.exs -t -d -p 0 -a -n -+ -b

defmodule ClockAngle do
  @hours_in_day 24
  @hours_in_circle 12
  @minutes_in_circle 60
  @radians_in_circle 2*:math.pi

  def integerToRadians(pValue, pMax) do
    pValue
    |> Kernel./(pMax)
    |> Kernel.*(@radians_in_circle)
  end

  def hourToRadians(pHour) do
    integerToRadians(pHour, @hours_in_circle)
  end

  def adjustHourInRadians(pMinute) do
    pMinute
    |> Kernel./(@minutes_in_circle)
    |> Kernel.*(@radians_in_circle)
    |> Kernel./(@hours_in_circle)
  end

  def minuteToRadians(pMinute) do
    integerToRadians(pMinute, @minutes_in_circle)
  end

  def normalize(pValue) when
    pValue < 0 or @radians_in_circle <= pValue
  do
    pValue
    |> Kernel./(@radians_in_circle) # number of revolutions
    |> Float.floor # number of complete revolutions
    |> Kernel.*(-@radians_in_circle) # excess complete revolutions
    |> Kernel.+(pValue) # partial revolution remains
  end

  def normalize(pValue) do
    pValue
  end

  def normalize_difference(pValue) when
    pValue < 0 or @radians_in_circle <= pValue
  do
    pValue
    |> normalize
    |> normalize_difference
  end

  def normalize_difference(pValue) when
    @radians_in_circle / 2 <= pValue
  do
    @radians_in_circle - pValue
  end

  def normalize_difference(pValue), do: pValue

  def radians(pHour, pMinute, pAdjustHour \\ false, pNormalize \\ false, pPositiveResult \\ false, pCheckBounds \\ false)

  def radians(pHour, pMinute, _pAdjustHour, _pNormalize, _pPositiveResult, true) when
    pHour < 0 or @hours_in_day <= pHour or
    pMinute < 0 or @minutes_in_circle <= pMinute
  do
    :error
  end

  # do not adjust hour hand
  def radians(pHour, pMinute, false, false, false, _pCheckBounds) do
    minuteToRadians(pMinute) - hourToRadians(pHour)
  end

  # adjust hour hand
  def radians(pHour, pMinute, true, false, false, _pCheckBounds) do
    minuteToRadians(pMinute) - hourToRadians(pHour) - adjustHourInRadians(pMinute)
  end

  # normalize result
  def radians(pHour, pMinute, pAdjustHour, true, false, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> normalize
  end

  # positive result
  def radians(pHour, pMinute, pAdjustHour, false, true, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> Kernel.abs
  end

  # normalize positive result
  def radians(pHour, pMinute, pAdjustHour, true, true, pCheckBounds) do
    radians(pHour, pMinute, pAdjustHour, false, false, pCheckBounds)
    |> normalize_difference
  end
end

defmodule Format do
  @degrees_in_circle 360
  @radians_in_circle 2*:math.pi

  def radiansToDegrees(pRadians) do
    pRadians
    |> Kernel.*(@degrees_in_circle)
    |> Kernel./(@radians_in_circle)
  end

  def angle(:error, _pOptions), do: "ERROR"

  def angle(pAngle, pOptions) do
    # [radians: false, degrees: false] -> [radians: true]
    # [radians: true, degrees: true] -> [radians: true]
    # [radians: true, degrees: false] -> [radians: true]
    # [radians: false, degrees: true] -> [radians: false]
    radians = pOptions[:radians] or not pOptions[:degrees]
    if radians do
      radians(pAngle, pOptions)
    else
      degrees(radiansToDegrees(pAngle), pOptions)
    end
  end

  def radians(pRadians, pOptions) do
    precision = pOptions[:precision]
    pi = pOptions[:pi]
    if pi do
      "#{pRadians |> Kernel./(:math.pi) |> round(precision)} PI radians"
    else
      "#{pRadians |> round(precision)} radians"
    end
  end

  def degrees(pDegrees, pOptions) do
    precision = pOptions[:precision]
    "#{pDegrees |> round(precision)} degrees"
  end

  def twoDigitNumber(pValue) do
    pValue
    |> Integer.to_string
    |> String.rjust(2, ?0)
  end

  def time(pHour, pMinute) do
    "#{pHour |> twoDigitNumber}:#{pMinute |> twoDigitNumber}"
  end

  def round(pValue, 0), do: pValue |> Float.round(0) |> trunc
  def round(pValue, pPrecision), do: pValue |> Float.round(pPrecision)
end

defmodule Test do
  @hours_in_day 24
  @minutes_in_circle 60
  @radians_in_circle 2*:math.pi

  def conditionalNormalize(pValue, true, true), do: ClockAngle.normalize_difference(pValue)
  def conditionalNormalize(pValue, true, false), do: ClockAngle.normalize(pValue)
  def conditionalNormalize(pValue, false, true), do: abs(pValue)
  def conditionalNormalize(pValue, false, false), do: pValue

  def round(pValue, _pPrecision) when is_atom(pValue), do: pValue
  def round(pValue, pPrecision), do: pValue |> Float.round(pPrecision)

  def case(pHour, pMinute, pExpected, pOptions) do
    adjust = pOptions[:adjust]
    normalize = pOptions[:normalize]
    positive = pOptions[:positive]
    bounds = pOptions[:bounds]
    precision = pOptions[:precision]

    actual = ClockAngle.radians(pHour, pMinute, adjust, normalize, positive, bounds)
    actual_rounded = actual |> round(precision)

    expected = pExpected
    |> conditionalAdjustMinutes(pMinute, adjust)
    |> conditionalNormalize(normalize, positive)
    |> conditionalBounds(pHour, pMinute, bounds)
    expected_rounded = expected |> round(precision)

    {result, operator} = if actual_rounded == expected_rounded do
      {"PASS", "=="}
    else
      {"FAIL", "!="}
    end
    IO.puts("#{result} #{Format.time(pHour, pMinute)} : Actual #{Format.angle(actual, pOptions)} #{operator} Expected #{Format.angle(expected, pOptions)}")
  end

  def minutesForward(pMinute) do
    pMinute / @minutes_in_circle * @radians_in_circle
  end

  def conditionalAdjustMinutes(pInitialValue, _pMinute, false), do: pInitialValue

  def conditionalAdjustMinutes(pInitialValue, pMinute, true) do
    pInitialValue - ClockAngle.adjustHourInRadians(pMinute)
  end

  def conditionalBounds(_pValue, pHour, pMinute, true) when
    pHour < 0 or @hours_in_day <= pHour or
    pMinute < 0 or @minutes_in_circle <= pMinute
  do
    :error
  end

  def conditionalBounds(pValue, _pHour, _pMinute, _bounds), do: pValue

  def all(pOptions) do
    case( 3,  0, minutesForward(-15), pOptions)
    case( 3,  5, minutesForward(-10), pOptions)
    case( 3, 10, minutesForward(-5), pOptions)
    case( 3, 15, minutesForward(0), pOptions)
    case( 3, 20, minutesForward(5), pOptions)
    case( 3, 25, minutesForward(10), pOptions)
    case( 3, 30, minutesForward(15), pOptions)
    case( 3, 35, minutesForward(20), pOptions)
    case( 3, 40, minutesForward(25), pOptions)
    case( 3, 45, minutesForward(30), pOptions)
    case( 3, 50, minutesForward(35), pOptions)
    case( 3, 55, minutesForward(40), pOptions)
    case( 3, 60, minutesForward(45), pOptions)
    case( 0, 37, minutesForward(37), pOptions)
    case( 1, 37, minutesForward(32), pOptions)
    case( 2, 37, minutesForward(27), pOptions)
    case( 3, 37, minutesForward(22), pOptions)
    case( 4, 37, minutesForward(17), pOptions)
    case( 5, 37, minutesForward(12), pOptions)
    case( 6, 37, minutesForward(7), pOptions)
    case( 7, 37, minutesForward(2), pOptions)
    case( 8, 37, minutesForward(-3), pOptions)
    case( 9, 37, minutesForward(-8), pOptions)
    case(10, 37, minutesForward(-13), pOptions)
    case(11, 37, minutesForward(-18), pOptions)
    case(12, 37, minutesForward(-23), pOptions)
    case(13, 37, minutesForward(-28), pOptions)
    case(14, 37, minutesForward(-33), pOptions)
    case(15, 37, minutesForward(-38), pOptions)
    case(16, 37, minutesForward(-43), pOptions)
    case(17, 37, minutesForward(-48), pOptions)
    case(18, 37, minutesForward(-53), pOptions)
    case(19, 37, minutesForward(-58), pOptions)
    case(20, 37, minutesForward(-63), pOptions)
    case(21, 37, minutesForward(-68), pOptions)
    case(22, 37, minutesForward(-73), pOptions)
    case(23, 37, minutesForward(-78), pOptions)
    case(24, 11, minutesForward(-109), pOptions)
    case(-2, 99, minutesForward(109), pOptions)
    case(-1, -1, minutesForward(4), pOptions)
    case(-1,  0, minutesForward(5), pOptions)
    case( 0, -1, minutesForward(-1), pOptions)
    case( 0,  0, minutesForward(0), pOptions)
    case(23, 59, minutesForward(-56), pOptions)
    case(23, 60, minutesForward(-55), pOptions)
    case(24, 59, minutesForward(-61), pOptions)
    case(24, 60, minutesForward(-60), pOptions)
  end
end

defmodule Script do
  def main(args) do
    defaults = [
      hour: 0,
      minute: 0,
      precision: 3,
      adjust: false,
      normalize: false,
      positive: false,
      bounds: false,
      radians: false,
      degrees: false,
      pi: true,
      test: false,
      help: false,
    ]
    switches = [
      hour: :integer,
      minute: :integer,
      precision: :integer,
      adjust: :boolean,
      normalize: :boolean,
      positive: :boolean,
      bounds: :boolean,
      radians: :boolean,
      degrees: :boolean,
      pi: :boolean,
      test: :boolean,
      help: :boolean,
    ]
    aliases = [
      h: :hour,
      m: :minute,
      p: :precision,
      a: :adjust,
      n: :normalize,
      "+": :positive,
      b: :bounds,
      r: :radians,
      d: :degrees,
      t: :test,
      P: :pi,
      "?": :help,
    ]
    {parsed, remaining, invalid} = OptionParser.parse(args, strict: switches, aliases: aliases)
    options = defaults
    |> Keyword.merge(parsed)
    cond do
      options[:help] or (0 < length remaining) or (0 < length invalid) ->
        usage
      options[:test] ->
        Test.all(options)
      true ->
        process(options)
    end
  end

  def usage() do
    IO.puts("Calculates the angle between hands of a clock.")
    IO.puts("The angle is measured from the hour hand to the minute hand in the clockwise direction.")
    IO.puts("The caluclation assumes the hour and minute hands point directly at the numbers on the clock.")
    IO.puts("Formatting flags affect test data output.")
    IO.puts("Usage:")
    IO.puts("  clock_angle --hour HOUR --minute MINUTE [--precision DIGITS] [--radians [--pi]|--degrees] [--normalize] [--test|--help]")
    IO.puts("    --hour      HOUR   : clock hour, default 0")
    IO.puts("    -h          HOUR   : clock hour, default 0")
    IO.puts("    --minute    MINUTE : clock minute, default 0")
    IO.puts("    -m          MINUTE : clock minute, default 0")
    IO.puts("    --precision DIGITS : number of digits to round result to, default 3")
    IO.puts("    -p          DIGITS : number of digits to round result to, default 3")
    IO.puts("    --radians          : result in radians, default")
    IO.puts("    -r                 : result in radians, default")
    IO.puts("    --degrees          : result in degrees")
    IO.puts("    -d                 : result in degrees")
    IO.puts("    --pi               : display radians as a multiple of PI, default")
    IO.puts("    -P                 : display radians as a multiple of PI, default")
    IO.puts("    --no-pi            : do not display radians as a multiple of PI")
    IO.puts("    --adjust           : adjust hour hand as minute hand moves")
    IO.puts("    -a                 : adjust hour hand as minute hand moves")
    IO.puts("    --normalize        : clamp result to [0, 2.0 PI) radians or [0, 360) degrees")
    IO.puts("    -n                 : clamp result to [0, 2.0 PI) radians or [0, 360) degrees")
    IO.puts("    --positive         : result is positive difference between clock hands")
    IO.puts("    -+                 : result is positive difference between clock hands")
    IO.puts("                       : use --normalize and --positive clamp result to [0, PI) radians or [0, 180) degrees")
    IO.puts("    --bounds           : error unless 0 <= hours < 24 and 0 <= minutes < 60")
    IO.puts("    -b                 : error unless 0 <= hours < 24 and 0 <= minutes < 60")
    IO.puts("    --test             : run test cases")
    IO.puts("    -t                 : run test cases")
    IO.puts("    --help             : display this usage summary")
    IO.puts("    -?                 : display this usage summary")
    IO.puts("Examples:")
    IO.puts("  clock_angle -h 3 -m 30")
    IO.puts("  clock_angle -h 8 -m 10 -r --no-pi -p 4")
    IO.puts("  clock_angle -h 8 -m 10 -r --no-pi -p 4 -n")
    IO.puts("  clock_angle -h 2 -m 37 -d -p 0")
    IO.puts("  clock_angle -h 24 -m 44")
    IO.puts("  clock_angle -h 24 -m 44 -a")
    IO.puts("  clock_angle -h 24 -m 44 -n")
    IO.puts("  clock_angle -h 24 -m 44 -+")
    IO.puts("  clock_angle -h 24 -m 44 -n -+")
    IO.puts("  clock_angle -h 24 -m 44 -b")
    IO.puts("  clock_angle -t -p 2")
    IO.puts("  clock_angle -t -d -p 0 -a -n -+ -b")
  end

  def process(pOptions) do
    hour = pOptions[:hour]
    minute = pOptions[:minute]
    adjust = pOptions[:adjust]
    normalize = pOptions[:normalize]
    positive = pOptions[:positive]
    bounds = pOptions[:bounds]
    angle = ClockAngle.radians(hour, minute, adjust, normalize, positive, bounds)
    output(Format.time(hour, minute), Format.angle(angle, pOptions))
  end

  def output(pTime, pAngle) do
    IO.puts("#{pTime} -> #{pAngle}")
  end
end

Script.main(System.argv)


Address lint warnings

Address lint warnings

defmodule Sentinel.Config do
  @moduledoc """
  Wraps application configuration for Sentinel for easy access
  """

  @doc """
  Wrapper for getting the application config of :app_name
  """
  def app_name do
    Application.get_env(:sentinel, :app_name)
  end

  @doc """
  Wrapper for getting the application config of :auth_handler
  """
  def auth_handler do
    Application.get_env(:sentinel, :auth_handler, Sentinel.AuthHandler)
  end

  @doc """
  Wrapper for getting the application config of :confirmable
  """
  def confirmable do
    Application.get_env(:sentinel, :confirmable, :optional)
  end

  @doc """
  Wrapper for getting the application config of :crypto_provider
  """
  def crypto_provider do
    Application.get_env(:sentinel, :crypto_provider, Comeonin.Bcrypt)
  end

  @doc """
  Wrapper for getting the application config of :email_css
  """
  def email_css_url do
    Application.get_env(:sentinel, :email_css, "")
  end

  @doc """
  Wrapper for getting the application config of :endpoint
  """
  def endpoint do
    Application.get_env(:sentinel, :endpoint)
  end

  @doc """
  Wrapper for getting the application config of :error_view
  """
  def error_view do
    Application.get_env(:sentinel, :error_view, Sentinel.ErrorView)
  end

  @doc """
  Wrapper for getting the application config of :invitable
  """
  def invitable do
    Application.get_env(:sentinel, :invitable, false)
  end

  @doc """
  Helper method ensuring invitable is properly configured
  """
  def invitable_configured? do
    invitable() && invitation_registration_url()
  end

  @doc """
  Wrapper for getting the application config of :confirmable_redirect_url
  """
  def confirmable_redirect_url do
    Application.get_env(:sentinel, :confirmable_redirect_url)
  end

  @doc """
  Wrapper for getting the application config of :password_reset_url
  """
  def password_reset_url do
    Application.get_env(:sentinel, :password_reset_url)
  end

  @doc """
  Wrapper for getting the application config of :invitation_registration_url
  """
  def invitation_registration_url do
    Application.get_env(:sentinel, :invitation_registration_url)
  end

  @doc """
  Wrapper for getting the application config of :reply_to, defaults to the send_address
  """
  def reply_to do
    Application.get_env(:sentinel, :reply_to, send_address())
  end

  @doc """
  Wrapper for getting the application config of :repo
  """
  def repo do
    Application.get_env(:sentinel, :repo)
  end

  @doc """
  Wrapper for getting the application config of :router
  """
  def router do
    Application.get_env(:sentinel, :router)
  end

  @doc """
  Wrapper for getting the application config of :router_helper
  """
  def router_helper do
    Module.concat(router(), Helpers)
  end

  @doc """
  Helper method ensuring router helper is properly configured
  """
  def router_helper_configured? do
    router() && endpoint()
  end

  @doc """
  Wrapper for getting the application config of :send_address
  """
  def send_address do
    Application.get_env(:sentinel, :send_address)
  end

  @doc """
  Wrapper for getting the application config of :send_emails.
  Defaults to true
  Used by Sentinel to know if the application should actually send emails in the environment
  you're operating in
  """
  def send_emails? do
    Application.get_env(:sentinel, :send_emails, true)
  end

  @doc """
  Retrieves list of tuples of {ueberauth_provider, auth_url}
  """
  def ueberauth_providers do
    :ueberauth
    |> Application.get_all_env
    |> ueberauth_env_filter
    |> Enum.filter(fn provider_config ->
      {provider, _config} = provider_config
      provider != :identity
    end)
    |> Enum.map(fn provider_config ->
      {provider, _details} = provider_config
      {Atom.to_string(provider), router_helper().auth_url(endpoint(), :request, provider)}
    end)
  end

  defp ueberauth_env_filter([head|tail]) do
    {module, _other} = head
    if module == Ueberauth do
      ueberauth_env_filter(head)
    else
      ueberauth_env_filter(tail)
    end
  end
  defp ueberauth_env_filter({_module, [providers: ueberauth_config]}) do
    ueberauth_config
  end

  @doc """
  Wrapper for getting the application config of :user_model
  """
  def user_model do
    Application.get_env(:sentinel, :user_model)
  end

  @doc """
  Wrapper for getting the application config of :user_model_validator
  """
  def user_model_validator do
    Application.get_env(:sentinel, :user_model_validator)
  end

  @doc """
  Wrapper for getting the application config of :user_view
  """
  def user_view do
    Application.get_env(:sentinel, :user_view, Sentinel.UserView)
  end

  def layout_view do
    Application.get_env(:sentinel, :layout_view, Sentinel.LayoutView)
  end

  def layout do
    Application.get_env(:sentinel, :layout, :app)
  end

  @doc """
  Wrapper for getting the application config of :registrator_callback
  """
  def registrator_callback do
    Application.get_env(:sentinel, :registrator_callback)
  end
end

defmodule Sentinel.Config do
  @moduledoc """
  Wraps application configuration for Sentinel for easy access
  """

  @doc """
  Wrapper for getting the application config of :app_name
  """
  def app_name do
    Application.get_env(:sentinel, :app_name)
  end

  @doc """
  Wrapper for getting the application config of :auth_handler
  """
  def auth_handler do
    Application.get_env(:sentinel, :auth_handler, Sentinel.AuthHandler)
  end

  @doc """
  Wrapper for getting the application config of :confirmable
  """
  def confirmable do
    Application.get_env(:sentinel, :confirmable, :optional)
  end

  @doc """
  Wrapper for getting the application config of :crypto_provider
  """
  def crypto_provider do
    Application.get_env(:sentinel, :crypto_provider, Comeonin.Bcrypt)
  end

  @doc """
  Wrapper for getting the application config of :email_css
  """
  def email_css_url do
    Application.get_env(:sentinel, :email_css, "")
  end

  @doc """
  Wrapper for getting the application config of :endpoint
  """
  def endpoint do
    Application.get_env(:sentinel, :endpoint)
  end

  @doc """
  Wrapper for getting the application config of :error_view
  """
  def error_view do
    Application.get_env(:sentinel, :error_view, Sentinel.ErrorView)
  end

  @doc """
  Wrapper for getting the application config of :invitable
  """
  def invitable do
    Application.get_env(:sentinel, :invitable, false)
  end

  @doc """
  Helper method ensuring invitable is properly configured
  """
  def invitable_configured? do
    invitable && invitation_registration_url
  end

  @doc """
  Wrapper for getting the application config of :confirmable_redirect_url
  """
  def confirmable_redirect_url do
    Application.get_env(:sentinel, :confirmable_redirect_url)
  end

  @doc """
  Wrapper for getting the application config of :password_reset_url
  """
  def password_reset_url do
    Application.get_env(:sentinel, :password_reset_url)
  end

  @doc """
  Wrapper for getting the application config of :invitation_registration_url
  """
  def invitation_registration_url do
    Application.get_env(:sentinel, :invitation_registration_url)
  end

  @doc """
  Wrapper for getting the application config of :reply_to, defaults to the send_address
  """
  def reply_to do
    Application.get_env(:sentinel, :reply_to, send_address)
  end

  @doc """
  Wrapper for getting the application config of :repo
  """
  def repo do
    Application.get_env(:sentinel, :repo)
  end

  @doc """
  Wrapper for getting the application config of :router
  """
  def router do
    Application.get_env(:sentinel, :router)
  end

  @doc """
  Wrapper for getting the application config of :router_helper
  """
  def router_helper do
    Module.concat(router, Helpers)
  end

  @doc """
  Helper method ensuring router helper is properly configured
  """
  def router_helper_configured? do
    router && endpoint
  end

  @doc """
  Wrapper for getting the application config of :send_address
  """
  def send_address do
    Application.get_env(:sentinel, :send_address)
  end

  @doc """
  Wrapper for getting the application config of :send_emails.
  Defaults to true
  Used by Sentinel to know if the application should actually send emails in the environment
  you're operating in
  """
  def send_emails? do
    Application.get_env(:sentinel, :send_emails, true)
  end

  @doc """
  Retrieves list of tuples of {ueberauth_provider, auth_url}
  """
  def ueberauth_providers do
    :ueberauth
    |> Application.get_all_env
    |> ueberauth_env_filter
    |> Enum.filter(fn provider_config ->
      {provider, _config} = provider_config
      provider != :identity
    end)
    |> Enum.map(fn provider_config ->
      {provider, _details} = provider_config
      {Atom.to_string(provider), router_helper.auth_url(endpoint, :request, provider)}
    end)
  end

  defp ueberauth_env_filter([head|tail]) do
    {module, _other} = head
    if module == Ueberauth do
      ueberauth_env_filter(head)
    else
      ueberauth_env_filter(tail)
    end
  end
  defp ueberauth_env_filter({_module, [providers: ueberauth_config]}) do
    ueberauth_config
  end

  @doc """
  Wrapper for getting the application config of :user_model
  """
  def user_model do
    Application.get_env(:sentinel, :user_model)
  end

  @doc """
  Wrapper for getting the application config of :user_model_validator
  """
  def user_model_validator do
    Application.get_env(:sentinel, :user_model_validator)
  end

  @doc """
  Wrapper for getting the application config of :user_view
  """
  def user_view do
    Application.get_env(:sentinel, :user_view, Sentinel.UserView)
  end

  def layout_view do
    Application.get_env(:sentinel, :layout_view, Sentinel.LayoutView)
  end

  def layout do
    Application.get_env(:sentinel, :layout, :app)
  end

  @doc """
  Wrapper for getting the application config of :registrator_callback
  """
  def registrator_callback do
    Application.get_env(:sentinel, :registrator_callback)
  end
end


Don't do any work when we don't have stale files

This was essentially true before, but this change makes this part of the code
and its effects a lot clearer.

defmodule Mix.Tasks.Compile.Thrift do
  use Mix.Task

  @moduledoc """
  Generate Erlang source files from .thrift schema files.

  When this task runs, it first checks the modification times of all source
  files that were generated by the set of .thrift files.  If the generated
  files are older than the .thrift file that generated them, this task will
  skip regenerating them.

  ## Command line options

    * `--force` - forces compilation regardless of modification times

  ## Configuration

    * `:thrift_files` - list of .thrift schema files to compile

    * `:thrift_output` - output directory into which the generated Erlang
      source file will be generated. Defaults to `"src"`.

    * `:thrift_options` - list of additional options that will be passed to
      the Thrift compiler.

    * `:thrift_version` - thrift compiler `Version` requirement
  """

  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean])
    force        = opts[:force]

    project        = Mix.Project.config
    thrift_files   = project[:thrift_files] || []
    thrift_options = project[:thrift_options] || []
    thrift_version = project[:thrift_version]
    output_dir     = project[:thrift_output] || "src"

    stale_files = Enum.filter(thrift_files, fn file ->
      force || stale?(file, output_dir)
    end)

    if(thrift_version && !Enum.empty?(stale_files)) do
      unless(Version.match?(v = get_thrift_version, thrift_version)) do
        Mix.raise "Unsupported Thrift version #{v} (requires #{thrift_version})"
      end
    end

    unless Enum.empty?(stale_files) do
      File.mkdir_p!(output_dir)
      options = build_options(output_dir, thrift_options)
      Enum.each stale_files, &generate(&1, options)
    end
  end

  defp get_thrift_version do
    case System.cmd("thrift", ~w[-version]) do
      {s, 0} -> hd(Regex.run(~r/\b(\d+\.\d+\.\d+)\b/, s, capture: :first) || [])
      {_, e} -> Mix.raise "Failed to execute `thrift -version` (error #{e})"
    end
  end

  defp get_generated_files(thrift_file, output_dir) do
    basename = Path.basename(thrift_file, ".thrift")
    pattern  = basename <> "_{constants,thrift,types}.{erl,hrl}"
    Mix.Utils.extract_files([output_dir], pattern)
  end

  defp stale?(thrift_file, output_dir) do
    targets = get_generated_files(thrift_file, output_dir)
    Enum.empty?(targets) || Mix.Utils.stale?([thrift_file], targets)
  end

  defp build_options(output_dir, user_options) do
    opts = ~w[--out] ++ [output_dir]
    unless Enum.member?(user_options, "--gen") do
      opts = opts ++ ~w[--gen erl]
    end
    opts ++ user_options
  end

  defp generate(thrift_file, options) do
    args = options ++ [thrift_file]
    case System.cmd("thrift", args) do
      {_, 0} -> Mix.shell.info "Compiled #{thrift_file}"
      {_, e} -> Mix.shell.error "Failed to compile #{thrift_file} (error #{e})"
    end
  end
end

defmodule Mix.Tasks.Compile.Thrift do
  use Mix.Task

  @moduledoc """
  Generate Erlang source files from .thrift schema files.

  When this task runs, it first checks the modification times of all source
  files that were generated by the set of .thrift files.  If the generated
  files are older than the .thrift file that generated them, this task will
  skip regenerating them.

  ## Command line options

    * `--force` - forces compilation regardless of modification times

  ## Configuration

    * `:thrift_files` - list of .thrift schema files to compile

    * `:thrift_output` - output directory into which the generated Erlang
      source file will be generated. Defaults to `"src"`.

    * `:thrift_options` - list of additional options that will be passed to
      the Thrift compiler.

    * `:thrift_version` - thrift compiler `Version` requirement
  """

  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean])
    force        = opts[:force]

    project        = Mix.Project.config
    thrift_files   = project[:thrift_files] || []
    thrift_options = project[:thrift_options] || []
    thrift_version = project[:thrift_version]
    output_dir     = project[:thrift_output] || "src"

    stale_files = Enum.filter(thrift_files, fn file ->
      force || stale?(file, output_dir)
    end)

    if(thrift_version && !Enum.empty?(stale_files)) do
      unless(Version.match?(v = get_thrift_version, thrift_version)) do
        Mix.raise "Unsupported Thrift version #{v} (requires #{thrift_version})"
      end
    end
    unless(Enum.empty?(stale_files), do: File.mkdir_p!(output_dir))

    options = build_options(output_dir, thrift_options)
    Enum.each stale_files, &generate(&1, options)
  end

  defp get_thrift_version do
    case System.cmd("thrift", ~w[-version]) do
      {s, 0} -> hd(Regex.run(~r/\b(\d+\.\d+\.\d+)\b/, s, capture: :first) || [])
      {_, e} -> Mix.raise "Failed to execute `thrift -version` (error #{e})"
    end
  end

  defp get_generated_files(thrift_file, output_dir) do
    basename = Path.basename(thrift_file, ".thrift")
    pattern  = basename <> "_{constants,thrift,types}.{erl,hrl}"
    Mix.Utils.extract_files([output_dir], pattern)
  end

  defp stale?(thrift_file, output_dir) do
    targets = get_generated_files(thrift_file, output_dir)
    Enum.empty?(targets) || Mix.Utils.stale?([thrift_file], targets)
  end

  defp build_options(output_dir, user_options) do
    opts = ~w[--out] ++ [output_dir]
    unless Enum.member?(user_options, "--gen") do
      opts = opts ++ ~w[--gen erl]
    end
    opts ++ user_options
  end

  defp generate(thrift_file, options) do
    args = options ++ [thrift_file]
    case System.cmd("thrift", args) do
      {_, 0} -> Mix.shell.info "Compiled #{thrift_file}"
      {_, e} -> Mix.shell.error "Failed to compile #{thrift_file} (error #{e})"
    end
  end
end


fix: add nil @spec overload to number_to_delimited

defmodule Number.Delimit do
  @moduledoc """
  Provides functions to delimit numbers into strings.
  """

  @doc """
  Formats a number into a string with grouped thousands using `delimiter`.

  ## Parameters

  * `number` - A float or integer to convert.

  * `options` - A keyword list of options. See the documentation of all
    available options below for more information.

  ## Options

  * `:precision` - The number of decimal places to include. Default: 2

  * `:delimiter` - The character to use to delimit the number by thousands.
    Default: ","

  * `:separator` - The character to use to separate the number from the decimal
    places. Default: "."

  Default configuration for these options can be specified in the `Number`
  application configuration.

      config :number, delimit: [
                        precision: 3,
                        delimiter: ",",
                        separator: "."
                      ]

  ## Examples

      iex> Number.Delimit.number_to_delimited(nil)
      nil

      iex> Number.Delimit.number_to_delimited(998.999)
      "999.00"

      iex> Number.Delimit.number_to_delimited(-234234.234)
      "-234,234.23"

      iex> Number.Delimit.number_to_delimited("998.999")
      "999.00"

      iex> Number.Delimit.number_to_delimited("-234234.234")
      "-234,234.23"

      iex> Number.Delimit.number_to_delimited(12345678)
      "12,345,678.00"

      iex> Number.Delimit.number_to_delimited(12345678.05)
      "12,345,678.05"

      iex> Number.Delimit.number_to_delimited(12345678, delimiter: ".")
      "12.345.678.00"

      iex> Number.Delimit.number_to_delimited(12345678, delimiter: ",")
      "12,345,678.00"

      iex> Number.Delimit.number_to_delimited(12345678.05, separator: " ")
      "12,345,678 05"

      iex> Number.Delimit.number_to_delimited(98765432.98, delimiter: " ", separator: ",")
      "98 765 432,98"

      iex> Number.Delimit.number_to_delimited(Decimal.from_float(9998.2))
      "9,998.20"

      iex> Number.Delimit.number_to_delimited "123456789555555555555555555555555"
      "123,456,789,555,555,555,555,555,555,555,555.00"

      iex> Number.Delimit.number_to_delimited Decimal.new("123456789555555555555555555555555")
      "123,456,789,555,555,555,555,555,555,555,555.00"
  """
  @spec number_to_delimited(nil, list) :: nil
  @spec number_to_delimited(Number.t(), list) :: String.t()
  def number_to_delimited(number, options \\ [])
  def number_to_delimited(nil, _options), do: nil

  def number_to_delimited(number, options) do
    float = number |> Number.Conversion.to_float()
    options = Keyword.merge(config(), options)
    prefix = if float < 0, do: "-", else: ""

    delimited =
      case to_integer(number) do
        {:ok, number} ->
          number = delimit_integer(number, options[:delimiter])

          if options[:precision] > 0 do
            decimals = String.pad_trailing("", options[:precision], "0")
            Enum.join([to_string(number), options[:separator], decimals])
          else
            number
          end

        {:error, other} ->
          other
          |> to_string
          |> Number.Conversion.to_decimal()
          |> delimit_decimal(options[:delimiter], options[:separator], options[:precision])
      end

    delimited = String.Chars.to_string(delimited)
    prefix <> delimited
  end

  defp to_integer(integer) when is_integer(integer) do
    {:ok, integer}
  end

  defp to_integer(%{__struct__: Decimal} = decimal) do
    try do
      {:ok, Decimal.to_integer(decimal)}
    rescue
      _ ->
        {:error, decimal}
    end
  end

  defp to_integer(string) when is_binary(string) do
    try do
      {:ok, String.to_integer(string)}
    rescue
      _ ->
        {:error, string}
    end
  end

  defp to_integer(other) do
    {:error, other}
  end

  defp delimit_integer(number, delimiter) do
    abs(number)
    |> Integer.to_charlist()
    |> :lists.reverse()
    |> delimit_integer(delimiter, [])
  end

  defp delimit_integer([a, b, c, d | tail], delimiter, acc) do
    delimit_integer([d | tail], delimiter, [delimiter, c, b, a | acc])
  end

  defp delimit_integer(list, _, acc) do
    :lists.reverse(list) ++ acc
  end

  @doc false
  def delimit_decimal(decimal, delimiter, separator, precision) do
    string =
      decimal
      |> Decimal.round(precision)
      |> Decimal.to_string(:normal)

    [number, decimals] =
      case String.split(string, ".") do
        [number, decimals] -> [number, decimals]
        [number] -> [number, ""]
      end

    decimals = String.pad_trailing(decimals, precision, "0")

    integer =
      number
      |> String.to_integer()
      |> delimit_integer(delimiter)

    separator = if precision == 0, do: "", else: separator
    Enum.join([integer, separator, decimals])
  end

  defp config do
    defaults = [
      delimiter: ",",
      separator: ".",
      precision: 2
    ]

    Keyword.merge(defaults, Application.get_env(:number, :delimit, []))
  end
end

defmodule Number.Delimit do
  @moduledoc """
  Provides functions to delimit numbers into strings.
  """

  @doc """
  Formats a number into a string with grouped thousands using `delimiter`.

  ## Parameters

  * `number` - A float or integer to convert.

  * `options` - A keyword list of options. See the documentation of all
    available options below for more information.

  ## Options

  * `:precision` - The number of decimal places to include. Default: 2

  * `:delimiter` - The character to use to delimit the number by thousands.
    Default: ","

  * `:separator` - The character to use to separate the number from the decimal
    places. Default: "."

  Default configuration for these options can be specified in the `Number`
  application configuration.

      config :number, delimit: [
                        precision: 3,
                        delimiter: ",",
                        separator: "."
                      ]

  ## Examples

      iex> Number.Delimit.number_to_delimited(nil)
      nil

      iex> Number.Delimit.number_to_delimited(998.999)
      "999.00"

      iex> Number.Delimit.number_to_delimited(-234234.234)
      "-234,234.23"

      iex> Number.Delimit.number_to_delimited("998.999")
      "999.00"

      iex> Number.Delimit.number_to_delimited("-234234.234")
      "-234,234.23"

      iex> Number.Delimit.number_to_delimited(12345678)
      "12,345,678.00"

      iex> Number.Delimit.number_to_delimited(12345678.05)
      "12,345,678.05"

      iex> Number.Delimit.number_to_delimited(12345678, delimiter: ".")
      "12.345.678.00"

      iex> Number.Delimit.number_to_delimited(12345678, delimiter: ",")
      "12,345,678.00"

      iex> Number.Delimit.number_to_delimited(12345678.05, separator: " ")
      "12,345,678 05"

      iex> Number.Delimit.number_to_delimited(98765432.98, delimiter: " ", separator: ",")
      "98 765 432,98"

      iex> Number.Delimit.number_to_delimited(Decimal.from_float(9998.2))
      "9,998.20"

      iex> Number.Delimit.number_to_delimited "123456789555555555555555555555555"
      "123,456,789,555,555,555,555,555,555,555,555.00"

      iex> Number.Delimit.number_to_delimited Decimal.new("123456789555555555555555555555555")
      "123,456,789,555,555,555,555,555,555,555,555.00"
  """
  @spec number_to_delimited(Number.t(), list) :: String.t()
  def number_to_delimited(number, options \\ [])
  def number_to_delimited(nil, _options), do: nil

  def number_to_delimited(number, options) do
    float = number |> Number.Conversion.to_float()
    options = Keyword.merge(config(), options)
    prefix = if float < 0, do: "-", else: ""

    delimited =
      case to_integer(number) do
        {:ok, number} ->
          number = delimit_integer(number, options[:delimiter])

          if options[:precision] > 0 do
            decimals = String.pad_trailing("", options[:precision], "0")
            Enum.join([to_string(number), options[:separator], decimals])
          else
            number
          end

        {:error, other} ->
          other
          |> to_string
          |> Number.Conversion.to_decimal()
          |> delimit_decimal(options[:delimiter], options[:separator], options[:precision])
      end

    delimited = String.Chars.to_string(delimited)
    prefix <> delimited
  end

  defp to_integer(integer) when is_integer(integer) do
    {:ok, integer}
  end

  defp to_integer(%{__struct__: Decimal} = decimal) do
    try do
      {:ok, Decimal.to_integer(decimal)}
    rescue
      _ ->
        {:error, decimal}
    end
  end

  defp to_integer(string) when is_binary(string) do
    try do
      {:ok, String.to_integer(string)}
    rescue
      _ ->
        {:error, string}
    end
  end

  defp to_integer(other) do
    {:error, other}
  end

  defp delimit_integer(number, delimiter) do
    abs(number)
    |> Integer.to_charlist()
    |> :lists.reverse()
    |> delimit_integer(delimiter, [])
  end

  defp delimit_integer([a, b, c, d | tail], delimiter, acc) do
    delimit_integer([d | tail], delimiter, [delimiter, c, b, a | acc])
  end

  defp delimit_integer(list, _, acc) do
    :lists.reverse(list) ++ acc
  end

  @doc false
  def delimit_decimal(decimal, delimiter, separator, precision) do
    string =
      decimal
      |> Decimal.round(precision)
      |> Decimal.to_string(:normal)

    [number, decimals] =
      case String.split(string, ".") do
        [number, decimals] -> [number, decimals]
        [number] -> [number, ""]
      end

    decimals = String.pad_trailing(decimals, precision, "0")

    integer =
      number
      |> String.to_integer()
      |> delimit_integer(delimiter)

    separator = if precision == 0, do: "", else: separator
    Enum.join([integer, separator, decimals])
  end

  defp config do
    defaults = [
      delimiter: ",",
      separator: ".",
      precision: 2
    ]

    Keyword.merge(defaults, Application.get_env(:number, :delimit, []))
  end
end