how to measure sleep duration: test_duration_w_date_and_time


This is part 4 of a program that calculates the difference between a given wake and sleep time.


I want to test the duration function with timestamps that have dates

red: make it fail

  • I make a copy of random_timestamp, change the name of the copy, then add a date to the return statement

    def random_timestamp_a():
        return (
            '1999/12/31 '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    
  • I also make a copy of test_duration_w_hours_and_minutes and change the name to test_duration_w_date_and_time

  • then add calls to random_timestamp_a

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time, sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                self.get_difference(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                )
            )
    

    which gives me ValueError

    ValueError: invalid literal for int() with base 10: '1999/12/31 03'
    ValueError: invalid literal for int() with base 10: '1999/12/31 07'
    ValueError: invalid literal for int() with base 10: '1999/12/31 11'
    ValueError: invalid literal for int() with base 10: '1999/12/31 21'
    

    the test calls duration, which calls read_timestamp, which uses the int constructor to change the timestamp string to a number after it calls str.split, but it is not in the right format

green: make it pass

  • I add the unittest.skip decorator to test_duration_w_date_and_time

    @unittest.skip
    def test_duration_w_date_and_time(self):
    ...
    
  • then add an assertion in test_converting_strings_to_numbers to see if I can make the same ValueError happen again to make sure the problem is with converting the string to a number

    def test_converting_strings_to_numbers(self):
        self.assertEqual(int('12'), 12)
        self.assertEqual(int('01'), 1)
    
        int('1999/12/31 21')
    

    the terminal shows ValueError with the same message from test_duration_w_date_and_time

    ValueError: invalid literal for int() with base 10: '1999/12/31 21'
    

    I cannot use the int constructor to change a timestamp string to a number when it has a date. I add an assertRaises to handle the ValueError

    def test_converting_strings_to_numbers(self):
        self.assertEqual(int('12'), 12)
        self.assertEqual(int('01'), 1)
    
        with self.assertRaises(ValueError):
            int('1999/12/31 21')
    

    and the test is green again

  • I remove the unittest.skip decorator from test_duration_w_date_and_time

  • then change the call in the assertion to a different function

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                self.get_difference(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                )
            )
    

    which gives me AttributeError

    AttributeError: module 'sleep_duration' has no attribute 'duration_a'...
    
  • I make a copy of the duration function in sleep_duration.py and change the name to duration_a to keep the working solution while I try a new one. I change the else block to return None

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return None
    

    and the terminal shows ValueError

    ValueError: invalid literal for int() with base 10: '1999/12/31 22'
    

    because the test calls the get_difference method in the expectation which uses the int constructor

  • I change it to return wake_time and sleep_time

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                sleep_time=sleep_time,
                wake_time=wake_time,
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration_a(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                ),
                (wake_time, sleep_time)
            )
    

    and get AssertionError

    AssertionError: None != ('1999/12/31 09:52', '1999/12/31 07:11')
    AssertionError: None != ('1999/12/31 18:16', '1999/12/31 11:21')
    AssertionError: None != ('1999/12/31 13:10', '1999/12/31 12:00')
    AssertionError: None != ('1999/12/31 16:41', '1999/12/31 12:35')
    

    I change the return statement in duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (wake_time, sleep_time)
    

    and the test passes

refactor: make it better

I want something that can read dates and times from timestamps. I search python’s online documentation for date and time to see if there is an existing solution and select the datetime module from the results. The available types in the module show datetime.datetime objects

class datetime.datetime
  A combination of a date and a time.
    Attributes: year, month, day, hour,
    minute, second, microsecond, and tzinfo.

test_datetime_objects

red: make it fail

I add a test to test_sleep_duration.py from Examples of usage: datetime for datetime.datetime objects

def test_datetime_objects(self):
    self.assertEqual(
        datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        ''
    )

def test_duration_w_date_and_time(self):
...

and the terminal shows NameError

NameError: name 'datetime' is not defined. Did you forget to import 'datetime'

green: make it pass

I add an import statement for the datetime module

import datetime
import random
import sleep_duration
import unittest
...

and the terminal shows AttributeError

AttributeError: module 'datetime' has no attribute 'strptime'

because my import statement is different from the example in the documentation, so I add the module name to the call

def test_datetime_objects(self):
    self.assertEqual(
        datetime.datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        ''
    )

and get AssertionError

AssertionError: datetime.datetime(2006, 11, 21, 16, 30) != ''

I copy the value from the left side of the AssertionError to change the expected value in the test

def test_datetime_objects(self):
    self.assertEqual(
        datetime.datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        datetime.datetime(
            2006, 11, 21, 16, 30
        )
    )

and it passes

The datetime.datetime.strptime method returns a datetime.datetime object when given 2 strings as inputs - a timestamp and a pattern that is for the timestamp. The pattern provided is

  • %d for days

  • %m for months

  • %y for 2 digit years

  • %H for hours

  • %M for minutes

there are more details in strftime() and strptime() behavior

