LMU ☀️ CMSI 3801
LANGUAGES AND AUTOMATA I
HOMEWORK #2 Due: 2022-09-29

Learning Objectives

With this assignment you will demonstrate:

Readings and Videos

Please:

Instructions

Perform your work in the same private GitHub repository you used for Homework 1. Submit to BrightSpace a single text file or pdf that contains the link to this repo. Submit only one submission per team. Structure the repository as follows:

  .
  ├── README.md
  ├── javascript/
  │   └── (existing files from previous assignments)
  └── python/
      ├── README.md
      ├── .gitignore
      ├── tests/
      │   ├── __init__.py
      │   └── exercises_test.py         (Given to you below)
      └── exercises.py

Install all your dependencies according to your own preferences. I ask only that your solution be compatible with the tests that I have given you, so that means you’ll be using PyTest. The suggested folder structure allows you to run Python tests simply with:

$ pytest

As before, since the repo is to be private, make sure you have added me and Julian as contributors to your repo (so we can run and comment on your work). Our github names are rtoal and jgonz156.

You will be graded on your programming style, so make sure you are set up so that your editor or IDE auto-formats your code. Use plugins for your editor that include formatter and a linter. VSCode has nice extensions for Python, and SonarLint can lint almost anything. I suggest you install both.

Your homework submission will be the state of your repository on the branch master, at 23:59 in the America/Los Angeles time zone on the due date above.

Make sure your README has the names of all the students that have worked on the project.

The Code You Are To Write

Most of the code you are to write will be “ports” of the JavaScript functions you wrote in Homework #1 into Python, with some minor modifications. There are brief descriptions of the problems below; for specifics, though, the unit tests provide the documentation you need! Well, almost everything, because there is one additional rule: you must use Type Hints on all of your functions.

Write the following functions:

  1. A function that accepts a number of U.S. cents and returns a Python tuple containing, respectively, the smallest number of U.S. quarters, dimes, nickels, and pennies that equal the given amount. Raise a TypeError if the argument is not an int.
  2. A function that accepts a string and returns a new string equal to the initial string with all whitespace removed and then with the ith character (Unicode scalar) repeated i times.
  3. A generator function that yields successive powers of a base starting at the 0th power, namely 1, and going up to some limit. Require the arguments to be named.
  4. A “chainable” function that accepts one string per call, but when called without arguments, returns the words previously passed, in order, separated by a single space. Require the argument, if any, to be positional.
  5. A method that returns the lowercased version of the first string in a list that satisfies a given property. Raise an error if there is no value in the original list that satisfies the property.
  6. A function that returns the top ten players by points-per-game among the players that have been in 15 games or more. See the unit tests for details on the expected inputs and outputs. Note a slight twist from the previous assignment: the output will be a list of strings, not a list of objects.
  7. A function that when called, returns two functions, one for encryption and one for decryption. The two functions are inverses of each other, and use an internally generated Fernet key.
  8. A frozen dataclass for Quaternions. Each quaternion object must be completely immutable. Make addition and multiplication work with operators + and -, and create a coefficients @property to return a tuple of coefficients:
  9. >>> q1 = Quaternion(1, -2, 5, 0)
    >>> q2 = Quaternion(3, 0, 5, -2)
    >>> (q1 + q2).coefficients
    (4, -2, 10, -2)
    >>> (q1 * q2).coefficients
    (-22, -16, 16, -12)

The Unit Tests

Place the following file in your test folder:

exercises_test.py
from dataclasses import FrozenInstanceError
import re
import math
import pytest
from exercises import (
    change, stretched, powers, say, find_first_then_lower,
    top_ten_scorers, crypto_functions, Quaternion)


def test_change():
    assert change(0) == (0, 0, 0, 0)
    assert change(97) == (3, 2, 0, 2)
    assert change(8) == (0, 0, 1, 3)
    assert change(250) == (10, 0, 0, 0)
    assert change(144) == (5, 1, 1, 4)
    assert change(97) == (3, 2, 0, 2)
    assert change(100000000000) == (4000000000, 0, 0, 0)
    with pytest.raises(TypeError) as excinfo:
        # No fractional amounts allowed
        change(988.25)
    with pytest.raises(TypeError) as excinfo:
        # Do type check before negative check
        change(-988.25)
    with pytest.raises(ValueError) as excinfo:
        change(-50)
    assert str(excinfo.value) == 'amount cannot be negative'


def test_stretched():
    assert stretched('') == ''
    assert stretched('H e   l\t\tlo') == 'Heelllllllooooo'
    assert stretched('$#') == '$##'
    assert stretched('       ') == ''
    assert stretched('A = πr²') == 'A==πππrrrr²²²²²'
    assert stretched("  ") == ""
    assert stretched("  \t\n  \t") == ""
    assert stretched("  Hi  hi  ") == "Hiihhhiiii"
    assert stretched("😁😂😱") == "😁😂😂😱😱😱"
    assert stretched("hello world") == "heelllllllooooowwwwwwooooooorrrrrrrrllllllllldddddddddd"
    with pytest.raises(TypeError):
        # Test that the argument MUST be a positional argument
        stretched(s = '')


