Skip to content

Commit

Permalink
decorator for time (#59)
Browse files Browse the repository at this point in the history
* rename origin -> start

* timedelta + testing

* lint

* cast years as int

* closes #54

* rename with maybe prepending decorator
  • Loading branch information
mathematicalmichael committed Jul 26, 2021
1 parent 340ec3d commit e3ac7b8
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 35 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ install_requires =
numpy
matplotlib
pandas
timedelta
dataclasses; python_version < "3.7"

# The usage of test_requires is discouraged, see `Dependency Management` docs
Expand Down
2 changes: 1 addition & 1 deletion simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def simulateCoOp(plotList, numYears, pruneYear=None, growthPattern=None, strateg

annualHarvest = []
harvestYear = []
start_year = min([plot.origin.year for plot in plotList])
start_year = min([plot.start.year for plot in plotList])
# start_year = 2020
for current_year in range(start_year, start_year + numYears + 1):

Expand Down
54 changes: 33 additions & 21 deletions src/cafe/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,39 @@

import numpy as np
import pandas as pd
import timedelta

_logger = logging.getLogger(__name__)


def maybe_time_like(cls):
@dataclass
class temporal(cls):
# start: Optional[datetime.datetime] = datetime.datetime(2020, 1, 1, 0, 0)
start: Optional[datetime.datetime] = None
end: Optional[datetime.datetime] = None

@property
def year_planted(self):
return self.start.year

def age(self, current_time=datetime.datetime.today()) -> datetime.timedelta:
if not self.start:
raise ValueError("Start time undefined, age indeterminable.")
return current_time - self.start

def years(self, current_time=datetime.datetime.today()) -> int:
return int(timedelta.Timedelta(self.age(current_time)).total.days / 365.25)

def days(self, current_time=datetime.datetime.today()) -> int:
return timedelta.Timedelta(self.age(current_time)).total.days

def mins(self, current_time=datetime.datetime.today()) -> int:
return timedelta.Timedelta(self.age(current_time)).total.minutes

return temporal


@dataclass(frozen=True)
class Config:
species: str
Expand All @@ -29,23 +58,19 @@ def __eq__(self, other_cls):
return self.species == other_cls.species and self.unit == other_cls.unit


@maybe_time_like
@dataclass
class Plot:
num: int = 1 # number of crops
area: float = 1.0
plot_id: int = 0
species: str = field(default_factory=str)
unit: str = "cuerdas"
origin: datetime = datetime.datetime(2020, 1, 1, 0, 0)

@property # TODO deprecate / change tests?
def size(self) -> float:
return self.area

@property # TODO deprecate / change tests?
def year_planted(self):
return self.origin.year

@staticmethod
def to_datetime(time) -> datetime.datetime:
if isinstance(time, Number): # assumes `time` = year
Expand All @@ -63,7 +88,7 @@ def from_series(cls, series):
species=series.treeType,
area=series.numCuerdas,
unit="cuerdas",
origin=cls.to_datetime(series.yearPlanted),
start=cls.to_datetime(series.yearPlanted),
)

@classmethod
Expand Down Expand Up @@ -105,11 +130,10 @@ def contains(self, species: str = "") -> bool:
return species in set(p.species for p in self.plot_list)


@maybe_time_like
@dataclass
class Event:
name: str
start: Optional[datetime.datetime] = None
end: Optional[datetime.datetime] = None
impact: Optional[Union[float, Callable]] = 1.0
scope: Optional[Union[bool, Dict]] = field(default_factory=dict)

Expand Down Expand Up @@ -146,18 +170,6 @@ def _check_scope(self, plot: Optional[Plot] = None):
raise NotImplementedError("Geographic scope not yet implemented.")
return False

def age(self, current_time=datetime.datetime.today()) -> datetime.timedelta:
return current_time - self.start

def years(self, current_time=datetime.datetime.today()) -> int:
return round(self.age(current_time).days / 365.25)

def days(self, current_time=datetime.datetime.today()) -> int:
return self.age(current_time).days

def mins(self, current_time=datetime.datetime.today()) -> int:
return round(self.age(current_time).seconds / 60)

def eval(self, *args, **kwargs):
if isinstance(self.impact, Callable):
return self.impact(*args, **kwargs, **self.__dict__)
Expand Down Expand Up @@ -194,7 +206,7 @@ def guate_harvest_function(
assert mature < retire

def growth(time: Union[datetime.datetime, float], plot: Plot, **kwargs):
birth_year = plot.origin.year
birth_year = plot.year_planted
current_year = time if isinstance(time, (float, int)) else time.year
age = current_year - birth_year
if age < mature - 1:
Expand Down
18 changes: 9 additions & 9 deletions tests/test_farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
__license__ = "mit"


@pytest.fixture()
def start_date():
return datetime.datetime(2020, 1, 1)


@pytest.fixture()
def configs():
return (
Expand All @@ -34,7 +39,7 @@ def configs():

@pytest.fixture()
def farm(start_date):
return Farm([Plot(species="a", origin=start_date)])
return Farm([Plot(species="a", start=start_date)])


@pytest.fixture()
Expand Down Expand Up @@ -77,7 +82,7 @@ def f(time, **kwargs):
@pytest.fixture()
def growth_function() -> Callable:
def f(time: Union[datetime.datetime, float], plot: Plot, **kwargs):
start = plot.origin.year
start = plot.start.year
year = time if isinstance(time, (float, int)) else time.year
age = year - start
if age == 0:
Expand All @@ -93,14 +98,9 @@ def f(time: Union[datetime.datetime, float], plot: Plot, **kwargs):
return f


@pytest.fixture()
def start_date():
return datetime.datetime(2020, 1, 1)


@pytest.fixture()
def dummy_event(start_date):
return Event("some_event", start_date)
return Event("some_event", start=start_date)


# CONFIG TESTS
Expand All @@ -123,7 +123,7 @@ def test_that_event_impact_works_with_callables(
start_date, event_impact_function, expected_proportions_for_event
):
# Arrange
e = Event("some_event", start_date, impact=event_impact_function)
e = Event("some_event", start=start_date, impact=event_impact_function)
list_of_years, expected_harvest_proportion = expected_proportions_for_event

# Act
Expand Down
8 changes: 4 additions & 4 deletions tests/test_harvest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@


@pytest.fixture()
def farm():
return Farm([Plot(species="borbon")])
def start_date():
return datetime.datetime(2020, 1, 1)


@pytest.fixture()
def start_date():
return datetime.datetime(2020, 1, 1)
def farm(start_date):
return Farm([Plot(species="borbon", start=start_date)])


def guate_growth_target(
Expand Down
31 changes: 31 additions & 0 deletions tests/test_plot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from cafe.farm import Plot
import pandas as pd
import datetime


@pytest.fixture
Expand All @@ -23,3 +24,33 @@ def test_we_can_instantiate_plot_from_series(series_fixture):
assert plot.size == info["numCuerdas"]
assert plot.year_planted == info["yearPlanted"]
assert plot.plot_id == info["plotID"]


def test_that_plots_inherit_time_properties():
# Arrange
plot = Plot("a", start=datetime.datetime(2000, 1, 1))
current_time = datetime.datetime(2020, 1, 1)

# Act
age = plot.age(current_time)
years = plot.years(current_time)
days = plot.days(current_time)
mins = plot.mins(current_time)

# Assert
assert age == datetime.timedelta(days=7305)
assert years == 20
assert days == 20 * 365.25
assert mins == days * 24 * 60


def test_that_plots_without_start_time_raises_error():
# Arrange
plot = Plot("a")
current_time = datetime.datetime(2020, 1, 1)

# Act

# Assert
with pytest.raises(ValueError):
plot.age(current_time)

0 comments on commit e3ac7b8

Please sign in to comment.