Appendix A. Solutions to Études

Here are the solutions that I came up with for the études in this book. Since I was learning Erlang as I wrote them, you may expect some of the code to be naïve in the extreme.

Solution 2-1

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

geom.erl

-module(geom).
-export([area/2]).

area(L,W) -> L * W.

Solution 2-2

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/2]).

%% @doc Calculates the area of a rectangle, given the
%% length and width. Returns the product
%% of its arguments.

-spec(area(number(),number()) -> number()).

area(L,W) -> L * W.

Solution 2-3

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/2]).

%% @doc Calculates the area of a rectangle, given the
%% length and width. Returns the product
%% of its arguments.

-spec(area(number(),number()) -> number()).

area(L,W) -> L * W.

Solution 3-1

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/3]).

%% @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()).

area(rectangle, L,W) -> L * W;

area(triangle, B, H) -> (B * H) / 2.0;

area(ellipse, A, B) -> math:pi() * A * B.

Solution 3-2

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/3]).

%% @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. Ensure that both arguments are greater than
%% or equal to zero.

-spec(area(atom(), number(),number()) -> number()).

area(rectangle, L,W) when L >=0, W >= 0 -> L * W;

area(triangle, B, H) when B>= 0, H >= 0 -> (B * H) / 2.0;

area(ellipse, A, B) when A >= 0, B >= 0 -> math:pi() * A * B.

Solution 3-3

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/3]).

%% @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. Invalid data returns zero.

-spec(area(atom(), number(),number()) -> number()).

area(rectangle, L,W) when L >=0, W >= 0 -> L * W;

area(triangle, B, H) when B>= 0, H >= 0 -> (B * H) / 2.0;

area(ellipse, A, B) when A >= 0, B >= 0 -> math:pi() * A * B;

area(_, _, _) -> 0.

Solution 3-4

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/1]).

%% @doc Calculates the area of a shape, given a tuple
%% containing a shape and two of the dimensions.
%% Works by calling a private function.

-spec(area({atom(), number(),number()}) -> number()).

area({Shape, Dim1, Dim2}) -> area(Shape, Dim1, Dim2).

%% @doc 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. Invalid data returns zero.

-spec(area(atom(), number(),number()) -> number()).

area(rectangle, L,W) when L >=0, W >= 0 -> L * W;

area(triangle, B, H) when B>= 0, H >= 0 -> (B * H) / 2.0;

area(ellipse, A, B) when A >= 0, B >= 0 -> math:pi() * A * B;

area(_, _, _) -> 0.

Solution 4-1

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/3]).

%% @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()).

area(Shape, A, B) when A >= 0, B >= 0 ->
  case Shape of
    rectangle -> A * B;
    triangle -> (A * B) / 2.0;
    ellipse -> math:pi() * A * B
  end.

Solution 4-2

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

dijkstra.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Recursive function for calculating GCD
%% of two numbers using Dijkstra's algorithm.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dijkstra).
-export([gcd/2]).

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

-spec(gcd(number(), number()) -> number()).

gcd(M, N) ->
  if
    M == N  -> M;
    M > N -> gcd(M - N, N);
    true -> gcd(M, N - M)
  end.

Solution 4-2

Here is another solution for Étude 4-2. This solution uses guards instead of if.

dijkstra.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Recursive function for calculating GCD
%% of two numbers using Dijkstra's algorithm.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dijkstra).
-export([gcd/2]).

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

-spec(gcd(number(), number()) -> number()).

gcd(M, N) when M == N ->
  M;

gcd(M,N) when M > N ->
  gcd(M - N, N);

gcd(M, N) ->
  gcd(M, N - M).

Solution 4-3

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

powers.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for raising a number to an integer power
%% and finding the Nth root of a number using Newton's method.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(powers).
-export([raise/2]).

%% @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(), integer()) -> number()).

raise(_, 0) -> 1;

raise(X, 1) -> X;

raise(X, N) when N > 0 -> X * raise(X, N - 1);

raise(X, N) when N < 0 -> 1 / raise(X, -N).

powers_traced.erl

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

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for raising a number to an integer power
%% and finding the Nth root of a number using Newton's method.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(powers_traced).
-export([raise/2]).

%% @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(), integer()) -> number()).

raise(_, 0) -> 1;

raise(X, 1) -> X;

raise(X, N) when N > 0 ->
  io:format("Enter X: ~p, N: ~p~n", [X, N]),
  Result = X * raise(X, N - 1),
  io:format("Result is ~p~n", [Result]),
  Result;

raise(X, N) when N < 0 -> 1 / raise(X, -N).

Solution 4-4

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

powers.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for raising a number to an integer power.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(powers).
-export([raise/2]).

%% @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(), integer()) -> number()).

raise(_, 0) -> 1;

raise(X, N) when N > 0 ->
  raise(X, N, 1);

raise(X, N) when N < 0 -> 1 / raise(X, -N).

%% @doc Helper function to raise X to N by passing an Accumulator
%% from call to call.
%% When N is 0, return the value of the Accumulator;
%% otherwise return raise(X, N - 1, X * Accumulator)

raise(_, 0, Accumulator) -> Accumulator;

raise(X, N, Accumulator) ->
  raise(X, N-1, X * Accumulator).

powers_traced.erl

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

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for raising a number to an integer power.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(powers_traced).
-export([raise/2]).

%% @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 negative, X^N is equal to 1.0 / X^N
%% When N is positive, call raise/3 with 1 as the accumulator.

-spec(raise(number(), integer()) -> number()).

raise(_, 0) -> 1;

raise(X, N) when N > 0 ->
  raise(X, N, 1);

raise(X, N) when N < 0 -> 1 / raise(X, -N).

%% @doc Helper function to raise X to N by passing an Accumulator
%% from call to call.
%% When N is 0, return the value of the Accumulator;
%% otherwise return raise(X, N - 1, X * Accumulator)

-spec(raise(number(), integer(), number()) -> number()).

raise(_, 0, Accumulator) ->
  io:format("N equals 0."),
  Result = Accumulator,
  io:format("Result is ~p~n", [Result]),
  Result;

raise(X, N, Accumulator) ->
  io:format("Enter: X is ~p, N is ~p, Accumulator is ~p~n",
    [X, N, Accumulator]),
  Result = raise(X, N-1, X * Accumulator),
  io:format("Result is ~p~n", [Result]),
  Result.

Solution 4-5

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

powers.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for raising a number to an integer power
%% and finding the Nth root of a number using Newton's method.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(powers).
-export([nth_root/2, raise/2]).

%% @doc Find the nth root of a given number.

-spec(nth_root(number(), integer()) -> number()).

nth_root(X, N) ->
  A = X / 2.0,
  nth_root(X, N, A).

%% @doc Helper function to find an nth_root by passing
%% an approximation from one call to the next.
%% If the difference between current and next approximations
%% is less than 1.0e-8, return the next approximation; otherwise return
%% nth_root(X, N, NextApproximation).

nth_root(X, N, A) ->
  io:format("Current guess is ~p~n", [A]), %% see the guesses converge
  F = raise(A, N) - X,
  Fprime = N * raise(A, N - 1),
  Next = A - F / Fprime,
  Change = abs(Next - A),
  if
    Change < 1.0e-8 -> Next;
    true -> nth_root(X, N, Next)
  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(), integer()) -> number()).

raise(_, 0) -> 1;

raise(X, N) when N > 0 ->
  raise(X, N, 1);

raise(X, N) when N < 0 -> 1 / raise(X, -N).

%% @doc Helper function to raise X to N by passing an Accumulator
%% from call to call.
%% When N is 0, return the value of the Accumulator;
%% otherwise return raise(X, N - 1, X * Accumulator)

raise(_, 0, Accumulator) -> Accumulator;

raise(X, N, Accumulator) ->
  raise(X, N-1, X * Accumulator).

Solution 5-1

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

geom.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating areas of geometric shapes.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(geom).
-export([area/3]).

%% @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()).

area(Shape, A, B) when A >= 0, B >= 0 ->
  case Shape of
    rectangle -> A * B;
    triangle -> (A * B) / 2.0;
    ellipse -> math:pi() * A * B
  end.

ask_area.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions to calculate areas of shape given user input.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(ask_area).
-export([area/0]).

%% @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.

