コード例 #1
0
 def test_new_timezone_wt_shift_forward(self):
     new_wt = Availability.new_timezone_wt(c.sunday_0000, c.et_ds, 'UTC')
     self.assertEqual(new_wt, WeeklyTime(0, 4, 0))
     new_wt = Availability.new_timezone_wt(c.tuesday_1715, c.et_no_ds,
                                           'Asia/Tokyo')
     self.assertEqual(new_wt, WeeklyTime(3, 7, 15))
     new_wt = Availability.new_timezone_wt(c.saturday_2345,
                                           c.kathmandu_2017_end,
                                           'Australia/West')
     self.assertEqual(new_wt, WeeklyTime(0, 2, 0))
コード例 #2
0
 def test_new_timezone_wt_value_error(self):
     with self.assertRaises(ValueError):
         Availability.new_timezone_wt(WeeklyTime(0, 0, 14), c.utc_halloween,
                                      'UTC')
     with self.assertRaises(ValueError):
         Availability.new_timezone_wt(WeeklyTime(0, 0, 0), c.dt_2000_1_1,
                                      'UTC')
     with self.assertRaises(ValueError):
         Availability.new_timezone_wt(WeeklyTime(0, 0, 0), c.utc_halloween,
                                      'utc')
コード例 #3
0
 def test_new_timezone_wt_shift_backward(self):
     new_wt = Availability.new_timezone_wt(c.sunday_0000, c.et_ds,
                                           'US/Arizona')
     self.assertEqual(new_wt, WeeklyTime(6, 21, 0))
     new_wt = Availability.new_timezone_wt(c.tuesday_1715, c.kabul_2000_1_1,
                                           'US/Samoa')
     self.assertEqual(new_wt, WeeklyTime(2, 1, 45))
     new_wt = Availability.new_timezone_wt(c.thursday_0630, c.chatham_ds,
                                           'Pacific/Midway')
     self.assertEqual(new_wt, WeeklyTime(3, 5, 45))
コード例 #4
0
 def test_get_availability_matches_two_matches(self):
     student = c.new_user(c.student,
                          {'availability': c.free_first_seven_avail})
     tutor = c.new_user(c.tutor, {'availability': c.always_free_avail})
     matches = student.get_availability_matches(tutor, 1)
     correct_matches = [
         Match(student, tutor, WeeklyTime(0, 5, 0), datetime(2018, 1, 1, 5),
               1),
         Match(student, tutor, WeeklyTime(0, 5, 15),
               datetime(2018, 1, 1, 5), 1)
     ]
     self.assertEqual(matches, correct_matches)
コード例 #5
0
 def test_constants(self):
     self.assertEqual(Availability.MINUTES_PER_SLOT, 15)
     self.assertEqual(Availability.MINUTES_PER_COURSE, 90)
     self.assertEqual(Availability.SLOTS_PER_WEEK, c.SLOTS_PER_WEEK)
     self.assertEqual(Availability.SLOT_START_TIMES[0], WeeklyTime(0, 0, 0))
     self.assertEqual(Availability.SLOT_START_TIMES[-1],
                      WeeklyTime(6, 23, 45))
     self.assertEqual(
         Availability.SLOT_START_TIME_TO_INDEX[WeeklyTime(0, 0, 0)], 0)
     self.assertEqual(
         Availability.SLOT_START_TIME_TO_INDEX[WeeklyTime(6, 23, 45)],
         c.SLOTS_PER_WEEK - 1)
コード例 #6
0
 def test_shared_course_start_times_UTC_kabul_et_no_ds_overlap_greater_than_one(
         self):
     user1 = c.new_user(
         c.student, {
             'tz_str': 'Asia/Kabul',
             'availability': c.always_free_avail,
             'earliest_start_date': c.dt_2000_1_1
         })
     user2 = c.new_user(
         c.student, {
             'tz_str': 'US/Eastern',
             'availability': c.free_first_seven_avail,
             'earliest_start_date': date(2017, 3, 12)
         })
     shared = user1.shared_course_start_times_UTC(user2)
     self.assertEqual(shared, [WeeklyTime(0, 5, 0), WeeklyTime(0, 5, 15)])
