Skip to content

Commit

Permalink
Support add/divide for weeks
Browse files Browse the repository at this point in the history
  • Loading branch information
bonjourmauko committed Aug 3, 2022
1 parent 51e7afe commit 39ed5ad
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 83 deletions.
24 changes: 24 additions & 0 deletions openfisca_core/periods/period_.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,30 @@ def stop(self) -> Instant:

# Reference periods

@property
def last_week(self) -> Period:
return self.first_week.offset(-1)

@property
def last_fortnight(self) -> Period:
start: Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 1)).offset(-2)

@property
def last_2_weeks(self) -> Period:
start: Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 2)).offset(-2)

@property
def last_26_weeks(self) -> Period:
start: Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 26)).offset(-26)

@property
def last_52_weeks(self) -> Period:
start: Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 52)).offset(-52)

@property
def last_month(self) -> Period:
return self.first_month.offset(-1)
Expand Down
70 changes: 0 additions & 70 deletions openfisca_core/periods/tests/period/test_properties.py

This file was deleted.

67 changes: 67 additions & 0 deletions openfisca_core/periods/tests/test_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,73 @@ def test_str_with_weekdays(date_unit, instant, size, expected):
assert str(Period((date_unit, instant, size))) == expected


@pytest.mark.parametrize("date_unit, instant, size, expected", [
[DateUnit.YEAR, Instant((2022, 12, 1)), 1, 1],
[DateUnit.YEAR, Instant((2022, 1, 1)), 2, 2],
])
def test_size_in_years(date_unit, instant, size, expected):
period = Period((date_unit, instant, size))
assert period.size_in_years == expected


@pytest.mark.parametrize("date_unit, instant, size, expected", [
[DateUnit.YEAR, Instant((2020, 1, 1)), 1, 12],
[DateUnit.YEAR, Instant((2022, 1, 1)), 2, 24],
[DateUnit.MONTH, Instant((2012, 1, 3)), 3, 3],
])
def test_size_in_months(date_unit, instant, size, expected):
period = Period((date_unit, instant, size))
assert period.size_in_months == expected


@pytest.mark.parametrize("date_unit, instant, size, expected", [
[DateUnit.YEAR, Instant((2022, 12, 1)), 1, 365],
[DateUnit.YEAR, Instant((2020, 1, 1)), 1, 366],
[DateUnit.YEAR, Instant((2022, 1, 1)), 2, 730],
[DateUnit.MONTH, Instant((2022, 12, 1)), 1, 31],
[DateUnit.MONTH, Instant((2020, 2, 3)), 1, 29],
[DateUnit.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31],
[DateUnit.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31],
[DateUnit.DAY, Instant((2022, 12, 31)), 1, 1],
[DateUnit.DAY, Instant((2022, 12, 31)), 3, 3],
[DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7],
[DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21],
[DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1],
[DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3],
])
def test_size_in_days(date_unit, instant, size, expected):
period = Period((date_unit, instant, size))
assert period.size_in_days == expected
assert period.size_in_days == period.days


@pytest.mark.parametrize("date_unit, instant, size, expected", [
[DateUnit.YEAR, Instant((2022, 12, 1)), 1, 52],
[DateUnit.YEAR, Instant((2020, 1, 1)), 5, 261],
[DateUnit.WEEK, Instant((2022, 12, 31)), 1, 1],
[DateUnit.WEEK, Instant((2022, 12, 31)), 3, 3],
])
def test_size_in_weeks(date_unit, instant, size, expected):
period = Period((date_unit, instant, size))
assert period.size_in_weeks == expected


@pytest.mark.parametrize("date_unit, instant, size, expected", [
[DateUnit.YEAR, Instant((2022, 12, 1)), 1, 364],
[DateUnit.YEAR, Instant((2020, 1, 1)), 1, 364],
[DateUnit.YEAR, Instant((2022, 1, 1)), 2, 728],
[DateUnit.DAY, Instant((2022, 12, 31)), 1, 1],
[DateUnit.DAY, Instant((2022, 12, 31)), 3, 3],
[DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7],
[DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21],
[DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1],
[DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3],
])
def test_size_in_weekdays(date_unit, instant, size, expected):
period = Period((date_unit, instant, size))
assert period.size_in_weekdays == expected


