Appendix A. Solutions to Études

Here are the solutions that I came up with for the études in this book.

Solution 2-1

Here is a suggested solution for Étude 2-1.

geom.ex

defmodule Geom do
  def area(length, width) do
    length * width
  end
end

Solution 2-2

Here is a suggested solution for Étude 2-2.

geom.ex

defmodule Geom do
  def area(length \\ 1, width \\ 1) do
    length * width
  end
end

Solution 2-3

Here is a suggested solution for Étude 2-3.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a rectangle, given the length and width.
  Returns the product of its arguments. The default value for
  both arguments is 1.
  """

  @spec area(number(), number()) :: number()

  def area(length \\ 1, width \\ 1) do
    length * width
  end
end

Solution 3-1

Here is a suggested solution for Étude 3-1.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the shape and two of the
  dimensions. Returns the product of its arguments for a rectangle,
  one half the product of the arguments for a triangle, and
  :math.pi times the product of the arguments for an ellipse.
  """

  @spec area(atom(), number(), number()) :: number()

  def area(:rectangle, length, width) do
    length * width
  end

  def area(:triangle, base, height) do
    base * height / 2.0
  end

  def area(:ellipse, a, b) do
    :math.pi * a * b
  end
end

Solution 3-2

Here is a suggested solution for Étude 3-2.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the shape and two of the
  dimensions. Returns the product of its arguments for a rectangle,
  one half the product of the arguments for a triangle, and
  :math.pi times the product of the arguments for an ellipse.
  Accepts only dimensions that are non-negative.
  """

  @spec area(atom(), number(), number()) :: number()

  def area(:rectangle, length, width) when length >= 0 and width >= 0 do
    length * width
  end

  def area(:triangle, base, height) when base >= 0 and height >= 0 do
    base * height / 2.0
  end

  def area(:ellipse, a, b) when a >= 0 and b >= 0 do
    :math.pi * a * b
  end
end

Solution 3-3

Here is a suggested solution for Étude 3-3.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the
  shape and two of the dimensions. Returns the product
  of its arguments for a rectangle, one half the
  product of the arguments for a triangle, and
  :math.pi times the product of the arguments for
  an ellipse. Any invalid data returns zero.
  """

  @spec area(atom(), number(), number()) :: number()

  def area(:rectangle, length, width) when length >= 0 and width >= 0 do
    length * width
  end

  def area(:triangle, base, height) when base >= 0 and height >= 0 do
    base * height / 2.0
  end

  def area(:ellipse, a, b) when a >= 0 and b >= 0 do
    :math.pi * a * b
  end

  def area(_, _, _) do
    0
  end
end

Solution 3-4

Here is a suggested solution for Étude 3-4.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the
  shape and two of the dimensions as a tuple.
  Returns the productof its arguments for a rectangle,
  one half the product of the arguments for a triangle, and
  :math.pi times the product of the arguments for
  an ellipse. Any invalid data returns zero.
  """

  @spec area({atom(), number(), number()}) :: number()

  def area({shape, dim1, dim2}) do
    area(shape, dim1, dim2)
  end

  # Individual functions to handle each shape

  @spec area(atom(), number(), number()) :: number()

  defp area(:rectangle, length, width) when length >= 0 and width >= 0 do
    length * width
  end

  defp area(:triangle, base, height) when base >= 0 and height >= 0 do
    base * height / 2.0
  end

  defp area(:ellipse, a, b) when a >= 0 and b >= 0 do
    :math.pi * a * b
  end

  defp area(_, _, _) do
    0
  end
end

Solution 4-1

Here is a suggested solution for Étude 4-1.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the shape and two of the
  dimensions. Returns the product of its arguments for a rectangle,
  one half the product of the arguments for a triangle, and
  :math.pi times the product of the arguments for an ellipse.
  Accepts only dimensions that are non-negative.
  """

  @spec area(atom(), number(), number()) :: number()

  def area(shape, a, b) when a >= 0 and b >= 0 do
    case shape do
      :rectangle -> a * b
      :triangle -> a * b  / 2.0
      :ellipse -> :math.pi * a * b
    end
  end
end

Solution 4-2

Here is a suggested solution for Étude 4-2.

dijkstra.ex

defmodule Dijkstra do

  @moduledoc """
  Recursive function for calculating GCD of two numbers using
  Dijkstra's algorithm.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the greatest common divisor of two integers.
  Uses Dijkstra's algorithm, which does not require any division.
  """

  @spec gcd(number(), number()) :: number()

  def gcd(m, n) do
    cond do
      m == n -> m
      m > n -> gcd(m - n, n)
      true -> gcd(m, n - m)
    end
  end
end

Solution 4-2

Here is another solution for Étude 4-2. This solution uses multiple clauses with guards instead of cond.

dijkstra.ex

defmodule Dijkstra do

  @moduledoc """
  Recursive function for calculating GCD of two numbers using
  Dijkstra's algorithm.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the greatest common divisor of two integers.
  Uses Dijkstra's algorithm, which does not require any division.
  """

  @spec gcd(number(), number()) :: number()

  def gcd(m, n) when m == n do
    m
  end

  def gcd(m, n) when m > n do
    gcd(m - n, n)
  end

  def gcd(m, n) do
    gcd(m, n - m)
  end
end

Solution 4-3

Here is a suggested solution for Étude 4-3.

powers.ex

defmodule Powers do
  import Kernel, except: [raise: 2]

  @moduledoc """
  Function for raising a number to an integer power.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Raise a number x to an integer power n.
  Any number to the power 0 equals 1.
  Any number to the power 1 is that number itself.
  When n is positive, x^n is equal to x times x^(n - 1)
  When n is negative, x^n is equal to 1.0 / x^n
  """

  @spec raise(number(), number()) :: number()

  def raise(_, 0) do
    1
  end

  def raise(x, 1) do
    x
  end

  def raise(x, n) when n > 0 do
    x * raise(x, n - 1)
  end

  def raise(x, n) when n < 0 do
    1.0 / raise(x, -n)
  end
end

powers_traced.ex

This code contains output that lets you see the progress of the recursion.

defmodule Powers do
  import Kernel, except: [raise: 2]

  @moduledoc """
  Function for raising a number to an integer power.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Raise a number x to an integer power n.
  Any number to the power 0 equals 1.
  Any number to the power 1 is that number itself.
  When n is positive, x^n is equal to x times x^(n - 1)
  When n is negative, x^n is equal to 1.0 / x^n
  """

  @spec raise(number(), number()) :: number()

  def raise(_, 0) do
    1
  end

  def raise(x, 1) do
    x
  end

  def raise(x, n) when n > 0 do
    IO.puts("Enter with x = #{x}, n = #{n}")
    result = x * raise(x, n - 1)
    IO.puts("Result is #{result}")
    result
  end

  def raise(x, n) when n < 0 do
    1.0 / raise(x, -n)
  end
end

Solution 4-4

Here is a suggested solution for Étude 4-4.

powers.ex

defmodule Powers do
  import Kernel, except: [raise: 2, raise: 3]

  @moduledoc """
  Function for raising a number to an integer power.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Raise a number x to an integer power n.
  Any number to the power 0 equals 1.
  Any number to the power 1 is that number itself.
  When n is positive, x^n is equal to x times x^(n - 1)
  When n is negative, x^n is equal to 1.0 / x^n
  """

  @spec raise(number(), number()) :: number()

  def raise(_, 0) do
    1
  end

  def raise(x, n) when n < 0 do
    1.0 / raise(x, -n)
  end

  def raise(x, n) when n > 0 do
    raise(x, n, 1)
  end

  # Helper function: counts down from n to 0,
  # multiplying the accumulator by x each time.
  # Returns the accumulator when n reaches zero.

  @spec raise(number(), number(), number()) :: number()

  defp raise(_x, 0, accumulator) do
    accumulator
  end

  defp raise(x, n, accumulator) do
    raise(x, n - 1, x * accumulator)
  end
end

Solution 4-5

Here is a suggested solution for Étude 4-5.

powers.ex

