A document from MCS 275 Spring 2022, instructor David Dumas. You can also get the notebook file.

MCS 275 Spring 2022 Worksheet 1 Solutions

  • Course instructor: David Dumas
  • Solutions prepared by: Jennifer Vaccaro, Johnny Joyce

Topics

This worksheet covers some initial setup and installation of software, followed by some coding exercises that use material from prerequisite courses.

Instructions

  • This isn't collected or graded.
  • If you took MCS 260 recently, perhaps even with me in Fall 2021, you may have some or all of the necessary tools installed already. That's fine. Be sure to test them out and read through the worksheet carefully to be sure you know what is assumed from now on.

Part I: Setting up a development enviroment

The tasks in this part of the worksheet expand on the Getting Started steps listed in the course web page.

We're starting out online, so you should complete these steps on whatever device you plan to use for writing programs while classes are online. It's best if that is also the device you use for zoom.

In case you need to attend class using a computer where you can't install things, it is possible to use UIC Technology Solutions' Virtual Computer Lab service instead. It provides a Windows 10 computer that you can control through a web browser. However, remote desktop systems like this don't feel as fast, responsive, or natural as using things installed locally. I think you will have a better experience if you can install the software necessary to develop programs on your own device. (For example, many common keyboard shortcuts don't work in the virtual lab.)

When we return to in person instruction, the computers in the labs will have the necessary software installed on them, and you can log in to those computers with your UIC netid and password. I guess that many students will still prefer to work on their own devices (e.g. by bringing a laptop to lab), and we support that.

The steps below guide you through the process of installing the necessary software on your computer. Work on them in today's lab so that the TA can offer assistance if you encounter problems.

Part II: Python Calisthenics

This section gives a series of exercises (of roughly increasing complexity) in which you'll write programs that are based on things you are expected to have seen in a prerequisite course. We'll talk about these in Lectures 2-3 as well, and the first quiz will cover the same kind of review material.

These start very easy and build up.

7. Squares

Create a simple program squares.py that prints the squares of the first 10 positive integers, so its output should look like

1
4
9
...
100

Then, add a feature to this program where it accepts an optional command line argument which is the largest integer whose square should be printed (while 10 remains the default if no argument is given). Recall that command line arguments appear in the list sys.argv which is accessible after you import the sys module.

Thus, for example, your modified program would print just

1
4
9

if run with the command

python squares.py 3

Solution

In [1]:
# range(1,11) for integers starting at 1 and ending at 10 (i.e. before 11)
for i in range(1,11):
    print(i**2)
1
4
9
16
25
36
49
64
81
100

Solution (modified version)

In [ ]:
import sys

n = 10 # Largest integer which should be squared and printed

if len(sys.argv) > 1: # If an optional command line argument was given
    n = int(sys.argv[1]) # Get the command line argument & convert to int
    
for i in range(1,n+1):
    print(i**2)

8. Getting to know our coding standards

Now that you've written a simple program from scratch, and have a full Python setup working, it's time to get acquainted with some code style rules.

All code you submit for credit needs to follow the rules described in the

Read that document now. Take your time, and ask the TA for help if you are unsure about any part of it.

