#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Felix Muzny
10/14/2022
DS 2000
Lecture 11 - iteration with lists of lists,
             lists and mutability (the final chapter of scope), 
             functions and optional parameters,
             functions and functions as parameters

Logistics:
    - Homework 5 is due *next* Friday 10/21 @ 9pm
    
    - Homework solutions 
    -> will not be posting these online
    -> always happy to discuss in office hours
    -> will continue to bring up common mistakes in lecture
    
    - remote attendance (https://bit.ly/remote-ds2000-muzny)
    
    - how to call functions
    - nested loops & how does scope interact
    - when writing functions, gettin the docstring (function comment)
    to generate
    - using for loops to read files
    -> see read_data in this file
    -> https://course.ccs.neu.edu/ds2000/felix_lectures/earnings_lec10.py
    -> also these notes: 
        https://course.ccs.neu.edu/ds2000/felix_lectures/lec7_lists_forloops.py
    
 Three ways to participate (please do one of these!)
 1) via the PollEverywhere website: https://pollev.com/muzny
 2) via text: text "muzny" to the number 22333 to join the session
 3) via Poll Everywhere app (available for iOS or Android)
    

Warm-up 1 - lists of lists & slicing
---

Which of the following lines of code will get the first three
elements from the 3rd row of a list of lists 
(in the variable ls)?



A. ls[:3]
B. ls[3:]
C. ls[2][:3] <---- ls[row_index][col_index]
D. ls[:3][2]
E. ls[2][3]

"""

"""
Warm-up 2 - lists of lists and loops
---

What line of code should go in the marked spot to print
out every item in a list of lists?
"""
ls = [[1, 2], [23, 24], [35, 36, 37]]

# this loop doesn't use range
for row in ls:
    # row = [1, 2] on the first iteration
    
    # this loop iterates by index
    for col_ind in range(len(row)):
        #HERE
        print(row[col_ind])
print()
        
# here's an alternate version that uses
# the range function in both loops
# for row_ind in range(3)
# range(3) -> [0, 1, 2]
for row_ind in range(len(ls)):
    # row_ind = 0 on the first iteration

    for col_ind in range(len(ls[row_ind])):
        print(ls[row_ind][col_ind])
        
print()

"""
A. print(row[col_ind]) <---
B. print(col_ind)
C. print(ls[row][col_ind]) # this uses row as an index, when it is actually a list
D. print(ls[col_ind][row])
E. None of these

"""

# nested loops and scope
# print(i) # ERROR, i not available yet
for i in range(3):
    print("i value", i)
    
    # print(special_count) #ERROR, special_count not available yet
    
    # print(j) # ERROR, j not available yet
    for j in range(5):
        # i is accessible in all loops/conditionals
        # inside of the for i in range(3) loop
        print("j value", j)
        # print(special_count) # ERROR
        special_count = i + j # can access i here
        print(special_count)
    
    # python will let us, but can make confusing code
    # so be aware of this
    print(special_count) 
    print()

# python will let us, but can make confusing code
# so be aware of this
print(i)
print(j)
print()


"""
Academic Integrity
---
This is not a fun topic, help me avoid having to address it
any further.

Homeworks:
    - done individually
    
You all are beginning programmers. I know very well that it 
can often *feel* like there is only one correct solution to 
a problem.

In reality, we expect to see a great deal of variation in 
programs that are submitted (variation beyond "surface level"
traits like variable names)

When we see two programs that are the same, this is equivalent
to seeing two essays submitted in an English course that are 
word-for-word copies of one another.

Check in with your DS 2001 section for policies about collaboration.
"""


"""
List function — convert to ints
---

Write a function, convert_int, that takes a list as a parameter
and returns a new list with all the values in the original 
list converted to an integer.

Assume that all values in the original list can be converted to ints.
"""

# This is a pure function (see below)
def convert_int(ls):
    """
    Converts all values in given list to integers and 
    returns as a new list.

    Parameters
    ----------
    ls : list
        list of values to be converted.

    Returns
    -------
    new_ls : list
        corresponding list of ints.
    """
    new_ls = []
    for value in ls:
        new_ls.append(int(value))
    
    return new_ls

# This is a mutator function
def convert_int_mutate(ls):
    """
    Converts all values in given list to integers.

    Parameters
    ----------
    ls : list
        list of values to be converted.

    Returns
    -------
    None
    """

    for i in range(len(ls)):
        ls[i] = int(ls[i])


print("convert int mutate test")
ls = ["1", "3", "6"]
print(ls)
# notice no return saved
convert_int_mutate(ls)
print(ls)
print()


# print("convert int test")
# as_ints = convert_int(ls)
# print(as_ints)
# as_ints2 = convert_int(ls2)
# print(as_ints2)
# print(convert_int([]))


"""
Lists and mutability (scope: the final chapter!)
---
So far: when we've passed parameters into functions,
we've passed *copies* of their *values*.