defmodule Powers do
  import Kernel, except: [raise: 2, raise: 3]

  @moduledoc """
  Function for raising a number to an integer power and finding
  the nth root of a number using Newton's method.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculate the nth root of x using the Newton-Raphson method.
  """

  @spec nth_root(number(), number()) :: number()

  def nth_root(x, n) do
    nth_root(x, n, x / 2.0)
  end

  # Helper function; given estimate for x^n,
  # recursively calculates next estimated root as
  # estimate - (estimate^n - x) / (n * estimate^(n-1))
  # until the next estimate is within a limit of previous estimate.

  defp nth_root(x, n, estimate) do
    IO.puts("Current guess is #{estimate}")
    f = raise(estimate, n) - x
    f_prime = n * raise(estimate, n - 1)
    next = estimate - f / f_prime
    change = abs(estimate - next)
    cond do
      change < 1.0e-8 -> next
      true -> nth_root(x, n, next)
    end
  end

  @doc """
  Raise a number x to an integer power n.
  Any number to the power 0 equals 1.
  Any number to the power 1 is that number itself.
  When n is positive, x^n is equal to x times x^(n - 1)
  When n is negative, x^n is equal to 1.0 / x^n
  """

  @spec raise(number(), number()) :: number()

  def raise(_, 0) do
    1
  end

  def raise(x, n) when n < 0 do
    1.0 / raise(x, -n)
  end

  def raise(x, n) when n > 0 do
    raise(x, n, 1)
  end

  # Helper function: counts down from n to 0,
  # multiplying the accumulator by x each time.
  # Returns the accumulator when n reaches zero.

  @spec raise(number(), number(), number()) :: number()

  defp raise(_x, 0, accumulator) do
    accumulator
  end

  defp raise(x, n, accumulator) do
    raise(x, n - 1, x * accumulator)
  end
end

Solution 5-1

Here is a suggested solution for Étude 5-1.

geom.ex

defmodule Geom do
  @moduledoc """
  Functions for calculating areas of geometric shapes.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculates the area of a shape, given the shape and two of the
  dimensions. Returns the product of its arguments for a rectangle,
  one half the product of the arguments for a triangle, and
  :math.pi times the product of the arguments for an ellipse.
  Accepts only dimensions that are non-negative.
  """

  @spec area(atom(), number(), number()) :: number()

  def area(shape, a, b) when a >= 0 and b >= 0 do
    case shape do
      :rectangle -> a * b
      :triangle -> a * b  / 2.0
      :ellipse -> :math.pi * a * b
    end
  end
end

ask_area.ex

defmodule AskArea do
  @moduledoc """
  Validate input.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Requests a character for the name of a shape,
  numbers for its dimensions, and calculates shape's area.
  The characters are R for rectangle, T for triangle,
  and E for ellipse. Input is allowed in either upper
  or lower case. For unknown shapes, the first "dimension" will
  be the unknown character.
  """

  @spec area() :: number()

  def area() do
    input = IO.gets("R)ectangle, T)riangle, or E)llipse: ")
    shape = char_to_shape(String.first(input))
    {d1, d2} = case shape do
      :rectangle -> get_dimensions("width", "height")
      :triangle -> get_dimensions("base ", "height" )
      :ellipse -> get_dimensions("major radius", "minor radius")
      :unknown -> {String.first(input), 0}
    end
    calculate(shape, d1, d2)
  end

  @doc """
  Given a character, returns an atom representing the
  specified shape (or the atom unknown if a bad character is given).
  """

  @spec char_to_shape(char()) :: atom()

  def char_to_shape(character) do
    cond do
      String.upcase(character) == "R" -> :rectangle
      String.upcase(character) == "T" -> :triangle
      String.upcase(character) == "E" -> :ellipse
      true -> :unknown
    end
  end

  @doc """
  Present a prompt and get a number from the
  user. Allow integers only.
  """
  @spec get_number(String.t()) :: number()

  def get_number(prompt) do
    input = IO.gets("Enter #{prompt} > ")
    binary_to_integer(String.strip(input))
  end

  @doc """
  Get dimensions for a shape. Input are the two prompts,
  output is a tuple {Dimension1, Dimension2}.
  """
  @spec get_dimensions(String.t(), String.t()) :: {number(), number()}

  def get_dimensions(prompt1, prompt2) do
    n1 = get_number(prompt1)
    n2 = get_number(prompt2)
    {n1, n2}
  end

  @doc """
  Calculate area of a shape, given its shape and dimensions.
  Handle errors appropriately.
  """
  @spec calculate(atom(), number(), number()) :: number()

  def calculate(shape, d1, d2) do
    cond do
      shape == :unknown -> IO.puts("Unknown shape #{d1}")
      d1 < 0 or d2 < 0 ->
        IO.puts("Both numbers must be greater than or equal to zero.")
      true -> Geom.area(shape, d1, d2)
    end
  end
end

Solution 5-2

Here is a suggested solution for Étude 5-2.

The geom.ex file is the same as in Étude 5-1.

ask_area.ex

defmodule AskArea do
  @moduledoc """
  Validate input.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Requests a character for the name of a shape,
  numbers for its dimensions, and calculates shape's area.
  The characters are R for rectangle, T for triangle,
  and E for ellipse. Input is allowed in either upper
  or lower case. For unknown shapes, the first "dimension" will
  be the unknown character.
  """

  @spec area() :: number()

  def area() do
    input = IO.gets("R)ectangle, T)riangle, or E)llipse: ")
    shape = char_to_shape(String.first(input))
    {d1, d2} = case shape do
      :rectangle -> get_dimensions("width", "height")
      :triangle -> get_dimensions("base ", "height" )
      :ellipse -> get_dimensions("major radius", "minor radius")
      :unknown -> {String.first(input), 0}
    end
    calculate(shape, d1, d2)
  end

  @doc """
  Given a character, returns an atom representing the
  specified shape (or the atom unknown if a bad character is given).
  """

  @spec char_to_shape(char()) :: atom()

  def char_to_shape(character) do
    cond do
      String.upcase(character) == "R" -> :rectangle
      String.upcase(character) == "T" -> :triangle
      String.upcase(character) == "E" -> :ellipse
      true -> :unknown
    end
  end

  @doc """
  Present a prompt and get a number from the
  user. Allow integers only.
  """
  @spec get_number(String.t()) :: number()

  def get_number(prompt) do
    input = IO.gets("Enter #{prompt} > ")
    binary_to_integer(String.strip(input))
  end

  @doc """
  Get dimensions for a shape. Input are the two prompts,
  output is a tuple {Dimension1, Dimension2}.
  """
  @spec get_dimensions(String.t(), String.t()) :: {number(), number()}

  def get_dimensions(prompt1, prompt2) do
    n1 = get_number(prompt1)
    n2 = get_number(prompt2)
    {n1, n2}
  end

  @doc """
  Calculate area of a shape, given its shape and dimensions.
  Handle errors appropriately.
  """
  @spec calculate(atom(), number(), number()) :: number()

  def calculate(shape, d1, d2) do
    cond do
      shape == :unknown -> IO.puts("Unknown shape #{d1}")
      d1 < 0 or d2 < 0 ->
        IO.puts("Both numbers must be greater than or equal to zero.")
      true -> Geom.area(shape, d1, d2)
    end
  end
end

Solution 5-3

Here is a suggested solution for Étude 5-3.

dates.ex

defmodule Dates do
  @moduledoc """
  Functions for manipulating calendar dates.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Takes a string in ISO date format (yyyy-mm-dd) and
  returns a list of integers in form [year, month, day].
  """
  @spec date_parts(list) :: list

  def date_parts(date_str) do
    [y_str, m_str, d_str] = String.split(date_str, ~r/-/)
    [binary_to_integer(y_str), binary_to_integer(m_str),
      binary_to_integer(d_str)]
  end
end

Solution 6-1

Here is a suggested solution for Étude 6-1.

stats.ex

defmodule Stats do
  @moduledoc """
  Functions for calculating basic statistics.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Recursively find the minimum entry in a list of numbers."

  @spec minimum([number]) :: number

  def minimum(list) do
    [head | tail] = list
    minimum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec minimum([number], number) :: number

  defp minimum([], result) do
    result
  end

  # If the current result is less than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.
  # Note that you can use a variable assigned as part of a cons in a guard.

  defp minimum([head | tail], result) when result < head do
    minimum(tail, result)
  end

  # Otherwise, the head of the list becomes the new minimum,
  # and recursively look at the remainder of the list.

  defp minimum([head | tail], _result) do
    minimum(tail, head)
  end

  @doc "Recursively find the maximum entry in a list of numbers."
  @spec maximum([number]) :: number

  def maximum(list) do
    [head | tail] = list
    maximum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec maximum([number], number) :: number

  defp maximum([], result) do
    result
  end

  # If the current result is greater than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.

  defp maximum([head | tail], result) when result > head do
    maximum(tail, result)
  end

  # Otherwise, the head of the list becomes the new maximum,
  # and recursively look at the remainder of the list.

  defp maximum([head | tail], _result) do
    maximum(tail, head)
  end

  # @doc "Find the range of a list of numbers as a list [min, max]."
  @spec range([number]) :: [number]

  def range(list) do
    [minimum(list), maximum(list)]
  end

end

Solution 6-2

Here is a suggested solution for Étude 6-2.

dates.ex

defmodule Dates do
  @moduledoc """
  Functions for manipulating calendar dates.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Calculate julian date from an ISO date string"

  @spec julian(String.t) :: number

  def julian(date_str) do
    [y, m, d] = date_parts(date_str)
    days_per_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    result = month_total(m, days_per_month, 0) + d
    cond  do
      is_leap_year(y) and m > 2 -> result + 1
      true -> result
    end
  end

  @spec month_total(number, [number], number) :: number

  # Helper function that recursively accumulates days
  # for all months up to (but not including) the current month

  defp month_total(1, _days_per_month, total) do
    total
  end

  defp month_total(m, [this_month|other_months], total) do
    month_total(m - 1, other_months, total + this_month)
  end

  defp is_leap_year(year) do
    (rem(year,4) == 0 and rem(year,100) != 0)
    or (rem(year, 400) == 0)
  end

  @doc """
  Takes a string in ISO date format (yyyy-mm-dd) and
  returns a list of integers in form [year, month, day].
  """
  @spec date_parts(list) :: list

  def date_parts(date_str) do
    [y_str, m_str, d_str] = String.split(date_str, ~r/-/)
    [binary_to_integer(y_str), binary_to_integer(m_str),
      binary_to_integer(d_str)]
  end
