Practical session

Duration3h45
Important

For this lab, we advise you to deactivate GiHub Copilot and AI code generation tools. You can try to do the exercises again with these tools afterwards for comparison.

Defensive programming

Exercise 1 (defensive programming): Extract a list of floats

We have a list L containing elements of different types. for example : L = [1, ‘3.14’, [], (2, 5), ‘hello’, {’d’: 3, ’e’: 2.145}]

We also have the following function which extracts all the floats from L to feed and returns a new list P :

def listOfFloats(L):
    P = []
    for i in L:
        if type(i) in (int,float):
            P.append(float(i))
    return P
  • Test this function on the example list L above. What do you notice?
  • Rewrite the function to take account numbers in the form of strings. To do this, you can use the string.digits constant.
  • Use the exceptions mechanism to achieve the same result. Test on lists L and L2 = [(2, 5), ‘hello’, {’d’: 3, ’e’: 2.145}]
Solution to the exercise
import string
def isNumber(s):
    for char in s:
        if char in string.digits: 
            return  True
    return False

def listOfFloats(mylist):
    p = []
    for i in mylist:
        if ((type(i) in (int,float)) or (type(i) is str and isNumber(i))):
            p.append(float(i))
    return p

l=[1, '3.14', [], (2,5), 'hello', {'d':3, 'e':2.145}]
print(listOfFloats(l))
import string

def isNumber(s):
    for char in s:
        if char in string.digits: 
            return True
    return False

def listOfFloats(mylist):
    p = []
    for i in mylist:
        try:
            p.append(float(i))
        except ValueError as e:
            print(f"ValueError: Could not convert {i} to float. {e}")
        except TypeError as e:
            print(f"TypeError: Invalid type for {i}. {e}")
    return p

l1 = [1, '3.14', [], (2, 5), 'hello', {'d': 3, 'e': 2.145}]
l2 = [(2, 5), 'hello', {'d': 3, 'e': 2.145}]
print(listOfFloats(l1))
print(listOfFloats(l2))

Using unit tests

Exercice 2 - Argmax

The argmax function returns the index of the (first occurrence of the) maximum value occuring in a list of values, if the list is empty or null (None or null) , it returns -1.

Here is an example of test for the function:

import unittest


class TestArgmax(unittest.TestCase):

    def test_argmax_1(self):
        self.assertEqual(1, argmax([1,5,3,5,2,0]))

if __name__ == '__main__':
    unittest.main()        
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,5,2,0}));
    }
}

You are going to code the function argmax(lst) and write some other tests to make sure it’s working properly. Before coding, you need to list a few unique examples that you can also use for your tests. You must also document the function and, if necessary, comment on the code.

Solution to the exercise

Function

def argmax(lst : list[int]) -> int:
    """
    This function returns the index of the maximum value occurring in a list of values.
    In case of an empty list, it returns -1.
    If the maximum value occurs multiple times, the index of the first occurrence is returned.
    :lst: list[int], the list of values
    :return: int, the index of the first occurrence of the maximum value
    """
    if lst is None or len(lst) == 0:
        return -1
    max_val = lst[0]
    max_index = 0
    for i, v in enumerate(lst):
        if v > max_val:
            max_val = v
            max_index = i
    # Alternatively, you can use a one-liner:
    # return lst.index(max(lst))
    return max_index
/**
 * This function returns the index of the maximum value occurring in a list of values.
 * In case of an empty list, it returns -1.
 * If the maximum value occurs multiple times, the index of the first occurrence is returned.
 *
 * @param lst the list of values
 * @return the index of the first occurrence of the maximum value
 */
public int argmax(int[] lst) {
    if (lst == null || lst.length == 0) {
        return -1;
    }
    int max_val = lst[0];
    int max_index = 0;
    for (int i = 1; i < lst.length; i++) {
        if (lst[i] > max_val) {
            max_val = lst[i];
            max_index = i;
        }
    }
    // Alternatively, you can use a one-liner:
    //return Arrays.stream(lst).max().stream().findFirst().getAsInt();
    return max_index;
}

Tests

import unittest


class TestArgmax(unittest.TestCase):

    def test_argmax_1(self):
        self.assertEqual(1, argmax([1,5,3,5,2,0]))

    def test_argmax_none(self):        
        self.assertEqual(-1, argmax(None))
    
    def test_argmax_empty(self):
        self.assertEqual(-1, argmax([]))
    
    def test_argmax_multiple_max(self):
        self.assertEqual(2, argmax([1,3,5,5,2,5]))