-spec(area() -> number()).

area() ->
  Answer = io:get_line("R)ectangle, T)riangle, or E)llipse > "),
  Shape = char_to_shape(hd(Answer)),
  case Shape of
    rectangle -> Numbers = get_dimensions("width", "height");
    triangle -> Numbers = get_dimensions("base", "height");
    ellipse -> Numbers = get_dimensions("major axis", "minor axis");
    unknown -> Numbers = {error, "Unknown shape " ++ [hd(Answer)]}
  end,

  Area = calculate(Shape, element(1, Numbers), element(2, Numbers)),
  Area.

%% @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()).

char_to_shape(Char) ->
  case Char of
    $R -> rectangle;
    $r -> rectangle;
    $T -> triangle;
    $t -> triangle;
    $E -> ellipse;
    $e -> ellipse;
    _ ->  unknown
  end.

%% @doc Present a prompt and get a number from the
%% user. Allow either integers or floats.

-spec(get_number(string()) -> number()).

get_number(Prompt) ->
  Str = io:get_line("Enter " ++ Prompt ++ " > "),
  {Test, _} = string:to_float(Str),
  case Test of
    error -> {N, _} = string:to_integer(Str);
    _ -> N = Test
  end,
  N.

%% @doc Get dimensions for a shape. Input are the two prompts,
%% output is a tuple {Dimension1, Dimension2}.

-spec(get_dimensions(string(), string()) -> {number(), number()}).

get_dimensions(Prompt1, Prompt2) ->
  N1 = get_number(Prompt1),
  N2 = get_number(Prompt2),
  {N1, N2}.

%% @doc Calculate area of a shape, given its shape and dimensions.
%% Handle errors appropriately.

-spec(calculate(atom(), number(), number()) -> number()).

calculate(unknown, _, Err) -> io:format("~s~n", [Err]);
calculate(_, error, _) -> io:format("Error in first number.~n");
calculate(_, _, error) -> io:format("Error in second number.~n");
calculate(_, A, B) when A < 0; B < 0 ->
  io:format("Both numbers must be greater than or equal to zero~n");
calculate(Shape, A, B) -> geom:area(Shape, A, B).

Solution 5-2

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

dates.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for splitting a date into a list of
%% year-month-day.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dates).
-export([date_parts/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()).

date_parts(DateStr) ->
  [YStr, MStr, DStr] = re:split(DateStr, "-", [{return, list}]),
  [element(1, string:to_integer(YStr)),
    element(1, string:to_integer(MStr)),
    element(1, string:to_integer(DStr))].

Solution 6-1

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

stats.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating basic statistics on a list of numbers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(stats).
-export([minimum/1]).

%% @doc Returns the minimum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(minimum(list(number())) -> number()).

minimum(NumberList) ->
  [Result | Rest] = NumberList,
  minimum(Rest, Result).

minimum([], Result) -> Result;

minimum([Head|Tail], Result) ->
  case Head < Result of
    true -> minimum(Tail, Head);
    false -> minimum(Tail, Result)
  end.

Solution 6-2

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

stats.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating basic statistics on a list of numbers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(stats).
-export([minimum/1, maximum/1, range/1]).

%% @doc Returns the minimum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(minimum(list(number())) -> number()).

minimum(NumberList) ->
  [Result | Rest] = NumberList,
  minimum(Rest, Result).

minimum([], Result) -> Result;

minimum([Head|Tail], Result) ->
  case Head < Result of
    true -> minimum(Tail, Head);
    false -> minimum(Tail, Result)
  end.

%% @doc Returns the maximum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(maximum(list(number())) -> number()).

maximum(NumberList) ->
  [Result | Rest] = NumberList,
  maximum(Rest, Result).

maximum([], Result) -> Result;

maximum([Head|Tail], Result) ->
  case Head > Result of
    true -> maximum(Tail, Head);
    false -> maximum(Tail, Result)
  end.

%% @doc Return the range (maximum and minimum) of a list of numbers
%% as a two-element list.
-spec(range([number()]) -> [number()]).

range(NumberList) -> [minimum(NumberList), maximum(NumberList)].

Solution 6-3

Here is a suggested solution for Étude 6-3 with leap years handled in the julian/5 function.

dates.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for splitting a date into a list of
%% year-month-day and finding Julian date.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dates).
-export([date_parts/1, julian/1, is_leap_year/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(string()) -> list(integer())).

date_parts(DateStr) ->
  [YStr, MStr, DStr] = re:split(DateStr, "-", [{return, list}]),
  [element(1, string:to_integer(YStr)),
    element(1, string:to_integer(MStr)),
    element(1, string:to_integer(DStr))].

%% @doc Takes a string in ISO date format (yyyy-mm-dd) and
%% returns the day of the year (Julian date).

-spec(julian(string()) -> pos_integer()).

julian(IsoDate) ->
  DaysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [Y, M, D] = date_parts(IsoDate),
  julian(Y, M, D, DaysPerMonth, 0).

%% @doc Helper function that recursively accumulates the number of days
%% up to the specified date.

-spec(julian(integer(), integer(), integer(), [integer()], integer) -> integer()).

julian(Y, M, D, MonthList, Total) when M > 13 - length(MonthList) ->
  [ThisMonth|RemainingMonths] = MonthList,
  julian(Y, M, D, RemainingMonths, Total + ThisMonth);

julian(Y, M, D, _MonthList, Total) ->
  case M > 2 andalso is_leap_year(Y) of
    true -> Total + D + 1;
    false -> Total + D
  end.

%% @doc Given a year, return true or false depending on whether
%% the year is a leap year.

-spec(is_leap_year(pos_integer()) -> boolean()).

is_leap_year(Year) ->
  (Year rem 4 == 0 andalso Year rem 100 /= 0)
    orelse (Year rem 400 == 0).

Here is a suggested solution for Étude 6-3 with leap years handled in the julian/1 function.

dates.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for splitting a date into a list of
%% year-month-day and finding Julian date.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dates).
-export([date_parts/1, julian/1, is_leap_year/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(string()) -> list(integer())).

date_parts(DateStr) ->
  [YStr, MStr, DStr] = re:split(DateStr, "-", [{return, list}]),
  [element(1, string:to_integer(YStr)),
    element(1, string:to_integer(MStr)),
    element(1, string:to_integer(DStr))].

%% @doc Takes a string in ISO date format (yyyy-mm-dd) and
%% returns the day of the year (Julian date).

-spec(julian(string()) -> pos_integer()).

julian(IsoDate) ->
  [Y, M, D] = date_parts(IsoDate),
  DaysInFeb = case is_leap_year(Y) of
    true -> 29;
    _else -> 28
  end,
  DaysPerMonth = [31, DaysInFeb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  julian(Y, M, D, DaysPerMonth, 0).

%% @doc Helper function that recursively accumulates the number of days
%% up to the specified date.

-spec(julian(integer(), integer(), integer(), [integer()], integer) -> integer()).

julian(Y, M, D, MonthList, Total) when M > 13 - length(MonthList) ->
  [ThisMonth|RemainingMonths] = MonthList,
  julian(Y, M, D, RemainingMonths, Total + ThisMonth);

julian(_Y, _M, D, _MonthList, Total) ->
  Total + D.

%% @doc Given a year, return true or false depending on whether
%% the year is a leap year.

-spec(is_leap_year(pos_integer()) -> boolean()).

is_leap_year(Year) ->
  (Year rem 4 == 0 andalso Year rem 100 /= 0)
    orelse (Year rem 400 == 0).

Solution 6-4

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

teeth.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Show teeth that need attention due to excessive pocket depth.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(teeth).
-export([alert/1]).

%% @doc Create a list of tooth numbers that require attention.

-spec(alert[integer()]) -> [integer()]).

alert(ToothList) -> alert(ToothList, 1, []).

%% @doc Helper function that accumulates the list of teeth needing attention

-spec(alert([integer()], integer(), [integer()]) -> [integer()]).

alert([], _Tooth_number, Result) -> lists:reverse(Result);

alert([Head | Tail ], ToothNumber, Result ) ->
  case stats:maximum(Head) >= 4 of
    true -> alert(Tail, ToothNumber + 1, [ToothNumber | Result]);
    false -> alert(Tail, ToothNumber + 1, Result)
  end.

stats.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating basic statistics on a list of numbers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(stats).
-export([minimum/1, maximum/1, range/1]).

%% @doc Returns the minimum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(minimum([number()]) -> number()).

minimum(NumberList) ->
  minimum(NumberList, hd(NumberList)).

minimum([], Result) -> Result;

minimum([Head|Tail], Result) ->
  case Head < Result of
    true -> minimum(Tail, Head);
    false -> minimum(Tail, Result)
  end.

%% @doc Returns the maximum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(maximum([number()]) -> number()).

maximum(NumberList) ->
  maximum(NumberList, hd(NumberList)).

maximum([], Result) -> Result;

maximum([Head|Tail], Result) ->
  case Head > Result of
    true -> maximum(Tail, Head);
    false -> maximum(Tail, Result)
  end.

%% @doc Return the range (maximum and minimum) of a list of numbers
%% as a two-element list.
-spec(range([number()]) -> [number()]).

range(NumberList) -> [minimum(NumberList), maximum(NumberList)].

Solution 6-5

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

non_fp.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Generate a random set of teeth, with a certain
%% percentage expected to be bad.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(non_fp).
-export([generate_teeth/2, test_teeth/0]).

%% @doc Generate a list of lists, six numbers per tooth, giving random
%% pocket depths. Takes a string where T="there's a tooth there"
%% and F="no tooth"), and a float giving probability that a tooth is good.