end

Here is a suggested solution for Étude 6-3 with leap years handled in a different way.

dates.ex

defmodule Dates do
  @moduledoc """
  Functions for manipulating calendar dates.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Calculate julian date from an ISO date string"

  @spec julian(String.t) :: number

  def julian(date_str) do
    [y, m, d] = date_parts(date_str)
    days_in_feb = cond do
      is_leap_year(y) -> 29
      true -> 28
    end
    days_per_month = [31, days_in_feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    month_total(m, days_per_month, 0) + d
  end

  @spec month_total(number, [number], number) :: number

  # Helper function that recursively accumulates days
  # for all months up to (but not including) the current month

  defp month_total(1, _days_per_month, total) do
    total
  end

  defp month_total(m, [this_month|other_months], total) do
    month_total(m - 1, other_months, total + this_month)
  end

  defp is_leap_year(year) do
    (rem(year,4) == 0 and rem(year,100) != 0)
    or (rem(year, 400) == 0)
  end

  @doc """
  Takes a string in ISO date format (yyyy-mm-dd) and
  returns a list of integers in form [year, month, day].
  """
  @spec date_parts(list) :: list

  def date_parts(date_str) do
    [y_str, m_str, d_str] = String.split(date_str, ~r/-/)
    [binary_to_integer(y_str), binary_to_integer(m_str),
      binary_to_integer(d_str)]
  end
end

Solution 6-3

Here is a suggested solution for Étude 6-3.

teeth.ex

defmodule Teeth do
  @moduledoc """
  Manipulate a list of lists representing tooth pocket depths.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Convenience function for providing a list of lists."
  @spec pocket_depths() :: list(list())

  def pocket_depths do
    [[0], [2,2,1,2,2,1], [3,1,2,3,2,3],
    [3,1,3,2,1,2], [3,2,3,2,2,1], [2,3,1,2,1,1],
    [3,1,3,2,3,2], [3,3,2,1,3,1], [4,3,3,2,3,3],
    [3,1,1,3,2,2], [4,3,4,3,2,3], [2,3,1,3,2,2],
    [1,2,1,1,3,2], [1,2,2,3,2,3], [1,3,2,1,3,3], [0],
    [3,2,3,1,1,2], [2,2,1,1,3,2], [2,1,1,1,1,2],
    [3,3,2,1,1,3], [3,1,3,2,3,2], [3,3,1,2,3,3],
    [1,2,2,3,3,3], [2,2,3,2,3,3], [2,2,2,4,3,4],
    [3,4,3,3,3,4], [1,1,2,3,1,2], [2,2,3,2,1,3],
    [3,4,2,4,4,3], [3,3,2,1,2,3], [2,2,2,2,3,3],
    [3,2,3,2,3,2]]
  end

  @doc """
  Given a list of list representing tooth pocket depths, return
  a list of the tooth numbers that require attention (any pocket
  depth greater than or equal to four).
  """
  @spec alert(list(list())) :: list()

  def alert(depths) do
    alert(depths, 1, [])
  end

  # Helper function that adds to the list of teeth needing attention

  def alert([], _tooth_number, result) do
    Enum.reverse(result)
  end

  def alert([h | tail], tooth_number, result) do
    cond do
      Stats.maximum(h) >= 4 ->
        alert(tail, tooth_number + 1, [tooth_number | result])
      true ->
        alert(tail, tooth_number + 1, result)
    end
  end
end

stats.ex

