Skip to content

Commit

Permalink
Create AgeRange class for age ranges #2714
Browse files Browse the repository at this point in the history
  • Loading branch information
iamleeg committed Jul 27, 2022
1 parent d0bb66a commit ad2fc93
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 10 deletions.
41 changes: 41 additions & 0 deletions data-serving/reusable-data-service/data_service/model/age_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import dataclasses

from data_service.model.document import Document
from data_service.util.errors import ValidationError

@dataclasses.dataclass
class AgeRange(Document):
"""I represent a numerical range within which a person's age lies (inclusive of both limits).
To avoid reidentifying people who have been anonymised by this
application, I will only tell you their age to within five years (unless they are infants)."""
lower: int = None
upper: int = None

def __post_init__(self):
"""Massage the supplied lower and upper bounds to fit our requirements. That doesn't
preclude somebody changing the values after initialisation so please do remember to
validate() me."""
if self.lower is not None and self.lower != 0:
self.lower = (self.lower // 5) * 5 + 1
if self.upper is not None and self.upper != 1 and self.upper % 5 != 0:
self.upper = ((self.upper // 5) + 1) * 5

def validate(self):
"""I must represent the range [0,1], or a range greater than five years, and must
have a positive lower bound and an upper bound below 121."""
if self.lower is None:
raise ValidationError("Age Range must have a lower bound")
if self.upper is None:
raise ValidationError("Age Range must have an upper bound")
if self.lower < 0:
raise ValidationError(f"Lower bound {self.lower} is below the minimum permissible 0")
if self.upper < 1:
raise ValidationError(f"Upper bound {self.upper} is below the minimum permissible 1")
if self.upper > 120:
raise ValidationError(f"Upper bound {self.upper} is above the maximum permissible 120")
# deal with the special case first
if self.lower == 0 and self.upper == 1:
return
if self.upper - self.lower < 4:
# remember range is inclusive of bounds so e.g. 1-5 is five years
raise ValidationError(f"Range [{self.lower}, {self.upper}] is too small")
68 changes: 68 additions & 0 deletions data-serving/reusable-data-service/tests/test_age_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import pytest

from data_service.model.age_range import AgeRange
from data_service.util.errors import ValidationError

from tests.util import does_not_raise


def test_age_range_massages_input_to_match_buckets():
ages = AgeRange(2,6)
assert ages.lower == 1
assert ages.upper == 10


def test_age_range_leaves_input_if_it_is_already_on_bucket_boundary():
ages = AgeRange(6, 10)
assert ages.lower == 6
assert ages.upper == 10


def test_age_range_invalid_if_boundaries_are_None():
ages = AgeRange(None, None)
with pytest.raises(ValidationError):
ages.validate()
ages = AgeRange(None, 12)
with pytest.raises(ValidationError):
ages.validate()
ages = AgeRange(5, None)
with pytest.raises(ValidationError):
ages.validate()


def test_age_range_invalid_if_lower_bound_negative():
ages = AgeRange(-12, 4)
with pytest.raises(ValidationError):
ages.validate()


def test_age_range_invalid_if_upper_bound_negative():
ages = AgeRange(5, -27)
with pytest.raises(ValidationError):
ages.validate()


def test_age_range_invalid_if_upper_bound_methuselan():
ages = AgeRange(15, 150)
with pytest.raises(ValidationError):
ages.validate()


def test_age_range_invalid_if_gap_too_small():
ages = AgeRange(None, None)
ages.lower = 10
ages.upper = 11
with pytest.raises(ValidationError):
ages.validate()


def test_age_range_ok_for_infants():
ages = AgeRange(0, 1)
with does_not_raise(ValidationError):
ages.validate()


def test_age_range_ok_for_large_positive_range():
ages = AgeRange(30, 45)
with does_not_raise(ValidationError):
ages.validate()
11 changes: 1 addition & 10 deletions data-serving/reusable-data-service/tests/test_geojson_model.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import pytest

from contextlib import contextmanager

from data_service.model.geojson import Point, Feature
from data_service.util.errors import ValidationError


@contextmanager
def does_not_raise(exception):
try:
yield
except exception:
raise pytest.fail(f"Exception {exception} expected not to be raised")

from tests.util import does_not_raise

def test_point_needs_two_coordinates():
p = Point()
Expand Down
10 changes: 10 additions & 0 deletions data-serving/reusable-data-service/tests/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest

from contextlib import contextmanager

@contextmanager
def does_not_raise(exception):
try:
yield
except exception:
raise pytest.fail(f"Exception {exception} expected not to be raised")

0 comments on commit ad2fc93

Please sign in to comment.