# 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}.