defmodule Stats do
  @moduledoc """
  Functions for calculating basic statistics.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Recursively find the minimum entry in a list of numbers."

  @spec minimum([number]) :: number

  def minimum(list) do
    [head | tail] = list
    minimum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec minimum([number], number) :: number

  defp minimum([], result) do
    result
  end

  # If the current result is less than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.
  # Note that you can use a variable assigned as part of a cons in a guard.

  defp minimum([head | tail], result) when result < head do
    minimum(tail, result)
  end

  # Otherwise, the head of the list becomes the new minimum,
  # and recursively look at the remainder of the list.

  defp minimum([head | tail], _result) do
    minimum(tail, head)
  end

  @doc "Recursively find the maximum entry in a list of numbers."
  @spec maximum([number]) :: number

  def maximum(list) do
    [head | tail] = list
    maximum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec maximum([number], number) :: number

  defp maximum([], result) do
    result
  end

  # If the current result is greater than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.

  defp maximum([head | tail], result) when result > head do
    maximum(tail, result)
  end

  # Otherwise, the head of the list becomes the new maximum,
  # and recursively look at the remainder of the list.

  defp maximum([head | tail], _result) do
    maximum(tail, head)
  end

  # @doc "Find the range of a list of numbers as a list [min, max]."
  @spec range([number]) :: [number]

  def range(list) do
    [minimum(list), maximum(list)]
  end

end

Solution 6-4

Here is a suggested solution for Étude 6-4.

non_fp.ex

defmodule NonFP do
  @moduledoc """
  Use non-functional-programming constructs to create
  a list of lists with random entries.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Generate a list of list representing pocket depths for a set of teeth.
  The first argument is a character list consisting of T and F for teeth
  that are present or absent. The second argument is the probability that
  any given tooth will be good.
  """
  @spec generate_pockets(list, number) :: list(list)

  def generate_pockets(tooth_list, prob_good) do
    :random.seed(:erlang.now())
    generate_pockets(tooth_list, prob_good, [])
  end

  # When the tooth list is empty, return the final list of lists
  # If a tooth is present, generate a set of six depths and add it
  # to the result; otherwise add a [0] to the result.

  defp generate_pockets([], _, result) do
    Enum.reverse(result)
  end

  defp generate_pockets([head | tail], prob_good, result) when head == ?T do
    tooth = generate_tooth(prob_good)
    generate_pockets(tail, prob_good, [tooth | result])
  end

  defp generate_pockets([_head | tail], _prob_good, result) do
    generate_pockets(tail, _prob_good, [[0] | result])
  end

  @doc """
  Generate a set of six pocket depths for a tooth, given a probability
  that a tooth is good.
  """
  @spec generate_tooth(number) :: list(number)

  def generate_tooth(prob_good) do
    r = :random.uniform()
    if (r < prob_good) do
      base_depth = 2
    else
      base_depth = 3
    end
    generate_tooth(base_depth, 6, [])
  end

  def generate_tooth(_base, 0, result) do
    result
  end

  def generate_tooth(base, n, result) do
    delta = :random.uniform(3) - 2  # result will be -1, 0, or 1
    generate_tooth(base, n - 1, [base + delta | result])
  end

  def test_pockets() do
    tlist = 'FTTTTTTTTTTTTTTFTTTTTTTTTTTTTTTT'
    big_list = generate_pockets(tlist, 0.75)
    print_pockets(big_list)
  end

  def print_pockets([]), do: IO.puts("Finished.")

  def print_pockets([head | tail]) do
    IO.puts(inspect(head))
    print_pockets(tail)
  end
end

Solution 7-1

Here is a suggested solution for Étude 7-1.

college.ex

defmodule College do
  @moduledoc """
  Using files and hash dictionaries.
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Open a file with columns course ID, name, and room.
  Construct a HashDict with the room as a key and the courses
  taught in that room as the value.
  """
  @spec make_room_list(String.t) :: HashDict.t

  def make_room_list(file_name) do
    {_result, device} = File.open(file_name, [:read, :utf8])
    room_list = HashDict.new()
    process_line(device, room_list)
  end

  # Read next line from file; if not end of file, process
  # the room on that line. Recursively read through end of file.

  defp process_line(device, room_list) do
    data = IO.read(device, :line)
    case data do
      :eof ->
        File.close(device)
        room_list
      _ ->
        updated_list = process_room(data, room_list)
        process_line(device, updated_list)
    end
  end

  # Extract information from a line in the file, and append
  # course to hash dictionary value for the given room.

  defp process_room(data, room_list) do
    [_id, course, room] = String.split(String.strip(data), ",")
    course_list = HashDict.get(room_list, room)
    case course_list do
      nil -> updated_list = HashDict.put_new(room_list, room, [course])
      _ -> updated_list = HashDict.put(room_list, room, [course | course_list])
    end
    updated_list
  end
end

Solution 7-1

Here is a suggested solution for Étude 7-2.

geography.ex

defmodule City do
  defstruct name: "",  population: 0, latitude: 0.0, longitude: 0.0
end

defmodule Country do
  defstruct name: "", language: "", cities: []
end

defmodule Geography do
  @moduledoc """
  Using files and structures.
  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Open a file with columns country code, city, population,
  latitude, and longitude.
  Construct a Country structure for each country containing
  the cities in that country.
  """
  @spec make_geo_list(String.t) :: Country.t

  def make_geo_list(file_name) do
    {_result, device} = File.open(file_name, [:read, :utf8])
    process_line(device, [])
  end

  # Read next line from file; if not end of file, process
  # the data on that line. Recursively read through end of file.

  defp process_line(device, geo_list) do
    data = IO.read(device, :line)
    case data do
      :eof ->
        File.close(device)
        geo_list
      _ ->
        info = String.split(String.strip(data), ",")
        updated_list = process_info(info, geo_list)
        process_line(device, updated_list)
    end
  end

  # If the info has only two elements, start a new country

  defp process_info([country, language], geo_list) do
   [%Country{name: country, language: language, cities: []}
      | geo_list]
  end

  # If it has four elements, it's a city; add it to the list of
  # cities. Notice the code for updating the cities field in the
  # head of the list.

  defp process_info([city, populn, lat, long], [hd|tail]) do
    new_cities = [%City{name: city, population: String.to_integer(populn),
      latitude: String.to_float(lat), longitude: String.to_float(long)} |
      hd.cities]
    [%Country{ hd | cities: new_cities} | tail]
  end

end

Solution 7-3

Here is a suggested solution for Étude 7-3.

geography.ex

defmodule City do
  defstruct name: "",  population: 0, latitude: 0.0, longitude: 0.0
end

defmodule Country do
  defstruct name: "", language: "", cities: []
end

defmodule Geography do
  @moduledoc """
  Using files and structures.
  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Open a file whose name is given in the first argument.
  The file contains country name and primary language,
  followed by (for each country) lines giving the name
  of a city, its population, latitude, and longitude.
  Construct a Country structure for each country containing
  the cities in that country.
  """
  @spec make_geo_list(String.t) :: Country.t

  def make_geo_list(file_name) do
    {_result, device} = File.open(file_name, [:read, :utf8])
    process_line(device, [])
  end

  @doc """
  Find the total population of all cities in the list
  that are in countries with a given primary language.
  """

  @spec total_population([Country], String.t) :: integer

  def total_population(geo_list, language) do
    total_population(geo_list, language, 0)
  end


  defp total_population([], _language, total) do
    total
  end

  defp total_population([head|tail], language, total) do
    if (head.language == language) do
      total_population(tail, language, subtotal(head.cities, 0))
    else
      total_population(tail, language, total)
    end
  end

  defp subtotal([], accumulator) do
    accumulator
  end

  defp subtotal([head | tail], accumulator) do
    subtotal(tail, accumulator + head.population)
  end

  # Read next line from file; if not end of file, process
  # the data on that line. Recursively read through end of file.

  defp process_line(device, geo_list) do
    data = IO.read(device, :line)
    case data do
      :eof ->
        File.close(device)
        geo_list
      _ ->
        info = String.split(String.strip(data), ",")
        updated_list = process_info(info, geo_list)
        process_line(device, updated_list)
    end
  end

  # If the info has only two elements, start a new country

  defp process_info([country, language], geo_list) do
   [%Country{name: country, language: language, cities: []}
      | geo_list]
  end

  # If it has four elements, it's a city; add it to the list of
  # cities. Notice the code for updating the cities field in the
  # head of the list.

  defp process_info([city, populn, lat, long], [hd|tail]) do
    new_cities = [%City{name: city, population: String.to_integer(populn),
      latitude: String.to_float(lat), longitude: String.to_float(long)} |
      hd.cities]
    [%Country{ hd | cities: new_cities} | tail]
  end

end

Solution 7-4

Here is a suggested solution for Étude 7-4. The code shown here is the code required to implement the additional protocols; the remaining code is the same as in

city.ex

defprotocol Valid do
  @doc "Returns true if data is considered valid"
  def valid?(data)
end

defmodule City do
  defstruct name: "",  population: 0, latitude: 0.0, longitude: 0.0
end

defimpl Valid, for: City do
  def valid?(%City{population: p, latitude: lat, longitude: lon}) do
    p > 0 && lat >= -90 && lat <= 90 &&
    lon >= -180 && lon <= 180
  end
end

defimpl Inspect, for: City do
  import Inspect.Algebra

  def inspect(item, _options) do
    lat = if (item.latitude < 0) do
      concat(to_string(Float.round(abs(item.latitude * 1.0), 2)), "°S")
    else
      concat(to_string(Float.round(item.latitude * 1.0, 2)), "°N")
    end

    lon = if (item.longitude < 0) do
      concat(to_string(Float.round(abs(item.longitude * 1.0), 2)), "°W")
    else
      concat(to_string(Float.round(item.longitude * 1.0, 2)), "°E")
    end

    msg = concat([item.name, break,
      "(", to_string(item.population), ")", break,
      lat, break, lon])
    pretty(msg, 80)
  end
end

Solution 8-1

Here is a suggested solution for Étude 8-1.

calculus.ex

defmodule Calculus do
  @moduledoc """
  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Calculate the approximation to the derivative of
  function f and point x by using the mathematical
  definition of a derivative.
  """
  @spec derivative(fun, number) :: number

  def derivative(f, x) do
    delta = 1.0e-10
    (f.(x + delta) - f.(x)) / delta
  end
end

Solution 8-2

Here is a suggested solution for Étude 8-2.

list_comp.ex

defmodule ListComp do

  # private function to return a list of people

  @spec get_people() :: list(tuple())

  defp get_people() do
    [{"Federico", "M", 22}, {"Kim", "F", 45}, {"Hansa", "F", 30},
      {"Tran", "M", 47}, {"Cathy", "F", 32}, {"Elias", "M", 50}]
  end

  @doc """
  Select all males older than 40 from a list of tuples giving
  name, gender, and age.
  """
  @spec older_males() :: list(tuple())

  def older_males() do
    lc {person, gender, age} inlist get_people(), age > 40, gender == "M" do
      {person, gender, age}
    end
  end

  @doc"""
  Select all people who are male or older than 40 from a list of
  tuples giving name, gender, and age.
  """
  @spec older_or_male() :: list
  def older_or_male() do
    lc {person, gender, age} inlist get_people(), age > 40 or gender == "M" do
      {person, gender, age}
    end
  end
end

Solution 8-3

Here is a suggested solution for Étude 8-3.

stats.ex

defmodule Stats do
  @moduledoc """
  Functions for calculating basic statistics.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Compute the mean of a list of numbers. Uses List.foldl/3"
  @spec mean([number]) :: number

  def mean(list) do
    List.foldl(list, 0, fn(x, acc) -> x + acc end) / Enum.count(list)
  end

  @doc """
  Compute the standard deviation of a list of numbers. Uses
  List.foldl/3 with a tuple as an accumulator.
  """
  @spec stdv([number]) :: number

  def stdv(list) do
    n = Enum.count(list)
    {sum, sum_sq} = List.foldl(list, {0,0}, fn(x, {acc, acc_sq}) ->
      {acc + x, acc_sq + x * x} end)
    :math.sqrt((n * sum_sq - sum * sum) / (n * (n - 1)))
  end

  @doc "Recursively find the minimum entry in a list of numbers."

  @spec minimum([number]) :: number

  def minimum(list) do
    [head | tail] = list
    minimum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec minimum([number], number) :: number

  defp minimum([], result) do
    result
  end

  # If the current result is less than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.
  # Note that you can use a variable assigned as part of a cons in a guard.

  defp minimum([head | tail], result) when result < head do
    minimum(tail, result)
  end

  # Otherwise, the head of the list becomes the new minimum,
  # and recursively look at the remainder of the list.

  defp minimum([head | tail], _result) do
    minimum(tail, head)
  end

  @doc "Recursively find the maximum entry in a list of numbers."
  @spec maximum([number]) :: number

  def maximum(list) do
    [head | tail] = list
    maximum(tail, head)
  end

  # When there are no more numbers, return the result.

  @spec maximum([number], number) :: number

  defp maximum([], result) do
    result
  end

  # If the current result is greater than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.

  defp maximum([head | tail], result) when result > head do
    maximum(tail, result)
  end

  # Otherwise, the head of the list becomes the new maximum,
  # and recursively look at the remainder of the list.

  defp maximum([head | tail], _result) do
    maximum(tail, head)
  end

  # @doc "Find the range of a list of numbers as a list [min, max]."
  @spec range([number]) :: [number]

  def range(list) do
    [minimum(list), maximum(list)]
  end

