Skip to content

Fix "Making the Grade" ('Loops`) Concept Exercise #2407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 18, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 0 additions & 103 deletions exercises/concept/making-the-grade/.docs/after.md

This file was deleted.

58 changes: 48 additions & 10 deletions exercises/concept/making-the-grade/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -2,22 +2,60 @@

## General

- Make sure you are comfortable with using the `in` concept while looping through in for loops.
- `while` loops are used for _indefinite_ (uncounted) iteration
- `for` loops are used for _definite_, (counted) iteration.
- The keywords `break` and `continue` help customize loop behavior.
- `range(<start>, stop, <step>)` can be used to generate a sequence for a loop counter.
- The bulit-in `enumerate()` will return (`<value>`, `<index>`) pairs to iterate over.

You are a teacher and you are correcting papers of your students who wrote an exam you just conducted. You are tasked with the following activities.
Also being familure with the following can help with completing the tasks:

## 1. Failed Students
- [`lists`][list]: indexing, nested lists, [`<list>.append`][append and pop], [`<list>.pop()`][append and pop].
- [`str`][str]: `str()` constructor, using the `+` to concatenate strings, optionally, [`f-strings`][f-strings].

- Iterate through the marks one by one. For each loop check if the mark is less than or equal to 40. If so, then increment a count and after iterating through all marks in the loop return the count.
## 1. Rounding Scores

## 2. Top Marks
- `While` loops will continue to execute until their condition evaluates to `False`.
- <list>.pop() will remove and return the last item in a list.
- Empty `lists` evaluae to `False` (most empty objects in Python are "Falsy")
-

- Iterate through the list of marks one by one. If you find a mark which is more than x, just continue and go to the next mark.
## 2. Non-Passing Students

## 3. First K Students.
- There's no need to declare loop counters or index counters when iterating through an object using a `for` loop.
- A results counter does need to set up and _incremented_ -- you'll want to `return` it when the loop terminates.
-

- Start an index variable at 0 and loop through the elements till we reach the index variable equals to k. once that happens you can return the marks.
## 3. The "Best"

## 4. Full Marks
- There's no need to declare loop counters or index counters when iterating through an object using a `for` loop.
- Having an emptey list to add the "best" marks to is helpful here.
- `<list>.append()` can help add things to the results list.
-

- There may be or may not be a student with full marks - 100. Either way you have to loop through all the marks and find whether we have a student with full marks else just return No Hundreds
## 4. Calculating Letter Grades

- These are _lower thresholds_. The _lower threshold_ for a "D" is a score of **41**, since an "F" is **<= 40**.
- [`range()`][range] can be helpful here to generate a sequence with the proper "F" -> "A" increments.
- [`round()`][round] without parameters should round off increments nicely.
- As with "the best" task, `<list>.append()` could be useful here to append items from `range()` into a results list.

## 5. Matching Names to Scores

- [`enumerate()`][enumerate] could be helpful here.
- If both lists are the same length and sorted the same way, could you use the index from one to retrieve a value from the other?
-

## 6. A "Perfect" Score

- There may be or may not be a student with a score of 100, and you can't return "No perfect score." without checking all the scores.
- The [`control flow`][control flow] satements `continue` and `break`break may be useful here to move past unwanted values.

[list]: https://docs.python.org/3/library/stdtypes.html#list
[str]: https://docs.python.org/3/library/stdtypes.html#str
[f-strings]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals
[append and pop]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
[enumerate]: https://docs.python.org/3/library/functions.html#enumerate
[control flow]: https://docs.python.org/3/tutorial/controlflo.html#break-and-continue-statements-and-else-clauses-on-loops
[range]: https://docs.python.org/3/tutorial/controlflow.html#the-range-function
[round]: https://docs.python.org/3/library/functions.html#round
99 changes: 66 additions & 33 deletions exercises/concept/making-the-grade/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,97 @@
# Instructions

You are a teacher and you are correcting papers of your students who wrote an exam you just conducted. You are tasked with the following activities.
You're a teaching assistant correcting student exams.
Keeping track of results manually is getting both tedious and mistake-prone.
You decide to make things a little more interesting by putting together some functions to count and calculate results for the class.

## 1. Failed Students
## 1. Rounding Scores

Create the function `count_failed_students()` that takes one parameter: `student_marks`. This function should count the number of students who have failed the subject, returning the count as an integer. We say that a student has failed if their mark is less than or equal to **40**.
While you can give "partial credit" on exam questions, overall exam scores have to be `int`s.
So before you can do anything else with the class scores, you need to go through the grades and turn any `float` scores into `ints`. Lucky for you, Python has the built-in [`round()`][round] function you can use.

Note: `Iterate` through the student marks to find out your answer.
Result should be an `int`.
A score of 75.45 or 75.49 will round to 75. A score of 43.50 or 43.59 will round to 44.
There shouldn't be any scores that have more than two places after the decimal point.

Create the function `round_scores()` that takes a `list` of `student_scores`.
This function should _consume_ the input `list` and `return` a new list with all the scores converted to `int`s.

```python
>>> student_scores = [90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3]
>>> round_scores(student_scores)
...
[40, 39, 95, 80, 25, 31, 70, 55, 40, 90]
```

## 2. Non-Passing Students

As you were grading the exam, you noticed some students weren't performing as well as you'd hoped.
But you were distracted, and forgot to note exactly _how many_ students.

Create the function `count_failed_students()` that takes a `list` of `student_scores`. This function should count up the number of students who don't have passing scores and return that count as an integer. A student needs a score greater than **40** to achive a passing grade on the exam.

```python
>>> count_failed_students(student_marks=[90,40,55,70,30]))
2
>>> count_failed_students(student_scores=[90,40,55,70,30,25,80,95,38,40])
5
```

## 2. Top Marks
## 3. The "Best"

The teacher you're assisting wants to find the group of students who've performed "the best" on this exam. What qualifies as "the best" fluctuates, so you'll need to find the student scores that are **greater than or equal to** the current threshold.

The Headmaster wants to find the group of top students. What qualifies student marks as `top marks` fluctuates, and you will need to find the student marks that are **greater than or equal to** the current threshold.
Create the function `above_threshold()` taking `student_scores` (a list of grades), and `threshold` (the "top score" threshold) as parameters. This function should return a `list` of all scores that are `>=` to `threshold`.

Create the function `above_threshold()` where `student_marks` and `threshold` are the two required parameters:
```python
>>> above_threshold(student_scores=[90,40,55,70,30,68,70,75,83,96], threshold=75)
[90,75,83,96]
```

1. `student_marks` are a list of marks for each student
2. `threshold` is the top mark threshold. Marks greater than or equal to this number are considered "top marks" .
## 4. Calculating Letter Grades

This function should return a `list` of all marks that are greater than or equal to a scoring threshold.
The teacher you're assisting likes to assign letter grades as well as mumeric scores.
Since students rarely score 100 on an exam, the "letter grade" lower thresholds are calculated based on the highest score achieved, and increment evenly between the high score and the failing threshold of **<= 40**.

**Note:** If you find a mark which is less than the threshold, you should `continue` to evaluating the next mark.​
Create the function `letter_grades()` that takes the `highest` score on the exam as a parameter, and returns a `list` of lower score thresholds for each letter grade from "F" to "A".

```python
>>> above_threshold(student_marks=[90,40,55,70,30], 70)
[90, 70]
>>> letter_grades(highest=100)
[41, 56, 71, 86]

>>> letter_grades(highest=88)
[41, 53, 65, 77]
```

## 3. First K Students.

Create the function `first_k_student_marks()` with parameters `student_marks` and `k`
## 5. Matching Names to Scores

You have a list of exam scores in descending order, and another list of student names also sorted in descending order by their exam scores. You'd like to match each student name with their exam score, and print out an overall class ranking.

1. Student marks are a list of marks of each student
2. k is the number of students.
Create the function `student_ranking()` with parameters `student_scores` and `student_names`. Match each student name on the `student_names` list with their score from the `student_scores` list. You can assume each argument `list` will be sorted from highest score(er) to lowest score(er). The function should return a `list` of strings with the format "<rank>. <student name> : <student score>".

You need to return the first K number of student Marks. Once you reach K number of students, you should exit out of the loop.

```python
>>> first_k_student_marks(student_marks=[90,80,100], k=1)
[90]
>>> student_scores = [100, 99, 90, 84, 66, 53, 47]
>>> student_names = ['Joci', 'Sara','Kora','Jan','John','Bern', 'Fred']
>>> student_ranking(student_scores, student_names)
...
['1. Joci: 100', '2. Sara: 99', '3. Kora: 90', '4. Jan: 84', '5. John: 66', '6. Bern: 53', '7. Fred: 47']
```

## 4. Full Marks
## 6. A "Perfect" Score

Create the function `perfect_score()` with parameter `student_info`.
`student_info` is a dictionary containing the names and marks of the students `{"Charles": 90, "Tony": 80}`
Although a "perfect" score of 100 is rare on an exam, it is interesting to know if at least one student has achieved it.

Find if we have any students who scored "full marks"(_100%_)on the exam. If we don't find any "full marks" in `student_info`, we should return "No hundreds"
Create the function `perfect_score()` with parameter `student_info`.
`student_info` is a `list` of lists containing the name and score of each student: `[["Charles", 90], ["Tony", 80]]`.
The function should `return` _the first_ [<name>, <score>] pair of the student who scored 100 on the exam.

Return the first student who has scored full marks - 100.
If no 100 scores are found in `student_info`, "No perfect scores." should be returned.

```python
>>> perfect_score(student_info={"Charles": 90, "Tony": 80, "Alex":100})
Alex
>>> perfect_score(student_info={"Charles": 90, "Tony": 80})
No hundreds
>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80], ["Alex", 100]])
["Alex", 100]

>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80]])
"No perfect scores."
```

[round]: https://docs.python.org/3/library/functions.html#round
216 changes: 162 additions & 54 deletions exercises/concept/making-the-grade/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,192 @@
# Introduction

## Loops in Python
Python has two looping constructs.
`while` loops for _indefinite_ (uncounted) iteration and `for` loops for _definite_, (counted) iteration.
The keywords `break`, `continue`, and `else` help customize loop behavior.

There are 2 general ways in Python to loop through objects.
<br>

- `while` loop (_indefinite_, or _uncounted_)
- `for` loop (_definite_, or _counted_)
## While

## While loops
[`while`][while statement] loops continue to exectute as long as the loop expression or "test" evaluates to `True` in a [`boolean context`][truth value testing], terminating when it evaluates to `False`.

While loops are _uncounted_ and based on a `conditional` statement for looping through objects.
```python

```
while expression:
set_of_statements
```
# Lists are considered "truthy" in a boolean context if they
# contain one or more values, and "falsy" if they are empty.

When the statement evaluates to `True`, the loop advances and executes the code in the indented block - or "body" of the loop. Looping continues in this fashion until the conditional statement evaluates to `False`.
>>> placeholders = ["spam", "ham", "eggs", "green_spam", "green_ham", "green_eggs"]

```python
i = 0
while i < 3:
print(i)
# => 0
# => 1
# => 2
>>> while placeholders:
... print(placeholders.pop(0))
...
spam
ham
eggs
green_spam
green_ham
green_eggs
```

## For Loops
<br>

Unlike `while` loops, `for` loops are based on a counter. The Loop will execute until the counter/object being counted is exhausted. The counter in this case could be the indexes in a `list`, `string`, or `tuple` -- or the indexes in a `range()` object.
## For

The basic [`for`][for statement] loop is better described as a _`for each`_ which cycles through the values of any [iterable object][iterable], terminating when there are no values returned from calling [`next()`][next built-in].

```python
for item in countable_object:
set_of_statements

>>> word_list = ["bird", "chicken", "barrel", "bongo"]

>>> for word in word_list:
... if word.startswith("b"):
... print(f"{word.title()} starts with a B.")
... else:
... print(f"{word.title()} doesn't start with a B.")
...
Bird starts with a B.
Chicken doesn't start with a B.
Barrel starts with a B.
Bongo starts with a B.

```

<br>

## Sequence Object range()

When there isn't a specific `iterable` given, the special [`range()`][range] sequence is used.
`range()` requires an `int` before which to `stop`, and can optionally take `start` and `step` parameters.
If no start `int` is provided, the sequence will begin with 0.
`range()` objects are `lazy` (_values are generated on request_), support all [common sequence operations][common sequence operations], and take up a fixed amount of memory, no matter how long the sequence.

```python
>>> numbers = [1, 2, 3, 4, 5]

>>> for number in numbers:
print(number)
#=> 1
#=> 2
#=> 3
#=> 4
#=> 5
# Here we use range to produce some numbers, rather than creating a list of them in memory.
# The values will start with 1 and stop *before* 7

>>> for number in range(1, 7):
... if number % 2 == 0:
... print(f"{number} is even.")
... else:
... print(f"{number} is odd.")
1 is odd.
2 is even.
3 is odd.
4 is even.
5 is odd.
6 is even.

# range() can also take a *step* parameter.
# Here we use range to produce only the "odd" numbers, starting with 3 and stopping *before* 15.

>>> for number in range(3, 15, 2):
... if number % 2 == 0:
... print(f"{number} is even.")
... else:
... print(f"{number} is odd.")
...
3 is odd.
5 is odd.
7 is odd.
9 is odd.
11 is odd.
13 is odd.

```

## Breaking from loops
<br>

Where you have a large set of objects that you want to loop through but would like to stop the loop execution when a certain condition is met, the `break` statement comes to the rescue.
## Values and Indexes with enumerate()

If both values and indexes are needed, the built-in [`enumerate()`][enumerate] will return (`index`, `value`) pairs:

```python
list_of_items = [1, 2, 3, 4, 5, 6, 7, 8]
for item in list_of_items:
if item > 5:
break
print(item)
# => 1
# => 2
# => 3
# => 4
# => 5

>>> word_list = ["bird", "chicken", "barrel", "apple"]

# *index* and *word* are the loop variables.
# Loop variables can be any valid python name.

>>> for index, word in enumerate(word_list):
... if word.startswith("b"):
... print(f"{word.title()} (at index {index}) starts with a B.")
... else:
... print(f"{word.title()} (at index {index}) doesn't start with a B.")
...
Bird (at index 0) starts with a B.
Chicken (at index 1) doesn't start with a B.
Barrel (at index 2) starts with a B.
Apple (at index 3) doesn't start with a B.



# The same method can be used as a "lookup" for pairing items between two lists.
# Of course, if the lengths or indexs don't line up, this doesn't work.

>>> word_list = ["cat", "chicken", "barrel", "apple", "spinach"]
>>> category_list = ["mammal", "bird", "thing", "fruit", "vegetable"]

>>> for index, word in enumerate(word_list):
... print(f"{word.title()} is in category: {category_list[index]}")
...
Cat is in category: mammal
Chicken is in category: bird
Barrel is in category: thing
Apple is in category: fruit
Spinach is in category: vegetable
```

## Continuing in loops
<br>

You will have important objects as well as non important ones. When you want to skip over the objects that you don't need to process you use the `continue` statement.
## Altering Loop Behavior

Example: you want to process only odd numbers in a loop and not the event numbers.
[`continue`][continue statement] can be used to skip forward to the next iteration.

```python
all_numbers = [1, 2, 3, 4, 5, 6, 7, 8]
for num in all_numbers:
if num % 2 == 0:
word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"]

# This will skip *bird*, at index 0
for index, word in enumerate(word_list):
if index == 0:
continue
print(num)
# => 1
# => 3
# => 5
# => 7
if word.startswith("b"):
print(f"{word.title()} (at index {index}) starts with a b.")

Barrel (at index 2) starts with a b.
Bongo (at index 3) starts with a b.
Bear (at index 6) starts with a b.

```

[`break`][break statement] (_like in many C-related languages_) can be used to stop the iteration and "break out" of the innermost enclosing loop.

```python
>>> word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple"]

>>> for index, word in enumerate(word_list):
... if word.startswith("b"):
... print(f"{word.title()} (at index {index}) starts with a B.")
... elif word == 'sliver':
... break
... else:
... print(f"{word.title()} doesn't start with a B.")
...print("loop broken.")
...
Bird (at index 0) starts with a B.
Chicken doesn't start with a B.
Barrel (at index 2) starts with a B.
Bongo (at index 3) starts with a B.
loop broken.

```

[for statement]: https://docs.python.org/3/reference/compound_stmts.html#for
[range]: https://docs.python.org/3/library/stdtypes.html#range
[break statement]: https://docs.python.org/3/reference/simple_stmts.html#the-break-statement
[continue statement]: https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement
[while statement]: https://docs.python.org/3/reference/compound_stmts.html#the-while-statement
[iterable]: https://docs.python.org/3/glossary.html#term-iterable
[truth value testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing
[enumerate]: https://docs.python.org/3/library/functions.html#enumerate
[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
[next built-in]: https://docs.python.org/3/library/functions.html#next
17 changes: 4 additions & 13 deletions exercises/concept/making-the-grade/.meta/config.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
{
"blurb": "Learn about loops by grading and organzing your students exam scores.",
"authors": [
"mohanrajanr",
"BethanyG"
],
"authors": ["mohanrajanr", "BethanyG"],
"files": {
"solution": [
"loops.py"
],
"test": [
"loops_test.py"
],
"exemplar": [
".meta/exemplar.py"
]
"solution": ["loops.py"],
"test": ["loops_test.py"],
"exemplar": [".meta/exemplar.py"]
}
}
64 changes: 40 additions & 24 deletions exercises/concept/making-the-grade/.meta/exemplar.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
def count_failed_students(student_marks):
failed_marks = 0
for mark in student_marks:
if mark <=40:
failed_marks += 1
return failed_marks
def round_scores(student_scores):
rounded = []
while student_scores:
rounded.append(round(student_scores.pop()))
return rounded

def above_threshold(student_marks, x):
above_marks = []
for mark in student_marks:
if mark >= x:
above_marks.append(mark)
return above_marks
def count_failed_students(student_scores):
non_passing = 0
for score in student_scores:
if score <= 40:
non_passing += 1
return non_passing

def first_k_student_marks(student_marks, k):
i = 0
marks = []
while i < k:
marks.append(student_marks[i])
i += 1
return marks
def above_threshold(student_scores, threshold):
above_threshold = []
for score in student_scores:
if score >= threshold:
above_threshold.append(score)
return above_threshold

def letter_grades(highest):
increment = round((highest - 40)/4)
scores = []
for score in range(41, highest, increment):
scores.append(score)
return scores

def student_ranking(student_scores, student_names):
results = []
for index, name in enumerate(student_names):
rank_string = str(index + 1) + ". " + name + ": " + str(student_scores[index])
results.append(rank_string)
#results.append(f"{index + 1}. {name}: {student_scores[index]}")
return results

def perfect_score(student_info):
for name, mark in student_info.items():
if mark == 100:
return name
else:
return "No Centums"
result = "No perfect score."
for item in student_info:
if item[1] == 100:
result = item
break
else:
continue
return result
37 changes: 34 additions & 3 deletions exercises/concept/making-the-grade/loops.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
def count_failed_students(student_marks):
def round_scores(student_scores):
'''
:param student_scores: list of student exame scores as float or int.
:return: list of student scores *rounded* to nearest integer value.
'''
pass

def above_threshold(student_marks, x):
def count_failed_students(student_scores):
'''
:param student_scores: list of integer studnet scores.
:return: integer count of student scores at or below 40.
'''
pass

def first_k_student_marks(student_marks, k):
def above_threshold(student_scores, threshold):
'''
:param student_scores: list of integer scores
:param threshold : integer
:return: list of integer scores that are at or above the "best" threshold.
'''
pass

def letter_grades(highest):
'''
:param highes: integer of higest exam score.
:return: list of integer score thresholds for each F-A letter grades.
'''
pass
def student_ranking(student_scores, student_names):
'''
:param student_scores: list of scores in descending order.
:param student_names: list of names in descending order by exam score.
:return: list of strings in format ["<rank>. <student name>: <score>"].
'''
pass
def perfect_score(student_info):
'''
:param student_info: list of [<student name>, <score>] lists
:return: First [<student name>, 100] found OR "No perfect score."
'''
pass

195 changes: 136 additions & 59 deletions exercises/concept/making-the-grade/loops_test.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,138 @@
import unittest
from loops import *


class TestLoops(unittest.TestCase):

def test_count_failed_students_1(self):
self.assertEqual(
count_failed_students(student_marks=[40,40,35,70,30]),
4,
msg="Number of failed students is invalid",
)

def test_count_failed_students_2(self):
self.assertEqual(
count_failed_students(student_marks=[90,80,55,70,65]),
0,
msg="Number of failed students is invalid",
)

def test_above_threshold_1(self):
self.assertEqual(
above_threshold(student_marks=[90,40,55,70,30], x=100),
from loops import (
round_scores,
count_failed_students,
above_threshold,
letter_grades,
student_ranking,
perfect_score,
)


class MakingTheGradeTest(unittest.TestCase):

def test_round_scores(self):
input_data = [
[90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3],
[],
msg="The Number of marks above given marks is incorrect",
)

def test_above_threshold_2(self):
self.assertEqual(
above_threshold(student_marks=[90,40,55,70,30], x=70),
[90, 70],
msg="The Number of marks above given marks is incorrect",
)

def test_first_k_student_marks_1(self):
self.assertEqual(
first_k_student_marks(student_marks=[90,80,100,70], k=4),
[90,80,100,70],
msg="The Number of First K Students are incorrect",
)

def test_first_k_student_marks_2(self):
self.assertEqual(
first_k_student_marks(student_marks=[90,80,100], k=1),
[90],
msg="The Number of First K Students are incorrect",
)

def test_full_mark_student_1(self):
self.assertEqual(
perfect_score(student_info={"Charles": 90, "Tony": 80, "Thor": 60}),
"No Centums",
msg="Centum Scorer name is wrong",
)

def test_full_mark_student_2(self):
self.assertEqual(
perfect_score(student_info={"Charles": 90, "Tony": 80, "Mark": 100}),
"Mark",
msg="Centum Scorer name is wrong",
)
[50, 36.03, 76.92, 40.7, 43, 78.29, 63.58, 91, 28.6, 88.0],
[.5],
[1.5]
]
result_data = [
[40, 39, 95, 80, 25, 31, 70, 55, 40, 90],
[],
[88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
[0],
[2]
]
number_of_variants = range(1, len(input_data) + 1)

for variant, scores, results in zip(number_of_variants, input_data, result_data):
with self.subTest(f"variation #{variant}", scores=scores, results=results):
self.assertEqual(round_scores(scores), results,
msg=f'Expected: {results} but one or more {scores} were rounded incorrectly.')


def test_no_failed_students(self):
scores = [89, 85, 42, 57, 90, 100, 95, 48, 70, 96]
expected = 0
self.assertEqual(count_failed_students(scores), expected,
msg=f"Expected the count to be {expected}, but the count wasn't calculated correctly.")

def test_some_failed_students(self):
scores = [40, 40, 35, 70, 30, 41, 90]
expected = 4
self.assertEqual(count_failed_students(scores), expected,
msg=f"Expected the count to be {expected}, but the count wasn't calculated correctly.")

def test_above_threshold(self):
input_data = [
[40, 39, 95, 80, 25, 31, 70, 55, 40, 90],
[88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
[100, 89],
[88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
[]
]
thresholds = [98, 80, 100, 78, 80]
result_data = [
[],
[88, 91],
[100],
[88, 91, 78],
[]
]
number_of_variants = range(1, len(input_data) + 1)

for variant, score, threshold, result in zip(number_of_variants, input_data, thresholds, result_data):
with self.subTest(f"variation #{variant}", score=score, threshold=threshold, result=result):
self.assertEqual(above_threshold(score, threshold), result,
msg=f'Expected: {result} but the number of scores above {threshold} is incorrect.')

def test_letter_grades(self):
input_data = [100, 97, 85, 92, 81]
result_data = [
[41, 56, 71, 86],
[41, 55, 69, 83],
[41, 52, 63, 74],
[41, 54, 67, 80],
[41, 51, 61, 71]
]
number_of_variants = range(1, len(input_data) + 1)

for variant, highest, result in zip(number_of_variants, input_data, result_data):
with self.subTest(f"variation #{variant}", highest=highest, result=result):
self.assertEqual(letter_grades(highest), result,
msg=f'Expected: {result} but the grade thresholds for a high score of {highest} are incorrect.')


def test_student_ranking(self):
scores = [
[100, 98, 92, 86, 70, 68, 67, 60, 50],
[82],
[88, 73],
]
names = [
['Rui', 'Betty', 'Joci', 'Yoshi', 'Kora', 'Bern', 'Jan', 'Rose'],
['Betty'],
['Paul', 'Ernest'],
]
result_data = [
['1. Rui: 100', '2. Betty: 98', '3. Joci: 92', '4. Yoshi: 86',
'5. Kora: 70', '6. Bern: 68', '7. Jan: 67', '8. Rose: 60'],
['1. Betty: 82'],
['1. Paul: 88', '2. Ernest: 73']
]
number_of_variants = range(1, len(scores) + 1)

for variant, scores, names, results in zip(number_of_variants, scores, names, result_data):
with self.subTest(f"variation #{variant}", scores=scores, names=names, results=results):\
self.assertEqual(student_ranking(scores, names), results,
msg=f'Expected: {results} but the rankings were compiled incorrectly.')

def test_perfect_score(self):
input_data = [
[['Rui', 60],['Joci', 58],['Sara', 91],['Kora', 93], ['Alex', 42],
['Jan', 81],['Lilliana', 40],['John', 60],['Bern', 28],['Vlad', 55]],

[['Yoshi', 52],['Jan', 86], ['Raiana', 100], ['Betty', 60],
['Joci', 100],['Kora', 81],['Bern', 41], ['Rose', 94]],

[['Joci', 100],['Vlad', 100],['Raiana', 100],['Alessandro', 100]],
[['Jill', 30], ['Paul', 73],],
[]
]
result_data = [
"No perfect score.",
['Raiana', 100],
['Joci', 100],
"No perfect score.",
"No perfect score."
]

number_of_variants = range(1, len(input_data) + 1)

for variant, scores, results in zip(number_of_variants, input_data, result_data):
with self.subTest(f"variation #{variant}", scores=scores, results=results):
self.assertEqual(perfect_score(scores), results,
msg=f'Expected: {results} but got something different for perfect scores.')