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

Learning Objectives

With this assignment you will demonstrate:

Readings and Videos

Please:

Instructions

Work in teams of 1 to 4 students.

Your work will be done in a private GitHub repository called lmu-cmsi-3801 or something similar. Submit to BrightSpace a single text file or pdf that contains the link to this repo. Submit only one submission per team.

Structure your repository, for now, as follows:

  .
  ├── README.md
  └── javascript/
      ├── README.md               (Include ALL students’ names)
      ├── .gitignore
      ├── package.json
      ├── .prettierrc.json
      ├── src/
      │   └── exercises.js        (write this yourself)
      └── test/
          └── exercises.test.js   (this is given to you below)  

Your package.json must be set up so that I only need to clone your repository, navigate to the proper directory, and run npm install then npm test and all the tests will run. I’ve given you a test script which assumes the testing framework called mocha is installed. Set up your package.json so that it will invoke mocha; it will look something like this (it’s up to you to customize, of course):

{
  "name": "exercises",
  "version": "1.0.0",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "mocha"
  },
  "type": "module",
  "author": "Your name and your teammates names",
  "license": "MIT",
  "description": "A JavaScript project for CMSI 3801 homework",
  "devDependencies": {
    "mocha": "^10.0.0"
  },
  "dependencies": {
    "node-fetch": "^3.2.10"
  }
}

Make the rest of the folders and files. Copy the exercises.test.js file at the bottom of this page verbatim into your project in the proper place.

Since the repo is to be private, please 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. I recommend the Prettier plugin for VS Code. You should also install Sonar Lint because it is amazing.

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 Module You Are To Write