def test_powers():
    p = powers(base=2, limit=10)
    assert next(p) == 1
    assert next(p) == 2
    assert next(p) == 4
    assert next(p) == 8
    with pytest.raises(StopIteration):
        next(p)
    assert list(powers(base=2, limit=-5)) == []
    assert list(powers(base=7, limit=0)) == []
    assert list(powers(limit=1, base=3)) == [1]
    assert list(powers(base=2, limit=63)) == [1, 2, 4, 8, 16, 32]
    assert list(powers(base=2, limit=64)) == [1, 2, 4, 8, 16, 32, 64]
    with pytest.raises(TypeError):
        # Test that the arguments MUST be keyword arguments
        powers(2, 100)


def test_say():
    assert say() == ''
    assert say('hi')() == 'hi'
    assert say('hi')('there')() == 'hi there'
    assert say('hello')('my')('name')('is')(
        'Colette')() == 'hello my name is Colette'
    with pytest.raises(TypeError):
        # Test that the argument MUST be a positional argument
        say(s = '')


def test_find_first_then_lower_case():
    with pytest.raises(ValueError):
        find_first_then_lower(lambda s: len(s) > 10, [])
    with pytest.raises(ValueError):
        find_first_then_lower(lambda s: len(s) > 5, ["hello", "world"])
    assert find_first_then_lower(lambda s: s.startswith("HELL"), ["HELLO", "WORLD"]) == "hello"
    assert find_first_then_lower(lambda s: "d!" in s, ["Hello", "World!!"]) == "world!!"


def test_top_ten_scorers():
    assert top_ten_scorers({}) == []
    assert top_ten_scorers({'T1': [['A', 3, 300]]}) == []
    assert top_ten_scorers({'T1': [['A', 30, 300]]}) == ['A|10.00|T1']
    input = {
        'ATL': [
            ['Betnijah Laney', 16, 263],
            ['Courtney Williams', 14, 193]],
        'CHI': [
            ['Kahleah Copper', 17, 267],
            ['Allie Quigley', 17, 260],
            ['Courtney Vandersloot', 17, 225]],
        'CONN': [
            ['DeWanna Bonner', 16, 285],
            ['Alyssa Thomas', 16, 241]],
        'DAL': [
            ['Arike Ogunbowale', 16, 352],
            ['Satou Sabally', 12, 153]],
        'IND': [
            ['Kelsey Mitchell', 16, 280],
            ['Tiffany Mitchell', 13, 172],
            ['Candice Dupree', 16, 202]],
        'LA': [
            ['Nneka Ogwumike', 14, 172],
            ['Chelsea Gray', 16, 224],
            ['Candace Parker', 16, 211]],
        'LV': [
            ['A’ja Wilson', 15, 304],
            ['Dearica Hamby', 15, 188],
            ['Angel McCoughtry', 15, 220]],
        'MIN': [
            ['Napheesa Collier', 16, 262],
            ['Crystal Dangerfield', 16, 254]],
        'NY': [
            ['Layshia Clarendon', 15, 188]],
        'PHX': [
            ['Diana Taurasi', 13, 236],
            ['Brittney Griner', 12, 212],
            ['Skylar Diggins-Smith', 16, 261],
            ['Bria Hartley', 13, 190]],
        'SEA': [
            ['Breanna Stewart', 16, 317],
            ['Jewell Loyd', 16, 223]],
        'WSH': [
            ['Emma Meesseman', 13, 158],
            ['Ariel Atkins', 15, 212],
            ['Myisha Hines-Allen', 15, 236]]}
    expected = [
        "Arike Ogunbowale|22.00|DAL",
        "A’ja Wilson|20.27|LV",
        "Breanna Stewart|19.81|SEA",
        "DeWanna Bonner|17.81|CONN",
        "Kelsey Mitchell|17.50|IND",
        "Betnijah Laney|16.44|ATL",
        "Napheesa Collier|16.38|MIN",
        "Skylar Diggins-Smith|16.31|PHX",
        "Crystal Dangerfield|15.88|MIN",
        "Myisha Hines-Allen|15.73|WSH"
    ]
    assert top_ten_scorers(input) == expected


def test_crypto_functions():
    encode, decode = crypto_functions()
    for message in (b'', b'abc', bytes('😜😍😞🦾👀', 'utf-8')):
        assert(decode(encode(message))) == message


def test_quaternions():
    q1 = Quaternion(1, -2, 5, 0)
    q2 = Quaternion(3, 0, 5, -2)
    assert (q1 + q2).coefficients == (4, -2, 10, -2)
    assert (q1 * q2).coefficients == (-22, -16, 16, -12)
    with pytest.raises(FrozenInstanceError):
        # Test immutability
        q2.a = 100