code: ################################################################################
  # Module: schedule.py
  # Description: Functions for handling conversion of EnergyPlus schedule objects
  # License: MIT, see full license in LICENSE.txt
  # Web: https://github.com/samuelduchesne/archetypal
  ################################################################################
  
  import functools
  import io
  import logging as lg
  from datetime import datetime, timedelta
  
  import archetypal
  import numpy as np
  import pandas as pd
  from archetypal import log
  
  
  class Schedule(object):
      """An object designed to handle any EnergyPlys schedule object"""
  
      def __init__(self, sch_name, idf=None, start_day_of_the_week=0,
                   strict=False, base_year=2018, schType=None, **kwargs):
          """
  
          Args:
              idf (IDF): IDF object
              sch_name (str): The schedule name in the idf file
              start_day_of_the_week (int): 0-based day of week (Monday=0)
              strict (bool): if True, schedules that have the Field-Sets such
                  as Holidays and CustomDay will raise an error if they are absent
                  from the IDF file. If False, any missing qualifiers will be
                  ignored.
              base_year (int): The base year of the schedule. Defaults to 2018
                  since the first day of that year is a Monday.
          """
          super(Schedule, self).__init__(**kwargs)
          self.strict = strict
          self.idf = idf
          self.schName = sch_name
          self.startDayOfTheWeek = self.get_sdow(start_day_of_the_week)
          self.year = base_year
          self.startDate = self.start_date()
          self.count = 0
          self.startHOY = 1
          self.endHOY = 24
          self.unit = "unknown"
  
          self.index_ = None
          self.values = None
          self.schType = schType
          _type = kwargs.get('Type', None)
          if _type is None:
              self.schTypeLimitsName = self.get_schedule_type_limits_name(
                  sch_type=self.schType)
          else:
              self.schTypeLimitsName = _type
  
      @classmethod
      def constant_schedule(cls, hourly_value=1, Name='AlwaysOn', **kwargs):
          idftxt = "VERSION, 8.9;"  # Not an emplty string. has just the
          # version number
          # we can make a file handle of a string
          fhandle = io.StringIO(idftxt)
          # initialize the IDF object with the file handle
          idf_scratch = archetypal.IDF(fhandle)
  
          idf_scratch.add_object(ep_object='Schedule:Constant'.upper(),
                                 **dict(Name=Name,
                                        Schedule_Type_Limits_Name='',
                                        Hourly_Value=hourly_value),
                                 save=False)
  
          sched = Schedule(sch_name=Name, idf=idf_scratch, **kwargs)
          return sched
  
      @property
      def all_values(self):
          """returns the values array"""
          if self.values is None:
              self.values = self.get_schedule_values(sch_name=self.schName,
                                                     sch_type=self.schType)
              return self.values
          else:
              return self.values
  
      @property
      def max(self):
          return max(self.all_values)
  
      @property
      def min(self):
          return min(self.all_values)
  
      @property
      def mean(self):
          return np.mean(self.all_values)
  
      @property
      def series(self):
          """Returns the schedule values as a pd.Series object with a
          DateTimeIndex"""
          index = pd.date_range(start=self.startDate, periods=len(
              self.all_values), freq='1H')
          return pd.Series(self.all_values, index=index)
  
      def get_schedule_type_limits_name(self, sch_name=None, sch_type=None):
          """Return the Schedule Type Limits name associated to a schedule
          name"""
          if sch_name is None:
              sch_name = self.schName
          if sch_type is None:
              schedule_values = self.idf.get_schedule_data_by_name(sch_name,
                                                                   sch_type=sch_type)
          try:
              schedule_limit_name = schedule_values.Schedule_Type_Limits_Name
          except:
              return 'unknown'
          else:
              return schedule_limit_name
  
      def get_schedule_type_limits_data(self, sch_name=None):
          """Returns Schedule Type Limits data from schedule name"""
  
          if sch_name is None:
              sch_name = self.schName
  
          schedule_values = self.idf.get_schedule_data_by_name(sch_name)
          try:
              schedule_limit_name = schedule_values.Schedule_Type_Limits_Name
          except:
              # this schedule is probably a 'Schedule:Week:Daily' which does
              # not have a Schedule_Type_Limits_Name field
              return '', '', '', ''
          else:
              lower_limit, upper_limit, numeric_type, unit_type = \
                  self.idf.get_schedule_type_limits_data_by_name(
                      schedule_limit_name)
  
              self.unit = unit_type
              if self.unit == "unknown":
                  self.unit = numeric_type
  
              return lower_limit, upper_limit, numeric_type, unit_type
  
      def get_schedule_type(self, sch_name=None):
          """Return the schedule type"""
          if sch_name is None:
              sch_name = self.schName
  
          schedule_values = self.idf.get_schedule_data_by_name(sch_name)
          sch_type = schedule_values.fieldvalues[0]
  
          return sch_type
  
      def start_date(self):
          """The start date of the schedule. Satisfies `startDayOfTheWeek`"""
          import calendar
          c = calendar.Calendar(firstweekday=self.startDayOfTheWeek)
          start_date = c.monthdatescalendar(self.year, 1)[0][0]
          return datetime(start_date.year, start_date.month, start_date.day)
  
      def plot(self, slice=None, **kwargs):
          hourlyvalues = self.all_values
          index = pd.date_range(self.startDate, periods=len(
              hourlyvalues),
                                freq='1H')
          series = pd.Series(hourlyvalues, index=index, dtype=float)
          if slice is None:
              slice = pd.IndexSlice[:]
          elif len(slice) > 1:
              slice = pd.IndexSlice[slice[0]:slice[1]]
          ax = series.loc[slice].plot(**kwargs, label=self.schName)
          return ax
  
      def get_interval_day_ep_schedule_values(self, sch_name=None):
          """'Schedule:Day:Interval"""
  
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('Schedule:Day:Interval'.upper(), sch_name)
          lower_limit, upper_limit, numeric_type, unit_type = \
              self.get_schedule_type_limits_data(sch_name)
  
          number_of_day_sch = int((len(values.fieldvalues) - 3) / 2)
  
          hourly_values = np.arange(24)
          start_hour = 0
          for i in range(number_of_day_sch):
              value = float(values['Value_Until_Time_{}'.format(i + 1)])
              until_time = [int(s.strip()) for s in
                            values['Time_{}'.format(i + 1)].split(":") if
                            s.strip().isdigit()]
              end_hour = int(until_time[0] + until_time[1] / 60)
              for hour in range(start_hour, end_hour):
                  hourly_values[hour] = value
  
              start_hour = end_hour
  
          if numeric_type.strip().lower() == "discrete":
              hourly_values = hourly_values.astype(int)
  
          return hourly_values
  
      def get_hourly_day_ep_schedule_values(self, sch_name=None):
          """'Schedule:Day:Hourly'"""
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('Schedule:Day:Hourly'.upper(), sch_name)
  
          fieldvalues_ = np.array(values.fieldvalues[3:])
  
          return fieldvalues_
  
      def get_compact_weekly_ep_schedule_values(self, sch_name=None,
                                                start_date=None, index=None):
          """'schedule:week:compact'"""
          if start_date is None:
              start_date = self.startDate
          if index is None:
              idx = pd.date_range(start=start_date, periods=168, freq='1H')
              slicer_ = pd.Series([False] * (len(idx)), index=idx)
          else:
              slicer_ = pd.Series([False] * (len(index)), index=index)
  
          if sch_name is None:
              sch_name = self.schName
          values = self.idf.getobject('schedule:week:compact'.upper(), sch_name)
  
          weekly_schedules = pd.Series([0] * len(slicer_), index=slicer_.index)
          # update last day of schedule
  
          if self.count == 0:
              self.schType = values.key
              self.endHOY = 168
  
          num_of_daily_schedules = int(len(values.fieldvalues[2:]) / 2)
  
          for i in range(num_of_daily_schedules):
              day_type = values['DayType_List_{}'.format(i + 1)].lower()
              how = self.field_set(day_type, slicer_)
              if not weekly_schedules.loc[how].empty:
                  # Loop through days and replace with day:schedule values
                  days = []
                  for name, day in weekly_schedules.loc[how].groupby(pd.Grouper(
                          freq='D')):
                      if not day.empty:
                          ref = values.get_referenced_object(
                              "ScheduleDay_Name_{}".format(i + 1))
                          day.loc[:] = self.get_schedule_values(
                              sch_name=ref.Name, sch_type=ref.key)
                          days.append(day)
                  new = pd.concat(days)
                  slicer_.update(
                      pd.Series([True] * len(new.index), index=new.index))
                  slicer_ = slicer_.apply(lambda x: x == True)
                  weekly_schedules.update(new)
              else:
                  return weekly_schedules.values
  
          return weekly_schedules.values
  
      def get_daily_weekly_ep_schedule_values(self, sch_name=None):
          """'schedule:week:daily'"""
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:week:daily'.upper(), sch_name)
  
          # 7 list for 7 days of the week
          hourly_values = []
          for day in ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
                      'Friday', 'Saturday', 'Sunday']:
              ref = values.get_referenced_object(
                  '{}_ScheduleDay_Name'.format(day))
              h = self.get_schedule_values(sch_name=ref.Name, sch_type=ref.key)
              hourly_values.append(h)
          hourly_values = np.array(hourly_values)
          # shift days earlier by self.startDayOfTheWeek
          hourly_values = np.roll(hourly_values, -self.startDayOfTheWeek, axis=0)
  
          return hourly_values.ravel()
  
      def get_list_day_ep_schedule_values(self, sch_name=None):
          """'schedule:day:list'"""
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:day:list'.upper(), sch_name)
          lower_limit, upper_limit, numeric_type, unit_type = \
              self.get_schedule_type_limits_data(sch_name)
  
          import pandas as pd
          freq = int(values['Minutes_per_Item'])  # Frequency of the values
          num_values = values.fieldvalues[5:]  # List of values
          method = values['Interpolate_to_Timestep']  # How to resample
  
          # fill a list of available values and pad with zeros (this is safer
          # but should not occur)
          all_values = np.arange(int(24 * 60 / freq))
          for i in all_values:
              try:
                  all_values[i] = num_values[i]
              except:
                  all_values[i] = 0
          # create a fake index to help us with the resampling
          index = pd.date_range(start=self.startDate,
                                periods=(24 * 60) / freq,
                                freq='{}T'.format(freq))
          series = pd.Series(all_values, index=index)
  
          # resample series to hourly values and apply resampler function
          series = series.resample('1H').apply(_how(method))
  
          return series.values
  
      def get_constant_ep_schedule_values(self, sch_name=None):
          """'schedule:constant'"""
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:constant'.upper(), sch_name)
          lower_limit, upper_limit, numeric_type, unit_type = \
              self.get_schedule_type_limits_data(sch_name)
  
          hourly_values = np.arange(8760)
          value = float(values['Hourly_Value'])
          for hour in hourly_values:
              hourly_values[hour] = value
  
          if numeric_type.strip().lower() == 'discrete':
              hourly_values = hourly_values.astype(int)
  
          return hourly_values
  
      def get_file_ep_schedule_values(self, sch_name=None):
          """'schedule:file'"""
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:file'.upper(), sch_name)
          lower_limit, upper_limit, numeric_type, unit_type = \
              self.get_schedule_type_limits_data(sch_name)
  
          filename = values['File_Name']
          column = values['Column_Number']
          rows = values['Rows_to_Skip_at_Top']
          hours = values['Number_of_Hours_of_Data']
          sep = values['Column_Separator']
          interp = values['Interpolate_to_Timestep']
  
          import pandas as pd
          import os
          idfdir = os.path.dirname(self.idf.idfname)
          file = os.path.join(idfdir, filename)
          delimeter = _separator(sep)
          skip_rows = int(rows) - 1  # We want to keep the column
          col = [int(column) - 1]  # zero-based
          values = pd.read_csv(file, delimiter=delimeter, skiprows=skip_rows,
                               usecols=col)
  
          return values.iloc[:, 0].values
  
      def get_compact_ep_schedule_values(self, sch_name=None):
          """'schedule:compact'"""
  
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:compact'.upper(), sch_name)
          lower_limit, upper_limit, numeric_type, unit_type = \
              self.get_schedule_type_limits_data(sch_name)
  
          field_sets = ['through', 'for', 'interpolate', 'until', 'value']
          fields = values.fieldvalues[3:]
  
          index = pd.date_range(start=self.startDate, periods=8760, freq='H')
          zeros = np.zeros(len(index))
  
          slicer_ = pd.Series([False] * len(index), index=index)
          series = pd.Series(zeros, index=index)
  
          from_day = self.startDate
          ep_from_day = datetime(self.year, 1, 1)
          from_time = '00:00'
          how_interpolate = None
          for field in fields:
              if any([spe in field.lower() for spe in field_sets]):
                  f_set, hour, minute, value = self.field_interpreter(field)
  
                  if f_set.lower() == 'through':
                      # main condition. All sub-conditions must obey a
                      # `Through` condition
  
                      # First, initialize the slice (all False for now)
                      through_conditions = self.invalidate_condition(series)
  
                      # reset from_time
                      from_time = '00:00'
  
                      # Prepare ep_to_day variable
                      ep_to_day = self.date_field_interpretation(value) + \
                                  timedelta(days=1)
  
                      # Calculate Timedelta in days
                      days = (ep_to_day - ep_from_day).days
                      # Add timedelta to start_date
                      to_day = from_day + timedelta(days=days) + timedelta(
                          hours=-1)
  
                      # slice the conditions with the range and apply True
                      through_conditions.loc[from_day:to_day] = True
  
                      from_day = to_day + timedelta(hours=1)
                      ep_from_day = ep_to_day
                  elif f_set.lower() == 'for':
                      # slice specific days
                      # reset from_time
                      from_time = '00:00'
  
                      for_condition = self.invalidate_condition(series)
                      values = value.split()
                      if len(values) > 1:
                          # if multiple `For`. eg.: For: Weekends Holidays,
                          # Combine both conditions
                          for value in values:
                              if value.lower() == 'allotherdays':
                                  # Apply condition to slice
                                  how = self.field_set(value, slicer_)
                                  # Reset though condition
                                  through_conditions = how
                                  for_condition = how
                              else:
                                  how = self.field_set(value, slicer_)
                                  for_condition.loc[how] = True
                      elif value.lower() == 'allotherdays':
                          # Apply condition to slice
                          how = self.field_set(value, slicer_)
                          # Reset though condition
                          through_conditions = how
                          for_condition = how
                      else:
                          # Apply condition to slice
                          how = self.field_set(value)
                          for_condition.loc[how] = True
  
                      # Combine the for_condition with all_conditions
                      all_conditions = through_conditions & for_condition
  
                      # update in memory slice
                      # self.sliced_day_.loc[all_conditions] = True
                  elif 'interpolate' in f_set.lower():
                      # we need to upsample to series to 8760 * 60 values
                      new_idx = pd.date_range(start=self.startDate,
                                              periods=525600, closed='left',
                                              freq='T')
                      series = series.resample('T').pad()
                      series = series.reindex(new_idx)
                      series.fillna(method='pad', inplace=True)
                      through_conditions = through_conditions.resample('T').pad()
                      through_conditions = through_conditions.reindex(new_idx)
                      through_conditions.fillna(method='pad', inplace=True)
                      for_condition = for_condition.resample('T').pad()
                      for_condition = for_condition.reindex(new_idx)
                      for_condition.fillna(method='pad', inplace=True)
                      how_interpolate = value.lower()
                  elif f_set.lower() == 'until':
                      until_condition = self.invalidate_condition(series)
                      if series.index.freq.name == 'T':
                          # until_time = str(int(hour) - 1) + ':' + minute
                          until_time = timedelta(hours=int(hour),
                                                 minutes=int(minute)) - timedelta(
                              minutes=1)
  
                      else:
                          until_time = str(int(hour) - 1) + ':' + minute
                      until_condition.loc[until_condition.between_time(from_time,
                                                                       str(
                                                                           until_time)).index] = True
                      all_conditions = for_condition & through_conditions & \
                                       until_condition
  
                      from_time = str(int(hour)) + ':' + minute
                  elif f_set.lower() == 'value':
                      # If the therm `Value: ` field is used, we will catch it
                      # here.
                      # update in memory slice
                      slicer_.loc[all_conditions] = True
                      series[all_conditions] = value
                  else:
                      # Do something here before looping to the next Field
                      pass
              else:
                  # If the term `Value: ` is not used; the variable is simply
                  # passed in the Field
                  value = float(field)
                  series[all_conditions] = value
  
                  # update in memory slice
                  slicer_.loc[all_conditions] = True
          if how_interpolate:
              return series.resample('H').mean().values
          else:
              return series.values
  
      def field_interpreter(self, field):
          """dealing with a Field-Set (Through, For, Interpolate,
          # Until, Value) and return the parsed string"""
  
          if 'through' in field.lower():
              # deal with through
              if ':' in field.lower():
                  # parse colon
                  f_set, statement = field.split(':')
                  hour = None
                  minute = None
                  value = statement.strip()
              else:
                  msg = 'The schedule "{sch}" contains a Field ' \
                        'that is not understood: "{field}"'.format(
                      sch=self.schName, field=field)
                  raise NotImplementedError(msg)
          elif 'for' in field.lower():
              if ':' in field.lower():
                  # parse colon
                  f_set, statement = field.split(':')
                  value = statement.strip()
                  hour = None
                  minute = None
              else:
                  # parse without a colon
                  msg = 'The schedule "{sch}" contains a Field ' \
                        'that is not understood: "{field}"'.format(
                      sch=self.schName, field=field)
                  raise NotImplementedError(msg)
          elif 'interpolate' in field.lower():
              msg = 'The schedule "{sch}" contains sub-hourly values (' \
                    'Field-Set="{field}"). The average over the hour is ' \
                    'taken'.format(sch=self.schName, field=field)
              log(msg, lg.WARNING)
              f_set, value = field.split(':')
              hour = None
              minute = None
          elif 'until' in field.lower():
              if ':' in field.lower():
                  # parse colon
                  try:
                      f_set, hour, minute = field.split(':')
                      hour = hour.strip()  # remove trailing spaces
                      minute = minute.strip()  # remove trailing spaces
                      value = None
                  except:
                      f_set = 'until'
                      hour, minute = field.split(':')
                      hour = hour[-2:].strip()
                      minute = minute.strip()
                      value = None
              else:
                  msg = 'The schedule "{sch}" contains a Field ' \
                        'that is not understood: "{field}"'.format(
                      sch=self.schName, field=field)
                  raise NotImplementedError(msg)
          elif 'value' in field.lower():
              if ':' in field.lower():
                  # parse colon
                  f_set, statement = field.split(':')
                  value = statement.strip()
                  hour = None
                  minute = None
              else:
                  msg = 'The schedule "{sch}" contains a Field ' \
                        'that is not understood: "{field}"'.format(
                      sch=self.schName, field=field)
                  raise NotImplementedError(msg)
          else:
              # deal with the data value
              f_set = field
              hour = None
              minute = None
              value = field[len(field) + 1:].strip()
  
          return f_set, hour, minute, value
  
      @staticmethod
      def invalidate_condition(series):
          index = series.index
          periods = len(series)
          return pd.Series([False] * periods, index=index)
  
      def get_yearly_ep_schedule_values(self, sch_name=None):
          """'schedule:year'"""
          # first week
  
          start_date = self.startDate
          idx = pd.date_range(start=start_date, periods=8760, freq='1H')
          hourly_values = pd.Series([0] * 8760, index=idx)
  
          # update last day of schedule
          self.endHOY = 8760
  
          if sch_name is None:
              sch_name = self.schName
  
          values = self.idf.getobject('schedule:year'.upper(), sch_name)
  
          # generate weekly schedules
          num_of_weekly_schedules = int(len(values.fieldvalues[3:]) / 5)
  
          for i in range(num_of_weekly_schedules):
              ref = values.get_referenced_object(
                  'ScheduleWeek_Name_{}'.format(i + 1))
  
              start_month = values['Start_Month_{}'.format(i + 1)]
              end_month = values['End_Month_{}'.format(i + 1)]
              start_day = values['Start_Day_{}'.format(i + 1)]
              end_day = values['End_Day_{}'.format(i + 1)]
  
              start = datetime.strptime(
                  '{}/{}/{}'.format(self.year, start_month, start_day),
                  '%Y/%m/%d')
              end = datetime.strptime(
                  '{}/{}/{}'.format(self.year, end_month, end_day),
                  '%Y/%m/%d')
              days = (end - start).days + 1
  
              end_date = start_date + timedelta(days=days) + timedelta(hours=23)
              how = pd.IndexSlice[start_date:end_date]
  
              weeks = []
              for name, week in hourly_values.loc[how].groupby(
                      pd.Grouper(freq='168H')):
                  if not week.empty:
                      try:
                          week.loc[:] = self.get_schedule_values(
                              sch_name=ref.Name, start_date=week.index[0],
                              index=week.index, sch_type=ref.key)
                      except ValueError:
                          week.loc[:] = self.get_schedule_values(
                              ref.Name, week.index[0])[0:len(week)]
                      finally:
                          weeks.append(week)
              new = pd.concat(weeks)
              hourly_values.update(new)
              start_date += timedelta(days=days)
  
          return hourly_values.values
  
      def get_schedule_values(self, sch_name=None, start_date=None, index=None,
                              sch_type=None):
          """Main function that returns the schedule values
  
          Args:
              sch_type:
              index:
              start_date:
          """
  
          if sch_name is None:
              sch_name = self.schName
  
          if sch_type is None:
              schedule_values = self.idf.get_schedule_data_by_name(sch_name)
              self.schType = schedule_values.key.upper()
              sch_type = self.schType
          if self.count == 0:
              # This is the first time, get the schedule type and the type limits.
              self.schTypeLimitsName = self.get_schedule_type_limits_name()
          self.count += 1
  
          if sch_type.upper() == "schedule:year".upper():
              hourly_values = self.get_yearly_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:day:interval".upper():
              hourly_values = self.get_interval_day_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:day:hourly".upper():
              hourly_values = self.get_hourly_day_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:day:list".upper():
              hourly_values = self.get_list_day_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:week:compact".upper():
              hourly_values = self.get_compact_weekly_ep_schedule_values(
                  sch_name, start_date, index)
          elif sch_type.upper() == "schedule:week:daily".upper():
              hourly_values = self.get_daily_weekly_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:constant".upper():
              hourly_values = self.get_constant_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:compact".upper():
              hourly_values = self.get_compact_ep_schedule_values(
                  sch_name)
          elif sch_type.upper() == "schedule:file".upper():
              hourly_values = self.get_file_ep_schedule_values(
                  sch_name)
          else:
              log('Archetypal does not support "{}" currently'.format(
                  self.schType), lg.WARNING)
  
              hourly_values = []
  
          return hourly_values
  
      def is_schedule(self, sch_name):
          """Returns True if idfobject is one of 'schedule_types'"""
          if sch_name.upper() in self.idf.schedules_dict:
              return True
          else:
              return False
  
      def to_year_week_day(self):
          """convert a Schedule Class to the 'Schedule:Year',
          'Schedule:Week:Daily' and 'Schedule:Day:Hourly' representation
  
          Returns:
              'Schedule:Year', list of ['Schedule:Week:Daily'],
              list of ['Schedule:Day:Hourly']
          """
  
          full_year = np.array(self.all_values)  # array of shape (8760,)
          values = full_year.reshape(-1, 24)  # shape (365, 24)
  
          # create unique days
          unique_days, nds = np.unique(values, axis=0, return_inverse=True)
  
          ep_days = []
          dict_day = {}
          count_day = 0
          for unique_day in unique_days:
              name = 'd_' + self.schName + '_' + '%03d' % count_day
              name, count_day = archetypal.check_unique_name('d', count_day,
                                                             name,
                                                             archetypal.settings.unique_schedules,
                                                             suffix=True)
              dict_day[name] = unique_day
  
              archetypal.settings.unique_schedules.append(name)
  
              # Create idf_objects for schedule:day:hourly
              ep_day = self.idf.add_object(
                  ep_object='Schedule:Day:Hourly'.upper(),
                  save=False,
                  **dict(Name=name,
                         Schedule_Type_Limits_Name=self.schType,
                         **{'Hour_{}'.format(i + 1): unique_day[i]
                            for i in range(24)})
              )
              ep_days.append(ep_day)
  
          # create unique weeks from unique days
          unique_weeks, nwsi, nws, count = np.unique(
              full_year[:364 * 24, ...].reshape(-1, 168), return_index=True,
              axis=0, return_inverse=True, return_counts=True)
  
          # Appending unique weeks in dictionary with name and values of weeks as
          # keys
          # {'name_week': {'dayName':[]}}
          dict_week = {}
          count_week = 0
          for unique_week in unique_weeks:
              week_id = 'w_' + self.schName + '_' + '%03d' % count_week
              week_id, count_week = archetypal.check_unique_name('w',
                                                                 count_week,
                                                                 week_id,
                                                                 archetypal.settings.unique_schedules,
                                                                 suffix=True)
              archetypal.settings.unique_schedules.append(week_id)
  
              dict_week[week_id] = {}
              for i in list(range(0, 7)):
                  day_of_week = unique_week[..., i * 24:(i + 1) * 24]
                  for key in dict_day:
                      if (day_of_week == dict_day[key]).all():
                          dict_week[week_id]['day_{}'.format(i)] = key
  
          # Create idf_objects for schedule:week:daily
          list_day_of_week = ['Sunday', 'Monday', 'Tuesday',
                              'Wednesday', 'Thursday', 'Friday', 'Saturday']
          ordered_day_n = np.array([6, 0, 1, 2, 3, 4, 5])
          ordered_day_n = np.roll(ordered_day_n, self.startDayOfTheWeek)
          ep_weeks = []
          for week_id in dict_week:
              ep_week = self.idf.add_object(
                  ep_object='Schedule:Week:Daily'.upper(),
                  save=False,
                  **dict(Name=week_id,
                         **{'{}_ScheduleDay_Name'.format(
                             weekday): dict_week[week_id][
                             'day_{}'.format(i)] for
                            i, weekday in
                            zip(ordered_day_n, list_day_of_week)
                            },
                         Holiday_ScheduleDay_Name=
                         dict_week[week_id]['day_6'],
                         SummerDesignDay_ScheduleDay_Name=
                         dict_week[week_id]['day_1'],
                         WinterDesignDay_ScheduleDay_Name=
                         dict_week[week_id]['day_1'],
                         CustomDay1_ScheduleDay_Name=
                         dict_week[week_id]['day_2'],
                         CustomDay2_ScheduleDay_Name=
                         dict_week[week_id]['day_5'])
              )
              ep_weeks.append(ep_week)
  
          import itertools
          blocks = {}
          from_date = datetime(self.year, 1, 1)
          bincount = [sum(1 for _ in group)
                      for key, group in itertools.groupby(nws + 1) if key]
          week_order = {i: v for i, v in enumerate(np.array(
              [key for key, group in itertools.groupby(nws + 1) if key]) - 1)}
          for i, (week_n, count) in enumerate(
                  zip(week_order, bincount)):
              week_id = list(dict_week)[week_order[i]]
              to_date = from_date + timedelta(days=int(count * 7), hours=-1)
              blocks[i] = {}
              blocks[i]['week_id'] = week_id
              blocks[i]['from_day'] = from_date.day
              blocks[i]['end_day'] = to_date.day
              blocks[i]['from_month'] = from_date.month
              blocks[i]['end_month'] = to_date.month
              from_date = to_date + timedelta(hours=1)
  
              # If this is the last block, force end of year
              if i == len(bincount) - 1:
                  blocks[i]['end_day'] = 31
                  blocks[i]['end_month'] = 12
  
          new_dict = dict(Name=self.schName + '_',
                          Schedule_Type_Limits_Name=self.schTypeLimitsName)
          for i in blocks:
              new_dict.update({"ScheduleWeek_Name_{}".format(i + 1):
                                   blocks[i]['week_id'],
                               "Start_Month_{}".format(i + 1):
                                   blocks[i]['from_month'],
                               "Start_Day_{}".format(i + 1):
                                   blocks[i]['from_day'],
                               "End_Month_{}".format(i + 1):
                                   blocks[i]['end_month'],
                               "End_Day_{}".format(i + 1):
                                   blocks[i]['end_day']})
  
          ep_year = self.idf.add_object(ep_object='Schedule:Year'.upper(),
                                        save=False, **new_dict)
          return ep_year, ep_weeks, ep_days
  
      def date_field_interpretation(self, field):
          """Date Field Interpretation
  
          Args:
              field (str): The EnergyPlus Field Contents
  
          Returns:
              (datetime): The datetime object
  
          Info:
              See EnergyPlus documentation for more details:
              1.6.8.1.2 Field: Start Date (Table 1.4: Date Field Interpretation)
          """
          # < number > Weekday in Month
          formats = ['%m/%d', '%d %B', '%B %d', '%d %b', '%b %d']
          date = None
          for format_str in formats:
              # Tru to parse using each defined formats
              try:
                  date = datetime.strptime(field, format_str)
              except:
                  pass
              else:
                  date = datetime(self.year, date.month, date.day)
          if date is None:
              # if the defined formats did not work, try the fancy parse
              try:
                  date = self.parse_fancy_string(field)
              except:
                  msg = "the schedule '{sch}' contains a " \
                        "Field that is not understood: '{field}'".format(
                      sch=self.schName,
                      field=field)
                  raise ValueError(msg)
              else:
                  return date
          else:
              return date
  
      def parse_fancy_string(self, field):
          """Will try to parse cases such as `3rd Monday in February` or `Last
          Weekday In Month`
  
          Args:
              field (str): The EnergyPlus Field Contents
  
          Returns:
              (datetime): The datetime object
          """
          import re
  
          # split the string at the term ' in '
          time, month = field.lower().split(' in ')
          month = datetime.strptime(month, '%B').month
  
          # split the first part into nth and dayofweek
          nth, dayofweek = time.split(' ')
          if 'last' in nth:
              nth = -1  # Use the last one
          else:
              nth = re.findall(r'\d+', nth)  # use the nth one
              nth = int(nth[0]) - 1  # python is zero-based
  
          weekday = {'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3,
                     'friday': 4, 'saturday': 5, 'sunday': 6}
  
          # parse the dayofweek eg. monday
          dayofweek = weekday.get(dayofweek, 6)
  
          # create list of possible days using Calendar
          import calendar
          c = calendar.Calendar(firstweekday=self.startDayOfTheWeek)
          monthcal = c.monthdatescalendar(self.year, month)
  
          # iterate though the month and get the nth weekday
          date = [day for week in monthcal for day in week if \
                  day.weekday() == dayofweek and \
                  day.month == month][nth]
          return datetime(date.year, date.month, date.day)
  
      def field_set(self, field, slicer_=None):
          """helper function to return the proper slicer depending on the
          field_set value.
  
          Available values are:
          Weekdays, Weekends, Holidays, Alldays, SummerDesignDay,
          WinterDesignDay, Sunday, Monday, Tuesday, Wednesday, Thursday,
          Friday, Saturday, CustomDay1, CustomDay2, AllOtherDays
  
          Args:
              field (str): The EnergyPlus field set value.
              slicer_ (pd.Series): The persistent slicer for this schedule
  
          Returns:
              (indexer-like): Returns the appropriate indexer for the series.
          """
  
          if field.lower() == 'weekdays':
              # return only days of weeks
              return lambda x: x.index.dayofweek < 5
          elif field.lower() == 'weekends':
              # return only weekends
              return lambda x: x.index.dayofweek >= 5
          elif field.lower() == 'alldays':
              log('For schedule "{}", the field-set "AllDays" may be overridden '
                  'by the "AllOtherDays" field-set'.format(
                  self.schName), lg.WARNING)
              # return all days := equivalenet to .loc[:]
              return pd.IndexSlice[:]
          elif field.lower() == 'allotherdays':
              # return unused days (including special days). Uses the global
              # variable `slicer_`
              import operator
              if slicer_ is not None:
                  return _conjunction(*[self.special_day(field, slicer_),
                                        ~slicer_], logical=operator.or_)
              else:
                  raise NotImplementedError
          elif field.lower() == 'sunday':
              # return only sundays
              return lambda x: x.index.dayofweek == 6
          elif field.lower() == 'monday':
              # return only mondays
              return lambda x: x.index.dayofweek == 0
          elif field.lower() == 'tuesday':
              # return only Tuesdays
              return lambda x: x.index.dayofweek == 1
          elif field.lower() == 'wednesday':
              # return only Wednesdays
              return lambda x: x.index.dayofweek == 2
          elif field.lower() == 'thursday':
              # return only Thursdays
              return lambda x: x.index.dayofweek == 3
          elif field.lower() == 'friday':
              # return only Fridays
              return lambda x: x.index.dayofweek == 4
          elif field.lower() == 'saturday':
              # return only Saturdays
              return lambda x: x.index.dayofweek == 5
          elif field.lower() == 'summerdesignday':
              # return design_day(self, field)
              return None
          elif field.lower() == 'winterdesignday':
              # return design_day(self, field)
              return None
          elif field.lower() == 'holiday' or field.lower() == 'holidays':
              field = 'holiday'
              return self.special_day(field, slicer_)
          elif not self.strict:
              # If not strict, ignore missing field-sets such as CustomDay1
              return pd.IndexSlice[:]
          else:
              raise NotImplementedError(
                  'Archetypal does not yet support The '
                  'Field_set "{}"'.format(field))
  
      def __len__(self):
          """returns the length of all values of the schedule"""
          return len(self.all_values)
  
      def __eq__(self, other):
          """Overrides the default implementation"""
          if isinstance(other, Schedule):
              return self.all_values == other.all_values
          else:
              raise NotImplementedError
  
      def __ne__(self, other):
          return ~(self.__eq__(other))
  
      def __add__(self, other):
          if isinstance(other, Schedule):
              return self.all_values + other.all_values
          elif isinstance(other, list):
              return self.all_values + other
          else:
              raise NotImplementedError
  
      def __sub__(self, other):
          if isinstance(other, Schedule):
              return self.all_values - other.all_values
          elif isinstance(other, list):
              return self.all_values - other
          else:
              raise NotImplementedError
  
      def __mul__(self, other):
          if isinstance(other, Schedule):
              return self.all_values * other.all_values
          elif isinstance(other, list):
              return self.all_values * other
          else:
              raise NotImplementedError
  
      def get_sdow(self, start_day_of_week):
          """Returns the start day of the week"""
          if start_day_of_week is None:
              return self.idf.day_of_week_for_start_day
          else:
              return start_day_of_week
  
      def special_day(self, field, slicer_):
          """try to get the RunPeriodControl:SpecialDays for the corresponding
          Day Type"""
          sp_slicer_ = slicer_.copy()
          sp_slicer_.loc[:] = False
          special_day_types = ['holiday', 'customday1', 'customday2']
  
          dds = self.idf.idfobjects['RunPeriodControl:SpecialDays'.upper()]
          dd = [dd for dd in dds if dd.Special_Day_Type.lower() == field
                or dd.Special_Day_Type.lower() in special_day_types]
          if len(dd) > 0:
              slice = []
              for dd in dd:
                  # can have more than one special day types
                  data = dd.Start_Date
                  ep_start_date = self.date_field_interpretation(data)
                  ep_orig = datetime(self.year, 1, 1)
                  days_to_speciald = (ep_start_date - ep_orig).days
                  duration = int(dd.Duration)
                  from_date = self.startDate + timedelta(days=days_to_speciald)
                  to_date = from_date + timedelta(days=duration) + timedelta(
                      hours=-1)
  
                  sp_slicer_.loc[from_date:to_date] = True
              return sp_slicer_
          elif not self.strict:
              return sp_slicer_
          else:
              msg = 'Could not find a "SizingPeriod:DesignDay" object ' \
                    'needed for schedule "{}" with Day Type "{}"'.format(
                  self.schName, field.capitalize()
              )
              raise ValueError(msg)
  
  
  def design_day(schedule, field):
      # try to get the SizingPeriod:DesignDay for the corresponding Day Type
      dds = schedule.idf.idfobjects['SizingPeriod:DesignDay'.upper()]
      dd = [dd for dd in dds if dd.Day_Type.lower() == field]
      if len(dd) > 0:
          # should have found only one design day matching the Day Type
  
          data = [dd[0].Month, dd[0].Day_of_Month]
          date = '/'.join([str(item).zfill(2) for item in data])
          date = schedule.date_field_interpretation(date)
          return lambda x: x.index == date
      else:
          msg = 'Could not find a "SizingPeriod:DesignDay" object ' \
                'needed for schedule "{}" with Day Type "{}"'.format(
              schedule.schName, field.capitalize()
          )
          raise ValueError(msg)
  
  
  def _conjunction(*conditions, logical=np.logical_and):
      """Applies a logical function on n conditions"""
      return functools.reduce(logical, conditions)
  
  
  def _separator(sep):
      """helper function to return the correct delimiter"""
      if sep == 'Comma':
          return ','
      elif sep == 'Tab':
          return '\t'
      elif sep == 'Fixed':
          return None
      elif sep == 'Semicolon':
          return ';'
      else:
          return ','
  
  
  def _how(how):
      """Helper function to return the correct resampler"""
      if how.lower() == 'average':
          return 'mean'
      elif how.lower() == 'linear':
          return 'interpolate'
      elif how.lower() == 'no':
          return 'max'
      else:
          return 'max'
  
apis: numpy.roll pandas.Series pandas.date_range pandas.read_csv numpy.arange pandas.Grouper pandas.concat numpy.array numpy.unique numpy.mean