The file exercises.js should be a JavaScript module exporting the following functions:

  1. A function that accepts a number of U.S. cents and returns an array containing, respectively, the smallest number of U.S. quarters, dimes, nickels, and pennies that equal the given amount.
    > change(96)
    [ 3, 2, 0, 1 ]
    > change(8)
    [ 0, 0, 1, 3 ]
    > change(-4)
    RangeError: amount cannot be negative
    > change(33.25)
    [ 1, 0, 1, 3.25 ]
    
  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.
    > stretched('Hello world')
    'Heelllllllooooowwwwwwooooooorrrrrrrrllllllllldddddddddd'
    > stretched('😁😂😱')
    '😁😂😂😱😱😱'
    
  3. A function that yields successive powers of a base starting at the 0th power, namely 1, and going up to some limit. Consume the values with a callback.
    > powers(2, 70, p => console.log(p))
    1
    2
    4
    8
    16
    32
    64
    
  4. A JavaScript generator function that yields successive powers of a base starting at the 0th power, namely 1, and going up to some limit.
    > const p = powersGenerator(2, 7);
    > p.next()
    { value: 1, done: false }
    > p.next()
    { value: 2, done: false }
    > p.next()
    { value: 4, done: false }
    > p.next()
    { value: undefined, done: true }
    
  5. 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.
    > say('Hello')('my')('name')('is')('Colette')()
    'Hello my name is Colette'
    
  6. A function that accepts one object argument that must contain three properties—a crypto key, a crypto algorithm, and an initialization vector—and returns an array of two functions. The first returned function is an encryption function that encrypts a string into a hex string, and the second is a decryption function that decrypts the hex string into a string. Use the functions createCipheriv and createDecipheriv from the built-in Node crypto module. Please use destructuring on the parameter when implementing your function!
    > let [e, d] = makeCryptoFunctions({
        forKey: '1jdiekcns783uejdhasdfhcewp90x1sm',
        using: 'aes-256-cbc',
        withIV: 'm3987dhcbxgs452w'})
    undefined
    > e('hello there')
    'b97280f15b0e11b5c84c786ea3a51562'
    > d('b97280f15b0e11b5c84c786ea3a51562')
    'hello there'
    > [e, d] = makeCryptoFunctions({
        forKey: 'super dog',
        using: 'sdkjfhsdkll7dasf',
        withIV: 'fake_iv'})
    > e('Hello world')
    Error: Unknown cipher
    
  7. A function that returns the top ten players by points-per-game among the players that have been in 15 games or more. The input to your function will be an object, keyed by team, with a list of player stats. Each player stat is an array with the player name, the number of games played, and the total number of points, for example:
    {
      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],
      ],
    }
    
    Your output is to be in the form of an array of (at most 10) objects, sorted by points-per-game descending, with each object holding the player name, the team, and the number of points per game, for example:
    [
      { name: 'Arike Ogunbowale', ppg: 22, team: 'DAL' },
      { name: 'A’ja Wilson', ppg: 20.266666666666666, team: 'LV' },
      { name: 'Breanna Stewart', ppg: 19.8125, team: 'SEA' },
      { name: 'DeWanna Bonner', ppg: 17.8125, team: 'CONN' },
      { name: 'Kelsey Mitchell', ppg: 17.5, team: 'IND' },
      { name: 'Betnijah Laney', ppg: 16.4375, team: 'ATL' },
      { name: 'Napheesa Collier', ppg: 16.375, team: 'MIN' },
      { name: 'Skylar Diggins-Smith', ppg: 16.3125, team: 'PHX' },
      { name: 'Crystal Dangerfield', ppg: 15.875, team: 'MIN' },
      { name: 'Myisha Hines-Allen', ppg: 15.733333333333333, team: 'WSH' }
    ]
    

    You must implement this function as a single return expression where the input is processed in stages (yes, we are doing some data science, friends!) Since this style of programming may be new to you, here it is in English; you need to construct the JavaScript from it:

    • First, use Object.entries so you are working with a stream of [team, playerlist] pairs.
    • FlatMap this entry array with the operation that adds the team to the end of each player array. (Full credit if you use the spread operator, fewer points for using concat.
    • Filter to keep only those players who have appeared in 15 or more games.
    • Map to build objects for each player with their points per game included.
    • Sort by decreasing points per game.
    • Keep the first 10 items (using slice).
  8. A async function to fetch a Pokémon’s id, name, and weight from the Poké API.
  9. > const data = await pokemonInfo("pikachu")
    > data
    { id: 25, name: 'pikachu', weight: 60 }
    
  10. A class for Quaternions. The class should keep the four coefficients as private fields so each quaternion object is completely immutable. In addition to a constructor, expose only three methods: plus, times, and coefficients:
  11. > const q1 = new Quaternion(1, -2, 5, 0)
    > const q2 = new Quaternion(3, 0, 5, -2)
    > q1.plus(q2).coefficients()
    [ 4, -2, 10, -2 ]
    > q1.times(q2).coefficients()
    [ -22, -16, 16, -12 ]

The Unit Tests

Place the following file in your test/ folder:

exercises.test.js
import { deepEqual, throws } from "node:assert/strict"
import {
  change,
  stretched,
  say,
  powers,
  powersGenerator,
  makeCryptoFunctions,
  topTenScorers,
  pokemonInfo,
  Quaternion,
} from "../src/exercises.js"

describe("The change function", () => {
  it("works for 0", () => {
    deepEqual(change(0), [0, 0, 0, 0])
  })
  it("throws on negative", () => {
    throws(() => change(-50), /RangeError/)
  })
  it("works for the usual cases", () => {
    deepEqual(change(1), [0, 0, 0, 1])
    deepEqual(change(99), [3, 2, 0, 4])
    deepEqual(change(42), [1, 1, 1, 2])
  })
  it("works for fractions of cents", () => {
    deepEqual(change(33.375), [1, 0, 1, 3.375])
  })
  it("can handle really big values", () => {
    deepEqual(change(100000000037), [4000000001, 1, 0, 2])
    deepEqual(change(10000000000005), [400000000000, 0, 1, 0])
  })
})

describe("The stretched function", () => {
  it("stretches okay", () => {
    deepEqual(stretched(""), "")
    deepEqual(stretched("dog house"), "dooggghhhhooooouuuuuussssssseeeeeeee")
    deepEqual(stretched("a        π§"), "aπ𧧧")
    deepEqual(stretched("😄🤗 💀"), "😄🤗🤗💀💀💀")
  })
})

describe("The world-famous say function", () => {
  it("works when there are no words", () => {
    deepEqual(say(), "")
  })

  it("works when there are words", () => {
    deepEqual(say("hi")(), "hi")
    deepEqual(say("hi")("there")(), "hi there")
    deepEqual(say("hello")("my")("name")("is")("Colette")(), "hello my name is Colette")
  })

  it("handles spaces and empty words", () => {
    deepEqual(say("h i")(), "h i")
    deepEqual(say("hi ")("   there")(), "hi     there")
    deepEqual(say("")("")("dog")("")("go")(), "  dog  go")
  })

  it("handles emojis", () => {
    deepEqual(say("😄🤗")("💀👊🏾")(), "😄🤗 💀👊🏾")
  })
})

describe("The powers function", () => {
  it("works as expected", () => {
    const a = []
    powers(2, 1, (x) => a.push(x))
    deepEqual(a, [1])
    powers(-3, 81, (x) => a.push(x))
    deepEqual(a, [1, 1, -3, 9, -27, 81, -243])
  })
})

describe("The powers generator", () => {
  it("works as expected", () => {
    const g1 = powersGenerator(2, 1)
    deepEqual(g1.next(), { value: 1, done: false })
    deepEqual(g1.next(), { value: undefined, done: true })
    const g2 = powersGenerator(3, 100)
    deepEqual(g2.next(), { value: 1, done: false })
    deepEqual(g2.next(), { value: 3, done: false })
    deepEqual(g2.next(), { value: 9, done: false })
    deepEqual(g2.next(), { value: 27, done: false })
    deepEqual(g2.next(), { value: 81, done: false })
    deepEqual(g2.next(), { value: undefined, done: true })
    deepEqual([...powersGenerator(3, 27)], [1, 3, 9, 27])
  })
})

describe("The crypto function generator", () => {
  it("works as expected", () => {
    const [e, d] = makeCryptoFunctions({
      forKey: "1jdiekcns783uejdhasdfhcewp90x1sm",
      using: "aes-256-cbc",
      withIV: "m3987dhcbxgs452w",
    })
    deepEqual(
      e("Where is the good stuff?"),
      "a9f51b9f63d4512456d2dcc19333b0e495b90d6846acf37363dc55f57fad4127"
    )
    deepEqual(
      d("a9f51b9f63d4512456d2dcc19333b0e495b90d6846acf37363dc55f57fad4127"),
      "Where is the good stuff?"
    )
  })
})

describe("The topTenScorers function", () => {
  it("handles an empty object", () => {
    deepEqual(topTenScorers({}), [])
  })
  it("handles a small data set", () => {
    let input = { T1: [["A", 3, 300]] }
    let expected = []
    deepEqual(topTenScorers(input), expected)
    input = { T1: [["A", 30, 300]] }
    expected = [{ name: "A", ppg: 10, team: "T1" }]
    deepEqual(topTenScorers(input), expected)
  })
  it("handles a larger data set", () => {
    let 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],
      ],
    }
    let expected = [
      { name: "Arike Ogunbowale", ppg: 22, team: "DAL" },
      { name: "A’ja Wilson", ppg: 20.266666666666666, team: "LV" },
      { name: "Breanna Stewart", ppg: 19.8125, team: "SEA" },
      { name: "DeWanna Bonner", ppg: 17.8125, team: "CONN" },
      { name: "Kelsey Mitchell", ppg: 17.5, team: "IND" },
      { name: "Betnijah Laney", ppg: 16.4375, team: "ATL" },
      { name: "Napheesa Collier", ppg: 16.375, team: "MIN" },
      { name: "Skylar Diggins-Smith", ppg: 16.3125, team: "PHX" },
      { name: "Crystal Dangerfield", ppg: 15.875, team: "MIN" },
      { name: "Myisha Hines-Allen", ppg: 15.733333333333333, team: "WSH" },
    ]
    deepEqual(topTenScorers(input), expected)
  })
})