if __name__ == '__main__':
    unittest.main()        
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,5,2,0}));
    }

    @Test
    public void testArgmaxNone() {
        Assertions.assertEquals(-1, argmax(null));
    }

    @Test
    public void testArgmaxEmpty() {
        Assertions.assertEquals(-1, argmax(new int[]{}));
    }

    @Test
    public void testArgmaxMultipleMax() {
        Assertions.assertEquals(2, argmax(new int[]{1,3,5,5,2,5}));
    }
}

Exercice 3 - Leap year

In this exercise, you will have to write a function is_leap(year) that takes a year as a positive integer parameter and returns a boolean value that is true if the year is a leap year and false otherwise.

Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400. src

We add an additional rule which specifies that if the integer passed as a parameter is strictly negative, then the value returned is False.

Writing tests

def is_leap(year: int) -> int:
    """
    This function takes a year as input and returns True if the year is a leap year, False otherwise.
    An additional constraint is that the year must be greater than 0, otherwise the function will return False.
    :param year: int, the year to be checked
    :return: bool, True if year is a leap year, False otherwise
    """
    return False
/**
 * This function takes a year as input and returns True if the year is a leap year, False otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return False.
 *
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year){
    return false;
}

Before coding the body of the function, you will write the tests. Following the principle of one test per concept, you will create the following six tests:

  1. The value returned is False for a negative value (given as an example);
  2. If the input value is odd, the value returned is False;
  3. If the input value is even and not divisible by 4, then the expected result is False;
  4. If the input value is divisible by 4 and divisible by 100, then the expected result is False;
  5. If the input value is divisible by 4 and not divisible by 100, then the expected result is True;
  6. If the input value is divisible by 400, then the expected result is True;
import unittest

class MyTestCase(unittest.TestCase):

    def test_negative_value(self):
        self.assertFalse(is_leap(-1000))


if __name__ == '__main__':
    unittest.main()
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {

    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }
}
Solution to the exercise
import unittest

class MyTestCase(unittest.TestCase):

    def test_negative_year(self):
        self.assertFalse(False, is_leap(-1000))

    def test_odd_year(self):
        self.assertFalse(is_leap(1333))
        self.assertFalse(is_leap(2023))

    def test_even_not_divisivle_by_4(self):
        self.assertFalse(is_leap(2018))
        self.assertFalse(is_leap(2026))

    def test_divisible_by_4_and_by_100(self):
        self.assertFalse(is_leap(1900))
        self.assertFalse(is_leap(2100))

    def test_divisible_by_4_and_not_by_100(self):
        self.assertTrue(is_leap(2008))
        self.assertTrue(is_leap(2024))

    def test_divisible_by_400(self):
        self.assertTrue(is_leap(1600))
        self.assertTrue(is_leap(2000))


if __name__ == '__main__':
    unittest.main()
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {
    
    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }

    @Test
    public void testOddYear() {
        Assertions.assertFalse(isLeap(2001));
    }

    @Test
    public void testEvenYearNotDivisibleBy4() {
        Assertions.assertFalse(isLeap(2018));
        Assertions.assertFalse(isLeap(2026));
    }

    @Test
    public void testDivisibleBy4AndBy100() {
        Assertions.assertFalse(isLeap(1900));
        Assertions.assertFalse(isLeap(2100));
    }

    @Test
    public void testDivisibleBy4AndNotBy100() {
        Assertions.assertTrue(isLeap(2008));
        Assertions.assertTrue(isLeap(2024));
    }

    @Test
    public void testDivisibleBy400() {
        Assertions.assertTrue(isLeap(1600));
        Assertions.assertTrue(isLeap(2000));
    }
}

Writing the function

You can now code the logic of the function in accordance with the specifications. You need to comment out lines in a meaningful way to make the code clearer, using #.

Make sure that all the tests pass.

Solution to the exercise
def is_leap(year: int) -> int:
    """
    This function takes a year as input and returns True if the year is a leap year, False otherwise.
    An additional constraint is that the year must be greater than 0, otherwise the function will return False.
    :param year: int, the year to be checked
    :return: bool, True if year is a leap year, False otherwise
    """
    if year >= 0:  
        if year % 4 == 0:
            if year % 100 == 0:
                return year % 400 == 0
                # since the year is divisible by 100, 
                # it must also be divisible by 400 to be a leap year     
            else:
                # divisible by 4 and not by 100
                return True
        else:
            # not divisible by 4
            return False
    else:
        # negative years are not leap years
        return False
/**
 * This function takes a year as input and returns true if the year is a leap year, false otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return false.
 *
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year) {
    if (year >= 0) {
        if (year % 4 == 0) {
            if (year % 100 == 0) {
                return year % 400 == 0;
                // since the year is divisible by 100, 
                // it must also be divisible by 400 to be a leap year     
            }else{
                // divisible by 4 and not by 100
                return true;
            }
        }else{
            // not divisible by 4
            return false;
        }
    }else{
        // negative years are not leap years
        return false;
    }
}

Exercice 4 - Valid password

To be considered sufficiently secure, a password must comply with the following rules:

  1. Must contain at least 8 characters.
  2. Must contain at least one uppercase letter.
  3. Must contain at least one lowercase letter.
  4. Must contain at least one number.
  5. Must contain at least one special character (from !@#$%^&*()).

Write the function is_strong_password(password) which takes a string (a.k.a., sequence of characters) as input and returns a Boolean indicating whether the password entered is strong. To help design the logic of the function, you can break it down into sub-functions, each of which will be tested independently. A skeleton of the function is provided below, divided into sub-functions. You must also document and comment on your code.

Finally, you need to check that the rules expressed in the specifications are correctly taken into account by writing unit tests. The test class is not provided, so you will need to write it yourself. Each sub-function must be tested independently with a variety of examples. Once each sub-function is tested, you can test the main function.

This last stage can be done in pairs: each person writes the tests for the other. Then, you can evaluate the correctness of your function by running the tests. Possibly, either the function or the tests will need to be modified to ensure that the function is working properly.

Solution to the exercise

Generating documentation

Exercice 5 - Valid password again

Now that you have a function that checks the strength of a password, you will document it using the docstring format. You must document the class and each of its methods with valuable information, such as the purpose of the function, the parameters it takes, and the value it returns. Such information will help other developers understand how to use the function. Once you have documented the code, you can generate the documentation using the pydoc or javadoc tool, depending on the language you are using.

First, open a terminal and navigate to the directory where the file is located.

Naming

The file must be named strongpassword.py.

Then, run the following command:

pydoc -w strongpassword.py

pydoc will generate an HTML file named strongpassword.html in the same directory. You can open this file in a web browser to view the documentation.

First, open a terminal and navigate to the directory where the file is located.

Naming

The file must be named StrongPassword.java.

Then, run the following command:

javadoc -d doc StrongPassword.java

javadoc will generate a directory named doc containing the documentation. You can open the index.html file in a web browser to view the documentation.

To go further

Exercice 6 - Takuzu checker

Takuzu is a logic game where you have to complete a pre-filled $n\times n$ grid using only 0s and 1s. A grid is completed correctly if it respects the three rules:

  1. Each row and column must contain an equal number of 0s and 1s.
  2. No more than two consecutive similar numbers are allowed.
  3. Each pair of rows is different, and each pair of columns is different.

An example of a correctly completed $4\times 4$ grid:

0 1 1 0 
1 0 0 1 
0 0 1 1 
1 1 0 0 

You are asking to code a Takuzu grid checker. A class skeleton will serve as a basis for your work. You must respect the partitioning of functions and implement each of them. Of course, you will also need to create and complete a test class.

Takuzu: Solution

Exercice 7 - Mastermind

Get ready to code the Mastermind game. In this game, a codemaker creates a code using 4 digits from 1-6. A codebreaker has to make up to ten guesses in order to break the code. For each guess, the codemaker gives hints to the codebreaker. Hints are made of 2 digits:

  • The first digit indicates the number of correct numbers in the correct position.
  • The second digit indicates the number of correct numbers in the wrong position.

Multiple occurrences of a value are allowed. If there are duplicate values, they cannot all get a hint unless they correspond to the same number of duplicate colours in the hidden code. For example, if the code is [6, 5, 1, 4] and the guess is [1, 1, 2, 2], the clues are (0, 1). In fact, the value 1 appears twice in the guess, but only once in the code. Since it is not in the right place, it counts as one in the wrong place. In addition, the value 2 also appears twice in the guess, but not in the code. It does not appear in the hints.

In order to progress gradually, we’ll break down the logic of the game into functions and code them as we go along. We’ll use the description provided to document the code and write tests.

We provide a partially implemented Mastermind class. Comments are provided for all methods present, whether implemented or not.

You’ll need to code the methods check_code(code), has_remaining_attempts(), is_right_answer(hints) and guess_pattern(code, guess). We recommend that you process one function after the other, writing the tests before or just after coding the function. As the last function is the most complicated, you can, if you wish, break it down into sub-functions, each of which will be tested independently.

You may need to counter values. The following data structures may be useful.

Solution to the exercise

## To go beyond

Use AI

Now that you have done the exercises without AI tools, try to see how you could solve them faster (and possibly better) using tools such as GitHub Copilot!