-spec(generate_teeth(string(), float()) -> list(list(integer()))).

generate_teeth(TeethPresent, ProbGood) ->
  random:seed(now()),
  generate_teeth(TeethPresent, ProbGood, []).

%% @doc Helper function that adds tooth data to the ultimate result.

-spec(generate_teeth(string(), float(), [[integer()]]) -> [[integer()]]).

generate_teeth([], _Prob, Result) -> lists:reverse(Result);

generate_teeth([$F | Tail], ProbGood, Result) ->
  generate_teeth(Tail, ProbGood, [[0] | Result]);

generate_teeth([$T | Tail], ProbGood, Result) ->
  generate_teeth(Tail, ProbGood,
  [generate_tooth(ProbGood) | Result]).

-spec(generate_tooth(float()) -> list(integer())).

%% @doc Generates a list of six numbers for a single tooth. Choose a
%% random number between 0 and 1. If that number is less than the probability
%% of a good tooth, it sets the "base depth" to 2, otherwise it sets the base
%% depth to 3.

generate_tooth(ProbGood) ->
  Good = random:uniform() < ProbGood,
  case Good of
    true -> BaseDepth = 2;
    false -> BaseDepth = 3
  end,
  generate_tooth(BaseDepth, 6, []).

%% @doc Take the base depth, add a number in range -1..1 to it,
%% and add it to the list.

generate_tooth(_Base, 0, Result) -> Result;

generate_tooth(Base, N, Result) ->
  [Base + random:uniform(3) - 2 | generate_tooth(Base, N - 1, Result)].

test_teeth() ->
  TList = "FTTTTTTTTTTTTTTFTTTTTTTTTTTTTTTT",
  N = generate_teeth(TList, 0.75),
  print_tooth(N).

print_tooth([]) -> io:format("Finished.~n");
print_tooth([H|T]) ->
  io:format("~p~n", [H]),
  print_tooth(T).

Solution 7-1

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

calculus.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Find the derivative of a function Fn at point X.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(calculus).
-export([derivative/2]).

%% @doc Calculate derivative by classical definition.
%% (Fn(X + H) - Fn(X)) / H

-spec(derivative(function(), float()) -> float()).

derivative(Fn, X) ->
        Delta = 1.0e-10,
        (Fn(X + Delta) - Fn(X)) / Delta.

Solution 7-2

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

patmatch.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Use pattern matching in a list comprehension.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(patmatch).
-export([older_males/0, older_or_male/0]).

%% @doc Select all males older than 40 from a list of tuples giving
%% name, gender, and age.

-spec(older_males() -> list()).

get_people() ->
  [{"Federico", $M, 22}, {"Kim", $F, 45}, {"Hansa", $F, 30},
  {"Vu", $M, 47}, {"Cathy", $F, 32}, {"Elias", $M, 50}].

older_males() ->
  People = get_people(),
  [Name || {Name, Gender, Age} <- People, Gender == $M, Age > 40].

older_or_male() ->
  People = get_people(),
  [Name || {Name, Gender, Age} <- People, (Gender == $M) orelse (Age > 40)].

Solution 7-3

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

stats.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating basic statistics on a list of numbers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(stats).
-export([minimum/1, maximum/1, range/1, mean/1, stdv/1, stdv_sums/2]).

%% @doc Returns the minimum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(minimum(list()) -> number()).

minimum(NumberList) ->
  minimum(NumberList, hd(NumberList)).

minimum([], Result) -> Result;

minimum([Head|Tail], Result) ->
  case Head < Result of
    true -> minimum(Tail, Head);
    false -> minimum(Tail, Result)
  end.

%% @doc Returns the maximum item in a list of numbers. Fails when given
%% an empty list, as there's nothing reasonable to return.

-spec(maximum(list()) -> number()).

maximum(NumberList) ->
  maximum(NumberList, hd(NumberList)).

maximum([], Result) -> Result;

maximum([Head|Tail], Result) ->
  case Head > Result of
    true -> maximum(Tail, Head);
    false -> maximum(Tail, Result)
  end.

%% @doc Return the range (maximum and minimum) of a list of numbers
%% as a two-element list.
-spec(range(list()) -> list()).

range(NumberList) -> [minimum(NumberList), maximum(NumberList)].

%% @doc Return the mean of the list.
-spec(mean(list) -> float()).

mean(NumberList) ->
  Sum = lists:foldl(fun(V, A) -> V + A end, 0, NumberList),
  Sum / length(NumberList).

stdv_sums(Value, Accumulator) ->
  [Sum, SumSquares] = Accumulator,
  [Sum + Value, SumSquares + Value * Value].

stdv(NumberList) ->
  N = length(NumberList),
  [Sum, SumSquares] = lists:foldl(fun stdv_sums/2, [0, 0], NumberList),
  math:sqrt((N * SumSquares - Sum * Sum) / (N * (N - 1))).

Solution 7-4

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