describe("The Pokemon API client", () => {
  it("works for snorlax", async () => {
    const data = await pokemonInfo("snorlax")
    deepEqual(data, { id: 143, name: "snorlax", weight: 4600 })
  })
  it("works for pikachu", async () => {
    const data = await pokemonInfo("pikachu")
    deepEqual(data, { id: 25, name: "pikachu", weight: 60 })
  })
  it("detects error for asdfghjkl", async () => {
    const data = await pokemonInfo("asdfghjkl")
    deepEqual(data, { error: "No information for asdfghjkl" })
  })
  it("defeats attempted URL injection hack", async () => {
    const data = await pokemonInfo("u/ha%e?been=pwned&x=1#x0r")
    deepEqual(data, { error: "No information for u/ha%e?been=pwned&x=1#x0r" })
  })
})

describe("The Quaternion class", () => {
  it("can echo coefficients", () => {
    deepEqual(new Quaternion(8, 5, -3, 1).coefficients(), [8, 5, -3, 1])
    deepEqual(new Quaternion(0, 0, 0, 0).coefficients(), [0, 0, 0, 0])
  })
  it("adds correctly", () => {
    const q1 = new Quaternion(13, 21, -5, -21)
    const q2 = new Quaternion(2, -1, -55, 2.5)
    deepEqual(q1.plus(q2).coefficients(), [15, 20, -60, -18.5])
  })
  it("multiples correctly", () => {
    const q1 = new Quaternion(3, -5, 1, -8)
    const q2 = new Quaternion(2, -13, -2, 3)
    deepEqual(q1.times(q2).coefficients(), [-33, -62, 115, 16])
  })
})