Then, take the program below (which works!) and fix it so that it does the same thing but complies with the MCS 275 rules. (Note: The rules say you need to add a declaration stating that the program is your own work, or that it is derived solely from a template provided by the instructor. That's because all graded work in MCS 275 is done individually. Since this is a worksheet, collaboration is allowed, and in this case your declaration should instead list your collaborators.)

# I am a program that doesn't follow MCS 275 coding guidelines
# Please fix me.
def thing(x):
 return "{} is one greater than {}".format(x,x-1)

def thing2(x):
  return "{} is one less than {}".format(x,x+1)

s = str(input("Please enter your name"))
print("Hello",s,", this is a sample Python program.")

ss = list(range(5))
sss = [ x+5 for x in ss ]
ssss = [ 10*x for x in sss ]

for i in range(len(ssss)):
      sssss = 'Some things that are true:\n' + thing(ssss[i]) + "\n" + thing2(ssss[i]) + '\n\n'
      print(sssss)

Solution

In [ ]:
# MCS 275 Spring 2022 Worksheet 1 problem 7
# Jennifer Vaccaro
# I declare that I changed this code given from a problem in 
# accordance with course policy.

def string_less_one(n):
    '''Given an input number n, returns a statement like
    $n is one greater than $n+1'''
    return "{} is one greater than {}".format(n,n-1)

def string_plus_one(n):
    '''Given an input number n, returns a statement like
    $n is one less than $n-1'''
    return "{} is one less than {}".format(n,n+1)

# Create an integer list 50, 60, ..., 90
int_list_size = list(range(5))
int_list_shifted = [ x+5 for x in int_list_size ]
int_list = [ 10*x for x in int_list_shifted ]

# Print out the corresponding strings with line spacing
for x in int_list:
    print('Some things that are true:')
    print(string_less_one(x))
    print(string_plus_one(x),"\n")

8. First two digits of squares

Let's write a slightly more complex program now that uses more Python concepts.

Consider the squares of all the 4-digit positive integers (the numbers 1000, 1001, ..., 9999).

What pairs of digits can appear as the first two digits in one of these numbers? For example, since 2187*2187=4782969, we know that 47 is one of the possibilites. What are the others? Do you get all two-digit combinations this way?

Write a program that answers this question, and which also determines how many times each two-digit combination arises in the list of squares of 1000...9999.

Have it print the result in a format like this:

There are NN two-digits combinations that appear as the first two digits of squares of the integers from 1000 to 9999.  Here is a table of them, with the number of times each one occurs:

Digits  Number of times
-----------------------
10      XXX
11      YYY
...

Here is the recommended structure for your program:

  • Make a dictionary that has all the two-character strings corresponding to pairs of digits as keys, and the integer 0 as each value.

  • Use a for loop to iterate over integers from 1000 to 9999. In the body of the loop, square the integer, convert it to a string, and extract the first two characters into a string. Then use this as a key in the dictionary, and increment the associated value.

(You are welcome---even encouraged---to think about a purely theoretical solution to this problem, but I also want you to get the coding practice that will come from exhaustively enumerating the squares and checking the digits.)

Solution 1

In [1]:
# Start off with a dictionary comprehension containing the number of times
# each two-digit combination occurs (count starts at 0 for each combination)
# (Try printing this dictionary out to see what it contains!)
first_two_digits_dict = {str(x):0 for x in range(10,100)}


# Check each number from 1000 to 9999
for i in range(1000, 10000):
    
    # String representation of i^2
    i_squared_str = str(i**2)
    
    # We can use the "+" operator here because we are looking at a string
    first_two = i_squared_str[0] + i_squared_str[1]
    
    # Add 1 to the count of the corresponding combination in the dictionary
    first_two_digits_dict[first_two] += 1
    

# Use a list comprehension to find out how many of the 
num_combinations = len([n for n in first_two_digits_dict.values() if n > 0])
    
print(f'''There are {num_combinations} two-digits combinations that appear as the first two 
digits of squares of the integers from 1000 to 9999.  
Here is a table of them, with the number of times each one occurs:

Digits  Number of times
-----------------------''')

for key, value in first_two_digits_dict.items():
    print(f"{key}          {value}")
There are 90 two-digits combinations that appear as the first two 
digits of squares of the integers from 1000 to 9999.  
Here is a table of them, with the number of times each one occurs:

Digits  Number of times
-----------------------
10          203
11          195
12          186
13          179
14          172
15          167
16          163
17          157
18          153
19          150
20          145
21          142
22          138
23          136
24          133
25          131
26          128
27          125
28          123
29          122
30          118
31          117
32          116
33          113
34          113
35          110
36          109
37          108
38          105
39          105
40          104
41          102
42          101
43          100
44          99
45          97
46          96
47          96
48          94
49          95
50          92
51          92
52          91
53          89
54          90
55          88
56          87
57          87
58          86
59          85
60          85
61          84
62          83
63          82
64          83
65          82
66          80
67          80
68          79
69          79
70          79
71          78
72          77
73          77
74          76
75          75
76          75
77          75
78          75
79          74
80          73
81          73
82          72
83          73
84          71
85          71
86          71
87          70
88          70
89          69
90          70
91          69
92          68
93          68
94          68
95          67
96          67
97          67
98          66
99          66

Solution 2

In [ ]:
# Create a dictionary where
# the keys are 10-99 as strings
# the values are all 0
num_str_dict = {}
for i in range(10,100):
    num_str = str(i)
    num_str_dict[num_str] = 0

# iterate through the four-digit numbers 1000-9999
total_combos = 0
for i in range(1000,10000):
    # square i, and convert it to a string
    i_squared = i**2
    i_squared_str = str(i_squared)

    # extract the key (first two digits) and
    # add one to the corresponding value
    i_key = i_squared_str[0:2]
    num_str_dict[i_key] += 1
    # If the first one, add to the total num of combos
    if num_str_dict[i_key] == 1:
        total_combos += 1

# Print out a table of all of the keys/values
print("There are {} two-digits combinations that appear as the first two digits of squares of the integers from 1000 to 9999.".format(total_combos))
print("Here is a table of them, with the number of times each one occurs:")
print("Digits  Number of times")
print("-----------------------")
for i in range(10,100):
    num_str = str(i)
    if num_str_dict[num_str] > 0:
        print("{} {}".format(num_str, num_str_dict[num_str]))

9. Box in a text file

One of the nice things about Python is that strings can contain any Unicode character (as opposed to strings in some other programming languages where the set of available characters is much more limited).

Some Unicode characters are made for drawing "text graphics" boxes, like this:

╔══════════╗ 
║  Look,   ║
║  a box!  ║
╚══════════╝

The box drawing characters used above are:

  • \u2550 or
  • \u2551 or
  • \u2554 or
  • \u2557 or
  • \u255a or
  • \u255d or

Write a program box.py that prints a unicode text box of specified width and height to an output text file. It should expect three command line arguments. The first, sys.argv[1], will be the the output filename. The other arguments are integers giving the width and height of the box.

The program should open the output file for writing, and then write lines of text to that file that create the desired box.

So, for example, the command

python box.py boxtest1.txt 3 4

should result in a file boxtest1.txt being created that contains the following text, exactly:

╔═╗
║ ║
║ ║
╚═╝

(This box has a width of 3 characters and a height of 4 lines). Similarly, the command

python box.py boxtest2.txt 12 2

should result in a file boxtest2.txt being created that contains the following text, exactly:

╔══════════╗
╚══════════╝

Recommended structure of your program:

There are three different lines in the output file: The top of the box, the line that repeats some number of times to make the middle of the box, and the bottom of the box.

I suggest you generate these three lines and store them in strings, and then use a loop to print the middle-of-box line the right number of times.

To generate these strings, you can use the fact that multiplication of a string by an integer will repeat the string a certain number of times. For example, "foo"*3 evaluates to "foofoofoo".

It would be a good idea to put the part of the program that writes the boxto a file into a function which accepts a file object as its only argument. That function should expect the file to already be opened for writing.

Then, the main program would parse the command line arguments, open the output file, and call the function that saves the box.

Solution

In [29]:
import sys

outfile = sys.argv[1]
width = int(sys.argv[2])
height = int(sys.argv[3])


def write_box_to_file(f):
    '''Given an already-opened file f, writes the box to the file'''
    
    # Create the top of the box
    # We subtract 2 from the width to account for the corners
    f.write("╔" + "═"*(width-2) + "╗")     # Looks like e.g.: ╔═════╗
    f.write("\n") # Move to a new line
    
    # Create the left and right sides of the box
    for i in range(height-2):
        f.write("║" + " "*(width-2) + "║") # Looks like e.g.: ║     ║
        f.write("\n") # Move to a new line
    
    
    # Create the bottom of the box
    f.write("╚" + "═"*(width-2) + "╝")     # Looks like e.g.: ╚═════╝
    f.write("\n") # Move to a new line
    
    
    
# By setting encoding to UTF-8, we can use the Unicode characters as specified
with open(outfile, "w", encoding="UTF-8") as f:
    write_box_to_file(f)

Bonus round

Work on this more challenging exercise if you finish the rest of the worksheet before discussion ends.

10. Parse grade statements

Write a program that reads a text file specified as a command line argument which contains statements written in the following format

Student ddumas scored 1 out of 23 on CoordinationTest.
Student ncomaneci scored 23 out of 23 on CoordinationTest.
Student enoether scored 11 out of 10 on AlgebraQuiz.

That is, the lines are all of the form

Student NAME scored POINTS out of MAXPOINTS on NAME_OF_ASSESSMENT.

where the all-caps items are placeholders that in the actual lines will be replaced by an integer (for POINTS or MAXPOINTS) or a string with no spaces (for NAME or NAME_OF_ASSESSMENT).

The program should compute the percentage score for each student on each assessment and store them in a dictionary (with the name of the assessment as a key). Then, it should print the scores for each assessment in increasing order, as percentages.

For example, the input above would give output:

Scores on CoordinationTest:
ddumas 4.3%
ncomaneci 100.0%

Scores on AlgebraQuiz:
enoether 110.0%

Solution

In [32]:
import sys

input_fn = sys.argv[1]
input_fin = open(input_fn, "r")

# Create a dictionary of dictionaries
# key: assignment
# value: dict with keys students, values percentages
assignments_dict = {}
for line in input_fin:
    words = line.split()
    if len(words) != 9:
        continue
    student = words[1]
    assignment = words[8][:-1] # Take out the last character, which is a period
    grade = int(words[3])
    out_of = int(words[6])
    if assignment in assignments_dict:
        assignments_dict[assignment][student] = grade/out_of
    else:
        new_dict = {student: grade/out_of}
        assignments_dict[assignment] = new_dict

for assignment in assignments_dict:
    print("Scores on {}".format(assignment))
    for student in assignments_dict[assignment]:
        print("{} {:.1f}%".format(student, assignments_dict[assignment][student]*100.0))
    print("")
Scores on CoordinationTest
ddumas 4.3%
ncomaneci 100.0%

Scores on AlgebraQuiz
enoether 110.0%

11. Robust grade statement parser

Modify the solution to problem 10 to be tolerant of lines in the input text file that don't follow the specified format. In particular, it should be able to determine if a given line is in the correct format.

If a malformed line is found, it should print an error message and proceed.

This requires several stages of error checking; for example, required parts of the input line might be missing, or the integer point values might be invalid integer literals, or the total number of points on the assignment might be zero (preventing the computation of a percentage).

Solution

In [ ]:
import sys

try:
    input_fn = sys.argv[1]
    input_fin = open(input_fn, "r")
except IndexError:
    print("IndexError: Please enter a command line argument.")
    exit()
except FileNotFoundError:
    print("FileNotFoundError: File name specified in command line argument was not found.")

# Create a dictionary of dictionaries
# key: assignment
# value: dict with keys students, values percentages
assignments_dict = {}
for line in input_fin:
    words = line.split()
    if len(words) != 9:
        print("Error: one or more lines does not have the correct number of words.")
        exit()
    student = words[1]
    assignment = words[8][:-1] # Take out the last character, which is a period
    try:
        grade = int(words[3])
    except ValueError:
        print(f"Error: score for {student} is not an integer")
        exit()
    try:
        out_of = int(words[6])
    except ValueError:
        print(f"Error: maximum possible score for {student} is not an integer")
        exit()
    if assignment in assignments_dict:
        assignments_dict[assignment][student] = grade/out_of
    else:
        try:
            new_dict = {student: grade/out_of}
        except ZeroDivisionError:
            print("Error: The maximum possible score for one or more students is 0. Cannot compute percentage.")
            exit()
        assignments_dict[assignment] = new_dict

for assignment in assignments_dict:
    print("Scores on {}".format(assignment))
    for student in assignments_dict[assignment]:
        print("{} {:.1f}%".format(student, assignments_dict[assignment][student]*100.0))
    print("")