refactor: make it better

  • I change the order in the date to test the pattern

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "06/11/21 16:30",
                "%d/%m/%y %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    and get AssertionError

    AssertionError: datetime.datetime(2021, 11, 6, 16, 30) != datetime.datetime(2006, 11, 21, 16, 30)
    

    when I change the pattern to match

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "06/11/21 16:30",
                "%y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the test passes

  • I change the year to four digits

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "2006/11/21 16:30",
                "%y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    which gives me ValueError

    ValueError: time data '2006/11/21 16:30' does not match format '%y/%m/%d %H:%M'
    

    and when I change the pattern to use %Y for the year

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "2006/11/21 16:30",
                "%Y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the terminal shows green again

  • I add calls to the datetime.datetime.strptime method in test_duration_w_date_and_time

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                (
                    datetime.datetime.strptime(
                        wake_time,
                        '%Y/%m/%d %H:%M'
                    ),
                    datetime.datetime.strptime(
                        sleep_time,
                        '%Y/%m/%d %H:%M'
                    )
                )
            )
    

    and get AssertionError

    AssertionError: Tuples differ: ('1999/12/31 07:20', '1999/12/31 03:08') != (datetime.datetime(1999, 12, 31, 7, 20), datetime.datetime(1999, 12, 31, 3, 8))
    AssertionError: Tuples differ: ('1999/12/31 15:01', '1999/12/31 00:37') != (datetime.datetime(1999, 12, 31, 15, 1), datetime.datetime(1999, 12, 31, 0, 37))
    AssertionError: Tuples differ: ('1999/12/31 20:50', '1999/12/31 14:22') != (datetime.datetime(1999, 12, 31, 20, 50), [35 chars] 22))
    AssertionError: Tuples differ: ('1999/12/31 16:40', '1999/12/31 13:39') != (datetime.datetime(1999, 12, 31, 16, 40), [35 chars] 39))
    

    duration_a returns the timestamps as strings and the test expects them as datetime.datetime objects. I change the return statement to match

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                datetime.datetime.strptime(
                    wake_time, '%Y/%m/%d %H:%M'
                ),
                datetime.datetime.strptime(
                    sleep_time, '%Y/%m/%d %H:%M'
                )
            )
    

    and the terminal shows NameError

    NameError: name 'datetime' is not defined. Did you forget to import 'datetime'
    

    I add an import statement to the top of sleep_duration.py

    import datetime
    
    
    def read_timestamp(timestamp=None, index=0):
    ...
    

    and the test passes

  • I just called datetime.datetime.strptime 5 times in a row with the same pattern, time to add a function to remove some repetition

    def get_datetime(timestamp):
        return datetime.datetime.strptime(
            timestamp, '%Y/%m/%d %H:%M'
        )
    
    
    def duration_a(wake_time=None, sleep_time=None):
    ...
    

    and call it in the return statement of duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                get_datetime(wake_time),
                get_datetime(sleep_time)
            )
    

    still green!

test_get_datetime

red: make it fail

I want a test for the get_datetime function so I change the name of test_datetime_objects to test_get_datetime and make it reference sleep_duration.get_datetime which calls the datetime.datetime.strptime method

def test_get_datetime(self):
    self.assertEqual(
        sleep_duration.get_datetime(
            "21/11/06 16:30"
        ),
        datetime.datetime(
            2006, 11, 21, 16, 30
        )
    )

I change the expectation to use datetime.datetime.strptime

def test_get_datetime(self):
    self.assertEqual(
        sleep_duration.get_datetime(
            "2006/11/21 16:30"
        ),
        datetime.datetime.strptime(
            '2006/11/21 16:30',
            '%Y/%m/%d %H:%M'
        )
    )

then add a variable for a random timestamp

def test_get_datetime(self):
    timestamp = random_timestamp_a()
    self.assertEqual(
        sleep_duration.get_datetime(
            timestamp
        ),
        datetime.datetime.strptime(
            '2006/11/21 16:30',
            '%Y/%m/%d %H:%M'
        )
    )

and get AssertionError

AssertionError: datetime.datetime(1999, 12, 31, 0, 23) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 8, 55) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 9, 16) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 15, 5) != datetime.datetime(2006, 11, 21, 16, 30)

green: make it pass

I add the variable to the expectation

def test_get_datetime(self):
    timestamp = random_timestamp_a()
    self.assertEqual(
        sleep_duration.get_datetime(
            timestamp
        ),
        datetime.datetime.strptime(
            timestamp,
            '%Y/%m/%d %H:%M'
        )
    )

and the terminal shows green again

refactor: make it better

I change the calls to datetime.datetime.strptime to sleep_duration.get_datetime

def test_duration_w_date_and_time(self):
    sleep_time = random_timestamp_a()
    wake_time = random_timestamp_a()

    while wake_time < sleep_time:
        self.assertWakeTimeEarlier(
            wake_time=wake_time,
            sleep_time=sleep_time
        )
        wake_time = random_timestamp_a()
    else:
        self.assertEqual(
            sleep_duration.duration_a(
                sleep_time=sleep_time,
                wake_time=wake_time
            ),
            (
                sleep_duration.get_datetime(
                    wake_time
                ),
                sleep_duration.get_datetime(
                    sleep_time
                )
            )
        )