コード例 #7
0
 def test_init_attributes(self):
     self.assertEqual(c.match_two_weeks.student, c.student)
     self.assertEqual(c.match_two_weeks.tutor, c.tutor)
     self.assertEqual(c.match_two_weeks.shared_courses, ['Math'])
     self.assertEqual(c.match_two_weeks.course_start_wt_UTC,
                      WeeklyTime(0, 5, 0))
     self.assertEqual(c.match_two_weeks.earliest_course_start_UTC,
                      datetime(2018, 1, 1, 5, 0))
     self.assertEqual(c.match_two_weeks.weeks_per_course, 2)
     student_course_schedule = [
         c.et.localize(datetime(2018, 1, 7, 0, 0)),
         c.et.localize(datetime(2018, 1, 14, 0, 0))
     ]
     self.assertEqual(c.match_two_weeks.student_course_schedule,
                      student_course_schedule)
     tutor_course_schedule = [
         c.kabul.localize(datetime(2018, 1, 7, 9, 30)),
         c.kabul.localize(datetime(2018, 1, 14, 9, 30))
     ]
     self.assertEqual(c.match_two_weeks.tutor_course_schedule,
                      tutor_course_schedule)
     UTC_course_schedule = [
         c.utc.localize(datetime(2018, 1, 7, 5, 0)),
         c.utc.localize(datetime(2018, 1, 14, 5, 0))
     ]
     self.assertEqual(c.match_two_weeks.UTC_course_schedule,
                      UTC_course_schedule)
コード例 #8
0
 def test_get_availability_matches_one_match_gender_compatible_shared_courses(
         self):
     matches = c.student.get_availability_matches(c.tutor, 1)
     correct_matches = [
         Match(c.student, c.tutor, WeeklyTime(0, 5, 0),
               datetime(2018, 1, 1, 5), 1)
     ]
     self.assertEqual(matches, correct_matches)
コード例 #9
0
 def test_get_availability_matches_one_match_gender_incompatible_no_shared_courses(
         self):
     student = c.new_user(c.student, {
         'gender_preference': 'FEMALE',
         'courses': ['English']
     })
     matches = student.get_availability_matches(c.tutor, 1)
     correct_matches = [
         Match(student, c.tutor, WeeklyTime(0, 5, 0),
               datetime(2018, 1, 1, 5), 1)
     ]
     self.assertEqual(matches, correct_matches)
コード例 #10
0
ファイル: match.py プロジェクト: pv711/pax-populi-scheduler
    def daylight_saving_valid(self):
        """Determines whether or not the match is valid during all weeks of the
        schedule. Even though the match will definitely be valid on 
        earliest_course_start_UTC, it is possible that the student or tutor will
        no longer be able to make the course if daylight saving occurs for the
        student or for the tutor as the course progresses. Also, some of the
        scheduled datetimes may be non-existent or ambiguous because of
        daylight saving.

        Returns:
            A boolean whether or not both the student and the tutor can make
                all courses in their respective schedules.
        """
        # Check that the student schedule is valid
        for student_dt in self.student_course_schedule:
            student_wt = WeeklyTime.from_datetime(student_dt)
            index = Availability.SLOT_START_TIME_TO_INDEX[student_wt]
            if not self.student.availability.free_course_slots[index]:
                return False
            # Check that all minutes of the class are valid
            naive_student_dt = student_dt.replace(tzinfo=None)
            for i in range(Availability.MINUTES_PER_COURSE):
                dt = naive_student_dt + timedelta(minutes=i)
                if not util.naive_dt_is_valid(dt, self.student.tz_str):
                    return False

        # Check that the tutor schedule is valid
        for tutor_dt in self.tutor_course_schedule:
            tutor_wt = WeeklyTime.from_datetime(tutor_dt)
            index = Availability.SLOT_START_TIME_TO_INDEX[tutor_wt]
            if not self.tutor.availability.free_course_slots[index]:
                return False
            # Check that all minutes of the class are valid
            naive_tutor_dt = tutor_dt.replace(tzinfo=None)
            for i in range(Availability.MINUTES_PER_COURSE):
                dt = naive_tutor_dt + timedelta(minutes=i)
                if not util.naive_dt_is_valid(dt, self.tutor.tz_str):
                    return False
        return True
