#!/usr/bin/ruby

define ℯ = Num.e
define π = Num.pi
define τ = Num.tau
define γ = Num.EulerGamma
define ϕ = Num.phi

# Unknown
# sqrt(2*pi*e) * e^(-((3 - sqrt(3)) / 6)) * (((3 - sqrt(3)) / 6 + n) / e)^(n + 1/2)
func f1(n) {
    define w = ((3 - √3) / 6)
    define d = (√(τ * ℯ) * exp(-w))
    d * pow((n+w) / ℯ, n + 1/2)
}

# S. Ramanujan's formula (simplified)
# sqrt(pi) * (n/e)^n * (n*(4*n*(2*n + 1) + 1))^(1/6)
func f2(n) {
    define d = √π
    d * exp(-n) * n**n * root(n*(4*n*(2*n + 1) + 1), 6)
}

# Stirling's formula + Trizen's approximation
# sqrt(2*pi*n) * (n/e)^n * (digamma(n+1) + gamma) / (log(n + 1/(phi * sqrt(n))) + gamma) / zeta(n)
func f3(n) {
    (√(τ*n) * exp(-n) * n**n * ((digamma(n+1) + γ) / (log(n + 1/(ϕ * √n)) + γ)) / zeta(n))
}

# Stirling's formula
# sqrt(2*pi*n) * (n/e)^n
func f4(n) {
    √(τ * n) * exp(-n) * n**n
}

# Stirling's formula + Laplace's method (1)
# sqrt(2*pi*n) * (n/e)^n * (1 + 1/(12*n))
func f5(n) {
    √(τ * n) * exp(-n) * n**n * (1 + 1/(12*n))
}

# S. Ramanujan (2)
# sqrt(pi) * n^n * e^(-n) * ((8 * n^3) + (4 * n^2) + n + 1/30)^(1/6)
func f6(n) {
    define w = √π
    w * pow(n, n) * exp(-n) * pow(((8 * n**3) + (4 * n**2) + n + 1/30), 1/6)
}

# N. Batir
# sqrt(2*pi) * n^n * e^(-n) * sqrt(n + 1/2) * e^(-(1 / (6 * (n + 3/8))))
func f7(n) {
    define w = √τ
    w * pow(n, n) * exp(-n) * √(n + 1/2) * exp(-(1 / (6 * (n + 3/8))))
}

# N. Batir(2)
# sqrt(2*pi) * n^n * e^(-n) * sqrt(n + 1/6 + 1/(72*n) - 31/(6480 * n^2) - 139/(155520 * n^3) + 9871/(6531840 * n^4))
func f8(n) {
    define w = √τ
    w * pow(n, n) * exp(-n) * √(n + 1/6 + 1/(72*n) - 31/(6480 * n**2) - 139/(155520 * n**3) + 9871/(6531840 * n**4))
}

# N. Batir(2) derivation by Wolfram|Alpha
# 1/216 * sqrt(pi/70) * exp(-n) * n^(n-2) * sqrt(42*n*(24*n*(90*n*(12*n*(6*n + 1) + 1) - 31) - 139) + 9871)
func f9(n) {
    1/216 * √(π/70) * exp(-n) * n**(n-2) * √(42*n*(24*n*(90*n*(12*n*(6*n + 1) + 1) - 31) - 139) + 9871);
}

# Gamma approximation
# n! ~ (1-i) * (-1)^(1/4) * sqrt(pi) * 12^(-n) * exp(-2*n) * sqrt(n) * ((12*e*n^2 + e)/n)^n
# n! ~ sqrt(pi) * 2^(1/2 - 2*n) * exp(-n) * n^(1/2-n) * (4*n^2 + 1/3)^n
func f10(n) {
    sqrt(π) * 2**(1/2 - 2*n) * exp(-n) * n**(1/2 - n) * (4 * n**2 + 1/3)**n
}

# Gamma approximation (2)
# n! ~ limit_{k -> Infinity} sqrt(2*pi) * ((12*n^2 + 24*k*n + 12*k^2 + 1)/(12*e*n + 12*k*e))^(n+k) / ((n+k-1)!/(n!)) / sqrt(n+k)
func f11(n) {
    define k = 50       # higher value of k => higher precision
    sqrt(τ) * ((12*n**2 + (24*k)*n + 12*k**2 + 1)/(12*ℯ*n + 12*k*ℯ))**(n+k) / rising_factorial(n+1, k-1) / sqrt(n+k)
}

# Gamma approximation (3)
# n! ~ (sqrt(2*pi) * ((12*n^2 + 24*n + 12 + 1)/(12*e*n + 12*e))^(n + 1))/sqrt(n + 1)
func f12(n) {
    sqrt(τ) * ((12*n**2 + 24*n + 12 + 1)/(12*ℯ*n + 12*ℯ))**(n+1) / sqrt(n+1)
}

const FORMULAS = [
    [Unknown            => f1],
    [Ramanujan          => f2],
    ["Stirling+Trizen"  => f3],
    [Stirling           => f4],
    ["Stirling+Laplace" => f5],
    [Ramanujan2         => f6],
    [Batir              => f7],
    [Batir2             => f8],
    [Batir2WA           => f9],
    [Gamma              => f10],
    [Gamma2             => f11],
    [Gamma3             => f12],
]

#
## TESTS
#

var report = Hash()
for n in (15..30) {

    for name,f in (FORMULAS) {
        report{name} := [] << f(n)
    }

    report{:EXACT} := [] << n!

    say "\n=> #{n}!"
    for k,v in (report.sort_by {|_,v| v }) {
        "%20s: %s\n".printf(k, v[-1]);
    }
}

func A(a) { a.sum / a.len }
func G(a) { a.prod.root(a.len) }
func H(a) { a.len / a.map{1/_}.sum }

var r1 = Hash()
var r2 = Hash()
var r3 = Hash()

for k,v in (report) {
    k == :EXACT && next;
    var arr = v.range.map {|i| abs(report{:EXACT}[i] - v[i])}
    r1{k} = A(arr);
    r2{k} = G(arr);
    r3{k} = H(arr);
}

func print_report(h) {
    for k,v in (h.sort_by {|_,v| v }) {
        "%20s: %s\n".printf(k, v);
    }
}

say ("\n", '-'*80);

say "\n=>> A <<=";
print_report(r1);

say "\n=>> G <<=";
print_report(r2)

say "\n=>> H <<=";
print_report(r3);

say ("\n", '-'*80);