Example:
"""

def increment(x):
    # x here only lives in increment
    x = x + 1
    print("int in increment", x)  # 8
    return x

def increment_first(ls):
    # print(main_ls) # ERROR, main_ls is not accessible
    ls[0] += 1
    print("ls in increment_first", ls)
    
def main():
    print("pass by copy vs reference examples")
    # x only lives in main()
    x = 7
    # increment(7)
    ans = increment(x)
    print("int in main", x)  # 7
    print()

    # now, we'll explore lists
    # when a list is passed to a function, we are not 
    # passing a copy
    main_ls = [0]
    print("before increment_first", main_ls)
    increment_first(main_ls)
    print("ls in main", main_ls)
    print()

main()

"""
Vocab
---
pass-by-value: ints, floats, strings, booleans--make a copy, pass in that copy
    
pass-by-reference: lists--tell the function where the list
                is stored in memory
"""



"""
List functions - pure vs. mutator
---

There are two kinds of list functions:
    
pure: does NOT change the orignal list
    sorted_ls = sorted(ls)
    
mutator: DOES change the orignal list
    ls.sort()
"""
print("sorted() example")
ls2 = [43.0, -34.7, 17.0, 100.1]
print(ls2)
ls2_sorted = sorted(ls2)
print(ls2) # unchanged
print(ls2_sorted)
print()


print("ls.sort() example")
ls2 = [43.0, -34.7, 17.0, 100.1]
print(ls2)
ls2.sort()
print(ls2) # sorted order
print()

"""
Optional parameters
---

examples with python built-in functions
(you've already used these a lot with matplotlib!)
"""
import matplotlib.pyplot as plt

xs = list(range(0, 10))
ys = [1, 5, 6, 4, 2, -4, 3, 2, 5, 10]
# color is an optional parameter
plt.plot(xs, ys, "o", color="red")
plt.show()
 
"""
Writing our own function with an optional parameter:
    

General syntax:
def function_name(positional, parameters, first, then = optional)
    
write a function, generate_list, that generates
a list of 10 random integers. Use optional parameters
to define the lower and upper range of integers to be
generated.
"""
import random

# this example has no positinal parameters
def generate_list(lower = 1, upper = 9):
    """
    Generate a list of length 10 filled with random ints from 
    a lower bound to an upper bound (1 - 9 by default).

    Parameters
    ----------
    lower : int, optional
        Inclusive lower bound of integers to generate. The default is 1.
    upper : int, optional
        Inclusive upper bound of integers to generate. The default is 9.

    Returns
    -------
    new_ls : list
        Generated list of ints.
    """
    
    new_ls = []
    for i in range(10):
        new_ls.append(random.randint(lower, upper))
    return new_ls

print("Generate list examples")
print(generate_list())  
print(generate_list(lower = -1000))
print(generate_list(upper = 1000))  
print()                        

"""
Passing functions as parameters
---

Using our function that we wrote from before (convert_int),
write a new version that will convert everything from a 
list into a string.
"""

def convert_mutate(ls, cast = int):
    """
    Converts all values in given list according to the given function.
    Changes to ints by default.
    Mutates the given list.

    Parameters
    ----------
    ls : list
        list of values to be converted.
    cast : function, optional
        Function to be applied to every element in the list. The default is int.

    Returns
    -------
    None.
    """

    for i in range(len(ls)):
        ls[i] = cast(ls[i])
        
        
print("convert_mutate examples")
ls3 = [43.2, 35.4]
convert_mutate(ls3)
print(ls3)
convert_mutate(ls3, cast = str)
print(ls3)
convert_mutate(ls3, cast = float)
print(ls3)

# if we have time (we didn't, we'll start here on Tuesday)
"""
if __name__ == "__main__":
---

Writing our very own modules!

"""


"""
Next time:
    - dictionaries
    - writing our own modules
"""
