Here are the solutions that I came up with for the études in this book.
Here is a suggested solution for Étude 2-1.
geom.ex
defmodule Geom do def area(length, width) do length * width end end
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
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
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
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
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
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
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
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
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
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
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
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
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} > ") string_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
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} > ") string_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
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
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
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
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
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
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
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
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
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 Solution 7-3
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
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
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 for {name, gender, age} <- get_people(), age > 40, gender == "M" do {name, 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 for {name, gender, age} <- get_people(), age> 40 or gender == "M" do {name, gender, age} end end end
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
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
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 for value <- ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"], suit <- ["Clubs", "Diamonds", "Hearts", "Spades"], do: {value, suit} end end
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 for value <- ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"], suit <- ["Clubs", "Diamonds", "Hearts", "Spades"], do: {value, suit} end end
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 for value <- values, suit <- 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
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
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} ") 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} ") IO.puts("Deposits may not be less than zero.") new_balance = balance amount >= 0 -> :error_logger.info_msg("Successful deposit of $#{amount} ") 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} ") 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} ") IO.puts("Withdrawals may not be less than zero.") new_balance = balance amount >= 0 -> :error_logger.info_msg("Successful withdrawal $#{amount} ") new_balance = balance - amount IO.puts("Your new balance is $#{new_balance}") end "B" -> :error_logger.info_msg("Balance inquiry $#{balance} ") 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
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} ") 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
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
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
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
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
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
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
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
18.216.151.164