and the test is still green


  • I change the expectation in the test to the difference between the timestamps because I can do arithmetic with datetime.datetime objects

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                (
                    sleep_duration.get_datetime(
                        wake_time
                    )
                  - sleep_duration.get_datetime(
                        sleep_time
                    )
                )
            )
    

    and get AssertionError

    AssertionError: (datetime.datetime(1999, 12, 31, 16, 1), datetime.datetime(1999, 12, 31, 14, 6)) != datetime.timedelta(seconds=6900)
    AssertionError: (datetime.datetime(1999, 12, 31, 9, 57), datetime.datetime(1999, 12, 31, 1, 3)) != datetime.timedelta(seconds=32040)
    AssertionError: (datetime.datetime(1999, 12, 31, 23, 59),[35 chars], 1)) != datetime.timedelta(seconds=7080)
    AssertionError: (datetime.datetime(1999, 12, 31, 16, 1), [35 chars] 55)) != datetime.timedelta(seconds=7560)
    

    the duration_a function returns datetime.datetime objects and the test expects a datetime.timedelta object. I change it to match the expectation

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                get_datetime(wake_time)
              - get_datetime(sleep_time)
            )
    

    and the test passes

  • I add the str constructor to the expectation in the test because I want the result as a string not a datetime.timedelta object

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                str(
                    sleep_duration.get_datetime(
                        wake_time
                    )
                  - sleep_duration.get_datetime(
                        sleep_time
                    )
                )
            )
    

    and the terminal shows AssertionError

    AssertionError: datetime.timedelta(seconds=4440) != '1:14:00'
    AssertionError: datetime.timedelta(seconds=15780) != '4:23:00'
    AssertionError: datetime.timedelta(seconds=31020) != '8:37:00'
    AssertionError: datetime.timedelta(seconds=49920) != '13:52:00'
    

    when I make the same change to the return statement in duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return str(
                get_datetime(wake_time)
              - get_datetime(sleep_time)
            )
    

    the test passes

  • I remove duration because duration_a is a better solution

  • which means I can remove read_timestamp because no one calls it anymore. The terminal shows AttributeError

    AttributeError: module 'sleep_duration' has no attribute 'duration'...
    
  • I change the name of duration_a to duration in sleep_duration.py and test_sleep_duration.py which leaves me with ValueError

    ValueError: time data '04:51' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '13:35' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '12:26' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '23:20' does not match format '%Y/%m/%d %H:%M'
    

    test_duration_w_hours_and_minutes does not have dates in its timestamps. I remove it because it is covered by test_duration_w_date_and_time

  • then remove get_difference because no one calls it anymore

  • I also remove the following tests because they do not test the solution directly

    • test_the_modulo_operation

    • test_floor_aka_integer_division

    • test_converting_strings_to_numbers

    • test_string_splitting

  • and remove random_timestamp because no one calls it anymore

  • then change the name of random_timestamp_a to random_timestamp

  • the new random_timestamp function always returns timestamps with the same date, I change it to return random dates as well

    def random_timestamp(date):
        return (
            f'{random.randint(0,9999):04}/'
            f'{random.randint(1,12):02}/'
            f'{random.randint(1,31):02} '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    

    and get a random ValueError

    ValueError: day is out of range for month
    

    I need to make sure the random dates that are made by the function are real dates

  • I change the name of random_timestamp to get_random_timestamp and make a new function to make sure the random timestamps generated are good

    def get_random_timestamp():
        return (
            f'{random.randint(0,9999):04}/'
            f'{random.randint(1,12):02}/'
            f'{random.randint(1,31):02} '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    
    
    def random_timestamp():
        result = get_random_timestamp()
        try:
            sleep_duration.get_datetime(result)
        except ValueError:
            return random_timestamp()
        else:
            return result
    

    The new random_timestamp function does the following

    • generates a random timestamp by calling get_random_timestamp

    • checks if the timestamp is good by calling sleep_duration.get_datetime

    • if the timestamp is good, the function returns it

    • if the timestamp is bad, it raises ValueError and repeats the process by calling itself

  • I can add another function to remove some repetition

    def random_number(start, end, digits=2):
        return f'{random.randint(start, end):0{digits}}'
    

    then add calls to it in get_random_timestamp

    def get_random_timestamp():
        return (
            f'{random_number(0,9999,4)}/'
            f'{random_number(1,12)}/'
            f'{random_number(1,31)} '
            f'{random_number(0,23)}:'
            f'{random_number(0,59)}'
        )
    

    all tests are still green

  • I change test_duration_w_date_and_time to test_duration and the terminal shows all tests are still passing

review

The challenge was to write a program that calculates the difference between a given wake and sleep time. I ran the following tests to get something that does it

I also ran into the following Exceptions

Would you like to write the solution without looking at test_sleep_duration.py?


how to measure sleep duration: tests and solution