# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Python Functions

# ## What is a function?
#
# * A function is a block of organized, reusable code that is used to perform a single, related action.
# * Single, organized, related always ? :)
#
#
# ### DRY - Do not Repeat Yourself principle
#
# * *Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.*
# http://wiki.c2.com/?DontRepeatYourself
#
# * Contrast WET - We Enjoy Typing, Write Everything Twice, Waste Everyone's Time

print("I want to go eat")
print("I am going to order food")


# Here we define our first function # order_food is more Pythonic style than orderFood which is camelCase
def order_food():
    print("I want to go eat")
    print("I am going to order food")
# so i've defined my function
# yet I have not run it


order_food()
order_food()


def going_out():
    print("Let's go out!")
    order_food()
    print("Let's run!")
    order_food()



going_out()


def place_order(dish):
    print(f"Hello waiter!")
    print(f"I'd like to order {dish}")
    print(f"Let's eat {dish.upper()}!")


place_order("potatoes")

dessert = "Ice cream"
place_order(dessert)

food_list = ["Beet soup", "Salad", "Meat and potatoes", "Ice cream"]
for food in food_list:
    place_order(food)

food, food_list, dessert


# Passing parameters(arguments)
def add(a, b):
    # you'd have an opportunity to verify, validate before using +
    print(a+b)
    # returns None


add(4,6)
add(9,233)
add("Hello ","Riga")
add([1,2,7],list(range(6,12)))

# what do we do if we want to modify some value, we could have global value which modify..
# but modifying global values generally can get messy


def mult(a, b):
    result = a*b # result is just a name of inner local variable could be anything
    print(f"Look Ma! I am multiplying {a} * {b} which is {result}")
    return result # with no return the default return is None


mult(5,9)

add(5,9)

t1 = mult(10,5)
t2 = add(10,5)

t1,t2

'soup' in food_list

food_list


def findNeedle(needle, mylist):
    for item in mylist:
        if needle in item:
            print(f"Eureka! Found {needle} in {item}")
            return item # so this fun will return first found item which contains needle
    


mysoup = findNeedle('soup', food_list)

mysoup


def addFood(food, mylist):
    # check for poison, give it to cour jester to try it out
    mylist.append(food) # remember append is IN PLACE meaning you modify mylist
    food = "Strawberries" # this will not matter outside the function, only for local use
    mylist.append(food) # remember append is IN PLACE meaning you modify mylist
    mylist = ["Diet soda", "Beyond Meat burger"]
    # remember our variables are like aliases, like shortcuts, 
    # so here we changed mylist to point a completely new list and are not point to incoming mylist anymore
    return mylist #not strictly necessary because well mylist will be modified


addFood(dessert, food_list) # this returns the modified list as well

dessert

food_list


def add_food(food, mylist):
    # check our food
    # if we do not want to modify our list we could do this
    # this is functional style
    new_list = mylist + [food] # so this will not modify mylist
    # do more stuff with new_list
    return new_list # so we will get a new list but old list will stay unmodifed


# i can make all values default
def make_cocktail(soda="tomato juice", alcohol="Vodka", mixer="Glass"):
    print(f"Mixing {soda} with {alcohol} in a {mixer}")
    return f"{soda}X{alcohol}"


make_cocktail("coke", "rum", "shaker")

make_cocktail("tonic", "gin") # i can be lazy i can use glass as default

make_cocktail() # all values are default

make_cocktail(alcohol="Grappa") # so I can skip some default values

new_list = add_food("Pancakes", food_list)


dessert



new_list

food_list

addFood("crab legs", food_list)

food_list

food_list.count("Salad")

addFood("Salad", food_list).count("Salad")

addFood(555, food_list)

food_list

result = mult(4,6)

print(result)

print(mult(5,7))
print(mult([3,6],4))
print(mult("Gunta ", 4))


help(mult)

mult(mult(2,3), mult(5,7))


def sub(a, b):
    print(a-b)
    return(a-b)
sub(20, 3)

result = 0


# +
# Avoid this, more of an anti-pattern

def add2(a,b):
    global result
    result += a+b
    # many calculations
    print(result)

add2(3,6)


# +
# Avoid this, more of an anti-pattern

def addResult(a, b, result):
    result += a+b # same result = result + (a+b)
    # many calculations
    print(result)
    return result



# -

result = addResult(5,10, result)
print(result)


def add3(a,b,c):
    print(a+b+c)
    return(a+b+c)
add3(13,26,864)

print(add3(list(range(5,10)), [1,3,6], [5,'VVVV']))

result = add3("A","BRACA","DABRA")

result


def isPrime(num):
    '''
    Super simple method of checking for prime. 
    '''
    for n in range(2,num): #How could we optimize this?
        if num % n == 0:
            print(f'{num} is not prime, it divides by {n}')
            return False
    else: # runs when no divisors found
        print(f'{num} is prime')
        return True
print(isPrime(53))
print(isPrime(51))
print(isPrime(59))

isPrime(10)