end

Solution 8-4

Here is a suggested solution for Étude 8-4.

dates.ex

defmodule Dates do
  @moduledoc """
  Functions for manipulating calendar dates.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Calculate julian date from an ISO date string"

  @spec julian(String.t) :: number

  def julian(date_str) do
    [y, m, d] = date_parts(date_str)
    days_per_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    result = month_total(m, days_per_month) + d
    cond  do
      is_leap_year(y) and m > 2 -> result + 1
      true -> result
    end
  end

  @spec month_total(number, [number]) :: number

  # Helper function that recursively accumulates days
  # for all months up to (but not including) the current month

  defp month_total(m, days_per_month) do
    {relevant, _} = Enum.split(days_per_month, m - 1)
    List.foldl(relevant, 0, fn(x, acc) -> x + acc end)
  end

  defp is_leap_year(year) do
    (rem(year,4) == 0 and rem(year,100) != 0)
    or (rem(year, 400) == 0)
  end

  @doc """
  Takes a string in ISO date format (yyyy-mm-dd) and
  returns a list of integers in form [year, month, day].
  """
  @spec date_parts(list) :: list

  def date_parts(date_str) do
    [y_str, m_str, d_str] = String.split(date_str, ~r/-/)
    [binary_to_integer(y_str), binary_to_integer(m_str),
      binary_to_integer(d_str)]
  end
end

Solution 8-5

Here is a suggested solution for Étude 8-5.

cards.ex

defmodule Cards do
  @moduledoc """
  Functions for simulating a deck of cards.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Create a deck of 52 tuples in the form [{"A", "Clubs"},
  {"A", "Diamonds"}...]
  """
  @spec make_deck() :: list(tuple)

  def make_deck() do
    lc value inlist ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
      suit inlist ["Clubs", "Diamonds", "Hearts", "Spades"], do:
      {value, suit}
  end
end

Solution 8-6

Here is a suggested solution for Étude 8-6.

cards.ex

defmodule Cards do
  @moduledoc """
  Functions for simulating a deck of cards.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Shuffle a list into random order using the Fisher-Yates method.
  """
  @spec shuffle(list) :: list

  def shuffle(list) do
    :random.seed(:erlang.now())
    shuffle(list, [])
  end

  # The helper function takes a list to shuffle as its first
  # argument and the accumulated (shuffled) list as its second
  # argument.

  # When there are no more cards left to shuffle,
  # return the accumulated list.

  def shuffle([], acc) do
    acc
  end

  # This is easier to understand if you look at it as a
  # physical process. Split the deck at a random point.
  # Put the part above the "split point" aside (leading), and
  # take the first card (h) off the part below the split (t).
  # That first card goes onto a new pile ([h | acc]).
  # Now put together the part above the split and the
  # part below the split (leading ++ t) and go through
  # the process with the deck (which now has one less card).
  # This keeps going until you run out of cards to shuffle;
  # at that point, all the cards will have gotten to the
  # new pile, and that's your shuffled deck.

  def shuffle(list, acc) do
    {leading, [h | t]} =
      Enum.split(list, :random.uniform(Enum.count(list)) - 1)
      shuffle(leading ++ t, [h | acc])
  end

  @doc """
  Create a deck of 52 tuples in the form [{"A", "Clubs"},
  {"A", "Diamonds"}...]
  """
  @spec make_deck() :: list(tuple)

  def make_deck() do
    lc value inlist ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
      suit inlist ["Clubs", "Diamonds", "Hearts", "Spades"], do:
      {value, suit}
  end

end

Solution 9-1

Here is a suggested solution for Étude 9-1.

cards.ex

defmodule Cards do
  @moduledoc """
  Functions for simulating a deck of cards.

  from *Études for Elixir*, O'Reilly Media, Inc., 2013.
  Copyright 2013 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Shuffle a list into random order using the Fisher-Yates method.
  """
  @spec shuffle(list) :: list

  def shuffle(list) do
    :random.seed(:erlang.now())
    shuffle(list, [])
  end

  # The helper function takes a list to shuffle as its first
  # argument and the accumulated (shuffled) list as its second
  # argument.

  # When there are no more cards left to shuffle,
  # return the accumulated list.

  def shuffle([], acc) do
    acc
  end

  # This is easier to understand if you look at it as a
  # physical process. Split the deck at a random point.
  # Put the part above the "split point" aside (leading), and
  # take the first card (h) off the part below the split (t).
  # That first card goes onto a new pile ([h | acc]).
  # Now put together the part above the split and the
  # part below the split (leading ++ t) and go through
  # the process with the deck (which is now has one less card).
  # This keeps going until you run out of cards to shuffle;
  # at that point, all the cards will have gotten to the
  # new pile, and that's your shuffled deck.

  def shuffle(list, acc) do
    {leading, [h | t]} =
      Enum.split(list, :random.uniform(Enum.count(list)) - 1)
      shuffle(leading ++ t, [h | acc])
  end

  @doc """
  Create a deck of 52 tuples in the form [{"A", "Clubs"},
  {"A", "Diamonds"}...]
  """
  @spec make_deck(list, list) :: list(tuple)

  def make_deck(values, suits) do
    lc value inlist values, suit inlist suits, do:
      {value, suit}
  end

end

game.ex

defmodule Game do

  def start() do
    deck = Cards.shuffle(Cards.make_deck(
#      ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
      [2, 3, 4, 5],
      ["Clubs", "Diamonds", "Hearts", "Spades"]))
    {hand1, hand2} = Enum.split(deck, trunc(Enum.count(deck) / 2))
    player1 = spawn(Player, :start, [hand1])
    player2 = spawn(Player, :start, [hand2])
    play([player1, player2], :pre_battle, [], [], 0, [])
  end

  @doc """
  Arguments are: list of player pids, state of game (atom),
  cards received from player 1, cards received from player 2,
  number of players who have given cards, and pile in middle of table
  """
  @spec play(list, atom, list, list, integer, list) :: nil

  def play(players, state, cards1, cards2, n_received,
    pile) do
    [player1, player2] = players
    case state do

      # Ask players to give you cards. If there are no
      # cards in the pile, it's a simple battle; otherwise
      # it's a war.

      :pre_battle ->
        IO.puts("") # for spacing
        case pile do
          [] ->
            IO.puts("Requesting 1 card from each player")
            request_cards(players, 1)
          _ ->
            IO.puts("Requesting 3 cards from each player")
            request_cards(players, 3)
        end
        play(players, :await_battle, cards1, cards2, n_received, pile)

      # When both players have given you their card(s),
      # you need to check them.

      :await_battle when n_received == 2 ->
        play(players, :check_cards, cards1, cards2, 0, pile)

      # Otherwise, wait for players to send you card(s). Each time
      # you receive card(s), remember them and add one to the
      # total number of players who have responded.

      :await_battle ->
        receive do
          {:take, new_cards, from} ->
            IO.puts("Got #{inspect(new_cards)} from #{inspect(from)}")
            cond do
              from == player1 -> play(players, state, new_cards,
                cards2, n_received + 1, pile)
              from == player2 -> play(players, state, cards1,
                new_cards, n_received + 1, pile)
            end
        end

      # If both people have run out of cards, it's a draw.
      # If one person is out of cards, the other player is the winner.
      # Otherwise, evaluate the cards and prepare for next
      # battle or war.

      :check_cards ->
        cond do
          cards1 == [] and cards2 == [] ->
            IO.puts("Draw")
            endgame(players)
          cards1 == [] ->
            IO.puts("Player 2 wins")
            endgame(players)
          cards2 == [] ->
            IO.puts("Player 1 wins")
            endgame(players)
          true ->
            new_pile = evaluate(players, cards1, cards2, pile)
            play(players, :pre_battle, [], [], 0, new_pile)
        end
    end
  end

  @spec evaluate(list, list, list, list) :: list

  # Evaluate the cards from both players. If their
  # values match, add them to the pile and.
  # If they don't, the winner is told to pick up the cards
  # (and whatever's in the pile), and the pile is cleared.
  #
  # Wait for players to respond before proceeding with the
  # game. Otherwise, a player with an empty hand might be
  # asked to give a card before picking up the cards she won.

  defp evaluate(players, cards1, cards2, pile) do
    [player1, player2] = players
    v1 = card_value(hd(cards1))
    v2 = card_value(hd(cards2))
    IO.puts("Value of card 1 is #{v1}; value of card 2 is #{v2}")
    new_pile = Enum.concat([pile, cards1, cards2])
    IO.puts("Card pile is now #{inspect(new_pile)}")
    cond do
      v1 == v2 ->
        IO.puts("Equal values; going to war.")
        new_pile  # it's a war
      v1 > v2 ->
        IO.puts("Telling player 1 to pick up the cards")
        send(player1, {:pick_up, new_pile, self()})
        wait_for_pickup()
        []
      v2 > v1 ->
        IO.puts("Telling player 2 to pick up the cards")
        send(player2, {:pick_up, new_pile, self()})
        wait_for_pickup()
        []
    end
  end

  # Wait for player to pick up cards
  @spec wait_for_pickup() :: pid
  def wait_for_pickup() do
    receive do
      {:got_cards, player} ->
        IO.puts("Player #{inspect(player)} picked up cards.")
        player
    end
  end

  # Send each player a requst to send n cards
  @spec request_cards(list, integer) :: nil

  defp request_cards([p1, p2], n) do
    send(p1, {:give, n, self()})
    send(p2, {:give, n, self()})
  end

  # Send message to all players to exit their receive loop.
  @spec endgame(list) :: nil

  defp endgame(players) do
    Enum.each(players, fn(x) -> send(x, :game_over) end)
  end

  # Return the value of a card; Aces are high.
  @spec card_value(tuple) :: integer

  defp card_value({value, _suit}) do
    case value do
      "A" -> 14
      "K" -> 13
      "Q" -> 12
      "J" -> 11
      _ -> value
    end
  end
end

player.ex

defmodule Player do
  def start(hand) do
    play(hand)
  end

  #
  # The player can either be told to give the dealer
  # n cards (1 or 3), pick up cards (after having won a battle),
  # or leave the game.

  def play(hand) do
    receive do
      {:give, n, dealer} ->
        {to_send, to_keep} = Enum.split(hand, n)
        send(dealer, {:take, to_send, self()})
        play(to_keep)
      {:pick_up, cards, dealer} ->
        new_hand = hand ++ cards
        IO.puts("Player #{inspect(self)} has #{inspect(new_hand)}")
        send(dealer, {:got_cards, self()})
        play(new_hand)
      :game_over -> IO.puts("Player #{inspect(self)} leaves.")
    end
  end
end

Solution 10-1

Here is a suggested solution for Étude 10-1.

stats.ex

defmodule Stats do
  @moduledoc """
  Functions for calculating basic statistics.

  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc "Compute the mean of a list of numbers. Uses List.foldl/3"
  @spec mean([number]) :: number

  def mean(list) do
    try do
      List.foldl(list, 0, fn(x, acc) -> x + acc end) / Enum.count(list)
    rescue
      err -> err
    end
  end

  @doc """
  Compute the standard deviation of a list of numbers. Uses
  List.foldl/3 with a tuple as an accumulator.
  """
  @spec stdv([number]) :: number

  def stdv(list) do
    try do
      n = Enum.count(list)
      {sum, sum_sq} = List.foldl(list, {0,0}, fn(x, {acc, acc_sq}) ->
      {acc + x, acc_sq + x * x} end)
      :math.sqrt((n * sum_sq - sum * sum) / (n * (n - 1)))
    rescue
      err -> err
    end
  end

  @doc "Recursively find the minimum entry in a list of numbers."

  @spec minimum([number]) :: number

  def minimum(list) do
    try do
      [head | tail] = list
      minimum(tail, head)
    rescue
      err -> err
    end
  end

  # When there are no more numbers, return the result.

  @spec minimum([number], number) :: number

  defp minimum([], result) do
    result
  end

  # If the current result is less than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.
  # Note that you can use a variable assigned as part of a cons in a guard.

  defp minimum([head | tail], result) when result < head do
    minimum(tail, result)
  end

  # Otherwise, the head of the list becomes the new minimum,
  # and recursively look at the remainder of the list.

  defp minimum([head | tail], _result) do
    minimum(tail, head)
  end

  @doc "Recursively find the maximum entry in a list of numbers."
  @spec maximum([number]) :: number

  def maximum(list) do
    try do
      [head | tail] = list
      maximum(tail, head)
    rescue
      err -> err
    end
  end

  # When there are no more numbers, return the result.

  @spec maximum([number], number) :: number

  defp maximum([], result) do
    result
  end

  # If the current result is greater than the first item in the list,
  # keep it as the result and recursively look at the remainder of the list.

  defp maximum([head | tail], result) when result > head do
    maximum(tail, result)
  end

  # Otherwise, the head of the list becomes the new maximum,
  # and recursively look at the remainder of the list.

  defp maximum([head | tail], _result) do
    maximum(tail, head)
  end

  # @doc "Find the range of a list of numbers as a list [min, max]."
  @spec range([number]) :: [number]

  def range(list) do
    [minimum(list), maximum(list)]
  end