コード例 #11
0
 def test_shared_course_start_times_UTC_kabul_et_no_ds_overlap_one(self):
     user1 = c.new_user(
         c.student, {
             'tz_str': 'Asia/Kabul',
             'availability': c.free_first_six_avail,
             'earliest_start_date': c.dt_2000_1_1
         })
     avail = Availability.from_dict({'6': [['14:30', '16:00']]})
     user2 = c.new_user(
         c.student, {
             'tz_str': 'US/Eastern',
             'availability': avail,
             'earliest_start_date': date(2017, 3, 12)
         })
     shared = user1.shared_course_start_times_UTC(user2)
     self.assertEqual(shared, [WeeklyTime(6, 19, 30)])
コード例 #12
0
class Availability:
    """Represents a weekly availability."""

    DAYS_PER_WEEK = 7
    HOURS_PER_DAY = 24
    MINUTES_PER_HOUR = 60
    MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR
    MINUTES_PER_SLOT = 15
    if MINUTES_PER_HOUR % MINUTES_PER_SLOT != 0:
        raise ValueError('MINUTES_PER_SLOT must be a divisor of 60')
    SLOTS_PER_HOUR = MINUTES_PER_HOUR // MINUTES_PER_SLOT
    SLOTS_PER_DAY = SLOTS_PER_HOUR * HOURS_PER_DAY
    SLOTS_PER_WEEK = MINUTES_PER_WEEK // MINUTES_PER_SLOT
    MINUTES_PER_COURSE = 90
    SLOTS_PER_COURSE = int(
        math.ceil(MINUTES_PER_COURSE / float(MINUTES_PER_SLOT)))
    SLOT_START_TIMES = []
    for day in range(DAYS_PER_WEEK):
        for hour in range(HOURS_PER_DAY):
            for k in range(SLOTS_PER_HOUR):
                SLOT_START_TIMES.append(
                    WeeklyTime(day, hour, k * MINUTES_PER_SLOT))
    SLOT_START_TIME_TO_INDEX = {}
    for i in range(SLOTS_PER_WEEK):
        SLOT_START_TIME_TO_INDEX[SLOT_START_TIMES[i]] = i

    def __init__(self, free_slots):
        """
        Args:
            free_slots: A boolean array of length SLOTS_PER_WEEK such that
                free_slots[i] is whether or not the user is free for the slot
                starting at SLOT_START_TIMES[i].
        """
        if len(free_slots) != self.SLOTS_PER_WEEK:
            raise ValueError('free_slots must have length SLOTS_PER_WEEK')
        self.free_slots = free_slots
        # boolean array such that i-th entry is whether or not user is free to
        # start a course at SLOT_START_TIMES[i]
        self.free_course_slots = []
        for i in range(self.SLOTS_PER_WEEK):
            is_free = all(self.free_slots[(i + j) % self.SLOTS_PER_WEEK]
                          for j in range(self.SLOTS_PER_COURSE))
            self.free_course_slots.append(is_free)

    def __str__(self):
        lines = []
        for i in range(self.SLOTS_PER_WEEK):
            if self.free_slots[i]:
                lines.append(
                    str(self.SLOT_START_TIMES[i]) + ' - ' +
                    str(self.SLOT_START_TIMES[(i + 1) % self.SLOTS_PER_WEEK]))
        return '\n'.join(lines)

    def __eq__(self, other):
        return self.free_slots == other.free_slots

    def __ne__(self, other):
        return not self.__eq__(other)

    @classmethod
    def time_str_to_index(cls, time_str):
        """Converts time string of the form 'HH:MM' to the corresponding index
        of SLOT_START_TIMES.

        Args:
            time_str: A string of the form 'HH:MM' representing a time with
                a specified hour and minute. Hour must be between 0 and 24,
                inclusive. (24 is allowed because of intervals like
                ['20:00','24:00'] exist in availability dictionaries.) Minute
                must be between 0 and 59, inclusive. Also the number of minutes
                elapsed between 00:00 and the time must be a multiple of 
                cls.MINUTES_PER_SLOT.

        Returns:
            An integer i such that cls.SLOT_START_TIMES[i] corresponds to
                Sunday at the time given by time_str.
        """
        if not re.match(r'[0-9][0-9]:[0-9][0-9]', time_str):
            raise ValueError('time_str must be of the form "HH:MM"')
        hours = int(time_str.split(':')[0])
        minutes = int(time_str.split(':')[1])
        if hours not in list(range(cls.HOURS_PER_DAY + 1)):
            raise ValueError('The hour part of time_str must be in range(25)')
        if minutes not in list(range(cls.MINUTES_PER_HOUR)):
            raise ValueError(
                'The minute part of time_str must be in range(60)')
        total_minutes = cls.MINUTES_PER_HOUR * hours + minutes
        if total_minutes % cls.MINUTES_PER_SLOT != 0:
            raise ValueError(
                'The number of minutes elapsed between 00:00 and time_str must be a multiple of MINUTES_PER_SLOT'
            )
        return total_minutes / cls.MINUTES_PER_SLOT

    @classmethod
    def parse_dict(cls, availability_dict):
        """
        Extracts the free time slots from an availability dictionary.

        Args:
            availability_dict: A dict mapping a day of the week index expressed
                as a string to a list of lists of length two. Each internal
                list of length two is of the form [start_time, end_time], where
                start_time and end_time are the start and end times in the form
                'HH:MM' of when the user is available, and start_time is not
                equal to end_time. Also the number of minutes elapsed between
                00:00 and each of start_time and end_time must be a multiple of
                cls.MINUTES_PER_SLOT.

                ex. {'0': [['00:00', '02:30'], ['23:00', '24:00']],
                     '4': [['17:30', '18:00']]}
                means that the user is free 12am-2:30am Sunday, 11:30pm Sunday
                to 12am Monday, and 5:30pm-6pm on Thursday.

        Returns:
            free_slots_indices: A set of indices i such that the user is free
                during the slot starting at cls.SLOT_START_TIMES[i]
        """
        for day_index_str in availability_dict:
            if day_index_str not in list(
                    map(str, list(range(cls.DAYS_PER_WEEK)))):
                raise ValueError(
                    'Each key in availability_dict must be a string form of an integer in range(7)'
                )
        free_slots_indices = set([])
        for day_str in availability_dict:
            intervals = availability_dict[day_str]
            day_slot_index = int(day_str) * cls.SLOTS_PER_DAY
            for interval in intervals:
                if len(interval) != 2:
                    raise ValueError(
                        'time interval in availability_dict must have length 2'
                    )
                if interval[0] == interval[1]:
                    raise ValueError(
                        'time interval in availability_dict must have different start time and end time'
                    )
                start_index = day_slot_index + cls.time_str_to_index(
                    interval[0])
                end_index = day_slot_index + cls.time_str_to_index(interval[1])
                free_slots_indices.update(list(range(start_index, end_index)))
        return free_slots_indices

    @classmethod
    def from_dict(cls, availability_dict):
        """Instantiates an Availability object from an availability dictionary.

        Args:
            availability_dict: A dict mapping a day of the week index expressed
                as a string to a list of lists of length two. Each internal
                list of length two is of the form [start_time, end_time], where
                start_time and end_time are the start and end times in the form
                'HH:MM' of when the user is available, and start_time is not
                equal to end_time. Also the number of minutes elapsed between
                00:00 and each of start_time and end_time must be a multiple of
                cls.MINUTES_PER_SLOT.

                ex. {'0': [['00:00', '02:30'], ['23:00', '24:00']],
                     '4': [['17:30', '18:00']]}
                means that the user is free 12am-2:30am Sunday, 11:30pm Sunday
                to 12am Monday, and 5:30pm-6pm on Thursday.

        Returns:
            An Availability object with free slots given by the intervals in
                availability_dict.
        """
        for day_index_str in availability_dict:
            if day_index_str not in list(
                    map(str, list(range(cls.DAYS_PER_WEEK)))):
                raise ValueError(
                    'Each key in availability_dict must be a string form of an integer in range(7)'
                )
        free_slots_indices = cls.parse_dict(availability_dict)
        free_slots = [(i in free_slots_indices)
                      for i in range(cls.SLOTS_PER_WEEK)]
        return cls(free_slots)

    @classmethod
    def UTC_offset_minutes(cls, aware_dt):
        """Converts a timezone-aware datetime to the number of minutes offset
        from UTC.

        Args:
            aware_dt: A timezone-aware datetime.

        Returns:
            offset_minutes: An integer representing the signed number of
                minutes that aware_dt is offset from UTC.
        """
        if aware_dt.tzinfo is None or aware_dt.tzinfo.utcoffset(
                aware_dt) is None:
            raise ValueError('aware_dt must be a timezone-aware datetime')
        offset_str = aware_dt.strftime('%z')
        minutes = cls.MINUTES_PER_HOUR * int(offset_str[1:3]) + int(
            offset_str[3:5])
        if offset_str[0] == '+':
            offset_minutes = minutes
        elif offset_str[0] == '-':
            offset_minutes = -minutes
        else:
            raise ValueError('offset_str must start with "+" or "-"')
        return offset_minutes

    @classmethod
    def new_timezone_wt(cls, wt, aware_dt, new_tz_str):
        """
        Shifts a WeeklyTime to a new timezone.

        Args:
            wt: A WeeklyTime object in SLOT_START_TIMES.
            aware_dt: A timezone-aware datetime whose timezone is the
                current timezone of wt. Also used as the reference datetime for
                the timezone conversion.
            new_tz_str: A string representing the new timezone to shift to.
                Must be in the pytz timezone database.

        Returns:
            new_wt: A WeeklyTime object that represents wt after shifting it to
                the timezone new_tz_str.
        """
        if wt not in cls.SLOT_START_TIMES:
            raise ValueError('wt must be in SLOT_START_TIMES')
        if aware_dt.tzinfo is None or aware_dt.tzinfo.utcoffset(
                aware_dt) is None:
            raise ValueError('aware_dt must be a timezone-aware datetime')
        if new_tz_str not in pytz.all_timezones_set:
            raise ValueError(
                'new_tz_str must be in the pytz timezone database')
        new_tz = pytz.timezone(new_tz_str)
        new_dt = aware_dt.astimezone(new_tz)
        forward_shift_minutes = (cls.UTC_offset_minutes(new_dt) -
                                 cls.UTC_offset_minutes(aware_dt))
        if forward_shift_minutes % cls.MINUTES_PER_SLOT != 0:
            raise ValueError(
                'MINUTES_PER_SLOT must be a divisor of forward_shift_minutes')
        n_slots = forward_shift_minutes / cls.MINUTES_PER_SLOT
        index = cls.SLOT_START_TIME_TO_INDEX[wt]
        new_wt = cls.SLOT_START_TIMES[(index + n_slots) % cls.SLOTS_PER_WEEK]
        return new_wt

    def shared_course_start_times(self, other_availability):
        """Computes weekly times during which both Availability objects are
        free to start a course.

        Args:
            other_availability: An Availability object.

        Returns:
            A list of WeeklyTime objects during which self and
                other_availability are both free to start a course.
        """
        return [
            self.SLOT_START_TIMES[i] for i in range(self.SLOTS_PER_WEEK)
            if self.free_course_slots[i]
            and other_availability.free_course_slots[i]
        ]

    def forward_shifted(self, forward_shift_minutes):
        """
        Returns a copy of self shifted forward in time.

        Args:
            forward_shift_minutes: An integer representing the number of
                minutes to shift self forward in time. This must be a multiple
                of self.MINUTES_PER_SLOT.

        Returns:
            shifted_availability: An Availability object representing self
                after shifting all time slots forward by forward_shift_minutes
                minutes. For example, if the user is free 1pm-2pm on Tuesday
                and forward_shift_minutes == 75, then the returned Availability
                object will be free 2:15pm-3:15pm on Tuesday. If the user is
                free 1pm-2pm on Tuesday and forward_shift_minutes == -60, then
                the returned Availability object will be free 12pm-1pm on
                Tuesday. 
        """
        if forward_shift_minutes % self.MINUTES_PER_SLOT != 0:
            raise ValueError(
                'forward_shift_minutes must be a multiple of MINUTES_PER_SLOT')
        n_slots = forward_shift_minutes / self.MINUTES_PER_SLOT
        shifted_free_slots = [
            self.free_slots[(i - n_slots) % self.SLOTS_PER_WEEK]
            for i in range(self.SLOTS_PER_WEEK)
        ]
        shifted_availability = Availability(shifted_free_slots)
        return shifted_availability

    def new_timezone(self, current_tz_str, new_tz_str, naive_dt_in_new_tz):
        """Returns a copy of self after shifting to a new timezone.

        Args:
            current_tz_str: A string representing the timezone of self.
                Must be in the pytz timezone database.
            new_tz_str: A string representing the new timezone to shift to.
                Must be in the pytz timezone database.
            naive_dt_in_new_tz: An naive datetime object that provides the
                reference time in the timezone new_tz_str with which to
                calculate UTC offsets. Must be a valid (neither non-existent
                nor ambiguous) in the timezone new_tz_str.

        Returns:
            An Availability object that represents self after shifting from the
                timezone current_tz_str to the timezone new_tz_str on the
                datetime naive_dt_in_new_tz in new_tz_str.
        """
        if current_tz_str not in pytz.all_timezones_set:
            raise ValueError(
                'current_tz_str must be in the pytz timezone database')
        if new_tz_str not in pytz.all_timezones_set:
            raise ValueError(
                'new_tz_str must be in the pytz timezone database')
        if (naive_dt_in_new_tz.tzinfo is not None
                and naive_dt_in_new_tz.tzinfo.utcoffset(naive_dt_in_new_tz)
                is not None):
            raise ValueError('naive_dt_in_new_tz must be a naive datetime')
        if not util.naive_dt_is_valid(naive_dt_in_new_tz, new_tz_str):
            raise ValueError(
                'naive_dt_in_new_tz must be a valid datetime in the timezone new_tz_str'
            )
        current_tz = pytz.timezone(current_tz_str)
        new_tz = pytz.timezone(new_tz_str)
        dt_new_tz = new_tz.localize(naive_dt_in_new_tz)
        dt_current_tz = dt_new_tz.astimezone(current_tz)
        forward_shift_minutes = (self.UTC_offset_minutes(dt_new_tz) -
                                 self.UTC_offset_minutes(dt_current_tz))
        return self.forward_shifted(forward_shift_minutes)
