# coding=utf-8
"""Object storing rules for how a given ScheduleDay gets applied over a year."""
from __future__ import division
from .day import ScheduleDay
from ..reader import parse_idf_string
from honeybee._lockable import lockable
from ladybug.dt import Date
[docs]
@lockable
class ScheduleRule(object):
"""Schedule rule including a ScheduleDay and when it should be applied.
Note that a ScheduleRule cannot be assigned to Rooms, Shades, etc. The
ScheduleRule must be added to a ScheduleRuleset and then the ScheduleRuleset
can be applied to such objects.
Args:
schedule_day: A ScheduleDay object associated with this rule.
apply_sunday: Boolean noting whether to apply schedule_day on Sundays.
apply_monday: Boolean noting whether to apply schedule_day on Mondays.
apply_tuesday: Boolean noting whether to apply schedule_day on Tuesdays.
apply_wednesday: Boolean noting whether to apply schedule_day on Wednesdays.
apply_thursday: Boolean noting whether to apply schedule_day on Thursdays.
apply_friday: Boolean noting whether to apply schedule_day on Fridays.
apply_saturday: Boolean noting whether to apply schedule_day on Saturdays.
start_date: A ladybug Date object for the start of the period over which
the schedule_day will be applied. If None, Jan 1 will be used.
end_date: A ladybug Date object for the end of the period over which
the schedule_day will be applied. If None, Dec 31 will be used.
Properties:
* schedule_day
* apply_sunday
* apply_monday
* apply_tuesday
* apply_wednesday
* apply_thursday
* apply_friday
* apply_saturday
* start_date
* end_date
* apply_weekday
* apply_weekend
* apply_all
* days_applied
* week_apply_tuple
* is_reversed
"""
__slots__ = ('_schedule_day', '_apply_sunday', '_apply_monday', '_apply_tuesday',
'_apply_wednesday', '_apply_thursday', '_apply_friday',
'_apply_saturday', '_start_date', '_end_date', '_start_doy',
'_end_doy', '_locked')
_year_start = Date(1, 1)
_year_end = Date(12, 31)
def __init__(self, schedule_day, apply_sunday=False, apply_monday=False,
apply_tuesday=False, apply_wednesday=False, apply_thursday=False,
apply_friday=False, apply_saturday=False, start_date=None, end_date=None):
"""Initialize Schedule Rule."""
self._locked = False # unlocked by default
self.schedule_day = schedule_day
self.apply_sunday = apply_sunday
self.apply_monday = apply_monday
self.apply_tuesday = apply_tuesday
self.apply_wednesday = apply_wednesday
self.apply_thursday = apply_thursday
self.apply_friday = apply_friday
self.apply_saturday = apply_saturday
# process the start date and end date
if start_date is not None:
self._check_date(start_date, 'start_date')
self._start_date = start_date
else:
self._start_date = self._year_start
self._start_doy = self._doy_non_leap_year(self._start_date)
self.end_date = end_date
@property
def schedule_day(self):
"""Get or set the ScheduleDay object associated with this rule.."""
return self._schedule_day
@schedule_day.setter
def schedule_day(self, value):
assert isinstance(value, ScheduleDay), \
'Expected ScheduleDay for ScheduleRule. Got {}.'.format(type(value))
self._schedule_day = value
@property
def apply_sunday(self):
"""Get or set a boolean noting whether to apply schedule_day on Sundays."""
return self._apply_sunday
@apply_sunday.setter
def apply_sunday(self, value):
self._apply_sunday = bool(value)
@property
def apply_monday(self):
"""Get or set a boolean noting whether to apply schedule_day on Mondays."""
return self._apply_monday
@apply_monday.setter
def apply_monday(self, value):
self._apply_monday = bool(value)
@property
def apply_tuesday(self):
"""Get or set a boolean noting whether to apply schedule_day on Tuesdays."""
return self._apply_tuesday
@apply_tuesday.setter
def apply_tuesday(self, value):
self._apply_tuesday = bool(value)
@property
def apply_wednesday(self):
"""Get or set a boolean noting whether to apply schedule_day on Wednesdays."""
return self._apply_wednesday
@apply_wednesday.setter
def apply_wednesday(self, value):
self._apply_wednesday = bool(value)
@property
def apply_thursday(self):
"""Get or set a boolean noting whether to apply schedule_day on Thursdays."""
return self._apply_thursday
@apply_thursday.setter
def apply_thursday(self, value):
self._apply_thursday = bool(value)
@property
def apply_friday(self):
"""Get or set a boolean noting whether to apply schedule_day on Fridays."""
return self._apply_friday
@apply_friday.setter
def apply_friday(self, value):
self._apply_friday = bool(value)
@property
def apply_saturday(self):
"""Get or set a boolean noting whether to apply schedule_day on Saturdays."""
return self._apply_saturday
@apply_saturday.setter
def apply_saturday(self, value):
self._apply_saturday = bool(value)
@property
def apply_weekday(self):
"""Get or set a boolean noting whether to apply schedule_day on week days."""
return self._apply_monday and self._apply_tuesday and self._apply_wednesday and \
self._apply_thursday and self._apply_friday
@apply_weekday.setter
def apply_weekday(self, value):
self._apply_monday = self._apply_tuesday = self._apply_wednesday = \
self._apply_thursday = self._apply_friday = bool(value)
@property
def apply_weekend(self):
"""Get or set a boolean noting whether to apply schedule_day on weekends."""
return self._apply_sunday and self._apply_saturday
@apply_weekend.setter
def apply_weekend(self, value):
self._apply_sunday = self._apply_saturday = bool(value)
@property
def apply_all(self):
"""Get or set a boolean noting whether to apply schedule_day on all days."""
return self._apply_sunday and self._apply_monday and self._apply_tuesday and \
self._apply_wednesday and self._apply_thursday and self._apply_friday and \
self._apply_saturday
@apply_all.setter
def apply_all(self, value):
self._apply_sunday = self._apply_monday = self._apply_tuesday = \
self._apply_wednesday = self._apply_thursday = self._apply_friday = \
self._apply_saturday = bool(value)
@property
def start_date(self):
"""Get or set a ladybug Date object for the start of the period."""
return self._start_date
@start_date.setter
def start_date(self, value):
if value is not None:
self._check_date(value, 'start_date')
self._start_date = value
else:
self._start_date = self._year_start
self._start_doy = self._doy_non_leap_year(self._start_date)
@property
def end_date(self):
"""Get or set a ladybug Date object for the end of the period."""
return self._end_date
@end_date.setter
def end_date(self, value):
if value is not None:
self._check_date(value, 'end_date')
self._end_date = value
else:
self._end_date = self._year_end
self._end_doy = self._doy_non_leap_year(self._end_date)
@property
def days_applied(self):
"""Get a list of text values for the days applied."""
day_names = ('sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday')
days = [name for name, apply in zip(day_names, self.week_apply_tuple) if apply]
return days
@property
def week_apply_tuple(self):
"""Get a tuple of 7 booleans for each of the days of the week."""
return (self._apply_sunday, self._apply_monday, self._apply_tuesday,
self._apply_wednesday, self._apply_thursday, self._apply_friday,
self._apply_saturday)
@property
def is_reversed(self):
"""A property to note whether the start date is after the end date.
This indicates that the rule applies through the end of the year into
the start of the year.
"""
return self._start_date > self._end_date
[docs]
def apply_day_by_name(self, day_name):
"""Set the rule to apply to the day of the week by its name.
Args:
day_name: A text string for the day on which this rule should be applied.
The following options are acceptable:
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'weekday', 'weekend', 'all'
"""
day_name = day_name.lower()
if day_name == 'sunday':
self.apply_sunday = True
elif day_name == 'monday':
self.apply_monday = True
elif day_name == 'tuesday':
self.apply_tuesday = True
elif day_name == 'wednesday':
self.apply_wednesday = True
elif day_name == 'thursday':
self.apply_thursday = True
elif day_name == 'friday':
self.apply_friday = True
elif day_name == 'saturday':
self.apply_saturday = True
elif day_name == 'weekday':
self.apply_weekday = True
elif day_name == 'weekend':
self.apply_weekend = True
elif day_name == 'all':
self.apply_all = True
else:
raise ValueError('ScheduleRule input "{}" is not an acceptable '
'day name.'.format(day_name))
[docs]
def apply_day_by_dow(self, dow):
"""Set the rule to apply to the day of the week by its dow integer.
Args:
week_day_index: An integer from 1-8 for the day of the week. Values
correspond to the following:
1 - Sunday
2 - Monday
3 - Tuesday
4 - Wednesday
5 - Thursday
6 - Friday
7 - Saturday
"""
if dow == 1:
self.apply_sunday = True
elif dow == 2:
self.apply_monday = True
elif dow == 3:
self.apply_tuesday = True
elif dow == 4:
self.apply_wednesday = True
elif dow == 5:
self.apply_thursday = True
elif dow == 6:
self.apply_friday = True
elif dow == 7:
self.apply_saturday = True
else:
raise ValueError('ScheduleRule input "{}" is not an acceptable '
'dow integer.'.format(dow))
[docs]
def does_rule_apply(self, doy, dow=None):
"""Check if this rule applies to a given day of the year and day of the week.
Args:
doy: An integer between 1 anf 365 for the day of the year to test.
dow: An integer between 1 anf 7 for the day of the week to test. If None,
this value will be derived from the doy, assuming the first day of
the year is a Sunday.
"""
dow = dow if dow is not None else doy % 7
return self.does_rule_apply_doy(doy) and self.week_apply_tuple[dow - 1]
[docs]
def does_rule_apply_leap_year(self, doy, dow=None):
"""Check if this rule applies to a given day of a leap year and day of the week.
Args:
doy: An integer between 1 anf 366 for the day of the leap year to test.
dow: An integer between 1 anf 7 for the day of the week to test. If None,
this value will be derived from the doy, assuming the first day of
the year is a Sunday.
"""
dow = dow if dow is not None else doy % 7
return self.does_rule_apply_doy_leap_year(doy) and self.week_apply_tuple[dow - 1]
[docs]
def does_rule_apply_doy(self, doy):
"""Check if this rule applies to a given day of the year.
Args:
doy: An integer between 1 anf 365 for the day of the year to test.
"""
if self.is_reversed:
return doy <= self._end_doy or self._start_doy <= doy
else:
return self._start_doy <= doy <= self._end_doy
[docs]
def does_rule_apply_doy_leap_year(self, doy):
"""Check if this rule applies to a given day of a leap year.
Args:
doy: An integer between 1 anf 366 for the day of the leap year to test.
"""
st_doy = self._start_doy if self._start_date.month <= 2 else self._start_doy + 1
end_doy = self._end_doy if self._end_date.month <= 2 else self._end_doy + 1
if self.is_reversed:
return doy <= end_doy or st_doy <= doy
else:
return st_doy <= doy <= end_doy
[docs]
@classmethod
def from_days_applied(cls, schedule_day, applicable_days=None,
start_date=None, end_date=None):
"""Initialize a ScheduleRule using a list of days when the rule is applied.
Args:
schedule_day: A ScheduleDay object associated with this rule.
applicable_days: A list of text strings for the days when theScheduleRule
will be applied. For example ['monday', 'weekend'].
The following options are acceptable:
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'weekday', 'weekend', 'all'
start_date: A ladybug Date object for the start of the period over which
the schedule_day will be applied. If None, Jan 1 will be used.
end_date: A ladybug Date object for the end of the period over which
the schedule_day will be applied. If None, Dec 31 will be used.
"""
rule = cls(schedule_day, start_date=start_date, end_date=end_date)
if applicable_days is not None:
for day in applicable_days:
rule.apply_day_by_name(day)
return rule
[docs]
@classmethod
def from_dict(cls, data):
"""Create a ScheduleRule from a dictionary.
Args:
data: ScheduleRule dictionary following the format below.
.. code-block:: python
{
"type": 'ScheduleRule'
"schedule_day": {
"type": 'ScheduleDay',
"identifier": 'Office Occupancy',
"values": [0, 1, 0],
"times": [(0, 0), (9, 0), (17, 0)],
"interpolate": False
},
"apply_sunday": False,
"apply_monday": True,
"apply_tuesday": True,
"apply_wednesday": True,
"apply_thursday": True,
"apply_friday": True,
"apply_saturday": False,
"start_date": (1, 1),
"end_date": (12, 31)
}
"""
assert data['type'] == 'ScheduleRule', \
'Expected ScheduleRule. Got {}.'.format(data['type'])
schedule_day = ScheduleDay.from_dict(data['schedule_day'])
sun, mon, tues, wed, thurs, fri, sat, start, end = \
cls._extract_apply_from_dict(data)
return cls(schedule_day, sun, mon, tues, wed, thurs, fri, sat, start, end)
[docs]
@classmethod
def from_dict_abridged(cls, data, schedule_day):
"""Create a ScheduleRule object from an abridged dictionary.
Args:
data: A ScheduleRuleAbridged dictionary in following the format below.
schedule_day: A honeybee ScheduleDay object that will be assigned to
this ScheduleRule.
.. code-block:: python
{
"type": 'ScheduleRuleAbridged',
"schedule_day": str,
"apply_sunday": False,
"apply_monday": True,
"apply_tuesday": True,
"apply_wednesday": True,
"apply_thursday": True,
"apply_friday": True,
"apply_saturday": False,
"start_date": (1, 1),
"end_date": (12, 31)
}
"""
assert data['type'] == 'ScheduleRuleAbridged', \
'Expected ScheduleRuleAbridged dictionary. Got {}.'.format(data['type'])
sun, mon, tues, wed, thurs, fri, sat, start, end = \
cls._extract_apply_from_dict(data)
return cls(schedule_day, sun, mon, tues, wed, thurs, fri, sat, start, end)
[docs]
def to_dict(self, abridged=False):
"""ScheduleRule dictionary representation.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True),
which only specifies the identifier of the schedule_day. Default: False.
"""
base = {'type': 'ScheduleRule'} if not abridged \
else {'type': 'ScheduleRuleAbridged'}
base['schedule_day'] = self.schedule_day.to_dict() if not abridged \
else self.schedule_day.identifier
base['apply_sunday'] = self.apply_sunday
base['apply_monday'] = self.apply_monday
base['apply_tuesday'] = self.apply_tuesday
base['apply_wednesday'] = self.apply_wednesday
base['apply_thursday'] = self.apply_thursday
base['apply_friday'] = self.apply_friday
base['apply_saturday'] = self.apply_saturday
base['start_date'] = self.start_date.to_array()
base['end_date'] = self.end_date.to_array()
return base
[docs]
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
[docs]
def lock(self):
"""The lock() method will also lock the schedule_day."""
self._locked = True
self.schedule_day.lock()
[docs]
def unlock(self):
"""The unlock() method will also unlock the schedule_day."""
self._locked = False
self.schedule_day.unlock()
@staticmethod
def _extract_apply_from_dict(data):
"""Extract the apply values from a dictionary."""
apply_sunday = data['apply_sunday'] if 'apply_sunday' in data else False
apply_monday = data['apply_monday'] if 'apply_monday' in data else False
apply_tuesday = data['apply_tuesday'] if 'apply_tuesday' in data else False
apply_wednesday = data['apply_wednesday'] if 'apply_wednesday' in data else False
apply_thursday = data['apply_thursday'] if 'apply_thursday' in data else False
apply_friday = data['apply_friday'] if 'apply_friday' in data else False
apply_saturday = data['apply_saturday'] if 'apply_saturday' in data else False
start_date = Date.from_array(data['start_date']) if \
'start_date' in data else ScheduleRule._year_start
end_date = Date.from_array(data['end_date']) if \
'end_date' in data else ScheduleRule._year_end
return apply_sunday, apply_monday, apply_tuesday, apply_wednesday, \
apply_thursday, apply_friday, apply_saturday, start_date, end_date
@staticmethod
def _check_date(date, date_name='date'):
assert isinstance(date, Date), 'Expected ladybug Date for ' \
'ScheduleRule {}. Got {}.'.format(date_name, type(date))
@staticmethod
def _doy_non_leap_year(date):
"""Get a doy for a non-leap-year even when the input date is for a leap year."""
if not date.leap_year:
return date.doy
else:
return date.doy if date <= Date(2, 29, True) else date.doy - 1
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (hash(self.schedule_day), self.schedule_day, self.apply_sunday,
self.apply_monday, self.apply_tuesday, self.apply_wednesday,
self.apply_thursday, self.apply_friday, self.apply_saturday,
hash(self.start_date), hash(self.end_date))
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, ScheduleRule) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __copy__(self):
return ScheduleRule(
self.schedule_day.duplicate(), self.apply_sunday,
self.apply_monday, self.apply_tuesday, self.apply_wednesday,
self.apply_thursday, self.apply_friday, self.apply_saturday,
self.start_date, self.end_date)
[docs]
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
return 'ScheduleRule: {} [days applied: {}] [date range: {} - {}]'.format(
self.schedule_day.display_name, ', '.join(self.days_applied),
self.start_date, self.end_date)