@pytest.mark.parametrize("period_unit, sub_unit, instant, start, cease, count", [
[DateUnit.YEAR, DateUnit.YEAR, Instant((2022, 12, 31)), Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3],
[DateUnit.YEAR, DateUnit.MONTH, Instant((2022, 12, 31)), Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36],
Expand Down
52 changes: 39 additions & 13 deletions openfisca_core/simulations/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,31 @@ def calculate_add(self, variable_name, period):
if period is not None and not isinstance(period, Period):
period = periods.period(period)

# Rule out incompatible periods.
if variable.definition_period in (DateUnit.MONTH, DateUnit.DAY) and period.unit == DateUnit.WEEK:
raise ValueError("Unable to compute variable '{0}' for period {1}, as {1} and {2} are incompatible periods. You can however change the requested period to 'period.this_year'.".format(
variable.name,
period,
variable.definition_period
))

if variable.definition_period in (DateUnit.WEEK, DateUnit.WEEKDAY) and period.unit == DateUnit.MONTH:
raise ValueError("Unable to compute variable '{0}' for period {1}, as {1} and {2} are incompatible periods. You can however change the requested period to 'period.this_year' or 'period.first_week'.".format(
variable.name,
period,
variable.definition_period
))

# Check that the requested period matches definition_period
if periods.unit_weight(variable.definition_period) > periods.unit_weight(period.unit):
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' can only be computed for {2}-long periods. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format(
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' can only be computed for {2}-long periods. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, the weekly value by 7, or change the requested period to 'period.this_year' or 'period.first_week'.".format(
variable.name,
period,
variable.definition_period
))

if variable.definition_period not in [DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR]:
raise ValueError("Unable to sum constant variable '{}' over period {}: only variables defined daily, monthly, or yearly can be summed over time.".format(
if variable.definition_period not in (DateUnit.isoformat + DateUnit.isocalendar):
raise ValueError("Unable to sum constant variable '{}' over period {}: eternal variables can't be summed over time.".format(
variable.name,
period))

Expand All @@ -176,19 +191,24 @@ def calculate_divide(self, variable_name, period):
period = periods.period(period)

# Check that the requested period matches definition_period
if variable.definition_period != DateUnit.YEAR:
raise ValueError("Unable to divide the value of '{}' over time on period {}: only variables defined yearly can be divided over time.".format(
if variable.definition_period not in (DateUnit.YEAR, DateUnit.WEEK):
raise ValueError("Unable to divide the value of '{}' over time on period {}: only variables defined yearly or weekly can be divided over time.".format(
variable_name,
period))

if period.size != 1:
raise ValueError("DIVIDE option can only be used for a one-year or a one-month requested period")
if period.unit not in (DateUnit.YEAR, DateUnit.MONTH, DateUnit.WEEKDAY) or period.size != 1:
raise ValueError("DIVIDE option can only be used for a one-year, one-month, or one-weekday requested period")

if period.unit == DateUnit.YEAR:
return self.calculate(variable_name, period)

if period.unit == DateUnit.MONTH:
elif period.unit == DateUnit.MONTH:
computation_period = period.this_year
return self.calculate(variable_name, period = computation_period) / 12.
elif period.unit == DateUnit.YEAR:
return self.calculate(variable_name, period)

elif period.unit == DateUnit.WEEKDAY:
computation_period = period.first_week
return self.calculate(variable_name, period = computation_period) / 7.

raise ValueError("Unable to divide the value of '{}' to match period {}.".format(
variable_name,
Expand Down Expand Up @@ -240,14 +260,20 @@ def _check_period_consistency(self, period, variable):
if variable.definition_period == DateUnit.ETERNITY:
return # For variables which values are constant in time, all periods are accepted

if variable.definition_period == DateUnit.YEAR and period.unit != DateUnit.YEAR:
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format(
variable.name,
period
))

if variable.definition_period == DateUnit.MONTH and period.unit != DateUnit.MONTH:
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole month. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_month'.".format(
variable.name,
period
))

if variable.definition_period == DateUnit.YEAR and period.unit != DateUnit.YEAR:
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format(
if variable.definition_period == DateUnit.WEEK and period.unit != DateUnit.WEEK:
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole week. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_week'.".format(
variable.name,
period
))
Expand All @@ -256,7 +282,7 @@ def _check_period_consistency(self, period, variable):
raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole {2}. You can use the ADD option to sum '{0}' over the requested period.".format(
variable.name,
period,
DateUnit.MONTH if variable.definition_period == DateUnit.MONTH else DateUnit.YEAR
variable.definition_period
))

def _cast_formula_result(self, value, variable):
Expand Down

0 comments on commit 39ed5ad

Please sign in to comment.