def test_portion_overlapping_with_month(self): self.assertEqual( None, self.negative_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( None, self.zero_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( self.one_day_range, self.one_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( self.single_month_range, self.single_month_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 3, 1)), self.multi_month_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 3, 1), upper_bound_exclusive_date=datetime.date(2019, 4, 1)), self.multi_month_range.portion_overlapping_with_month(2019, 3)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 4, 1), upper_bound_exclusive_date=datetime.date(2019, 4, 10)), self.multi_month_range.portion_overlapping_with_month(2019, 4))
def is_fully_incarcerated_for_range(self, range_to_cover: DateRange) -> bool: """Returns True if this person is incarcerated for the full duration of the date range.""" months_range_overlaps = range_to_cover.get_months_range_overlaps_at_all( ) if not months_range_overlaps: return False months_without_complete_incarceration = [] for year, month in months_range_overlaps: was_incarcerated_all_month = ( year, month) in self.months_fully_incarcerated if not was_incarcerated_all_month: months_without_complete_incarceration.append((year, month)) for year, month in months_without_complete_incarceration: overlapping_periods = self.month_to_overlapping_incarceration_periods[ year][month] range_portion_overlapping_month = range_to_cover.portion_overlapping_with_month( year, month) if not range_portion_overlapping_month: raise ValueError( f'Expecting only months that overlap with the range, month ({year}, {month}) does not.' ) remaining_ranges_to_cover = self._get_portions_of_range_not_covered_by_periods_subset( range_portion_overlapping_month, overlapping_periods) if remaining_ranges_to_cover: return False return True
def setUp(self) -> None: self.negative_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 2), ) self.zero_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 3), ) self.one_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 4), ) self.single_month_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 1), upper_bound_exclusive_date=datetime.date(2019, 3, 1), ) self.multi_month_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 4, 10), )
def test_exactly_overlapping_ranges(self): range_1 = DateRange.for_month(2019, 2) range_2 = DateRange.for_month(2019, 2) time_range_diff = DateRangeDiff(range_1, range_2) self.assertEqual(range_1, time_range_diff.overlapping_range) self.assertEqual([], time_range_diff.range_1_non_overlapping_parts) self.assertEqual([], time_range_diff.range_2_non_overlapping_parts)
def test_non_overlapping_ranges(self) -> None: range_1 = DateRange.for_month(2019, 2) range_2 = DateRange.for_month(2019, 3) time_range_diff = DateRangeDiff(range_1, range_2) self.assertEqual(None, time_range_diff.overlapping_range) self.assertEqual([range_1], time_range_diff.range_1_non_overlapping_parts) self.assertEqual([range_2], time_range_diff.range_2_non_overlapping_parts)
def test_partially_overlapping_ranges(self): range_1 = DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 5, 2)) range_2 = DateRange(datetime.date(2019, 3, 1), datetime.date(2019, 6, 5)) time_range_diff = DateRangeDiff(range_1, range_2) self.assertEqual( DateRange(datetime.date(2019, 3, 1), datetime.date(2019, 5, 2)), time_range_diff.overlapping_range) self.assertEqual( [DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 3, 1))], time_range_diff.range_1_non_overlapping_parts) self.assertEqual( [DateRange(datetime.date(2019, 5, 2), datetime.date(2019, 6, 5))], time_range_diff.range_2_non_overlapping_parts) time_range_diff = DateRangeDiff(range_2, range_1) self.assertEqual( DateRange(datetime.date(2019, 3, 1), datetime.date(2019, 5, 2)), time_range_diff.overlapping_range) self.assertEqual( [DateRange(datetime.date(2019, 5, 2), datetime.date(2019, 6, 5))], time_range_diff.range_1_non_overlapping_parts) self.assertEqual( [DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 3, 1))], time_range_diff.range_2_non_overlapping_parts)
def _months_excluded_from_supervision_population( self) -> Set[Tuple[int, int]]: """For each StateIncarcerationPeriod, identifies months where the person was incarcerated for every day during that month. Returns a set of months in the format (year, month) for which the person spent the entire month in a prison, where the incarceration prevents the person from being counted simultaneously in the supervision population. """ months_excluded_from_supervision_population: Set[Tuple[int, int]] = set() for (incarceration_period ) in self.incarceration_periods_not_under_supervision_authority: months_overlaps_at_all = (incarceration_period.duration. get_months_range_overlaps_at_all()) for year, month in months_overlaps_at_all: overlapping_periods = ( self. month_to_overlapping_ips_not_under_supervision_authority[ year][month]) remaining_ranges_to_cover = ( self._get_portions_of_range_not_covered_by_periods_subset( DateRange.for_month(year, month), overlapping_periods)) if not remaining_ranges_to_cover: months_excluded_from_supervision_population.add( (year, month)) return months_excluded_from_supervision_population
def run_is_excluded_from_supervision_population_for_range_check( self, incarceration_periods: List[StateIncarcerationPeriod], range_start_num_days_from_periods_start: int, range_end_num_days_from_periods_end: int, is_excluded_from_supervision_population: bool, ): """Runs a test for the is_excluded_from_supervision_population function with the given parameters.""" period_range_start = incarceration_periods[0].admission_date if not period_range_start: raise ValueError("Expected admission date") period_range_end = date_or_tomorrow(incarceration_periods[-1].release_date) lower_bound_inclusive = period_range_start + timedelta( days=range_start_num_days_from_periods_start ) upper_bound_exclusive = period_range_end + timedelta( days=range_end_num_days_from_periods_end ) index = IncarcerationPeriodIndex(incarceration_periods) time_range = DateRange( lower_bound_inclusive_date=lower_bound_inclusive, upper_bound_exclusive_date=upper_bound_exclusive, ) if is_excluded_from_supervision_population: self.assertTrue( index.is_excluded_from_supervision_population_for_range(time_range) ) else: self.assertFalse( index.is_excluded_from_supervision_population_for_range(time_range) )
def run_is_fully_incarcerated_for_range_check( self, incarceration_periods: List[StateIncarcerationPeriod], range_start_num_days_from_periods_start: int, range_end_num_days_from_periods_end: int, is_fully_incarcerated: bool): period_range_start = incarceration_periods[0].admission_date if not period_range_start: raise ValueError("Expected admission date") period_range_end = date_or_tomorrow( incarceration_periods[-1].release_date) lower_bound_inclusive = period_range_start + timedelta( days=range_start_num_days_from_periods_start) upper_bound_exclusive = period_range_end + timedelta( days=range_end_num_days_from_periods_end) index = IncarcerationPeriodIndex(incarceration_periods) time_range = DateRange( lower_bound_inclusive_date=lower_bound_inclusive, upper_bound_exclusive_date=upper_bound_exclusive) if is_fully_incarcerated: self.assertTrue(index.is_fully_incarcerated_for_range(time_range)) else: self.assertFalse(index.is_fully_incarcerated_for_range(time_range))
def test_for_year_of_date_early_month(self) -> None: year_range = DateRange.for_year_of_date(datetime.date(2019, 1, 25)) self.assertEqual( (datetime.date(2019, 1, 1), datetime.date(2020, 1, 1)), ( year_range.lower_bound_inclusive_date, year_range.upper_bound_exclusive_date, ), )
def test_for_year_of_date(self): year_range = DateRange.for_year_of_date(datetime.date(2019, 12, 4)) self.assertEqual( (datetime.date(2019, 1, 1), datetime.date(2020, 1, 1)), ( year_range.lower_bound_inclusive_date, year_range.upper_bound_exclusive_date, ), )
def duration(self) -> DateRange: """Generates a DateRange for the days covered by the supervision period. Since DateRange is never open, if the supervision period is still active, then the exclusive upper bound of the range is set to tomorrow. """ if not self.start_date: raise ValueError( f'Expected start date for period {self.supervision_period_id}, found None' ) return DateRange.from_maybe_open_range(start_date=self.start_date, end_date=self.termination_date)
def test_range_fully_overlaps_other(self) -> None: range_1 = DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 5, 2)) range_2 = DateRange(datetime.date(2019, 3, 1), datetime.date(2019, 3, 5)) time_range_diff = DateRangeDiff(range_1, range_2) self.assertEqual(range_2, time_range_diff.overlapping_range) self.assertEqual( [ DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 3, 1)), DateRange(datetime.date(2019, 3, 5), datetime.date(2019, 5, 2)), ], time_range_diff.range_1_non_overlapping_parts, ) self.assertEqual([], time_range_diff.range_2_non_overlapping_parts) time_range_diff = DateRangeDiff(range_2, range_1) self.assertEqual(range_2, time_range_diff.overlapping_range) self.assertEqual([], time_range_diff.range_1_non_overlapping_parts) self.assertEqual( [ DateRange(datetime.date(2019, 2, 5), datetime.date(2019, 3, 1)), DateRange(datetime.date(2019, 3, 5), datetime.date(2019, 5, 2)), ], time_range_diff.range_2_non_overlapping_parts, )
def test_no_periods(self): index = IncarcerationPeriodIndex([]) self.assertFalse( index.is_fully_incarcerated_for_range( DateRange(lower_bound_inclusive_date=date(2019, 1, 2), upper_bound_exclusive_date=date(2020, 2, 1)))) self.assertFalse( index.is_fully_incarcerated_for_range( DateRange(lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 2, 1)))) self.assertFalse( index.is_fully_incarcerated_for_range( DateRange(lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 1, 2)))) self.assertFalse( index.is_fully_incarcerated_for_range( DateRange(lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 1, 1))))
def violation_history_window_pre_commitment_from_supervision( self, admission_date: datetime.date, sorted_and_filtered_violation_responses: List[ StateSupervisionViolationResponse], default_violation_history_window_months: int, ) -> DateRange: """Returns the window of time before a commitment from supervision in which we should consider violations for the violation history prior to the admission. Default behavior is to use the date of the last violation response recorded prior to the |admission_date| as the upper bound of the window, with a lower bound that is |default_violation_history_window_months| before that date. Should be overridden by state-specific implementations if necessary. """ # We will use the date of the last response prior to the admission as the # window cutoff. responses_before_admission = violation_responses_in_window( violation_responses=sorted_and_filtered_violation_responses, upper_bound_exclusive=admission_date + relativedelta(days=1), lower_bound_inclusive=None, ) violation_history_end_date = admission_date if responses_before_admission: # If there were violation responses leading up to the incarceration # admission, then we want the violation history leading up to the last # response_date instead of the admission_date on the # incarceration_period last_response = responses_before_admission[-1] if not last_response.response_date: # This should never happen, but is here to silence mypy warnings # about empty response_dates. raise ValueError( "Not effectively filtering out responses without valid" " response_dates.") violation_history_end_date = last_response.response_date violation_window_lower_bound_inclusive = ( violation_history_end_date - relativedelta(months=default_violation_history_window_months)) violation_window_upper_bound_exclusive = (violation_history_end_date + relativedelta(days=1)) return DateRange( lower_bound_inclusive_date=violation_window_lower_bound_inclusive, upper_bound_exclusive_date=violation_window_upper_bound_exclusive, )
def is_excluded_from_supervision_population_for_range( self, range_to_cover: DateRange) -> bool: """Returns True if this person is incarcerated for the full duration of the date range.""" months_range_overlaps = range_to_cover.get_months_range_overlaps_at_all( ) if not months_range_overlaps: return False months_without_exclusion_from_supervision = [] for year, month in months_range_overlaps: was_incarcerated_all_month = ( year, month, ) in self.months_excluded_from_supervision_population if not was_incarcerated_all_month: months_without_exclusion_from_supervision.append((year, month)) for year, month in months_without_exclusion_from_supervision: overlapping_periods = ( self.month_to_overlapping_ips_not_under_supervision_authority[ year][month]) range_portion_overlapping_month = ( range_to_cover.portion_overlapping_with_month(year, month)) if not range_portion_overlapping_month: raise ValueError( f"Expecting only months that overlap with the range, month ({year}, {month}) does not." ) remaining_ranges_to_cover = ( self._get_portions_of_range_not_covered_by_periods_subset( range_portion_overlapping_month, overlapping_periods)) if remaining_ranges_to_cover: return False return True
def test_no_periods(self): index = IncarcerationPeriodIndex([]) self.assertFalse( index.is_excluded_from_supervision_population_for_range( DateRange( lower_bound_inclusive_date=date(2019, 1, 2), upper_bound_exclusive_date=date(2020, 2, 1), ) ) ) self.assertFalse( index.is_excluded_from_supervision_population_for_range( DateRange( lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 2, 1), ) ) ) self.assertFalse( index.is_excluded_from_supervision_population_for_range( DateRange( lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 1, 2), ) ) ) self.assertFalse( index.is_excluded_from_supervision_population_for_range( DateRange( lower_bound_inclusive_date=date(2019, 1, 1), upper_bound_exclusive_date=date(2019, 1, 1), ) ) )
def test_default_violation_history_window_pre_commitment_from_supervision_filter_after( self, ): state_code = "US_XX" supervision_violation_response_1 = ( StateSupervisionViolationResponse.new_with_defaults( state_code=state_code, supervision_violation_response_id=123, response_type=StateSupervisionViolationResponseType. VIOLATION_REPORT, response_date=date(2008, 12, 7), )) supervision_violation_response_2 = ( StateSupervisionViolationResponse.new_with_defaults( supervision_violation_response_id=234, response_type=StateSupervisionViolationResponseType. VIOLATION_REPORT, state_code=state_code, response_date=date(2009, 11, 13), )) # This is after the admission_date supervision_violation_response_3 = ( StateSupervisionViolationResponse.new_with_defaults( state_code=state_code, supervision_violation_response_id=345, response_type=StateSupervisionViolationResponseType. VIOLATION_REPORT, response_date=date(2012, 12, 1), )) violation_window = UsXxCommitmentFromSupervisionDelegate( ).violation_history_window_pre_commitment_from_supervision( admission_date=date(2009, 12, 14), sorted_and_filtered_violation_responses=[ supervision_violation_response_1, supervision_violation_response_2, supervision_violation_response_3, ], default_violation_history_window_months= VIOLATION_HISTORY_WINDOW_MONTHS, ) expected_violation_window = DateRange( lower_bound_inclusive_date=date(2008, 11, 13), upper_bound_exclusive_date=date(2009, 11, 14), ) self.assertEqual(expected_violation_window, violation_window)
def test_default_violation_history_window_pre_commitment_from_supervision_no_responses( self, ): violation_window = UsXxCommitmentFromSupervisionDelegate( ).violation_history_window_pre_commitment_from_supervision( admission_date=date(2009, 12, 14), sorted_and_filtered_violation_responses=[], default_violation_history_window_months= VIOLATION_HISTORY_WINDOW_MONTHS, ) expected_violation_window = DateRange( lower_bound_inclusive_date=date(2008, 12, 14), upper_bound_exclusive_date=date(2009, 12, 15), ) self.assertEqual(expected_violation_window, violation_window)
def test_us_nd_violation_history_window_pre_commitment_from_supervision( self, ) -> None: violation_window = UsNdCommitmentFromSupervisionDelegate( ).violation_history_window_pre_commitment_from_supervision( admission_date=date(2000, 1, 1), sorted_and_filtered_violation_responses=[], default_violation_history_window_months=0, ) expected_violation_window = DateRange( # 90 days before lower_bound_inclusive_date=date(1999, 10, 3), # 90 days, including admission_date upper_bound_exclusive_date=date(2000, 3, 31), ) self.assertEqual(expected_violation_window, violation_window)
def duration(self) -> DateRange: """Generates a DateRange for the days covered by the incarceration period. Since DateRange is never open, if the incarceration period is still active, then the exclusive upper bound of the range is set to tomorrow. """ if not self.admission_date: raise ValueError( f'Expected start date for period {self.incarceration_period_id}, found None' ) if (not self.release_date and self.status != StateIncarcerationPeriodStatus.IN_CUSTODY): raise ValueError( "Unexpected missing release date. _infer_missing_dates_and_statuses is not properly" " setting missing dates.") return DateRange.from_maybe_open_range(start_date=self.admission_date, end_date=self.release_date)
def _months_fully_incarcerated(self) -> Set[Tuple[int, int]]: """For each StateIncarcerationPeriod, identifies months where the person was incarcerated for every day during that month. Returns a set of months in the format (year, month) for which the person spent the entire month in a prison. """ months_fully_incarcerated: Set[Tuple[int, int]] = set() for incarceration_period in self.incarceration_periods: months_overlaps_at_all = incarceration_period.duration.get_months_range_overlaps_at_all( ) for year, month in months_overlaps_at_all: overlapping_periods = self.month_to_overlapping_incarceration_periods[ year][month] remaining_ranges_to_cover = self._get_portions_of_range_not_covered_by_periods_subset( DateRange.for_month(year, month), overlapping_periods) if not remaining_ranges_to_cover: months_fully_incarcerated.add((year, month)) return months_fully_incarcerated
def violation_history_window_pre_commitment_from_supervision( self, admission_date: datetime.date, sorted_and_filtered_violation_responses: List[ StateSupervisionViolationResponse], default_violation_history_window_months: int, ) -> DateRange: """For US_ND we look for violation responses with a response_date within 90 days of a commitment from supervision admission to incarceration. 90 days is an arbitrary buffer for which we accept discrepancies between the SupervisionViolationResponse response_date and the StateIncarcerationPeriod's admission_date. """ violation_window_lower_bound_inclusive = admission_date - relativedelta( days=90) violation_window_upper_bound_exclusive = admission_date + relativedelta( days=90) return DateRange( lower_bound_inclusive_date=violation_window_lower_bound_inclusive, upper_bound_exclusive_date=violation_window_upper_bound_exclusive, )
@classmethod def get_or_default(cls, text: Optional[str]) -> 'SnapshotType': if text is None: return SnapshotType.DAY return cls(text) DAY = 'DAY' FIRST_DAY_OF_MONTH = 'FIRST_DAY_OF_MONTH' LAST_DAY_OF_MONTH = 'LAST_DAY_OF_MONTH' SNAPSHOT_CONVERTERS: Dict[SnapshotType, DateRangeConverterType] = { SnapshotType.DAY: DateRange.for_day, SnapshotType.FIRST_DAY_OF_MONTH: lambda date: DateRange.for_day(first_day_of_month(date)), SnapshotType.LAST_DAY_OF_MONTH: lambda date: DateRange.for_day(last_day_of_month(date)), } class Metric: @property @abstractmethod def filters(self) -> List[Dimension]: """Any dimensions where the data only represents a subset of values for that dimension. For instance, a table for the population metric may only cover data for the prison population, not those on parole or probation. In that case filters would contain PopulationType.PRISON. """
def should_include_in_release_cohort( status: StateIncarcerationPeriodStatus, release_date: Optional[date], release_reason: Optional[ReleaseReason], next_incarceration_period: Optional[StateIncarcerationPeriod]) -> bool: """Identifies whether a period of incarceration with the given features should be included in the release cohort.""" # If the person is still in custody, there is no release to include in a cohort. if status == StateIncarcerationPeriodStatus.IN_CUSTODY: return False if not release_date: # If the person is not in custody, there should be a release_date. # This should not happen after validation. Throw error. raise ValueError("release_date is not set where it should be.") if not release_reason: # If there is no recorded release reason, then we cannot classify this as a valid release for the cohort return False if next_incarceration_period: time_range_release = DateRange.for_day(release_date) if DateRangeDiff(time_range_release, next_incarceration_period.duration).overlapping_range: # If the release overlaps with the following incarceration period, this is not an actual release from # incarceration return False if next_incarceration_period.release_date and release_date == next_incarceration_period.release_date: # This release shares a release_date with the next incarceration period. Do not include this release. return False if release_reason in [ReleaseReason.DEATH, ReleaseReason.EXECUTION]: # If the person was released from this incarceration period because they died or were executed, do not include # them in the release cohort. return False if release_reason == ReleaseReason.ESCAPE: # If the person was released from this incarceration period because they escaped, do not include them in the # release cohort. return False if release_reason == ReleaseReason.RELEASED_FROM_TEMPORARY_CUSTODY: # If the person was released from a period of temporary custody, do not include them in the release_cohort. return False if release_reason == ReleaseReason.RELEASED_IN_ERROR: # If the person was released from this incarceration period due to an error, do not include them in the # release cohort. return False if release_reason == ReleaseReason.TRANSFER: # If the person was released from this incarceration period because they were transferred elsewhere, do not # include them in the release cohort. return False if release_reason == ReleaseReason.TRANSFERRED_OUT_OF_STATE: # Releases where the person has been transferred out of state don't really count as true releases. return False if release_reason == ReleaseReason.COURT_ORDER: # If the person was released from this incarceration period due to a court order, do not include them in the # release cohort. return False if release_reason in (ReleaseReason.EXTERNAL_UNKNOWN, ReleaseReason.INTERNAL_UNKNOWN): # We do not have enough information to determine whether this release qualifies for inclusion in the release # cohort. return False if release_reason in (ReleaseReason.COMMUTED, ReleaseReason.COMPASSIONATE, ReleaseReason.CONDITIONAL_RELEASE, ReleaseReason.PARDONED, ReleaseReason.RELEASED_FROM_ERRONEOUS_ADMISSION, ReleaseReason.SENTENCE_SERVED, ReleaseReason.VACATED): return True raise ValueError("Enum case not handled for " "StateIncarcerationPeriodReleaseReason of type:" f" {release_reason}.")
class TestDateRange(unittest.TestCase): """Tests for DateRange""" def setUp(self) -> None: self.negative_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 2)) self.zero_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 3)) self.one_day_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 2, 4)) self.single_month_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 1), upper_bound_exclusive_date=datetime.date(2019, 3, 1)) self.multi_month_range = DateRange( lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 4, 10)) def test_get_months_range_overlaps_at_all(self): self.assertEqual( [], self.negative_day_range.get_months_range_overlaps_at_all()) self.assertEqual( [], self.zero_day_range.get_months_range_overlaps_at_all()) self.assertEqual([(2019, 2)], self.one_day_range.get_months_range_overlaps_at_all()) self.assertEqual( [(2019, 2)], self.single_month_range.get_months_range_overlaps_at_all()) self.assertEqual( [(2019, 2), (2019, 3), (2019, 4)], self.multi_month_range.get_months_range_overlaps_at_all()) def test_portion_overlapping_with_month(self): self.assertEqual( None, self.negative_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( None, self.zero_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( self.one_day_range, self.one_day_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( self.single_month_range, self.single_month_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 2, 3), upper_bound_exclusive_date=datetime.date(2019, 3, 1)), self.multi_month_range.portion_overlapping_with_month(2019, 2)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 3, 1), upper_bound_exclusive_date=datetime.date(2019, 4, 1)), self.multi_month_range.portion_overlapping_with_month(2019, 3)) self.assertEqual( DateRange(lower_bound_inclusive_date=datetime.date(2019, 4, 1), upper_bound_exclusive_date=datetime.date(2019, 4, 10)), self.multi_month_range.portion_overlapping_with_month(2019, 4))