end

Solution 10-2

Here is a suggested solution for Étude 10-2.

bank.ex

defmodule Bank do
  @moduledoc """
  Manipulate a "bank account" and log messages.

  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Create an account with a given balance, and repeatedly
  ask for and perform transactions.
  """
  @spec account(number()) :: nil

  def account(balance) do
    input = IO.gets("D)eposit, W)ithdraw, B)alance, Q)uit: ")
    action = String.upcase(String.first(input))
    if (action != "Q") do
      new_balance = transaction(action, balance)
      account(new_balance)
    end
  end

  @spec transaction(String.t(), number()) :: number()

  def transaction(action, balance) do
    case action do
      "D" ->
        amount = get_number("Amount to deposit: ")
        cond do
          amount >= 10000 ->
            :error_logger.warning_msg("Large deposit $#{amount}\n")
            IO.puts("Your deposit of $#{amount} may be subject to hold.")
            new_balance = balance + amount
            IO.puts("Your new balance is $#{new_balance}")
          amount < 0 ->
            :error_logger.error_msg("Negative deposit $#{amount}\n")
            IO.puts("Deposits may not be less than zero.")
            new_balance = balance
          amount >= 0 ->
            :error_logger.info_msg("Successful deposit of $#{amount}\n")
            new_balance = balance + amount
            IO.puts("Your new balance is $#{new_balance}")
        end
      "W"->
        amount = get_number("Amount to withdraw: ")
        cond do
          amount > balance ->
            :error_logger.error_msg("Overdraw $#{amount} from $#{balance}\n")
            IO.puts("You cannot withdraw more than your current balance of $#{balance}")
            new_balance = balance
          amount < 0 ->
            :error_logger.error_msg("Negative withdrawal amount $#{amount}\n")
            IO.puts("Withdrawals may not be less than zero.")
            new_balance = balance
          amount >= 0 ->
            :error_logger.info_msg("Successful withdrawal $#{amount}\n")
            new_balance = balance - amount
            IO.puts("Your new balance is $#{new_balance}")
        end
      "B" ->
        :error_logger.info_msg("Balance inquiry $#{balance}\n")
        IO.puts("Your current balance is $#{balance}")
        new_balance = balance
      _ ->
        IO.puts("Unknown command #{action}")
        new_balance = balance
    end
    new_balance
  end

  @doc """
  Present a prompt and get a number from the
  user. Allow either integers or floats.
  """
  @spec get_number(String.t()) :: number()

  def get_number(prompt) do
    input = IO.gets(prompt)
    input_str = String.strip(input)
    cond do
      Regex.match?(~r/^[+-]?\d+$/, input_str) ->
        binary_to_integer(input_str)
      Regex.match?(~r/^[+-]?\d+\.\d+([eE][+-]?\d+)?$/, input_str) ->
        binary_to_float(input_str)
      true -> :error
    end
  end
end

Solution 11-1

Here is a suggested solution for Étude 11-1.

phone_ets.ex

defmodule Phone do
 require Record;

    Record.defrecord :phone, [number: "",
      start_date: "1900-01-01",
      start_time: "00:00:00",
      end_date: "1900-01-01",
      end_time: "00:00:00"]
end

