'''
Calculadora de logaritmos mediante la serie ln(x)=2*suma(1/(2*n+1)*((x-1)/(x+1))**(2*n+1)) desde n=0:

2*(1/(2*0+1)*((x-1)/(x+1))**(2*0+1) + 1/(2*1+1)*((x-1)/(x+1))**(2*1+1) + ...)
2*((x-1)/(x+1) + 1/3*((x-1)/(x+1))**3 + ...)

Si y=(x-1)/(x+1): 2*(y + 1/3*y**3 + ...)

Se aplica la equivalencia logb(x)=ln(x)/ln(b) - logb es logaritmo base b

'''
from math import log # para comparar resultados con función predefinida en módulo math

def ln_s(x):
    '''Devuelve ln(x) calculado en forma repetitiva'''

    # Cálculo de la serie
    y, n = (x-1)/(x+1), 0
    potencia = y
    término = potencia # primer término de la serie
    suma = término # valor inicial de l serie igual al primer término
    
    while abs(término)>=1e-12: # se pretende una precisión de 12 cifras
        n += 1
        potencia *= y*y # el exponente de la potencia de cada término es igual al del término previo más 2
        término = potencia/(2*n+1) # siguiente término de la serie
        suma += término

    print(f'Se calcularon {n+1} términos de la serie para obtener ln({x}) en forma repetitiva')

    return 2.0 * suma

def ln_s_opt(x):
    '''Devuelve ln(x) calculado en forma repetitiva'''
    # Reducción de argumento a valores cercanos a 1.0 para minimizar cálculo de términos de la serie
    factor = 2.0 # valor inicial para mulotiplicar el resultado de la serie, según definición
    while x<0.5 or x>1.5: # se puede reducir x para calcular ln(x)=2*ln(x**(1/2))
        factor *= 2.0
        x = x**0.5

    # Cálculo de la serie
    y, n = (x-1)/(x+1), 0
    potencia = y
    término = potencia # primer término de la serie
    suma = término # valor inicial de l serie igual al primer término
    
    while abs(término)>=1e-12: # se pretende una precisión de 12 cifras
        n += 1
        potencia *= y*y # el exponente de la potencia de cada término es igual al del término previo más 2
        término = potencia/(2*n+1) # siguiente término de la serie
        suma += término

    print(f'Se calcularon {n+1} términos de la serie para obtener {factor/2}*ln({x}) en forma repetitiva optimizada')

    return factor * suma

def pot(x, n):
    '''Calcula x**n para n entero, en forma recursiva'''
    if n>1:
        mn = n//2 # mitad de n
        mp = pot(x, mn) # mitad de potencia
        if n%2==0: return mp*mp
        else: return x*mp*mp
    elif n==1: return x
    elif n==0: return 1

def ln_r(x, n=0):
    '''Devuelve ln(x) calculado en forma recursiva optimizada para reducir argumentos entre 0.5 y 2.0
    Se aplica la equivalencia ln(x**n)=n*ln(x): ln(x)=ln((x**0.5)**2)=2*ln(x**0.5)'''
    if x<0.5 or x>1.5: return 2.0*ln_r(x**0.5)
    else: 
        v = 2*n+1
        término = 2.0 * pot((x-1)/(x+1), v) / v
        if abs(término)>=1e-12: return término + ln_r(x, n+1) # se pretende una precisión de 12 cifras
        else:
            print(f'Se calcularon {n+1} términos de la serie para obtener ln({x}) en forma recursiva')
            return término
    
# PROGRAMA PRINCIPAL
print('CALCULADORA DE LOGARITMOS')
print('''\nSe calculan logaritmos con series numéricas en forma repetitiva y en forma recursiva con precisión de 12 cifras,
y con la función definida en Python. Se muestran resultados con 15 cifras fraccionarias para compararlos.''')

x = float(input('\nIngrese el número cuyo logaritmo desea obtener: '))
while x!=0.0:
    b = float(input('Ingrese la base del logaritmo a calcular, o 0 para ln: '))
    print()
    if b==0.0:
        print(f'{ln_s(x):.15f} es el valor calculado en forma repetitiva\n')
        print(f'{ln_s_opt(x):.15f} es el valor calculado en forma repetitiva optimizada\n')
        print(f'{ln_r(x):.15f} es el valor calculado en forma recursiva\n')
        print(f'{log(x):.15f} es el valor según la función definida en Python')
    else:
        print(f'{ln_s(x)/ln_s(b):.15f} es el valor calculado en forma repetitiva\n')
        print(f'{ln_s_opt(x)/ln_s_opt(b):.15f} es el valor calculado en forma repetitiva optimizada\n')
        print(f'{ln_r(x)/ln_r(b):.15f} es el valor calculado en forma recursiva\n')
        print(f'{log(x, b):.15f} es el valor según la función definida en Python')
    x = float(input('\n\nIngrese el número cuyo logaritmo desea obtener o 0 para terminar: '))