def isPrimeO(num):
    '''
    Faster method of checking for prime. 
    '''
    if num % 2 == 0 and num > 2: 
        return False
    for i in range(3, int(num**0.5) + 1, 2): ## notice we only care about odd numbers  and do not need to check past sqrt of num
        if num % i == 0:
            return False
    return True
isPrimeO(23)

# ## Jupyter magic
# * *%%HTML* lets you render cell as HTML
# * *%%time* times your cell operation, *%time* times your single line run time
# * *%%timeit* runs your cell multiple time and gives you average
#
# ### Magic docs: http://ipython.readthedocs.io/en/stable/interactive/magics.html

# %timeit isPrimeO(100001)

# %timeit isPrime(100001)


# +
# Why are the tests not comparable?
# Hint: What is different about the function outputs?
# -

def isPrimeSimple(num):
    '''
    Super simple method of checking for prime. 
    '''
    for n in range(2,num): #How could we optimize this?
        if num % n == 0:
#             print(f'{num} is not prime, it divides by {n}')
            return False
    else: # runs when no divisors found
#         print(f'{num} is prime')
        return True


isPrimeSimple(100001)

# %timeit isPrimeSimple(100001)

max(3,7,2)

# notice that max function works with variable argument count(could 2 could 1000) or more
max(5,2,7,222,1000, -555)


def getLargest(a,b,c):
    result = 0
    if a > b:
        print("Aha a is largest",a)
        result = a
    else:
        print("Aha b is largest",b)
        result = b
        
    if c > result:
        print("Hmm c is the largest of them all", c)
        result = c 
        
    return result


getLargest(-33,-455, -555)

getLargest(333,0,500)

5 > 3 > 2

3 > 2 > 6

# with import we can use other libraries
import random

random.random()

random.randrange(2)


def guessnum():
    '''
    Plays the number guessing game
    '''
    secret = random.randrange(100)
    #print(secret)
    x=-1 #Why did we need this declaration? How could we change the code to not require this assignment?
    while x != secret:
        x = int(input("Enter an integer please! "))
        if x > secret:
            print("your number is too large")
        elif x < secret:
            print("your number is too small")
        elif x == 555:
            print("Secret Exit")
            break
        else:
            print("YOU WON!")
            print(f"secret number is {secret}")
            break
guessnum()
    

def guessnumCorr():
    '''
    Plays the number guessing game
    '''
    secret = random.randrange(100)
    #print(secret)
    x=-1 #Why did we need this declaration? How could we change the code to not require this assignment?
    while x != secret:
        x = int(input("Enter an integer please! "))
        if x == 555:
            print("Secret Exit")
            break
        elif x > secret:
            print("your number is too large")
        elif x < secret:
            print("your number is too small")
        else:
            print("YOU WON!")
            print(f"secret number is {secret}")
            break
guessnumCorr()
    

guessnum()


# +
## Possible improvements, count how many tries it took to play the game
# -

def lazypow(a, b=2):
    '''Returns a taken to the power of b
    b default is 2'''
    return(a**b)



lazypow(5)

lazypow(5,3)

print(lazypow(3,4))
print(lazypow(11))

#Chaining function calls
print(lazypow(mult(2,6)))

print(lazypow(mult(2,6), 3))

12**3


print(lazypow(mult(3,5), 4))


#Returning multiple values
def multdiv(a=6,b=3):
    '''Returns two values as a tuple!:
    1. multiplication of arguments
    2. a/b
    '''
    return a*b, a/b



res = multdiv()
print(res)

type(res)

result = multdiv(4,3)

result

type(result)

result[0]

result[1]

result = None

mytuple = tuple(range(1,11))

mytuple

mytuple[::-1]

mytuple[3:7:2]

mylist = list(mytuple)

mylist



print(multdiv())
print(multdiv(12))
print(multdiv(b=4))
print(multdiv(15,3))
# we could just return two values separately

Create a lazybuzz function which takes four arguments with default values of 3,5 , 1 and 100 representing the two divisors the beggining and end

# # Side effects
#
# In computer science, a function or expression is said to have a side effect if it modifies some state outside its scope or has an observable interaction with its calling functions or the outside world besides returning a value.
# * Ideal (Platonic?) function has none, but not always possible(input/output, globals)
# * Functional programming style strives towards this ideal, but real life is mixture of styles

# +
# %%time
import time #this time library has nothing to do with %%time Jupyter command
def hello():
    print("HW")
    time.sleep(.100)
    print("Awake")
    
hello()
hello()
# -

##Built-in Functions		
abs()   dict()	help()	min()	setattr()
all()	dir()	hex()	next()	slice()
any()	divmod()	id()	object()	sorted()
ascii()	enumerate()	input()	oct()	staticmethod()
bin()	eval()	int()	open()	str()
bool()	exec()	isinstance()	ord()	sum()
bytearray()	filter()	issubclass()	pow()	super()
bytes()	float()	iter()	print()	tuple()
callable()	format()	len()	property()	type()
chr()	frozenset()	list()	range()	vars()
classmethod()	getattr()	locals()	repr()	zip()
compile()	globals()	map()	reversed()	__import__()
complex()	hasattr()	max()	round()	 
delattr()	hash()	memoryview()	set()


# ### More info on builtin functions: https://docs.python.org/3/library/functions.html