defmodule PhoneEts do

  require Phone;

  @moduledoc """
  Validate input using regular expressions.

  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  @doc """
  Given a file name containing a CSV table of phone number,
  start date/time, and end date/time, construct a corresponding
  ETS table.
  """
  @spec setup(String.t) :: atom

  def setup(file_name) do
    # delete table if it exists
    case :ets.info(:call_table) do
      :undefined -> false
      _ -> :ets.delete(:call_table)
    end

    :ets.new(:call_table, [:named_table, :bag,
      {:keypos, Phone.phone(:number) + 1}])
    {result, input_file} = File.open(file_name)
    if result == :ok do
      add_rows(input_file)
    end
  end

  #
  # Recursively read input file and add rows to
  # the ETS table.
  @spec add_rows(IO.device) :: atom

  defp add_rows(input_file) do
    data = IO.read(input_file, :line)
    cond do
      data != :eof ->
        [number, sdate, stime, edate, etime] =
          String.split(String.strip(data), ",")
        :ets.insert(:call_table, Phone.phone(number: number,
          start_date: gregorianize(sdate, "-"),
          start_time: gregorianize(stime, ":"),
          end_date: gregorianize(edate, "-"),
          end_time: gregorianize(etime, ":")))
        add_rows(input_file)
      true ->
        :ok
    end
  end

  # Convert a time or date string (given its delimiter, : or -)
  # to a three-tuple of the constituent elements
  @spec gregorianize(String.t, String.t) :: {integer, integer, integer}

  defp gregorianize(str, delimiter) do
    list_to_tuple(for item <- String.split(str, delimiter), do:
      binary_to_integer(item))
  end

  @doc """
  Summarize the number of minutes for a given phone number.
  """
  @spec summary(String.t) :: list(tuple(String.t, integer))

  def summary(phone_number) do
    [calculate(phone_number)]
  end

  @doc """
  Summarize the number of minutes for all phone numbers in the
  data bases.
  """
  @spec summary(String.t) :: list(tuple(String.t, integer))

  def summary() do
    summary(:ets.first(:call_table), [])
  end

  defp summary(key, acc) do
    case key do
      :"$end_of_table" -> acc
      _ -> summary(:ets.next(:call_table, key),
        [calculate(key) | acc])
    end
  end

  # Calculate total number of minutes used by given phone number.
  # Returns tuple {phone_number, total}
  @spec calculate(String.t) :: {String.t, integer}

  defp calculate(phone_number) do
    calls = :ets.lookup(:call_table, phone_number)
    total = List.foldl(calls, 0, &call_minutes/2)
    {phone_number, total}
  end

  # Helper function for calculate; adds the number of minutes
  # for a given call to the accumulator
  @spec call_minutes(Phone.t, integer) ::integer

  defp call_minutes(phonecall, acc) do
    c_start = :calendar.datetime_to_gregorian_seconds(
      {Phone.phone(phonecall, :start_date),
      Phone.phone(phonecall, :start_time)})
    c_end = :calendar.datetime_to_gregorian_seconds(
      {Phone.phone(phonecall, :end_date),
      Phone.phone(phonecall, :end_time)})
    div((c_end - c_start) + 59, 60) + acc
  end
end

generate_calls.ex

This is the program I used to generate the list of phone calls.

defmodule GenerateCalls do
  @moduledoc """
  Generate a random set of phone calls

  from *Études for Elixir*, O'Reilly Media, Inc., 2014.
  Copyright 2014 by J. David Eisenberg.
  """
  @vsn 0.1

  def make_call_list(n) do
    now = :calendar.datetime_to_gregorian_seconds({{2014, 3, 10},
      {9, 0, 0}})
    numbers = [
      {"213-555-0172", now},
      {"301-555-0433", now},
      {"415-555-7871", now},
      {"650-555-3326", now},
      {"729-555-8855", now},
      {"838-555-1099", now},
      {"946-555-9760", now}
    ]
    call_list = make_call_list(n, numbers, [])
    {result, output_file} = File.open("call_list.csv", [:write,
      :utf8])
    case result do
      :ok -> write_item(output_file, call_list)
      :error -> IO.puts("Error creating output file")
    end
  end

  def make_call_list(0, _numbers, result) do
    Enum.reverse(result)
  end

  def make_call_list(n, numbers, result) do
    entry = :random.uniform(Enum.count(numbers))
    {head, tail} = Enum.split(numbers, entry - 1)
    {number, last_call} = hd(tail)
    start_call = last_call + :random.uniform(120) + 20
    duration = :random.uniform(180) + 40
    end_call = start_call + duration
    item = [number, format_date(start_call), format_time(start_call),
      format_date(end_call), format_time(end_call)]
    updated_numbers = head ++ [{number, end_call} | tl(tail)]
    make_call_list(n - 1, updated_numbers, [item | result])
  end

  def write_item(output_file, []) do
    File.close(output_file)
  end

  def write_item(output_file, [h | t]) do
    [n, sd, st, ed, et] = h
    IO.write(output_file, "#{n},#{sd},#{st},#{ed},#{et}\n")
    write_item(output_file, t)
  end

  def format_date(g_seconds) do
    {{y, m, d}, _time} =
      :calendar.gregorian_seconds_to_datetime(g_seconds)
    "#{y}-#{leadzero(m)}-#{leadzero(d)}"
  end

  def format_time(g_seconds) do
    {_date, {h, m, s}} =
      :calendar.gregorian_seconds_to_datetime(g_seconds)
    "#{leadzero(h)}:#{leadzero(m)}:#{leadzero(s)}"
  end

  def leadzero(n) do
    cond do
      n < 10 -> "0" <> integer_to_binary(n)
      true -> integer_to_binary(n)
    end
  end
end

Solution 12-1

Here is a suggested solution for Étude 12-1.

weather.ex

defmodule Weather do
  use GenServer

  # convenience method for startup
  def start_link do
    GenServer.start_link(__MODULE__, [], [{:name, __MODULE__}])
  end

  # callbacks for GenServer.Behaviour
  def init([]) do
    :inets.start()
    {:ok, []}
  end

  def handle_call(request, _from, state) do
    {reply, new_state} = get_weather(request, state)
    {:reply, reply, new_state}
  end

  def handle_cast(_msg, state) do
    IO.puts("Recently viewed: #{inspect(state)}")
    {:noreply, state}
  end

  def handle_info(_info, state) do
    {:noreply, state}
  end

  def terminate(_reason, _state) do
    {:ok}
  end

  def code_change(_old_version, state, _extra) do
    {:ok, state}
  end

  # internal functions

  @doc """
  Given a weather station name and the current server state,
  get the weather data for that station, and add the station name
  to the state (the list of recently viewed stations).
  """

  def get_weather(station, state) do
    url = "http://w1.weather.gov/xml/current_obs/" <> station
      <> ".xml"
    {status, data} = :httpc.request(to_char_list(url))
    case status do
      :error ->
        reply = {status, data}
        new_state = state
      :ok ->
        {{_http, code, _message}, _attrs, xml_as_chars} = data
        case code do
          200 ->
            xml = to_string(xml_as_chars)
            reply = {:ok,
              (for item <- [:location, :observation_time_rfc822,
              :weather, :temperature_string], do:
                get_content(item, xml))}
            # remember only the last 10 stations
            new_state = [station | Enum.take(state, 9)]
          _ ->
            reply = {:error, code}
            new_state = state
        end
    end
    {reply, new_state}
  end


  # Given an element name (as an atom) and an XML string,
  # return a tuple with the element name and that element's text
  # content.

  @spec get_content(atom, String.t) :: {atom, String.t}

  defp get_content(element_name, xml) do
    {_, pattern} = Regex.compile(
    "<#{element_name}>([^<]+)</#{atom_to_binary(element_name)}>")
    result = Regex.run(pattern, xml)
    case result do
      [_all, match] -> {element_name, match}
      nil -> {element_name, nil}
    end
  end

end

weather_sup.ex

defmodule WeatherSup do
  use Supervisor

  # convenience method for startup
  def start_link do
    Supervisor.start_link(__MODULE__, [], [{:name, __MODULE__}])
  end

  # supervisor callback
  def init([]) do
    child = [worker(Weather, [], [])]
    supervise(child, [{:strategy, :one_for_one}, {:max_restarts, 1},
      {:max_seconds, 5}])
  end

  # Internal functions (none here)

end

Solution 12-2

Here is a suggested solution for Étude 12-2. Since the bulk of the code is identical to the code in the previous étude, the only code shown here is the revised -export list and the added functions.

weather.ex

def report(station) do
  :gen_server.call(Weather, station)
end

def recent() do
  :gen_server.cast(__MODULE__, "")
end

Solution 12-3

Here is a suggested solution for Étude 12-3. Since the bulk of the code is identical to the previous étude, the only code shown here is the added and revised code.

# convenience method for startup
def start_link do
  :gen_server.start_link({:global, __MODULE__}, __MODULE__, [], [])
end

def connect(other_node) do
  case :net_adm.ping(other_node) do
    :pong ->
      IO.puts("Connected to server.")
      :ok
    :pang ->
      IO.puts("Could not connect.")
      :error
  end
end

def report(station) do
  :gen_server.call({:global, __MODULE__}, station)
end

def recent() do
  result = :gen_server.call({:global, __MODULE__}, :recent)
  IO.puts("Recently visited: #{inspect(result)}")
end

Solution 12-4

Here is a suggested solution for Étude 12-4.

chatroom.ex

defmodule Chatroom do
  use GenServer

  # convenience method for startup
  def start_link do
    GenServer.start_link(__MODULE__, [], [{:name, __MODULE__}])
  end

  # callbacks for GenServer.Behaviour
  def init([]) do
    {:ok, []}
  end

  # A login request gets a user name and node (server).
  # Check that user name and server are unique; if they are,
  # add that person's user name, server, and pid to the state.

  def handle_call({:login, user, server}, from, state) do
    {pid, _reference} = from
    key = {user, server}
    IO.puts("#{user} #{server} logging in from #{inspect(pid)}")
    if List.keymember?(state, key, 0) do
      reply = {:error, "#{user} #{server} already logged in"}
      new_state = state
    else
      new_state = [{key,pid} | state]
      reply = {:ok, "#{user}@#{server} logged in."}
    end
    {:reply, reply, new_state}
  end


  # If a person isn't logged in, note error;
  # otherwise, delete person from the state.

  def handle_call(:logout, from, state) do
    {pid, _reference} = from
    result = List.keyfind(state, pid, 1)
    case result do
      nil ->
        reply = {:error, "Not logged in."}
        new_state = state
      {{user, server}, _pid} ->
        new_state = List.keydelete(state, pid, 1)
        reply = {:ok, "#{user}@#{server} logged out."}
    end
    {:reply, reply, new_state}
  end

  # Send a message to all other participants.
  # First, find sender's name and node. Then
  # go through the list of participants and send each of them
  # the message via GenServer.cast.
  #
  # Don't send a message to the originator (though if this were
  # connected to a GUI, it would be useful to do so), as the
  # originator wants to see the message she has typed in a text area
  # as well as in the chat window.

  def handle_call({:say, text}, from, state) do
    {from_pid, _ref} = from

    # get sender's name and server
    person = List.keyfind(state, from_pid, 1)

    case person do
      {{from_user, from_server}, _pid} ->
        Enum.each(state, fn(item) ->
          {{_user, _server}, pid} = item
          if pid != from_pid do
            GenServer.cast(pid, {:message,
              {from_user, from_server}, text})
          end
        end)
        reply = "Message sent."
      nil ->
        reply = "Unknown sender pid #{inspect(from_pid)}"
    end
    {:reply, reply, state}
  end

  # Return a list of all the users and their servers

  def handle_call(:users, _from, state) do
    reply = for {{name, server}, _pid} <- state, do:
      {name,server}
    {:reply, reply, state}
  end

  # Get the profile of a person at a given server

  def handle_call({:who, person, server}, _from, state) do
    case List.keyfind(state, {person, server}, 0) do
      {{_u, _s}, pid}  ->
        reply = GenServer.call(pid, :get_profile)
      nil ->
        reply = "Cannot find #{person} #{server}"
    end
    {:reply, reply, state}
  end

  # Catchall to handle any errant calls to the server.

  def handle_call(item, from, state) do
    IO.puts("Unknown #{inspect(item)} from #{inspect(from)}")
    {:reply, "unknown", state}
  end

  def handle_cast(_msg, state) do
    {:noreply, state}
  end

  def handle_info(_info, state) do
    {:noreply, state}
  end

  def terminate(_reason, _state) do
    {:ok}
  end

  def code_change(_old_version, state, _extra) do
    {:ok, state}
  end
end

person.ex

defmodule Person do
  use GenServer
#  require Record

#  defmodule State do
#    Record.defrecord :state, [server: nil, profile: nil]
#  end

  # convenience method for startup
  def start_link(chatroom_server) do
    GenServer.start_link(__MODULE__, [chatroom_server], [{:name, __MODULE__}])
  end

  # callbacks for GenServer.Behaviour
  def init([chatroom_server]) do
    {:ok, %{server: {Chatroom, chatroom_server},
      profile: HashDict.new}}
  end

  # The server is asked to either:
  # a) return the chat host name from the state,
  # b) return the user profile
  # c) update the user profile
  # d) log a user in
  # e) send a message to all people in chat room
  # f) log a user out

  def handle_call(:get_chat_server, _from, state) do
    {:reply, state.server, state}
  end

  def handle_call(:get_profile, _from, state) do
    {:reply, state.profile, state}
  end

  def handle_call({:set_profile, key,value}, _from, state) do
    new_profile = HashDict.put(state.profile, key, value)
    reply = {:ok, "Added #{key}/#{value} to profile"}
    {:reply, reply, %{server: state.server, profile: new_profile} }
  end

  def handle_call({:login, user_name}, _from, state) do
    GenServer.call(state.server, {:login, user_name, node()})
    {:reply, "Sent login request", state}
  end

  def handle_call({:say, text}, _from, state) do
    reply = GenServer.call(state.server, {:say, text})
    {:reply, reply, state}
  end

  def handle_call(:logout, _from, state) do
    reply = GenServer.call(state.server, :logout)
    {:reply, reply, state}
  end

  def handle_call(item, from, state) do
    IO.puts("Unknown message #{inspect(item)} from #{inspect(from)}")
    {:reply, "unknown msg to person", state}
  end

  def handle_cast({:message, {user, server}, text}, state) do
    IO.puts("#{user} (#{server}) says: #{text}")
    {:noreply, state}
  end

  def handle_cast(_msg, state) do
    {:noreply, state}
  end

  def handle_info(_info, state) do
    {:noreply, state}
  end

  def terminate(_reason, _state) do
    {:ok}
  end

  def code_change(_old_version, state, _extra) do
    {:ok, state}
  end

  # Internal convenience functions
  def chat_server() do
    GenServer.call(__MODULE__, :get_chat_server)
  end

  # These requests can go straight to the chat server,
  # because it doesn't care who they're from.

  def users do
    GenServer.call(chat_server, :users)
  end

  def who(user_name, user_node) do
    GenServer.call(chat_server, {:who, user_name, user_node})
  end

  #
  # Forward these requests to the person server,
  # because they have to send the request to the chat room,
  # not the shell!
  #
  def login(user_name) when is_atom(user_name) do
    login(Atom.to_string(user_name))
  end

  def login(user_name) do
    GenServer.call(__MODULE__, {:login, user_name})
  end

  def logout do
    GenServer.call(__MODULE__, :logout)
  end

  def say(text) do
    GenServer.call(__MODULE__, {:say, text})
  end

  #
  # This request is local to the Person server.
  #
  def set_profile(key, value) do
    GenServer.call(__MODULE__, {:set_profile, key, value})
  end
end

Solution 13-1

Here is a suggested solution for Étude 13-1.

atomic_maker.ex

defmodule AtomicMaker do
  defmacro create_functions(element_list) do
    Enum.map element_list, fn {symbol, weight} ->
      quote do
        def unquote(symbol)() do
          unquote(weight)
        end
      end
    end
  end
end

atomic.ex

defmodule Atomic do
  require AtomicMaker

  AtomicMaker.create_functions([{:h, 1.008}, {:he, 4.003},
    {:li, 6.94}, {:be, 9.012}, {:b, 10.81}, {:c, 12.011},
    {:n, 14.007}, {:o, 15.999}, {:f, 18.998}, {:ne, 20.178},
    {:na, 22.990}, {:mg, 24.305}, {:al, 26.981}, {:si, 28.085},
    {:p, 30.974}, {:s, 32.06}, {:cl, 35.45}, {:ar, 39.948},
    {:k, 39.098}, {:ca, 40.078}, {:sc, 44.956}, {:ti, 47.867},
    {:v, 50.942}, {:cr, 51.996}, {:mn, 54.938}, {:fe, 55.845}])
end

Solution 13-2

Here is a suggested solution for Étude 13-2.

duration.ex

defmodule Duration do

  defmacro add({m1,s1}, {m2, s2}) do
    quote do
      t_sec = rem(unquote(s1) + unquote(s2), 60)
      t_min = div(unquote(s1) + unquote(s2), 60)
      {unquote(m1) + unquote(m2) + t_min, t_sec}
    end
  end

end

Solution 13-3

Here is a suggested solution for Étude 13-3.

duration.ex

defmodule Duration do

  defmacro {m1,s1} + {m2, s2} do
    quote do
      t_sec = rem(unquote(s1) + unquote(s2), 60)
      t_min = div(unquote(s1) + unquote(s2), 60)
      {unquote(m1) + unquote(m2) + t_min, t_sec}
    end
  end

  defmacro a + b do
    quote do
      unquote(a) + unquote(b)
    end
  end
end