dates.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for splitting a date into a list of
%% year-month-day and finding Julian date.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(dates).
-export([date_parts/1, julian/1, is_leap_year/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()).

date_parts(DateStr) ->
  [YStr, MStr, DStr] = re:split(DateStr, "-", [{return, list}]),
  [element(1, string:to_integer(YStr)),
    element(1, string:to_integer(MStr)),
    element(1, string:to_integer(DStr))].

%% @doc Takes a string in ISO date format (yyyy-mm-dd) and
%% returns the day of the year (Julian date).
%% Works by summing the days per month up to, but not including,
%% the month in question, then adding the number of days.
%% If it's a leap year and past February, add a leap day.

-spec(julian(list()) -> integer()).

julian(DateStr) ->
  DaysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [Y, M, D] = date_parts(DateStr),
  {Sublist, _} = lists:split(M - 1, DaysPerMonth),
  Total = lists:foldl(fun(V, A) -> V + A end, 0, Sublist),
  case M > 2 andalso is_leap_year(Y) of
    true -> Total + D + 1;
    false -> Total + D
  end.

is_leap_year(Year) ->
  (Year rem 4 == 0 andalso Year rem 100 /= 0)
    orelse (Year rem 400 == 0).

Solution 7-5

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

cards.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for playing a card game.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(cards).
-export([make_deck/0, show_deck/1]).

%% @doc generate a deck of cards
make_deck() ->
  [{Value, Suit} || Value <- ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
    Suit <- ["Clubs", "Diamonds", "Hearts", "Spades"]].

show_deck(Deck) ->
  lists:foreach(fun(Item) -> io:format("~p~n", [Item]) end, Deck).

Solution 7-6

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

cards.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for playing a card game.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(cards).
-export([make_deck/0, shuffle/1]).

%% @doc generate a deck of cards
make_deck() ->
  [{Value, Suit} || Value <- ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
    Suit <- ["Clubs", "Diamonds", "Hearts", "Spades"]].

shuffle(List) -> shuffle(List, []).

%% If the list is empty, return the accumulated value.
shuffle([], Acc) -> Acc;

%% Otherwise, find a random location in the list and split the list
%% at that location. Let's say the list has 52 elements and the random
%% location is location 22. The first 22 elements go into Leading, and the
%% last 30 elements go into [H|T]. Thus, H would contain element 23, and
%% T would contain elements 24 through 52.
%%
%% H is the "chosen element". It goes into the accumulator (the shuffled list)
%% and then we call shuffle again with the remainder of the deck: the
%% leading elements and the tail of the split list.

shuffle(List, Acc) ->
  {Leading, [H | T]} = lists:split(random:uniform(length(List)) - 1, List),
  shuffle(Leading ++ T, [H | Acc]).

Solution 8-1

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

cards.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for playing card games.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(cards).
-export([make_deck/0, shuffle/1]).

%% @doc generate a deck of cards
-type card()::{string()|integer(), string()}.
-spec(make_deck() -> [card()]).

%%make_deck() ->
%%  [{Value, Suit} || Value <- ["A", 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K"],
%%    Suit <- ["Clubs", "Diamonds", "Hearts", "Spades"]].

make_deck() ->
  [{Value, Suit} || Value <- ["A", 2, 3, 4],
    Suit <- ["Clubs", "Diamonds"]].

%% Do a Fisher-Yates shuffle of a deck
-spec(shuffle([card()])-> [card()]).

shuffle(List) -> shuffle(List, []).

%% If the list is empty, return the accumulated value.
shuffle([], Acc) -> Acc;

%% Otherwise, find a random location in the list and split the list
%% at that location. Let's say the list has 52 elements and the random
%% location is location 22. The first 22 elements go into Leading, and the
%% last 30 elements go into [H|T]. Thus, H would contain element 23, and
%% T would contain elements 24 through 52.
%%
%% H is the "chosen element". It goes into the accumulator (the shuffled list)
%% and then we call shuffle again with the remainder of the deck: the
%% leading elements and the tail of the split list.

shuffle(List, Acc) ->
  {Leading, [H | T]} = lists:split(random:uniform(length(List)) - 1, List),
  shuffle(Leading ++ T, [H | Acc]).

game.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Play the card game "war" with two players.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(game).
-export([play_game/0, dealer/0, player/2, value/1]).

%% @doc create a dealer
play_game() ->
  spawn(game, dealer, []).

dealer() ->
  random:seed(now()),
  DealerPid = self(),
  Deck = cards:shuffle(cards:make_deck()),
  {P1Cards, P2Cards} = lists:split(trunc(length(Deck) / 2), Deck),
  io:format("About to spawn players each with ~p cards.~n",
    [trunc(length(Deck) / 2)]),
  P1 = spawn(game, player, [DealerPid, P1Cards]),
  P2 = spawn(game, player, [DealerPid, P2Cards]),
  io:format("Spawned players ~p and ~p~n", [P1, P2]),
  dealer([P1, P2], pre_battle, [], [], 0, []).

%% The dealer has to keep track of the players' process IDs,
%% the cards they have given to the dealer for comparison,
%% how many players have responded (0, 1, or 2), and the pile
%% in the middle of the table in case of a war.

dealer(Pids, State, P1Cards, P2Cards, Count, Pile) ->
  [P1, P2] = Pids,
  NCards = if
    Pile == []  -> 1;
    Pile /= [] -> 3
  end,
  case State of
    pre_battle ->
      P1 ! {give_cards, NCards},
      P2 ! {give_cards, NCards},
      dealer(Pids, await_battle, P1Cards, P2Cards, Count, Pile);
    await_battle ->
      receive
        {accept, Pid, Data} ->
          NextCount = Count + 1,
          case Pid of
            P1 -> Next_P1Cards = Data, Next_P2Cards = P2Cards;
            P2 -> Next_P1Cards = P1Cards, Next_P2Cards = Data
          end
      end,
      if
        NextCount == 2 -> NextState = check_cards;
        NextCount /= 2 -> NextState = State
      end,
      dealer(Pids, NextState, Next_P1Cards, Next_P2Cards,
        NextCount, Pile);
    check_cards ->
      Winner = game_winner(P1Cards, P2Cards),
      case Winner of
        0 ->
          io:format("Compare ~p to ~p~n", [P1Cards, P2Cards]),
          NewPile = Pile ++ P1Cards ++ P2Cards,
          case battle_winner(P1Cards, P2Cards) of
             0 -> dealer(Pids, pre_battle, [], [], 0, NewPile);
             1 ->
              P1 ! {take_cards, NewPile},
              dealer(Pids, await_confirmation, [], [], 0, []);
             2 ->
              P2 ! {take_cards, NewPile},
              dealer(Pids, await_confirmation, [], [], 0, [])
           end;
        3 ->
          io:format("It's a draw!~n"),
          end_game(Pids);
        _ ->
          io:format("Player ~p wins~n", [Winner]),
          end_game(Pids)
      end;
    await_war->
      io:format("Awaiting war~n");
    await_confirmation ->
      io:format("Awaiting confirmation of player receiving cards~n"),
      receive
        {confirmed, _Pid, _Data} ->
        dealer(Pids, pre_battle, [], [], 0, [])
      end
  end.

end_game(Pids) ->
  lists:foreach(fun(Process) -> exit(Process, kill) end, Pids),
  io:format("Game finished.~n").

%% Do we have a winner? If both players are out of cards,
%% it's a draw. If one player is out of cards, the other is the winner.

game_winner([], []) -> 3;
game_winner([], _) -> 2;
game_winner(_, []) -> 1;
game_winner(_, _) -> 0.

battle_winner(P1Cards, P2Cards) ->
  V1 = value(hd(lists:reverse(P1Cards))),
  V2 = value(hd(lists:reverse(P2Cards))),
  Winner = if
    V1 > V2 -> 1;
    V2 > V1 -> 2;
    V1 == V2 -> 0
  end,
  io:format("Winner of ~p vs. ~p is ~p~n", [V1, V2, Winner]),
  Winner = Winner.

player(Dealer, Hand) ->
  receive
    {Command, Data} ->
      case Command of
        give_cards ->
          {ToSend, NewHand} = give_cards(Hand, Data),
          io:format("Sending ~p to ~p~n", [ToSend, Dealer]),
          Dealer!{accept, self(), ToSend};
        take_cards ->
          io:format("~p now has ~p (cards)~n", [self(),
            length(Data) + length(Hand)]),
          NewHand = Hand ++ Data,
          Dealer!{confirmed, self(), []}
      end
  end,
  player(Dealer, NewHand).

%% Player gives N cards from current Hand. N is 1 or 3,
%% depending if there is a war or not.
%% If a player is asked for 3 cards but doesn't have enough,
%% give all the cards in the hand.
%% This function returns a tuple: {[cards to send], [remaining cards in hand]}

give_cards([], _N) -> {[],[]};
give_cards([A], _N) -> {[A],[]};
give_cards([A, B], N) ->
  if
    N == 1 -> {[A], [B]};
    N == 3 -> {[A, B], []}
  end;
give_cards(Hand, N) ->
  if
    N == 1 -> {[hd(Hand)], tl(Hand)};
    N == 3 ->
      [A, B, C | Remainder] = Hand,
      {[A, B, C], Remainder}
  end.

%% @doc Returns the value of a card. Aces are high; K > Q > J
-spec(value({cards:card()}) -> integer()).

value({V, _Suit}) ->
  if
    is_integer(V) -> V;
    is_list(V) ->
      case hd(V) of
        $J -> 11;
        $Q -> 12;
        $K -> 13;
        $A -> 14
      end
  end.

Solution 9-1

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

stats.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Functions for calculating basic statistics on a list of numbers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(stats).
-export([minimum/1, maximum/1, range/1, mean/1, stdv/1, stdv_sums/2]).

%% @doc Returns the minimum item in a list of numbers. Uses
%% try/catch to return an error when there's an empty list,
%% as there's nothing reasonable to return.

-spec(minimum(list()) -> number()).

minimum(NumberList) ->
  try minimum(NumberList, hd(NumberList)) of
    Answer -> Answer
  catch
    error:Error -> {error, Error}
  end.

minimum([], Result) -> Result;

minimum([Head|Tail], Result) ->
  case Head < Result of
    true -> minimum(Tail, Head);
    false -> minimum(Tail, Result)
  end.

%% @doc Returns the maximum item in a list of numbers. Catches
%% errors when given an empty list.

-spec(maximum(list()) -> number()).

maximum(NumberList) ->
  try
    maximum(NumberList, hd(NumberList))
  catch
    error:Error-> {error, Error}
  end.

maximum([], Result) -> Result;

maximum([Head|Tail], Result) ->
  case Head > Result of
    true -> maximum(Tail, Head);
    false -> maximum(Tail, Result)
  end.

%% @doc Return the range (maximum and minimum) of a list of numbers
%% as a two-element list.
-spec(range(list()) -> list()).

range(NumberList) -> [minimum(NumberList), maximum(NumberList)].

%% @doc Return the mean of the list.
-spec(mean(list()) -> float()).

mean(NumberList) ->
  try
    Sum = lists:foldl(fun(V, A) -> V + A end, 0, NumberList),
    Sum / length(NumberList)
  catch
    error:Error -> {error, Error}
  end.

%% @doc Helper function to generate sums and sums of squares
%% when calculating standard deviation.

-spec(stdv_sums(number(),[number()]) -> [number()]).

stdv_sums(Value, Accumulator) ->
  [Sum, SumSquares] = Accumulator,
  [Sum + Value, SumSquares + Value * Value].

%% @doc Calculate the standard deviation of a list of numbers.

-spec(stdv([number()]) -> float()).

stdv(NumberList) ->
  N = length(NumberList),
  try
    [Sum, SumSquares] = lists:foldl(fun stdv_sums/2, [0, 0], NumberList),
    math:sqrt((N * SumSquares - Sum * Sum) / (N * (N - 1)))
  catch
    error:Error -> {error, Error}
  end.

Solution 9-2

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

bank.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Implement a bank account that logs its transactions.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(bank).
-export([account/1]).

-spec(account(number()) -> number()).

%% @doc create a client and give it a starting balance

account(Balance) ->
  Input = io:get_line("D)eposit, W)ithdraw, B)alance, Q)uit: "),
  Action = hd(Input),

  case Action of
    $D ->
      Amount = get_number("Amount to deposit: "),
      NewBalance = transaction(deposit, Balance, Amount);
    $W ->
      Amount = get_number("Amount to withdraw: "),
      NewBalance = transaction(withdraw, Balance, Amount);
    $B ->
      NewBalance = transaction(balance, Balance);
    $Q ->
      NewBalance = Balance;
    _ ->
      io:format("Unknown command ~c~n", [Action]),
      NewBalance = Balance
  end,
  if
    Action /= $Q ->
    account(NewBalance);
    true -> true
  end.


%% @doc Present a prompt and get a number from the
%% user. Allow either integers or floats.
get_number(Prompt) ->
  Str = io:get_line(Prompt),
  {Test, _} = string:to_float(Str),
  case Test of
    error -> {N, _} = string:to_integer(Str);
    _ -> N = Test
  end,
  N.

transaction(Action, Balance, Amount) ->
  case Action of
    deposit ->
      if
        Amount >= 10000 ->
          error_logger:warning_msg("Excessive deposit ~p~n", [Amount]),
          io:format("Your deposit of $~p may be subject to hold.", [Amount]),
          io:format("Your new balance is ~p~n", [Balance + Amount]),
          NewBalance = Balance + Amount;
        Amount < 0 ->
          error_logger:error_msg("Negative deposit amount ~p~n", [Amount]),
          io:format("Deposits may not be less than zero."),
          NewBalance = Balance;
        Amount >= 0 ->
          error_logger:info_msg("Successful deposit ~p~n", [Amount]),
          NewBalance = Balance + Amount,
          io:format("Your new balance is ~p~n", [NewBalance])
      end;
    withdraw ->
      if
        Amount > Balance ->
          error_logger:error_msg("Overdraw ~p from balance ~p~n", [Amount,
            Balance]),
          io:format("You cannot withdraw more than your current balance of ~p.~n",
            [Balance]),
          NewBalance = Balance;
        Amount < 0 ->
          error_logger:error_msg("Negative withdrawal amount ~p~n", [Amount]),
          io:format("Withdrawals may not be less than zero."),
          NewBalance = Balance;
        Amount >= 0 ->
          error_logger:info_msg("Successful withdrawal ~p~n", [Amount]),
          NewBalance = Balance - Amount,
          io:format("Your new balance is ~p~n", [NewBalance])
      end
  end,
  NewBalance.

transaction(balance, Balance) ->
  error_logger:info_msg("Balance inquiry ~p~n", [Balance]),
  Balance.

Solution 10-1

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

phone_records.hrl

-record(phone_call,
  {phone_number, start_date, start_time, end_date, end_time}).

phone_ets.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Read in a database of phone calls
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(phone_ets).
-export([setup/1, summary/0, summary/1]).
-include("phone_records.hrl").

%% @doc Create an ets table of phone calls from the given file name.

-spec(setup(string()) -> atom()).

setup(FileName) ->

  %% If the table exists, delete it
  case ets:info(call_table) of
    undefined -> false;
    _ -> ets:delete(call_table)
   end,

  %% and create it anew
  ets:new(call_table, [named_table, bag,
    {keypos, #phone_call.phone_number}]),

  {ResultCode, InputFile} = file:open(FileName, [read]),
  case ResultCode of
    ok -> read_item(InputFile);
    _ -> io:format("Error opening file: ~p~n", [InputFile])
  end.

%% Read a line from the input file, and insert its contents into
%% the call_table. This function is called recursively until end of file

-spec(read_item(file:io_device()) -> atom()).

read_item(InputFile) ->
  RawData = io:get_line(InputFile, ""),
  if
    is_list(RawData) ->
      Data = string:strip(RawData, right, $\n),
      [Number, SDate, STime, EDate, ETime] =
        re:split(Data, ",", [{return, list}]),
      ets:insert(call_table, #phone_call{phone_number = Number,
        start_date = to_date(SDate), start_time = to_time(STime),
        end_date = to_date(EDate), end_time= to_time(ETime)}),
      read_item(InputFile);
    RawData == eof -> ok
  end.

%% @doc Convert a string in form "yyyy-mm-dd" to a tuple {yyyy, mm, dd}
%% suitable for use with the calendar module.

-spec(to_date(string()) -> {integer(), integer(), integer()}).

to_date(Date) ->
  [Year, Month, Day] = re:split(Date, "-", [{return, list}]),
  [{Y, _}, {M, _}, {D, _}] = lists:map(fun string:to_integer/1,
    [Year, Month, Day]),
  {Y, M, D}.

%% @doc Convert a string in form "hh:mm:ss" to a tuple {hh, mm, ss}
%% suitable for use with the calendar module.

-spec(to_time(string()) -> {integer(), integer(), integer()}).

to_time(Time) ->
  [Hour, Minute, Second] = re:split(Time, ":", [{return, list}]),
  [{H, _}, {M, _}, {S, _}] = lists:map(fun string:to_integer/1,
    [Hour, Minute, Second]),
  {H, M, S}.

%% @doc Create a summary of number of minutes used by all phone numbers.

-spec(summary() -> [tuple(string(), integer())]).

summary() ->
  FirstKey = ets:first(call_table),
  summary(FirstKey, []).

summary(Key, Result) ->
  NextKey = ets:next(call_table, Key),
  case NextKey of
    '$end_of_table' -> Result;
    _ -> summary(NextKey, [hd(summary(Key)) | Result])
  end.

%% @doc Create a summary of number of minutes used by one phone number.

-spec(summary(string()) -> [tuple(string(), integer())]).

summary(PhoneNumber) ->
  Calls = ets:lookup(call_table, PhoneNumber),
  Total = lists:foldl(fun subtotal/2, 0, Calls),
  [{PhoneNumber, Total}].

subtotal(Item, Accumulator) ->
  StartSeconds = calendar:datetime_to_gregorian_seconds(
    {Item#phone_call.start_date, Item#phone_call.start_time}),
  EndSeconds = calendar:datetime_to_gregorian_seconds(
    {Item#phone_call.end_date, Item#phone_call.end_time}),
  Accumulator + ((EndSeconds - StartSeconds + 59) div 60).

generate_calls.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Generate a random set of data for phone calls
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(generate_calls).
-export([make_call_list/1, format_date/1, format_time/1]).

make_call_list(N) ->
  Now = calendar:datetime_to_gregorian_seconds({{2013, 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}
  ],
  CallList = make_call_list(N, Numbers, []),
  {Result, OutputFile} = file:open("call_list.csv", [write]),
  case Result of
    ok -> write_item(OutputFile, CallList);
    error -> io:format("Error: ~p~n", OutputFile)
  end.

make_call_list(0, _Numbers, Result) -> lists:reverse(Result);

make_call_list(N, Numbers, Result) ->
  Entry = random:uniform(length(Numbers)),
  {Head, Tail} = lists:split(Entry - 1, Numbers),
  {Number, LastCall} = hd(Tail),
  StartCall = LastCall + random:uniform(120) + 20,
  Duration = random:uniform(180) + 40,
  EndCall = StartCall + Duration,
  Item = [Number, format_date(StartCall), format_time(StartCall),
    format_date(EndCall), format_time(EndCall)],
  UpdatedNumbers = Head ++ [{Number, EndCall} | tl(Tail)],
  make_call_list(N - 1, UpdatedNumbers, [Item | Result]).

write_item(OutputFile, []) ->
  file:close(OutputFile);

write_item(OutputFile, [H|T]) ->
  io:format("~s ~s ~s ~s ~s~n", H),
  io:fwrite(OutputFile, "~s,~s,~s,~s,~s~n", H),
  write_item(OutputFile, T).

format_date(GSeconds) ->
  {Date, _Time} = calendar:gregorian_seconds_to_datetime(GSeconds),
  {Y, M, D} = Date,
  lists:flatten(io_lib:format("~4b-~2..0b-~2..0b", [Y, M, D])).

format_time(GSeconds) ->
  {_Date, Time} = calendar:gregorian_seconds_to_datetime(GSeconds),
  {M, H, S} = Time,
  lists:flatten(io_lib:format("~2..0b:~2..0b:~2..0b", [M, H, S])).

Solution 10-2

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

phone_records.hrl

-record(phone_call,
  {phone_number, start_date, start_time, end_date, end_time}).
-record(customer,
  {phone_number, last_name, first_name, middle_name, rate}).

phone_mnesia.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Read in a database of phone calls and customers.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(phone_mnesia).
-export([setup/2, summary/3]).
-include("phone_records.hrl").
-include_lib("stdlib/include/qlc.hrl").

%% @doc Set up Mnesia tables for phone calls and customers
%% given their file names

-spec(setup(string(), string()) -> atom()).

setup(CallFileName, CustomerFileName) ->

  mnesia:create_schema([node()]),
  mnesia:start(),
  mnesia:delete_table(phone_call),
  mnesia:delete_table(customer),

  fill_table(phone_call, CallFileName, fun add_call/1,
    record_info(fields, phone_call), bag),
  fill_table(customer, CustomerFileName, fun add_customer/1,
    record_info(fields, customer), set).

%% @doc Fill the given table with data from given file name.
%% AdderFunction assigns data to fields and writes it to the table;
%% RecordInfo is used when creating the table, as is the TableType.

fill_table(TableName, FileName, AdderFunction, RecordInfo, TableType) ->
  mnesia:create_table(TableName, [{attributes, RecordInfo}, {type, TableType}]),

  {OpenResult, InputFile} = file:open(FileName, [read]),
  case OpenResult of
    ok ->
      mnesia:transaction(
        fun() -> read_file(InputFile, AdderFunction) end);
    _ -> io:format("Error opening file: ~p~n", [FileName])
  end.

%% @doc Read a line from InputFile, and insert its contents into
%% the appropriate table by using AdderFunction.

-spec(read_file(file:io_device(), function()) -> atom()).

read_file(InputFile, AdderFunction) ->
  RawData = io:get_line(InputFile, ""),
  if
    is_list(RawData) ->
      Data = string:strip(RawData, right, $\n),
      ItemList = re:split(Data, ",", [{return, list}]),
      AdderFunction(ItemList),
      read_file(InputFile, AdderFunction);
    RawData == eof -> ok
  end.


%% Add a phone call record; the data is in an ItemList.

-spec(add_call(list()) -> undefined).

add_call(ItemList) ->
  [Number, SDate, STime, EDate, ETime] = ItemList,
  mnesia:write(#phone_call{phone_number = Number,
        start_date = to_date(SDate), start_time = to_time(STime),
        end_date = to_date(EDate), end_time= to_time(ETime)}).

%% Add a customer record; the data is in an ItemList.

-spec(add_customer(list()) -> undefined).

add_customer(ItemList) ->
  [Phone, Last, First, Middle, Rate] = ItemList,
  mnesia:write(#customer{phone_number = Phone, last_name = Last,
    first_name = First, middle_name = Middle, rate = to_float(Rate)}).

%% @doc Convert a string in form "yyyy-mm-dd" to a tuple {yyyy, mm, dd}
%% suitable for use with the calendar module.

-spec(to_date(string()) -> {integer(), integer(), integer()}).

to_date(Date) ->
  [Year, Month, Day] = re:split(Date, "-", [{return, list}]),
  [{Y, _}, {M, _}, {D, _}] = lists:map(fun string:to_integer/1,
    [Year, Month, Day]),
  {Y, M, D}.

%% @doc Convert a string in form "hh:mm:ss" to a tuple {hh, mm, ss}
%% suitable for use with the calendar module.

-spec(to_time(string()) -> {integer(), integer(), integer()}).

to_time(Time) ->
  [Hour, Minute, Second] = re:split(Time, ":", [{return, list}]),
  [{H, _}, {M, _}, {S, _}] = lists:map(fun string:to_integer/1,
    [Hour, Minute, Second]),
  {H, M, S}.


%% @doc Convenience routine to convert a string to float.
%% In case of an error, return zero.

-spec(to_float(string()) -> float()).

to_float(Str) ->
  {FPart, _} = string:to_float(Str),
  case FPart of
    error -> 0;
    _ -> FPart
  end.

summary(Last, First, Middle) ->

  QHandle = qlc:q([Customer ||
    Customer <- mnesia:table(customer),
    Customer#customer.last_name == Last,
    Customer#customer.first_name == First,
    Customer#customer.middle_name == Middle ]),

  {_Result, [ThePerson|_]} =
    mnesia:transaction(fun() -> qlc:e(QHandle) end),

  {_Result, Calls} = mnesia:transaction(
    fun() ->
       qlc:e(
        qlc:q( [Call ||
          Call <- mnesia:table(phone_call),
          QCustomer <- QHandle,
          QCustomer#customer.phone_number == Call#phone_call.phone_number
        ]
        )
      )
    end
  ),

  TotalMinutes = lists:foldl(fun subtotal/2, 0, Calls),

  [{ThePerson#customer.phone_number,
    TotalMinutes, TotalMinutes * ThePerson#customer.rate}].

subtotal(Item, Accumulator) ->
  StartSeconds = calendar:datetime_to_gregorian_seconds(
    {Item#phone_call.start_date, Item#phone_call.start_time}),
  EndSeconds = calendar:datetime_to_gregorian_seconds(
    {Item#phone_call.end_date, Item#phone_call.end_time}),
  Accumulator + ((EndSeconds - StartSeconds + 59) div 60).

pet_records.hrl

-record(person,
  {id_number, name, age, gender, city, amount_owed}).
-record(animal,
  {id_number, name, species, gender, owner_id}).

pet_mnesia.erl

%% @author J D Eisenberg <jdavid.eisenberg@gmail.com>
%% @doc Read in a database of people and their pets
%% appointments.
%% @copyright 2013 J D Eisenberg
%% @version 0.1

-module(pet_mnesia).
-export([setup/2, get_info/0, get_info_easier/0]).
-include("pet_records.hrl").
-include_lib("stdlib/include/qlc.hrl").

%% @doc Set up Mnesia tables for phone calls and customers
%% given their file names

-spec(setup(string(), string()) -> atom()).

setup(PersonFileName, AnimalFileName) ->

  mnesia:create_schema([node()]),
  mnesia:start(),
  mnesia:delete_table(person),
  mnesia:delete_table(animal),

  fill_table(person, PersonFileName, fun add_person/1,
    record_info(fields, person), set),
  fill_table(animal, AnimalFileName, fun add_animal/1,
    record_info(fields, animal), set).

%% @doc Fill the given table with data from given file name.
%% AdderFunction assigns data to fields and writes it to the table;
%% RecordInfo is used when creating the table, as is the TableType.

fill_table(TableName, FileName, AdderFunction, RecordInfo, TableType) ->
  mnesia:create_table(TableName, [{attributes, RecordInfo}, {type, TableType}]),

  {OpenResult, InputFile} = file:open(FileName, [read]),
  case OpenResult of
    ok ->
      TransResult = mnesia:transaction(
        fun() -> read_file(InputFile, AdderFunction) end),
        io:format("Transaction result ~p~n", [TransResult]);
    _ -> io:format("Error opening file: ~p~n", [FileName])
  end.

%% @doc Read a line from InputFile, and insert its contents into
%% the appropriate table by using AdderFunction.

-spec(read_file(file:io_device(), function()) -> atom()).

read_file(InputFile, AdderFunction) ->
  RawData = io:get_line(InputFile, ""),
  if
    is_list(RawData) ->
      Data = string:strip(RawData, right, $\n),
      ItemList = re:split(Data, ",", [{return, list}]),
      AdderFunction(ItemList),
      read_file(InputFile, AdderFunction);
    RawData == eof -> ok
  end.


%% Add a person record; the data is in an ItemList.

-spec(add_person(list()) -> undefined).

add_person(ItemList) ->
  [Id, Name, Age, Gender, City, Owed] = ItemList,
  mnesia:write(#person{id_number = to_int(Id), name = Name,
    age = to_int(Age), gender = Gender, city = City,
    amount_owed = to_float(Owed)}).

%% Add an animal record; the data is in an ItemList.

-spec(add_animal(list()) -> undefined).

add_animal(ItemList) ->
  [Id, Name, Species, Gender, Owner] = ItemList,
  mnesia:write(#animal{id_number = to_int(Id),
    name = Name, species = Species, gender = Gender,
    owner_id = to_int(Owner)}).

%% @doc Convenience routine to convert a string to integer.
%% In case of an error, return zero.

-spec(to_int(string()) -> integer()).

to_int(Str) ->
  {IPart, _} = string:to_integer(Str),
  case IPart of
    error -> 0;
    _ -> IPart
  end.

%% @doc Convenience routine to convert a string to float.
%% In case of an error, return zero.

-spec(to_float(string()) -> float()).

to_float(Str) ->
  {FPart, _} = string:to_float(Str),
  case FPart of
    error -> 0;
    _ -> FPart
  end.

get_info() ->
  People = mnesia:transaction(
    fun() -> qlc:e(
      qlc:q( [ P ||
        P <- mnesia:table(person),
        P#person.age >= 21,
        P#person.gender == "M",
        P#person.city == "Podunk"]
        )
      )
    end
  ),

  Pets = mnesia:transaction(
    fun() -> qlc:e(
      qlc:q( [{A#animal.name, A#animal.species, P#person.name} ||
        P <- mnesia:table(person),
        P#person.age >= 21,
        P#person.gender == "M",
        P#person.city == "Podunk",
        A <- mnesia:table(animal),
        A#animal.owner_id == P#person.id_number])
      )
    end
  ),
  [People, Pets].

get_info_easier() ->

  %% "Pre-process" the list comprehension for finding people

  QHandle = qlc:q( [ P ||
    P <- mnesia:table(person),
    P#person.age >= 21,
    P#person.gender == "M",
    P#person.city == "Podunk"]
  ),

  %% Evaluate it to retrieve the people you want

  People = mnesia:transaction(
    fun() -> qlc:e( QHandle ) end
  ),

  %% And use the handle again when retrieving
  %% information about their pets

  Pets = mnesia:transaction(
    fun() -> qlc:e(
      qlc:q( [{A#animal.name, A#animal.species, P#person.name} ||
        P <- QHandle,
        A <- mnesia:table(animal),
        A#animal.owner_id == P#person.id_number])
      )
    end
  ),
  [People, Pets].

Solution 11-1

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

weather.erl

-module(weather).
-behaviour(gen_server).
-include_lib("xmerl/include/xmerl.hrl").
-export([start_link/0]). % convenience call for startup
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2,
         code_change/3]). % gen_server callbacks
-define(SERVER, ?MODULE). % macro that just defines this module as server

%%% convenience method for startup
start_link() ->
        gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%% gen_server callbacks
init([]) ->
  inets:start(),
  {ok, []}.

handle_call(Request, _From, State) ->
  {Reply, NewState} = get_weather(Request, State),
  {reply, Reply, NewState}.

handle_cast(_Message, State) ->
  io:format("Most recent requests: ~p\n", [State]),
  {noreply, State}.

handle_info(_Info, State) ->
  {noreply, State}.

terminate(_Reason, _State) ->
  inets:stop(),
  ok.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%%% Internal functions

%% Given a 4-letter station code as the Request, return its basic
%% weather information as a {key,value} list. If successful, add the
%% station name to the State, which will keep track of recently-accessed
%% weather stations.

get_weather(Request, State) ->
  URL = "http://w1.weather.gov/xml/current_obs/" ++ Request ++ ".xml",
  {Result, Info} = httpc:request(URL),
  case Result of
    error -> {{Result, Info}, State};
    ok ->
      {{_Protocol, Code, _CodeStr}, _Attrs, WebData} = Info,
      case Code of
        404 ->
          {{error, 404}, State};
        200 ->
          Weather = analyze_info(WebData),
          {{ok, Weather}, [Request | lists:sublist(State, 10)]}
      end
  end.

%% Take raw XML data and return a set of {key, value} tuples

analyze_info(WebData) ->
  %% list of fields that you want to extract
  ToFind = [location, observation_time_rfc822, weather, temperature_string],

  %% get just the parsed data from the XML parse result
  Parsed = element(1, xmerl_scan:string(WebData)),

  %% This is the list of all children under <current_observation>
  Children = Parsed#xmlElement.content,

  %% Find only XML elements and extract their names and their text content.
  %% You need the guard so that you don't process the newlines in the
  %% data (they are XML text descendants of the root element).
  ElementList = [{El#xmlElement.name, extract_text(El#xmlElement.content)}
    || El <- Children, element(1, El) == xmlElement],

  %% ElementList is now a keymap; get the data you want from it.
  lists:map(fun(Item) -> lists:keyfind(Item, 1, ElementList) end, ToFind).


%% Given the parsed content of an XML element, return its first node value
%% (if it's a text node); otherwise return the empty string.

extract_text(Content) ->
  Item = hd(Content),
  case element(1, Item) of
    xmlText -> Item#xmlText.value;
    _ -> ""
  end.

weather_sup.erl

-module(weather_sup).
-behaviour(supervisor).
-export([start_link/0]). % convenience call for startup

-export([init/1]). % supervisor calls
-define(SERVER, ?MODULE).


%%% convenience method for startup
start_link() ->
  supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%% supervisor callback
init([]) ->
    RestartStrategy = one_for_one,
    MaxRestarts = 1, % one restart every
    MaxSecondsBetweenRestarts = 5, % five seconds

    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},

    Restart = permanent, % or temporary, or transient
    Shutdown = 2000, % milliseconds, could be infinity or brutal_kill
    Type = worker, % could also be supervisor

    Weather = {weather, {weather, start_link, []},
                      Restart, Shutdown, Type, [weather]},

    {ok, {SupFlags, [Weather]}}.

Solution 11-2

Here is a suggested solution for Étude 11-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.erl

-export([report/1, recent/0]). % wrapper functions

%% Wrapper to hide internal details when getting a weather report
report(Station) ->
  gen_server:call(?SERVER, Station).

%% Wrapper to hide internal details when getting a list of recently used
%% stations.
recent() ->
  gen_server:cast(?SERVER, "").

Solution 11-3

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

%% @doc Connect to a named server
connect(ServerName) ->
  Result = net_adm:ping(ServerName),
  case Result of
    pong -> io:format("Connected to server.~n");
    pang -> io:format("Cannot connect to ~p.~n", [ServerName])
  end.

%% Wrapper to hide internal details when getting a weather report
report(Station) ->
  gen_server:call({global, weather}, Station).

%% Wrapper to hide internal details when getting a list of recently used
%% stations.
recent() ->
  gen_server:call({global,weather}, recent).

%%% convenience method for startup
start_link() ->
  gen_server:start_link({global, ?SERVER}, ?MODULE, [], []).

%%% gen_server callbacks
init([]) ->
  inets:start(),
  {ok, []}.

handle_call(recent, _From, State) ->
  {reply, State, State};
handle_call(Request, _From, State) ->
  {Reply, NewState} = get_weather(Request, State),
  {reply, Reply, NewState}.

handle_cast(_Message, State) ->
  io:format("Most recent requests: ~p\n", [State]),
  {noreply, State}.

Solution 11-4

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

chatroom.erl

-module(chatroom).
-behaviour(gen_server).
-export([start_link/0]). % convenience call for startup
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2,
         code_change/3]). % gen_server callbacks

-define(SERVER, ?MODULE). % macro that defines this module as the server

% The server state consists of a list of tuples for each person in chat.
% Each tuple has the format {{UserName, UserServer}, PID of person}

%%% convenience method for startup
start_link() ->
  gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%% gen_server callbacks
init([]) ->
  {ok, []}.

%% Check to see if a user name/server pair is unique;
%% if so, add it to the server's state

handle_call({login, UserName, ServerRef}, From, State) ->
  {FromPid, _FromTag} = From,
  case lists:keymember({UserName, ServerRef}, 1, State) of
    true ->
      NewState = State,
      Reply = {error, "User " ++ UserName ++ " already in use."};
    false ->
      NewState = [{{UserName, ServerRef}, FromPid} | State],
      Reply = {ok, "Logged in."}
  end,
  {reply, Reply, NewState};

%% Log out the person sending the message, but only
%% if they're logged in already.

handle_call(logout, From, State) ->
  {FromPid, _FromTag} = From,
  case lists:keymember(FromPid, 2, State) of
    true ->
      NewState = lists:keydelete(FromPid, 2, State),
      Reply  = {ok, logged_out};
    false ->
      NewState = State,
      Reply = {error, not_logged_in}
  end,
  {reply, Reply, NewState};

%% When receiving a message from a person, use the From PID to
%% get the user's name and server name from the chatroom server state.
%% Send the message via a "cast" to everyone who is NOT the sender.

handle_call({say, Text}, From, State) ->
  {FromPid, _FromTag} = From,

  case lists:keymember(FromPid, 2, State) of
    true ->
    {value, {{SenderName, SenderServer}, _}} =
      lists:keysearch(FromPid, 2, State),

    % For debugging: get the list of recipients.
    RecipientList = [{RecipientName, RecipientServer} ||
      {{RecipientName, RecipientServer}, _} <- State,
      {RecipientName, RecipientServer} /= {SenderName, SenderServer}],
    io:format("Recipient list: ~p~n", [RecipientList]),

    [gen_server:cast({person, RecipientServer},
      {message, {SenderName, SenderServer}, Text}) ||
      {{RecipientName, RecipientServer}, _} <- State,
     RecipientName /= SenderName];

    false -> ok
  end,
  {reply, ok, State};

%% Get the state of another person and return it to the asker

handle_call({who, Person, ServerRef}, _From, State) ->
  % Find pid of the person at the serverref
  Found = lists:keyfind({Person, ServerRef}, 1, State),

  case Found of
    {{_FromUser, _FromServer}, Pid} ->
      Reply = gen_server:call(Pid, get_profile);
    _ ->
      Reply = "Cannot find that user"
  end,
  {reply, Reply, State};

%% Return a list of all users currently in the chat room

handle_call(users, _From, State) ->
  UserList = [{UserName, UserServer} ||
    {{UserName, UserServer}, _} <- State],
  {reply, UserList, State};

handle_call(Request, _From, State) ->
  {ok, {error, "Unhandled Request", Request}, State}.

handle_cast(_Request, State) ->
  {noreply, State}.

handle_info(Info, State) ->
  io:format("Received unknown message ~p~n", [Info]),
  {noreply, State}.

terminate(_Reason, _State) ->
  ok.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%%% Internal functions

person.erl

-module(person).
-behaviour(gen_server).
-export([start_link/1]). % convenience call for startup
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2,
         code_change/3]). % gen_server callbacks

-record(state, {chat_node, profile}).

% internal functions
-export([login/1, logout/0, say/1, users/0, who/2, set_profile/2]).

-define(CLIENT, ?MODULE). % macro that defines this module as the client

%%% convenience method for startup
start_link(ChatNode) ->
  gen_server:start_link({local, ?CLIENT}, ?MODULE, ChatNode, []).

init(ChatNode)->
  io:format("Chat node is: ~p~n", [ChatNode]),
  {ok, #state{chat_node=ChatNode, profile=[]}}.

%% 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

handle_call(get_chat_node, _From, State) ->
  {reply, State#state.chat_node, State};

handle_call(get_profile, _From, State) ->
  {reply, State#state.profile, State};

handle_call({set_profile, Key, Value}, _From, State) ->
  case lists:keymember(Key, 1, State#state.profile) of
    true -> NewProfile = lists:keyreplace(Key, 1, State#state.profile,
      {Key, Value});
    false -> NewProfile = [{Key, Value} | State#state.profile]
  end,
  {reply, NewProfile,
    #state{chat_node = State#state.chat_node, profile=NewProfile}};

handle_call({login, UserName}, _From, State) ->
  Reply = gen_server:call({chatroom, State#state.chat_node},
    {login, UserName, node()}),
  {reply, Reply, State};

handle_call({say, Text}, _From, State) ->
  Reply = gen_server:call({chatroom, State#state.chat_node},
    {say, Text}),
  {reply, Reply, State};

handle_call(logout, _From, State) ->
  Reply = gen_server:call({chatroom, State#state.chat_node}, logout),
  {reply, Reply, State};

handle_call(_, _From, State) -> {ok, [], State}.

handle_cast({message, {FromUser, FromServer}, Text}, State) ->
  io:format("~s (~p) says: ~p~n", [FromUser, FromServer, Text]),
  {noreply, State};

handle_cast(_Request, State) ->
  io:format("Unknown request ~p~n", _Request),
  {noReply, State}.

handle_info(Info, State) ->
  io:format("Received unexpected message: ~p~n", [Info]),
  {noreply, State}.

terminate(_Reason, _State) ->
  ok.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.


% internal functions

%% @doc Gets the name of the chat host. This is a really
%% ugly hack; it works by sending itself a call to retrieve
%% the chat node name from the server state.

get_chat_node() ->
  gen_server:call(person, get_chat_node).

%% @doc Login to a server using a name
%% If you connect, tell the server your user name and node.
%% You don't need a reply from the server for this.

-spec(login(string()) -> term()).

login(UserName) ->
  if
    is_atom(UserName) ->
      gen_server:call(?CLIENT,
        {login, atom_to_list(UserName)});
    is_list(UserName) ->
      gen_server:call(?CLIENT,
        {login, UserName});
    true ->
      {error, "User name must be an atom or a list"}
  end.


%% @doc Log out of the system. The person server will send a From that tells
%% who is logging out; the chatroom server doesn't need to reply.

-spec(logout() -> atom()).

logout() ->
  gen_server:call(?CLIENT, logout),
  ok.


%% @doc Send the given Text to the chat room server. No reply needed.

-spec(say(string()) -> atom()).

say(Text) ->
  gen_server:call(?CLIENT, {say, Text}),
  ok.

%% @doc Ask chat room server for a list of users.

-spec(users() -> [string()]).

users() ->
  gen_server:call({chatroom, get_chat_node()}, users).

%% @doc Ask chat room server for a profile of a given person.

-spec(who(string(), atom()) -> [tuple()]).

who(Person, ServerRef) ->
  gen_server:call({chatroom, get_chat_node()},
    {who, Person, ServerRef}).

%% @doc Update profile with a key/value pair.

-spec(set_profile(atom(), term()) -> term()).

set_profile(Key, Value) ->
  % ask *this* server for the current state
  NewProfile = gen_server:call(?CLIENT, {set_profile, Key, Value}),
  {ok, NewProfile}.