# # Usage of *args 
#
#  *args and **kwargs are mostly used in function definitions. *args and **kwargs allow you to pass a variable number of arguments to a function. What does variable mean here is that you do not know before hand that how many arguments can be passed to your function by the user so in this case you use these two keywords. *args is used to send a non-keyworded variable length argument list to the function.
#  
#
#

# +
# you can pass multiple arguments in a function without specifying them in advance
def test_var_args(f_arg="Valdis", *argv):
    print("first normal arg:", f_arg)
    for arg in argv:
        print("another arg through *argv :", arg)

test_var_args('yasoob','python','eggs','test')
# -

test_var_args("Valdis", "RTU")

test_var_args("Valdis")

# this will not work unless i made f_arg a default
test_var_args()

print(5, dessert, food) # so print takes multiple arguments


# Write a function to return result of multiplying ALL arguments
# If no arguments given function should return 1
def multMany(*argv):
    result = 1
    for num in argv:
        result *= num # this is the same as result = result * num
    return result


multMany()

multMany(1,3,5,353,2)

# #Usage of **kwargs
#
#   **kwargs allows you to pass keyworded variable length of arguments to a function. You should use **kwargs if you want to handle named arguments in a function.
#   



edict = {}

edict.items()


def greetMe(**kwargs):
    if kwargs is not None:
        for key, value in kwargs.items():
            print(f"{key} == {value}")


greetMe(name="Valdis",hobby="biking",work="programming")


def defaultFun(a=6):
    print(a)
defaultFun(333)
defaultFun()


# ## Homework Problems

# Easy
# Write a function to calculate volume for Rectangular Cuboid (visas malas ir taisnsturas 3D objektam)
def getRectVol(l,w,h):
    '''
    Gets the volume of Cuboid
    l - length
    w - widght
    h - height
    '''
    return l * w * h #You should be returning something not None!
print(getRectVol(2,5,7) == 70)
print(getRectVol(0,5,7) == 0)

getRectVol()


# +
# Medium
# Write a function to check if string is a palindrome
# ignore whitespace and capital letters
def isPalindrome(s):
    '''
    A palindrome is a word, number, phrase, or other sequence of characters which reads the same backward as forward, such as madam, racecar
    '''
    # lets do a bit of preprocessing first
    clean_text = s.lower().replace(" ", "") # could use upper doesnt matter
    # this clean_text will live only inside the function
    return clean_text == clean_text[::-1] # so we compare string to its reverse
# test your function, you do not touch these test cases

print(isPalindrome("Abba") == True)
print(isPalindrome("madam") == True)
print(isPalindrome("madamm") == False)
print(isPalindrome("race    car") == True)
print(isPalindrome('alusariirasula') == True)
print(isPalindrome('normaltext') == False)
# -

dir("string")

string.ascii_lowercase

# +
# One liner is possible! Okay to do it a longer way
# Hints: dir("mystring") for string manipulation(might need more than one method)
# Also remember one "unique" data structure we covered

import string
print(string.ascii_lowercase)
def isPangram(mytext, a=string.ascii_lowercase):
    '''
    '''
    print(mytext)
    return None # here it should return True or False
# assert(isPangram('dadfafd') == False)
# assert(isPangram("The quick brown fox jumps over the lazy dog") == True)
# assert(isPangram("The five boxing wizards jump quickly") == True)


# -

isPangram('badac')


def isAbigger(a,b):
    #Anti-pattern
    if a > b:
        return True
    else:
        return False


5 > 6


# We can check Truth in a single line
def isAbigger2(a,b):
    return a > b


# # Small Town Population Exercise
#
# From: https://www.codewars.com/kata/5b39e3772ae7545f650000fc
#
# In a small town the population is p0 = 1000 at the beginning of a year. The population regularly increases by 2 percent per year and moreover 50 new inhabitants per year come to live in the town. How many years does the town need to see its population greater or equal to p = 1200 inhabitants?
#
# At the end of the first year there will be: 
# 1000 + 1000 * 0.02 + 50 => 1070 inhabitants
#
# At the end of the 2nd year there will be: 
# 1070 + 1070 * 0.02 + 50 => 1141 inhabitants (number of inhabitants is an integer)
#
# At the end of the 3rd year there will be:
# 1141 + 1141 * 0.02 + 50 => 1213
#
# It will need 3 entire years.
# More generally given parameters:
#
# p0, percent, aug (inhabitants coming or leaving each year), p (population to surpass)
#
# the function nb_year should return n number of entire years needed to get a population greater or equal to p.
#
# aug is an integer, percent a positive or null number, p0 and p are positive integers (> 0)
#
# Examples:
#
# **nb_year(1500, 5, 100, 5000) -> 15**
#
# **nb_year(1500000, 2.5, 10000, 2000000) -> 10**
#
# Note: Don't forget to convert the percent parameter as a percentage in the body of your function: if the parameter percent is 2 you have to convert it to 0.02.

def nb_year(start, perc, delta, target):
    
    return 0 # FIXME how many years are needed to reach the target population

StarcoderdataJupyterScriptsDedupFiltered