コード例 #13
0
 def test_shared_course_start_times_overlap_greater_than_one(self):
     times = c.always_free_avail.shared_course_start_times(
         c.free_first_seven_avail)
     self.assertEqual(times, [WeeklyTime(0, 0, 0), WeeklyTime(0, 0, 15)])
コード例 #14
0
 def test_sunday_0000_equals_sunday_0000(self):
     self.assertEqual(c.sunday_0000, WeeklyTime(0, 0, 0))
コード例 #15
0
 def test_shared_course_start_times_overlap_one(self):
     times = c.always_free_avail.shared_course_start_times(
         c.free_sat_sun_six_avail)
     self.assertEqual(times, [WeeklyTime(6, 23, 15)])
コード例 #16
0
 def test_from_datetime_2017_01_29_0059(self):
     self.assertEqual(WeeklyTime.from_datetime(c.dt_2017_01_29_0059),
                      c.sunday_0059)
コード例 #17
0
 def test_from_datetime_2001_09_10(self):
     self.assertEqual(WeeklyTime.from_datetime(c.dt_2001_09_10),
                      c.monday_0000)
コード例 #18
0
 def test_first_datetime_after_six_day_difference(self):
     wt = WeeklyTime(1, 0, 0)
     first_dt_after = wt.first_datetime_after(datetime(2017, 1, 31, 17, 44))
     self.assertEqual(first_dt_after, datetime(2017, 2, 6, 0, 0))
コード例 #19
0
from availability import Availability
from match import Match
from user import User
from weekly_time import WeeklyTime

# Numerical constants
SLOTS_PER_WEEK = 672
MINUTES_PER_HOUR = 60
HOURS_PER_DAY = 24
DAYS_PER_WEEK = 7
MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
MINUTES_PER_WEEK = MINUTES_PER_DAY * DAYS_PER_WEEK

# WeeklyTime objects
sunday_0000 = WeeklyTime(0, 0, 0)
sunday_0115 = WeeklyTime(0, 1, 15)
sunday_0215 = WeeklyTime(0, 2, 15)
sunday_0059 = WeeklyTime(0, 0, 59)
sunday_2300 = WeeklyTime(0, 23, 0)
monday_0000 = WeeklyTime(1, 0, 0)
tuesday_1715 = WeeklyTime(2, 17, 15)
thursday_0630 = WeeklyTime(4, 6, 30)
saturday_0000 = WeeklyTime(6, 0, 0)
saturday_2345 = WeeklyTime(6, 23, 45)

# Free slot boolean arrays
always_free_slots = [True for i in range(SLOTS_PER_WEEK)]
free_first_five_slots = [
    True if i < 5 else False for i in range(SLOTS_PER_WEEK)
]