diff --git a/lib/statisaur.ex b/lib/statisaur.ex index 01541f8..4fa29de 100644 --- a/lib/statisaur.ex +++ b/lib/statisaur.ex @@ -10,11 +10,37 @@ defmodule Statisaur do ### Examples iex>Statisaur.min([1,2,3]) - 1 + {:ok, 1} iex>Statisaur.min([5,0.5,2,3]) - 0.5 + {:ok, 0.5} + + Statisaur will return an error tuple for empty lists. + iex>Statisaur.min([]) + {:error, "argument must be nonempty list of numbers"} """ def min(list) when is_list(list) and length(list) > 1 do + response = do_min(list) + {:ok, response} + end + + def min(_list) do + {:error, "argument must be nonempty list of numbers"} + end + + @doc """ + Same as `min/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def min!(list) do + case min(list) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end + + defp do_min(list) when is_list(list) and length(list) > 1 do Enum.min(list) end @@ -22,11 +48,37 @@ defmodule Statisaur do Calculate the largest value from a list of numbers. ### Examples iex>Statisaur.max([1,2,3]) - 3 + {:ok, 3} iex>Statisaur.max([5.1,0.5,2,3]) - 5.1 + {:ok, 5.1} + + Statisaur returns an error tuple with empty lists. + iex>Statisaur.max([]) + {:error, "argument must be nonempty list of numbers"} """ def max(list) when is_list(list) and length(list) > 1 do + response = do_max(list) + {:ok, response} + end + + def max(_list) do + {:error, "argument must be nonempty list of numbers"} + end + + @doc """ + Same as `max/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def max!(list) do + case max(list) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end + + defp do_max(list) when is_list(list) and length(list) > 1 do Enum.max(list) end @@ -35,50 +87,119 @@ defmodule Statisaur do ### Examples iex>Statisaur.sum([1,3,5,7,9]) - 25 + {:ok, 25} iex>Statisaur.sum([1,1]) - 2 + {:ok, 2} iex>Statisaur.sum([0.5,0.5]) - 1.0 + {:ok, 1.0} + iex> Statisaur.sum(:banana) + {:error, "argument must be list of numbers"} + + """ + @spec sum([number]) :: {:ok, number} | {:error, String.t} + def sum(list = [number | _tail]) when is_list(list) and is_number(number) do + try do + result = Enum.sum(list) + {:ok, result} + rescue + ArithmeticError -> {:error, "argument must be list of numbers"} + end + end + def sum(_not_list), do: {:error, "argument must be list of numbers"} + + @doc """ + Same as `sum/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. """ - def sum(list) when is_list(list), do: Enum.sum(list) + + def sum!(list) do + case sum(list) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end + @doc """ Calculate the mean from a list of numbers ### Examples iex>Statisaur.mean([1,3,5,7,9]) - 5.0 + {:ok, 5.0} iex>Statisaur.mean([0.1,0.2,0.6]) - 0.3 + {:ok, 0.3} """ - def mean(list) when is_list(list), do: sum(list)/length(list) + @spec mean([number]) :: {:ok, float} | {:error, String.t} + def mean(list) when is_list(list) do + with {:ok, result} <- sum(list) do + {:ok, result/length(list)} + else + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `mean/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + + def mean!(list) do + case mean(list) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end @doc """ Calculate the median from a list of numbers ### Examples iex>Statisaur.median([1,3,5,7,9]) - 5 + {:ok, 5} iex>Statisaur.median([1,1]) - 1.0 + {:ok, 1.0} iex>Statisaur.median([0.1,0.4,0.6,0.9]) - 0.5 + {:ok, 0.5} """ - def median(list) when is_list(list) do + @spec median([number]) :: {:ok, number} | {:error, String.t} + def median(list = [number | _tail]) when is_list(list) and is_number(number) do n = length(list) sorted = list |> Enum.sort pivot = round(n/2) - case rem(n,2) do + result = case rem(n,2) do 0 -> a = sorted |> Enum.at(pivot) b = sorted |> Enum.at(pivot - 1) (a+b)/2 # median for an even-sized set is the mean of the middle numbers _ -> sorted |> Enum.at(round(Float.floor(n/2))) # this seems weird, but Float floor yields float not int :() end + {:ok, result} + end + + def median(_list) do + {:error, "argument must be nonempty list of numbers"} + end + + + @doc """ + Same as `median/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def median!(list) do + case median(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -86,15 +207,33 @@ defmodule Statisaur do ### Examples iex>Statisaur.frequencies([1]) - [{1,1}] + {:ok, [{1,1}]} iex>Statisaur.frequencies([1,2,2,3]) - [{1,1},{2,2},{3,1}] + {:ok, [{1,1},{2,2},{3,1}]} """ def frequencies(list) when is_list(list) do sorted = list |> Enum.sort vals = sorted |> Enum.uniq freqs = vals |> Enum.map( fn(v) -> Enum.count(sorted, fn(x)-> x == v end) end) - Enum.zip(vals,freqs) + result = Enum.zip(vals,freqs) + {:ok, result} + end + + def frequencies(_list) do + {:error, "Argument must be an enumerable"} + end + + @doc """ + Same as `frequencies/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def frequencies!(list) do + case frequencies(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -102,15 +241,33 @@ defmodule Statisaur do ### Examples iex>Statisaur.mode([1,1,2,3]) - [1] + {:ok, [1]} iex>Statisaur.mode([1.0,2.0,2.0,3.0,3.0]) - [2.0,3.0] + {:ok, [2.0,3.0]} """ - def mode(list) when is_list(list) and length(list) > 0 do - freqs = frequencies(list) + def mode(list = [number | _tail]) when is_list(list) and is_number(number) and length(list) > 0 do + {:ok, freqs} = frequencies(list) sorted_freqs = freqs |> Enum.sort_by(fn({_,f})->f end, &>=/2) {_, mode_guess} = sorted_freqs |> Enum.at(0) - sorted_freqs |> Enum.filter( fn({_,f})-> f >= mode_guess end) |> Enum.map( fn({v,_})->v end) + result = sorted_freqs |> Enum.filter( fn({_,f})-> f >= mode_guess end) |> Enum.map( fn({v,_})->v end) + {:ok, result} + end + + def mode(_list) do + {:error, "argument must be nonempty list of numbers"} + end + + @doc """ + Same as `mode/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def mode!(list) do + case mode(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -118,10 +275,33 @@ defmodule Statisaur do ### Examples iex>Statisaur.powered_error( [1,2,3], 2, 1) - [-1.0,0.0,1.0] + {:ok, [-1.0,0.0,1.0]} """ - def powered_error( list, reference, k) when is_list(list) and length(list) > 1 do - list |> Enum.map( fn(x) -> :math.pow( x - reference, k) end ) + def powered_error(list = [number | _tail], reference, k) when is_list(list) and length(list) > 1 and is_number(number) + and is_number(reference) and is_number(k) do + try do + result = list |> Enum.map( fn(x) -> :math.pow( x - reference, k) end ) + {:ok, result} + rescue + ArgumentError -> {:error, "argument must be nonempty list of numbers, a number, and a number"} + end + end + + def powered_error(_list, _ref, _k) do + {:error, "argument must be nonempty list of numbers, a number, and a number"} + end + + @doc """ + Same as `powered_error/3`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def powered_error!(list, ref, k) do + case powered_error(list, ref, k) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -129,29 +309,66 @@ defmodule Statisaur do ### Examples iex>Statisaur.variance([1,3,5,7,9]) - 10.0 + {:ok, 10.0} iex>Statisaur.variance([0.1,0.2,0.6]) - 0.06999999999999999 + {:ok, 0.06999999999999999} """ - def variance(list) when is_list(list) and length(list) > 1 do - mu = mean(list) - diffmeans = list |> powered_error( mu, 2 ) |> Enum.sum - df = length(list) - 1 - diffmeans/df + def variance(list) when is_list(list) and length(list) > 1 do + with {:ok, mu} <- mean(list), + {:ok, pe} <- powered_error(list, mu, 2), + diffmeans <- Enum.sum(pe), + df <- length(list) - 1, + result <- diffmeans/df + do {:ok, result} + else {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `variance/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def variance!(list) do + case variance(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ Calculates the standard deviation from a list of numbers. ### Examples - iex>Statisaur.stddev([1,3,5,7,9]) |> Float.round(6) + iex>with {:ok, result} <- Statisaur.stddev([1,3,5,7,9]), do: Float.round(result, 6) 3.162278 - iex>Statisaur.stddev([0.1,0.2,0.6]) |> Float.round(6) + iex>with {:ok, result} <- Statisaur.stddev([0.1,0.2,0.6]), do: Float.round(result, 6) 0.264575 """ def stddev(list) when is_list(list) and length(list) > 1 do - list |> variance |> :math.sqrt + with {:ok, var} <- variance(list), + result <- :math.sqrt(var) + do {:ok, result} + else + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `stddev/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def stddev!(list) do + case stddev(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -159,17 +376,37 @@ defmodule Statisaur do ###Examples iex>Statisaur.raw_moment([1,2,3,4],1) - 2.5 + {:ok, 2.5} iex>Statisaur.raw_moment([1,2,3,4],2) - 7.5 + {:ok, 7.5} iex>Statisaur.raw_moment([1,2,3,4],3) - 25.0 + {:ok, 25.0} iex>Statisaur.raw_moment([1,2,3,4],4) - 88.5 + {:ok, 88.5} """ def raw_moment(list, k) when is_list(list) and length(list) > 1 do + try do count = length(list) - (list |> Enum.map( fn(x) -> :math.pow( x, k) end ) |> Enum.sum) / count + result = (list |> Enum.map( fn(x) -> :math.pow( x, k) end ) |> Enum.sum) / count + {:ok, result} + rescue + ArithmeticError -> {:error, "argument must be list of numbers and an integer"} + ArgumentError -> {:error, "argument must be list of numbers and an integer"} + end + end + + + @doc """ + Same as `raw_moment/2`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def raw_moment!(list, k) do + case raw_moment(list, k) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -177,18 +414,37 @@ defmodule Statisaur do ###Examples iex>Statisaur.central_moment([1,2,3,4],1) - 0.0 + {:ok, 0.0} iex>Statisaur.central_moment([1,2,3,4],2) - 1.25 + {:ok, 1.25} iex>Statisaur.central_moment([1,2,3,4],3) - 0.0 - iex>Statisaur.central_moment([1,2,3,4],4) |> Float.round(4) - 2.5625 + {:ok, 0.0} + iex>with {:ok, moment} <- Statisaur.central_moment([1,2,3,4],4), do: Float.round(moment, 4) + 2.5625 """ def central_moment(list, k) when is_list(list) and length(list) > 1 do - count = length(list) - mu = mean(list) - (list |> powered_error(mu, k) |> Enum.sum) / count + with count <- length(list), + {:ok, mu} <- mean(list), + {:ok, pe} <- powered_error(list, mu, k) + do + {:ok, Enum.sum(pe) / count} + else + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `central_moment/2`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def central_moment!(list, k) do + case central_moment(list, k) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -196,19 +452,40 @@ defmodule Statisaur do ###Examples iex>Statisaur.standardized_moment([1,2,3,4],1) - 0.0 + {:ok, 0.0} iex>Statisaur.standardized_moment([1,2,3,4],2) - 1.0 + {:ok, 1.0} iex>Statisaur.standardized_moment([1,2,3,4],3) - 0.0 + {:ok, 0.0} iex>Statisaur.standardized_moment([1,2,3,4],4) - 1.64 + {:ok, 1.64} + """ + def standardized_moment(list, k) when is_list(list) and length(list) > 1 do + with {:ok, m1} <- raw_moment(list, 1), + {:ok, pe} <- powered_error(list, m1, k), + len <- length(list), + num <- Enum.sum(pe) / len, + {:ok, pe2} <- powered_error(list, m1, 2), + denom <- :math.pow( (Enum.sum(pe2)/len), k/2 ) + do + {:ok, num/denom} + else + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `standardized_moment/2`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. """ - def standardized_moment(list, k) when is_list(list) and length(list) > 1 do - m1 = raw_moment(list,1) - num = (powered_error(list, m1, k) |> Enum.sum) / length(list) - denom = :math.pow( (powered_error(list, m1, 2) |> Enum.sum)/length(list), k/2) - num/denom + def standardized_moment!(list, k) do + case central_moment(list, k) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -216,10 +493,28 @@ defmodule Statisaur do ###Examples iex>Statisaur.skewness([1,2,3,4]) - 0.0 + {:ok, 0.0} """ def skewness(list) when is_list(list) and length(list) > 1 do - standardized_moment(list,3) + case standardized_moment(list,3) do + {:ok, result} -> + {:ok, result} + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `skewness/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def skewness!(list) do + case standardized_moment(list,3) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ @@ -227,20 +522,60 @@ defmodule Statisaur do ###Examples iex>Statisaur.kurtosis([1,2,3,4]) - 1.64 + {:ok, 1.64} """ def kurtosis(list) when is_list(list) and length(list) > 1 do - standardized_moment(list,4) + case standardized_moment(list,4) do + {:ok, result} -> + {:ok, result} + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Same as `kurtosis/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def kurtosis!(list) when is_list(list) and length(list) > 1 do + case standardized_moment(list,4) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc """ Calculates the coefficient of variation for a list of numbers. ###Examples - iex>Statisaur.coefficient_of_variation([1,2,3,4]) |> Float.round(4) + iex>with {:ok, result} <- Statisaur.coefficient_of_variation([1,2,3,4]), do: Float.round(result, 4) 0.5164 """ def coefficient_of_variation(list) when is_list(list) and length(list) > 1 do - stddev(list) / mean(list) + with {:ok, dev} <- stddev(list), + {:ok, me } <- mean(list) + do + {:ok, dev/me} + else + {:error, reason} -> + {:error, reason} + end end + + + @doc """ + Same as `coefficient_of_variation/1`, but but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + def coefficient_of_variation!(list) do + case coefficient_of_variation(list) do + {:ok, result} -> + result + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end + end diff --git a/lib/statisaur/bivariate.ex b/lib/statisaur/bivariate.ex index e1aef25..1f1c55b 100644 --- a/lib/statisaur/bivariate.ex +++ b/lib/statisaur/bivariate.ex @@ -13,8 +13,8 @@ defmodule Statisaur.Bivariate do """ def covariance(list1, list2) when is_list(list1) and is_list(list2) and length(list1) == length(list2) do n = length(list1) - mu_x = Statisaur.mean(list1) - mu_y = Statisaur.mean(list2) + {:ok, mu_x} = Statisaur.mean(list1) + {:ok, mu_y} = Statisaur.mean(list2) numerator = Enum.zip(list1, list2) |> Enum.map( fn {x, y} -> (x - mu_x) * (y + mu_y) end ) |> Enum.sum() @@ -31,14 +31,15 @@ defmodule Statisaur.Bivariate do {2, {21.244330227661493, 0.08711964265011925}} """ def simple_linear_regression(list1,list2) do - m1 = Statisaur.mean(list1) - m2 = Statisaur.mean(list2) - b1_num_p1 = Statisaur.powered_error(list1, m1, 1) - b1_num_p2 = Statisaur.powered_error(list2, m2, 1) - b1_num = Statisaur.sum(Enum.map(Enum.zip(b1_num_p1, b1_num_p2), fn({x, y})-> x*y end)) - b1_denom = Statisaur.sum( Statisaur.powered_error(list1, m1, 2)) + {:ok, m1} = Statisaur.mean(list1) + {:ok, m2} = Statisaur.mean(list2) + {:ok, b1_num_p1} = Statisaur.powered_error(list1, m1, 1) + {:ok, b1_num_p2} = Statisaur.powered_error(list2, m2, 1) + {:ok, b1_num} = Statisaur.sum(Enum.map(Enum.zip(b1_num_p1, b1_num_p2), fn({x, y})-> x*y end)) + {:ok, list1_err} = Statisaur.powered_error(list1, m1, 2) + {:ok, b1_denom} = Statisaur.sum( list1_err ) b1 = b1_num/b1_denom - b0 = Statisaur.mean(list2) - b1 * Statisaur.mean(list1) + b0 = m2 - b1 * m1 b1 = b1 {2, {b0, b1}} end @@ -73,8 +74,8 @@ defmodule Statisaur.Bivariate do end def pearson_correlation(list1, list2) when is_list(list1) and is_list(list2) and length(list1) != 0 and length(list2)!=0 and length(list1) == length(list2) do covXY = covariance(list1, list2) - sigmaX = Statisaur.stddev(list1) - sigmaY = Statisaur.stddev(list2) + {:ok, sigmaX} = Statisaur.stddev(list1) + {:ok, sigmaY} = Statisaur.stddev(list2) # check that the std. deviations are nonzero @@ -118,13 +119,13 @@ defmodule Statisaur.Bivariate do def pooled_stddev( list1, list2 ) when is_list(list1) and is_list(list2) and length(list1) > 0 and length(list2) > 0 do - mu1 = Statisaur.mean(list1) - mu2 = Statisaur.mean(list2) - err_list1 = Statisaur.powered_error(list1, mu1, 2) - err_list2 = Statisaur.powered_error(list2, mu2, 2) + {:ok, mu1} = Statisaur.mean(list1) + {:ok, mu2} = Statisaur.mean(list2) + {:ok, err_list1} = Statisaur.powered_error(list1, mu1, 2) + {:ok, err_list2} = Statisaur.powered_error(list2, mu2, 2) - sum_err_1 = Statisaur.sum(err_list1) - sum_err_2 = Statisaur.sum(err_list2) + {:ok, sum_err_1} = Statisaur.sum(err_list1) + {:ok, sum_err_2} = Statisaur.sum(err_list2) (sum_err_1 + sum_err_2) / ( length(list1) + length(list2) - 2) end @@ -163,8 +164,8 @@ defmodule Statisaur.Bivariate do def pooled_stderr( list1, list2 ) when is_list(list1) and is_list(list2) and length(list1) > 0 and length(list2) > 0 do - v1 = Statisaur.variance(list1) - v2 = Statisaur.variance(list2) + {:ok, v1} = Statisaur.variance(list1) + {:ok, v2} = Statisaur.variance(list2) n1 = length(list1) n2 = length(list2) @@ -207,8 +208,8 @@ defmodule Statisaur.Bivariate do raise ArgumentError, "arguments have insufficient degrees of freedom" end def t_score(list1, list2) do - mu1 = Statisaur.mean(list1) - mu2 = Statisaur.mean(list2) + {:ok, mu1} = Statisaur.mean(list1) + {:ok, mu2} = Statisaur.mean(list2) std_err = pooled_stderr(list1, list2) (mu1 - mu2)/ ( std_err ) diff --git a/lib/statisaur/combinatorics.ex b/lib/statisaur/combinatorics.ex index 9ed6317..9241126 100644 --- a/lib/statisaur/combinatorics.ex +++ b/lib/statisaur/combinatorics.ex @@ -13,28 +13,47 @@ defmodule Statisaur.Combinatorics do The factorial of 5 is (5 * 4 * 3 * 2 * 1), or 120. iex(1)> Statisaur.Combinatorics.factorial(5) - 120 + {:ok, 120} The factorial of 0 is 1, according to the convention for an empty product. iex(2)> Statisaur.Combinatorics.factorial(0) - 1 + {:ok, 1} - Statisaur will raise an error in the case of negative integers. - iex(3)> Statisaur.Combinatorics.factorial(-5) - ** (ArgumentError) argument must be positive integer + Statisaur will return an error tuple in the case of negative integers. + iex(3)> Statisaur.Combinatorics.factorial(-5) + {:error, "argument must be positive integer"} """ - @spec factorial(non_neg_integer) :: non_neg_integer - def factorial(0) do - 1 + @spec factorial(non_neg_integer) :: {:ok, non_neg_integer} | {:error, String.t} + def factorial(n) when is_integer(n) and n >= 0 do + response = do_factorial(n) + {:ok, response} end - def factorial(n) when is_integer(n) and n > 0 do - Enum.reduce((1..n), 1, fn(x, acc) -> x * acc end) + def factorial(_term) do + {:error, "argument must be positive integer"} end - def factorial(_term) do - raise ArgumentError, "argument must be positive integer" + @doc """ + Same as `factorial/1` but returns the integer directly, or + throws `ArgumentError` if an error is returned. + """ + @spec factorial(non_neg_integer) :: non_neg_integer | no_return + def factorial!(n) do + case factorial(n) do + {:ok, integer} -> + integer + {:error, reason} -> + raise ArgumentError, "#{reason}" + end + end + + defp do_factorial(0) do + 1 + end + + defp do_factorial(n) when is_integer(n) and n > 0 do + Enum.reduce((1..n), 1, fn(x, acc) -> x * acc end) end @doc ~S""" @@ -45,19 +64,42 @@ defmodule Statisaur.Combinatorics do ### Examples The number of outcomes of two from five possibilities is ten. iex(1)> Statisaur.Combinatorics.n_choose_k(5, 2) - 10 + {:ok, 10} The number of outcomes of eight from twenty possibilities is 125970. iex(2)> Statisaur.Combinatorics.n_choose_k(20, 8) - 125970 + {:ok, 125970} """ - @spec n_choose_k(non_neg_integer, non_neg_integer) :: non_neg_integer + @spec n_choose_k(non_neg_integer, non_neg_integer) :: {:ok, non_neg_integer} | {:error, String.t} def n_choose_k(n, k) when (n >= k) and (k >= 0) and is_integer(n) and is_integer(k) do - div(falling_factorial(n, k), factorial(k)) + with {:ok, fact_k} <- factorial(k), + {:ok, fallen_fact} <- falling_factorial(n, k), + response <- div(fallen_fact, fact_k) + do + {:ok, response} + else + {:error, reason} -> + {:error, reason} + end end def n_choose_k(_n, _k) do - raise ArgumentError, "arguments must be positive integers" + {:error, "arguments must be positive integers"} + end + + + @doc """ + Same as `n_choose_k/1` but returns the integer directly, or + throws `ArgumentError` if an error is returned. + """ + @spec n_choose_k(non_neg_integer, non_neg_integer) :: non_neg_integer | no_return + def n_choose_k!(n, k) do + case n_choose_k(n, k) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end @doc ~S""" @@ -69,53 +111,74 @@ defmodule Statisaur.Combinatorics do ### Examples The falling factorial of (5, 4) would be `5 * 4 * 3 * 2` iex(1)> Statisaur.Combinatorics.falling_factorial(5, 4) - 120 + {:ok, 120} The falling factorial of (5, 3) would be `5 * 4 * 3` iex(2)> Statisaur.Combinatorics.falling_factorial(5, 3) - 60 + {:ok, 60} The falling factorial of (5, 2) would be `5 * 4` iex(3)> Statisaur.Combinatorics.falling_factorial(5, 2) - 20 + {:ok, 20} With a second argument of `1`, the return value is simply the first argument iex(4)> Statisaur.Combinatorics.falling_factorial(5, 1) - 5 + {:ok, 5} Listing the second input as `0` returns 1 iex(5)> Statisaur.Combinatorics.falling_factorial(5, 0) - 1 + {:ok, 1} The return value is `0` when the second argument is larger than the first, and both arguments are positive iex(6)> Statisaur.Combinatorics.falling_factorial(5, 7) - 0 + {:ok, 0} When the second argument is negative, the function returns positive values between 1 and 0 iex(7)> Statisaur.Combinatorics.falling_factorial(1, -2) - 0.16666666666666666 + {:ok, 0.16666666666666666} - When the both arguments are negative, the function raises an ArithmeticError. + When the both arguments are negative, the function returns an error. """ - @spec falling_factorial(integer, integer) :: integer | float - def falling_factorial(n, 0) when is_integer(n) do - 1 + @spec falling_factorial(integer, integer) :: {:ok, integer} | {:ok, float} | {:error, String.t} + def falling_factorial(n, k) when is_integer(n) and is_integer(k) do + response = do_falling_factorial(n,k) + {:ok, response} end - def falling_factorial(n, k) when is_integer(n) and is_integer(k) and k < 0 do - m = abs(k) - 1 / falling_factorial((n+m), m) + def falling_factorial(_n, _k) do + {:error, "arguments must be integers"} end - def falling_factorial(n, k) when is_integer(n) and is_integer(k) do - Enum.reduce(n..(n-(k-1)), 1, fn(x, acc) -> x * acc end) + + @doc """ + Same as `falling_factorial/2` but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + @spec falling_factorial!(integer, integer) :: integer | float | no_return + def falling_factorial!(n, k) do + case falling_factorial(n, k) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end - def falling_factorial(_n, _k) do - raise ArgumentError, "arguments must be integers" + + @spec do_falling_factorial(integer, integer) :: integer | float + defp do_falling_factorial(n, 0) when is_integer(n) do + 1 + end + + defp do_falling_factorial(n, k) when is_integer(n) and is_integer(k) and k < 0 do + m = abs(k) + 1 / do_falling_factorial((n+m), m) end + defp do_falling_factorial(n, k) when is_integer(n) and is_integer(k) do + Enum.reduce(n..(n-(k-1)), 1, fn(x, acc) -> x * acc end) + end @doc ~S""" The rising factorial, also known as the 'rising sequential product' or 'Pochhammer polynomial', @@ -126,29 +189,48 @@ defmodule Statisaur.Combinatorics do ### Examples `rising_factorial(5, 4)` is equivalent to `5 * (5 + 1) * (5 + 2) * (5 + 3)` iex(1)> Statisaur.Combinatorics.rising_factorial(5, 4) - 1680 + {:ok, 1680} The return value is always `1` when the second argument is `0` iex(2)> Statisaur.Combinatorics.rising_factorial(5, 0) - 1 - - The function raises an `ArithmeticError` when the second argument is negative - """ - @spec rising_factorial(integer, integer) :: integer - def rising_factorial(n, 0) when is_integer(n) do - 1 - end - + {:ok, 1} + """ + @spec rising_factorial(integer, integer) :: {:ok, integer} | {:error, String.t} def rising_factorial(n, k) when is_integer(n) and is_integer(k) and k < 0 do - raise ArithmeticError + {:error, "bad argument in arithmetic expression"} end def rising_factorial(n, k) when is_integer(n) and is_integer(k) do - Enum.reduce(n..(n+(k-1)), 1, fn(x, acc) -> x * acc end) + response = do_rising_factorial(n, k) + {:ok, response} end def rising_factorial(_n, _k) do - raise ArgumentError, "arguments must be integers" + {:error, "arguments must be integers"} + end + + + + @doc """ + Same as `rising_factorial/2` but returns the response directly, or + throws `ArgumentError` if an error is returned. + """ + @spec rising_factorial!(integer, integer) :: integer | no_return + def rising_factorial!(n, k) do + case rising_factorial(n, k) do + {:ok, response} -> + response + {:error, reason} -> + raise ArgumentError, "#{reason}" + end end + @spec do_rising_factorial(integer, integer) :: integer + defp do_rising_factorial(n, 0) when is_integer(n) do + 1 + end + + defp do_rising_factorial(n, k) when is_integer(n) and is_integer(k) do + Enum.reduce(n..(n+(k-1)), 1, fn(x, acc) -> x * acc end) + end end \ No newline at end of file diff --git a/test/combinatorics_test.exs b/test/combinatorics_test.exs index 848689b..9571634 100644 --- a/test/combinatorics_test.exs +++ b/test/combinatorics_test.exs @@ -4,78 +4,102 @@ defmodule CombinatoricsTest do alias Statisaur.Combinatorics test "factorial 5 returns 120" do - assert Combinatorics.factorial(5) == 120 + assert Combinatorics.factorial(5) == {:ok, 120} end test "factorial 0 returns 1" do - assert Combinatorics.factorial(0) == 1 + assert Combinatorics.factorial(0) == {:ok, 1} end - test "factorial raises an error with negative integers" do - assert_raise ArgumentError, fn -> Combinatorics.factorial(-10) end + test "factorial returns error tuple with negative integers" do + assert Combinatorics.factorial(-10) == {:error, "argument must be positive integer"} + end + + test "factorial! raises an error with negative integers" do + assert_raise ArgumentError, fn -> Combinatorics.factorial!(-10) end end test "n_choose_k(4, 2) returns 6" do - assert Combinatorics.n_choose_k(4, 2) == 6 + assert Combinatorics.n_choose_k(4, 2) == {:ok, 6} end test "n_choose_k(15, 8) returns 6435" do - assert Combinatorics.n_choose_k(15, 8) == 6435 + assert Combinatorics.n_choose_k(15, 8) == {:ok, 6435} + end + + test "n_choose_k returns an error tuple with negative integers" do + assert Combinatorics.n_choose_k(10, -5) == {:error, "arguments must be positive integers"} + end + + test "n_choose_k! raises an error with negative integers" do + assert_raise ArgumentError, fn -> Combinatorics.n_choose_k!(10, -5) end end - test "n_choose_k raises an error with negative integers" do - assert_raise ArgumentError, fn -> Combinatorics.n_choose_k(10, -5) end + test "n_choose_k returns an error tuple with non-integer inputs" do + assert Combinatorics.n_choose_k("string", 5) == {:error, "arguments must be positive integers"} end - test "n_choose_k raises an error with non-integer inputs" do - assert_raise ArgumentError, fn -> Combinatorics.n_choose_k("string", 5) end + test "n_choose_k! raises an error with non-integer inputs" do + assert_raise ArgumentError, fn -> Combinatorics.n_choose_k!("string", 5) end end test "falling_factorial with two positive integers" do - assert Combinatorics.falling_factorial(10, 5) == 30240 + assert Combinatorics.falling_factorial(10, 5) == {:ok, 30240} end test "falling_factorial with first argument negative" do - assert Combinatorics.falling_factorial(-10, 5) == -240240 + assert Combinatorics.falling_factorial(-10, 5) == {:ok, -240240} end test "falling_factorial with second argument negative returns float between zero and one" do - assert Combinatorics.falling_factorial(4, -2) == 1/30 + assert Combinatorics.falling_factorial(4, -2) == {:ok, 1/30} end test "falling_factorial with second argument as 0 returns 1" do - assert Combinatorics.falling_factorial(4, 0) == 1 + assert Combinatorics.falling_factorial(4, 0) == {:ok, 1} end - test "falling_factorial with non-integer arguments raises ArgumentError" do - assert_raise ArgumentError, fn -> Combinatorics.falling_factorial(:bad, "info") end + test "falling_factorial with non-integer arguments returns error tuple" do + assert Combinatorics.falling_factorial(:bad, "info") == {:error, "arguments must be integers"} + end + + test "falling_factorial! with non-integer arguments raises ArgumentError" do + assert_raise ArgumentError, fn -> Combinatorics.falling_factorial!(:bad, "info") end end test "rising_factorial with two positive integers" do - assert Combinatorics.rising_factorial(5, 3) == 210 + assert Combinatorics.rising_factorial(5, 3) == {:ok, 210} end test "rising factorial with first argument smaller than second argument" do - assert Combinatorics.rising_factorial(3, 5) == 2520 + assert Combinatorics.rising_factorial(3, 5) == {:ok, 2520} end test "rising_factorial with second argument as 0 returns 1" do - assert Combinatorics.rising_factorial(5, 0) == 1 + assert Combinatorics.rising_factorial(5, 0) == {:ok, 1} end test "rising factorial with first argument as 0 returns zero" do - assert Combinatorics.rising_factorial(0, 5) == 0 + assert Combinatorics.rising_factorial(0, 5) == {:ok, 0} end test "rising_factorial with first argument negative" do - assert Combinatorics.rising_factorial(-5, 3) == -60 + assert Combinatorics.rising_factorial(-5, 3) == {:ok, -60} end test "rising_factorial with second argument negative raises ArithmeticError" do - assert_raise ArithmeticError, fn -> Combinatorics.rising_factorial(5, -3) end + assert Combinatorics.rising_factorial(5, -3) == {:error, "bad argument in arithmetic expression"} end test "rising_factorial with non-integer arguments raises ArgumentError" do - assert_raise ArgumentError, fn -> Combinatorics.rising_factorial("not", [:good]) end + assert Combinatorics.rising_factorial("not", [:good]) == {:error, "arguments must be integers"} + end + + test "rising_factorial! with second argument negative raises ArgumentError" do + assert_raise ArgumentError, fn -> Combinatorics.rising_factorial!(5, -3) end + end + + test "rising_factorial! with non-integer arguments raises ArgumentError" do + assert_raise ArgumentError, fn -> Combinatorics.rising_factorial!("not", [:good]) end end end \ No newline at end of file diff --git a/test/statisaur_test.exs b/test/statisaur_test.exs index 0e64883..5fe9eea 100644 --- a/test/statisaur_test.exs +++ b/test/statisaur_test.exs @@ -6,57 +6,207 @@ defmodule StatisaurTest do @large Enum.to_list(1..10000) @eps 1.0e-4 + @bad_data [:a, :b, :c] test "min([1,1,2,3,5,8]) returns 1" do - assert 1 == Statisaur.min([1,1,2,3,5,8]) + assert {:ok, 1} == Statisaur.min([1,1,2,3,5,8]) + end + + test "min([]) returns error tuple" do + assert {:error, "argument must be nonempty list of numbers"} == Statisaur.min([]) + end + + test "min!([]) raises error" do + assert_raise ArgumentError, fn -> Statisaur.min!([]) end end test "max([1,1,2,3,5,8]) returns 8" do - assert 8 == Statisaur.max([1,1,2,3,5,8]) + assert {:ok, 8} == Statisaur.max([1,1,2,3,5,8]) end + test "max([]) returns error tuple" do + assert {:error, "argument must be nonempty list of numbers"} == Statisaur.max([]) + end + + test "max!([]) raises error" do + assert_raise ArgumentError, fn -> Statisaur.max!([]) end + end + + test "sum([1,3,5,7,9])returns 25" do - assert 25 == Statisaur.sum([1,3,5,7,9]) + assert {:ok, 25} == Statisaur.sum([1,3,5,7,9]) end test "sum([1,1])returns 2" do - assert 2 == Statisaur.sum([1,1]) + assert {:ok, 2} == Statisaur.sum([1,1]) + end + + test "sum/1 returns an error tuple when given non-list argument" do + assert {:error, "argument must be list of numbers"} == Statisaur.sum(:not_a_list) + end + + test "sum/1 returns an error tuple when given list of non-numbers as argument" do + assert {:error, "argument must be list of numbers"} == Statisaur.sum(@bad_data) + end + + test "sum/1 returns an error turble when given list with first argument as number and second argument invalid" do + assert {:error, "argument must be list of numbers"} == Statisaur.sum(@bad_data) + end + + test "sum!([1,3,5,7,9]) returns 25" do + assert 25 == Statisaur.sum!([1,3,5,7,9]) + end + + test "sum! raises an error when given a list of bad data" do + assert_raise ArgumentError, fn -> Statisaur.sum!(@bad_data) end end test "mean([1,3,5,7,9]) returns 5" do - assert 5 == Statisaur.mean([1,3,5,7,9]) + assert {:ok, 5} == Statisaur.mean([1,3,5,7,9]) end test "mean([0.1,0.2,0.6]) returns 0.3" do - assert 0.3 == Statisaur.mean([0.1,0.2,0.6]) + assert {:ok, 0.3} == Statisaur.mean([0.1,0.2,0.6]) end test "mean(@large) returns 5000.5" do - assert 5000.5 == Statisaur.mean(@large) + assert {:ok, 5000.5} == Statisaur.mean(@large) + end + + test "mean returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers"} == Statisaur.mean(@bad_data) + end + + test "mean! rasies an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.mean!(@bad_data) end + end + + test "mean!([1,3,5,7,9]) returns 5 directly" do + assert 5 == Statisaur.mean!([1,3,5,7,9]) end test "median([0.1,0.2,0.6) returns 0.2" do - assert 0.2 == Statisaur.median([0.1,0.2,0.6]) + assert {:ok, 0.2} == Statisaur.median([0.1,0.2,0.6]) end test "median([0.7,0.4,0.6,0.1]) returns 0.5" do - assert 0.5 == Statisaur.median([0.7,0.4,0.6,0.1]) + assert {:ok, 0.5} == Statisaur.median([0.7,0.4,0.6,0.1]) + end + + test "median returns an error tuple when given bad data" do + assert {:error, "argument must be nonempty list of numbers"} == Statisaur.median(@bad_data) + end + + test "median!([9,3,7,5,1]) returns 5 directly" do + assert 5 == Statisaur.median!([9, 3, 7, 5, 1]) + end + + test "median! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.median!(@bad_data) end + end + + test "powered_error returns error tuple with bad data" do + assert {:error, "argument must be nonempty list of numbers, a number, and a number"} + == Statisaur.powered_error(@bad_data, 2, 1) + end + + test "powered_error! raises an error with bad data" do + assert_raise ArgumentError, fn -> Statisaur.powered_error!(@bad_data, 2, 1) end end test "variance([0.1,0.2,0.6]) returns 0.06999999999999999" do - assert 0.06999999999999999 == Statisaur.variance([0.1,0.2,0.6]) + assert {:ok, 0.06999999999999999} == Statisaur.variance([0.1,0.2,0.6]) end test "variance(@large) returns 8334166.666666667" do - assert 8334166.666666667 == Statisaur.variance(@large) + assert {:ok, 8334166.666666667} == Statisaur.variance(@large) + end + + test "variance returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers"} == Statisaur.variance(@bad_data) + end + + test "variance!(@large) returns 8334166.666666667 directly" do + assert 8334166.666666667 == Statisaur.variance!(@large) + end + + test "variance! raises an exception when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.variance!(@bad_data) end end test "stddev([0.1,0.2,0.6]) returns 0.2645751" do - assert_in_delta( 0.2645751, Statisaur.stddev([0.1,0.2,0.6]), @eps ) + {:ok, dev} = Statisaur.stddev([0.1,0.2,0.6]) + assert_in_delta( 0.2645751, dev, @eps ) end test "stddev(@large) returns 2886.895679" do - assert_in_delta(2886.895679, Statisaur.stddev(@large), @eps ) + {:ok, dev} = Statisaur.stddev(@large) + assert_in_delta(2886.895679, dev, @eps) + end + + test "stddev returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers"} == Statisaur.stddev(@bad_data) + end + + test "stddev!([0.1,0.2,0.6]) returns 0.2645751 directly" do + dev = Statisaur.stddev!([0.1,0.2,0.6]) + assert_in_delta( 0.2645751, dev, @eps ) + end + + test "stddev!(@large) returns 2886.895679 directly" do + dev = Statisaur.stddev!(@large) + assert_in_delta(2886.895679, dev, @eps) + end + + test "stddev! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.stddev!(@bad_data) end + end + + test "raw_moment returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers and an integer"} == Statisaur.raw_moment(@bad_data, 6) + end + + test "raw_moment! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.raw_moment!(@bad_data, 5) end + end + + test "central_moment returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers"} == Statisaur.central_moment(@bad_data, 8) end + test "central_moment! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.central_moment!(@bad_data, 8) end + end + + test "standardized_moment returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers and an integer"} == Statisaur.standardized_moment(@bad_data, 8) + end + + test "standardized_moment! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.standardized_moment!(@bad_data, 8) end + end + + test "skewness returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers and an integer"} == Statisaur.skewness(@bad_data) + end + + test "skewness! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.skewness!(@bad_data) end + end + + test "kurtosis returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers and an integer"} == Statisaur.kurtosis(@bad_data) + end + + test "kurtosis! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.kurtosis!(@bad_data) end + end + + test "coefficient_of_variation returns an error tuple when given bad data" do + assert {:error, "argument must be list of numbers"} == Statisaur.coefficient_of_variation(@bad_data) + end + + test "coefficient_of_variation! raises an error when given bad data" do + assert_raise ArgumentError, fn -> Statisaur.coefficient_of_variation!(@bad_data) end + end end