# -*- coding: utf-8 -*-
"""
Created on Wed Sep 30 12:28:51 2020

@author: jbobowsk
"""

# This tutorial will demonstrate some implementations of nested control
# structures in Python.   Nested structures very quickly become complicated
# and hard to decipher.  Python requires you to indent your code properly
# which is an advantage because it makes the code easier to read/follow.  

# In the while loop tutorial we should how you could calculate factorials
# using while loops.  Suppose you wanted to restrict users to inputing only
# integer values of n.  You can do this by placing your while loop inside
# an if statement.  The if statement will be used to check for valid
# values of n.

# First, I introduce the % function.  y % x outputs the remainder of
# y/x.
x = 5 % 2
print(x)
x = 4 % 2
print(x)

# We can use n % 1 to check that n is an integer.
x = 5.2 % 1
print(x)
x = 5 % 1
print(x)

# So, here is our first nested set of control structures.
n = 4.3
if n % 1 == 0:
    print('n =', n)
    factorial = 1
    while n > 1:
        factorial = factorial*n
        n -= 1
    print('n! =', factorial)
else:
    print('Invalid entry: n is noninteger.')

# Notice that we can still trick the system into producing nonsense by
# entering a negative integer.
n = -4
if n % 1 == 0:
    print('n =', n)
    factorial = 1
    while n > 1:
        factorial = factorial*n
        n -= 1
    print('n! =', factorial)
else:
    print('Invalid entry: n is noninteger.')

# To fix this problem, we need to check that n is an integer AND positive.
# We will use 'and' to add another condition to our if statement.  
# We will also implement some elseif statements so that we can present proper 
# error messages.
#
# In the elseif statements != means 'not equal to' and <= means 'less than
# or equal to'.  Try different entries for n.  If you enter, for example, a
# string for n you'll get an error message from Python.  As you can see,
# accounting for all the possible silly inputs users could enter requires a
# lot of thorough and careful programming!
n = 13
if n % 1 == 0 and n > 0:
    print('n =', n)
    factorial = 1
    while n > 1:
        factorial = factorial*n
        n -= 1
    print('n! =', factorial)
elif n == 0:
    print('n =', n)
    print('n! = 1')
elif n % 1 != 0 and n > 0:
    print('Invalid entry: n is noninteger.')
elif n % 1 == 0 and n < 0:
    print('Invalid entry: n negative.')
elif n % 1 != 0 and n < 0:
    print('Invalid entry: n is noninteger and negative.')


# Now let's put our factorial code into a for loop so that we can calculate
# n! as a function of n.
import numpy as np

start = 0
finish = 25
ns = np.arange(start, finish + 1, 1)
print(ns)
facts = []
for i in ns:
    if i % 1 == 0 and i > 0:
        n = int(i)
        factorial = 1
        while n > 1:
            factorial = factorial*n
            n -= 1
        facts = facts + [factorial]
    elif i == 0:
        facts = facts + [1]
    elif i % 1 != 0 and n > 0:
        print('Invalid entry: n is noninteger.')
    elif i % 1 == 0 and n < 0:
        print('Invalid entry: n negative.')
    elif i % 1 != 0 and n < 0:
        print('Invalid entry: n is noninteger and negative.')
print(facts)

# We can now easily make a plot of n! vs n.  Notice how sharply n! rises.
import matplotlib.pyplot as plt
plt.plot(ns, facts, 'bo', fillstyle = 'none')
plt.ylabel('n!')
plt.xlabel('n')
plt.title('linear-linear plot')

# Here's the same data on a log-log scale.
plt.figure()
plt.loglog(ns, facts, 'ro', fillstyle = 'none')
plt.ylabel('n!');
plt.xlabel('n')
plt.title('log-log plot')

# Here, we compare n! to exp(n).  Above about n = 5, n! passes exp(n) and
# continues to increase at a higher rate than exp(n).
xx = np.arange(1, finish, 0.01)
yexp = np.exp(xx)
plt.loglog(xx, yexp, 'k--')
plt.legend(('n!', 'exp(n)'))

# Finally, here's an implementation of our nested control structures in
# which the user is prompted to enter in start and end values for the n!
# calculation and then the plot comparing n! and e^n is generated.
start = int(input("Enter an integer start value: ")) 
finish = int(input("Enter an integer end value: ")) 
ns = np.arange(start, finish + 1, 1)
facts = []
for i in ns:
    if i % 1 == 0 and i > 0:
        n = int(i)
        factorial = 1
        while n > 1:
            factorial = factorial*n
            n -= 1
        facts = facts + [factorial]
    elif i == 0:
        facts = facts + [1]
    elif i % 1 != 0 and n > 0:
        print('Invalid entry: n is noninteger.')
    elif i % 1 == 0 and n < 0:
        print('Invalid entry: n negative.')
    elif i % 1 != 0 and n < 0:
        print('Invalid entry: n is noninteger and negative.')

plt.figure()
plt.loglog(ns, facts, 'ro', fillstyle = 'none')
plt.ylabel('n!');
plt.xlabel('n')
plt.title('log-log plot')

xx = np.arange(start, finish, 0.01)
yexp = np.exp(xx)
plt.loglog(xx, yexp, 'k--')
plt.legend(('n!', 'exp(n)'))