Upgrade origin-src to google transit feed 1.2.6
[bus.git] / origin-src / transitfeed-1.2.6 / transitfeed / serviceperiod.py
blob:a/origin-src/transitfeed-1.2.6/transitfeed/serviceperiod.py -> blob:b/origin-src/transitfeed-1.2.6/transitfeed/serviceperiod.py
--- a/origin-src/transitfeed-1.2.6/transitfeed/serviceperiod.py
+++ b/origin-src/transitfeed-1.2.6/transitfeed/serviceperiod.py
@@ -1,1 +1,322 @@
-
+#!/usr/bin/python2.5
+
+# Copyright (C) 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import re
+import time
+
+import problems as problems_module
+import util
+
+class ServicePeriod(object):
+  """Represents a service, which identifies a set of dates when one or more
+  trips operate."""
+  _DAYS_OF_WEEK = [
+    'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
+    'saturday', 'sunday'
+    ]
+  _FIELD_NAMES_REQUIRED = [
+    'service_id', 'start_date', 'end_date'
+    ] + _DAYS_OF_WEEK
+  _FIELD_NAMES = _FIELD_NAMES_REQUIRED  # no optional fields in this one
+  _FIELD_NAMES_CALENDAR_DATES = ['service_id', 'date', 'exception_type']
+
+  def __init__(self, id=None, field_list=None):
+    self.original_day_values = []
+    if field_list:
+      self.service_id = field_list[self._FIELD_NAMES.index('service_id')]
+      self.day_of_week = [False] * len(self._DAYS_OF_WEEK)
+
+      for day in self._DAYS_OF_WEEK:
+        value = field_list[self._FIELD_NAMES.index(day)] or ''  # can be None
+        self.original_day_values += [value.strip()]
+        self.day_of_week[self._DAYS_OF_WEEK.index(day)] = (value == u'1')
+
+      self.start_date = field_list[self._FIELD_NAMES.index('start_date')]
+      self.end_date = field_list[self._FIELD_NAMES.index('end_date')]
+    else:
+      self.service_id = id
+      self.day_of_week = [False] * 7
+      self.start_date = None
+      self.end_date = None
+    self.date_exceptions = {}  # Map from 'YYYYMMDD' to 1 (add) or 2 (remove)
+
+  def _IsValidDate(self, date):
+    if re.match('^\d{8}$', date) == None:
+      return False
+
+    try:
+      time.strptime(date, "%Y%m%d")
+      return True
+    except ValueError:
+      return False
+
+  def HasExceptions(self):
+    """Checks if the ServicePeriod has service exceptions."""
+    if self.date_exceptions:
+      return True
+    else:
+      return False
+
+  def GetDateRange(self):
+    """Return the range over which this ServicePeriod is valid.
+
+    The range includes exception dates that add service outside of
+    (start_date, end_date), but doesn't shrink the range if exception
+    dates take away service at the edges of the range.
+
+    Returns:
+      A tuple of "YYYYMMDD" strings, (start date, end date) or (None, None) if
+      no dates have been given.
+    """
+    start = self.start_date
+    end = self.end_date
+
+    for date in self.date_exceptions:
+      if self.date_exceptions[date] == 2:
+        continue
+      if not start or (date < start):
+        start = date
+      if not end or (date > end):
+        end = date
+    if start is None:
+      start = end
+    elif end is None:
+      end = start
+    # If start and end are None we did a little harmless shuffling
+    return (start, end)
+
+  def GetCalendarFieldValuesTuple(self):
+    """Return the tuple of calendar.txt values or None if this ServicePeriod
+    should not be in calendar.txt ."""
+    if self.start_date and self.end_date:
+      return [getattr(self, fn) for fn in self._FIELD_NAMES]
+
+  def GenerateCalendarDatesFieldValuesTuples(self):
+    """Generates tuples of calendar_dates.txt values. Yield zero tuples if
+    this ServicePeriod should not be in calendar_dates.txt ."""
+    for date, exception_type in self.date_exceptions.items():
+      yield (self.service_id, date, unicode(exception_type))
+
+  def GetCalendarDatesFieldValuesTuples(self):
+    """Return a list of date execeptions"""
+    result = []
+    for date_tuple in self.GenerateCalendarDatesFieldValuesTuples():
+      result.append(date_tuple)
+    result.sort()  # helps with __eq__
+    return result
+
+  def SetDateHasService(self, date, has_service=True, problems=None):
+    if date in self.date_exceptions and problems:
+      problems.DuplicateID(('service_id', 'date'),
+                           (self.service_id, date),
+                           type=problems_module.TYPE_WARNING)
+    self.date_exceptions[date] = has_service and 1 or 2
+
+  def ResetDateToNormalService(self, date):
+    if date in self.date_exceptions:
+      del self.date_exceptions[date]
+
+  def SetStartDate(self, start_date):
+    """Set the first day of service as a string in YYYYMMDD format"""
+    self.start_date = start_date
+
+  def SetEndDate(self, end_date):
+    """Set the last day of service as a string in YYYYMMDD format"""
+    self.end_date = end_date
+
+  def SetDayOfWeekHasService(self, dow, has_service=True):
+    """Set service as running (or not) on a day of the week. By default the
+    service does not run on any days.
+
+    Args:
+      dow: 0 for Monday through 6 for Sunday
+      has_service: True if this service operates on dow, False if it does not.
+
+    Returns:
+      None
+    """
+    assert(dow >= 0 and dow < 7)
+    self.day_of_week[dow] = has_service
+
+  def SetWeekdayService(self, has_service=True):
+    """Set service as running (or not) on all of Monday through Friday."""
+    for i in range(0, 5):
+      self.SetDayOfWeekHasService(i, has_service)
+
+  def SetWeekendService(self, has_service=True):
+    """Set service as running (or not) on Saturday and Sunday."""
+    self.SetDayOfWeekHasService(5, has_service)
+    self.SetDayOfWeekHasService(6, has_service)
+
+  def SetServiceId(self, service_id):
+    """Set the service_id for this schedule. Generally the default will
+    suffice so you won't need to call this method."""
+    self.service_id = service_id
+
+  def IsActiveOn(self, date, date_object=None):
+    """Test if this service period is active on a date.
+
+    Args:
+      date: a string of form "YYYYMMDD"
+      date_object: a date object representing the same date as date.
+                   This parameter is optional, and present only for performance
+                   reasons.
+                   If the caller constructs the date string from a date object
+                   that date object can be passed directly, thus avoiding the 
+                   costly conversion from string to date object.
+
+    Returns:
+      True iff this service is active on date.
+    """
+    if date in self.date_exceptions:
+      if self.date_exceptions[date] == 1:
+        return True
+      else:
+        return False
+    if (self.start_date and self.end_date and self.start_date <= date and
+        date <= self.end_date):
+      if date_object is None:
+        date_object = util.DateStringToDateObject(date)
+      return self.day_of_week[date_object.weekday()]
+    return False
+
+  def ActiveDates(self):
+    """Return dates this service period is active as a list of "YYYYMMDD"."""
+    (earliest, latest) = self.GetDateRange()
+    if earliest is None:
+      return []
+    dates = []
+    date_it = util.DateStringToDateObject(earliest)
+    date_end = util.DateStringToDateObject(latest)
+    delta = datetime.timedelta(days=1)
+    while date_it <= date_end:
+      date_it_string = date_it.strftime("%Y%m%d")
+      if self.IsActiveOn(date_it_string, date_it):
+        dates.append(date_it_string)
+      date_it = date_it + delta
+    return dates
+
+  def __getattr__(self, name):
+    try:
+      # Return 1 if value in day_of_week is True, 0 otherwise
+      return (self.day_of_week[self._DAYS_OF_WEEK.index(name)]
+              and 1 or 0)
+    except KeyError:
+      pass
+    except ValueError:  # not a day of the week
+      pass
+    raise AttributeError(name)
+
+  def __getitem__(self, name):
+    return getattr(self, name)
+
+  def __eq__(self, other):
+    if not other:
+      return False
+
+    if id(self) == id(other):
+      return True
+
+    if (self.GetCalendarFieldValuesTuple() !=
+        other.GetCalendarFieldValuesTuple()):
+      return False
+
+    if (self.GetCalendarDatesFieldValuesTuples() !=
+        other.GetCalendarDatesFieldValuesTuples()):
+      return False
+
+    return True
+
+  def __ne__(self, other):
+    return not self.__eq__(other)
+
+  def ValidateServiceId(self, problems):
+    if util.IsEmpty(self.service_id):
+      problems.MissingValue('service_id')
+
+  def ValidateStartDate(self, problems):
+    if self.start_date is not None:
+      if util.IsEmpty(self.start_date):
+        problems.MissingValue('start_date')
+        self.start_date = None
+      elif not self._IsValidDate(self.start_date):
+        problems.InvalidValue('start_date', self.start_date)
+        self.start_date = None
+
+  def ValidateEndDate(self, problems):
+    if self.end_date is not None:
+      if util.IsEmpty(self.end_date):
+        problems.MissingValue('end_date')
+        self.end_date = None
+      elif not self._IsValidDate(self.end_date):
+        problems.InvalidValue('end_date', self.end_date)
+        self.end_date = None
+
+  def ValidateEndDateAfterStartDate(self, problems):
+    if self.start_date and self.end_date and self.end_date < self.start_date:
+      problems.InvalidValue('end_date', self.end_date,
+                            'end_date of %s is earlier than '
+                            'start_date of "%s"' %
+                            (self.end_date, self.start_date))
+
+  def ValidateDaysOfWeek(self, problems):
+    if self.original_day_values:
+      index = 0
+      for value in self.original_day_values:
+        column_name = self._DAYS_OF_WEEK[index]
+        if util.IsEmpty(value):
+          problems.MissingValue(column_name)
+        elif (value != u'0') and (value != '1'):
+          problems.InvalidValue(column_name, value)
+        index += 1
+
+  def ValidateHasServiceAtLeastOnceAWeek(self, problems):
+    if (True not in self.day_of_week and
+        1 not in self.date_exceptions.values()):
+      problems.OtherProblem('Service period with service_id "%s" '
+                            'doesn\'t have service on any days '
+                            'of the week.' % self.service_id,
+                            type=problems_module.TYPE_WARNING)
+
+  def ValidateDates(self, problems):
+    for date in self.date_exceptions:
+      if not self._IsValidDate(date):
+        problems.InvalidValue('date', date)
+
+  def Validate(self, problems=problems_module.default_problem_reporter):
+
+    self.ValidateServiceId(problems)
+
+    # self.start_date/self.end_date is None in 3 cases:
+    # ServicePeriod created by loader and
+    #   1a) self.service_id wasn't in calendar.txt
+    #   1b) calendar.txt didn't have a start_date/end_date column
+    # ServicePeriod created directly and
+    #   2) start_date/end_date wasn't set
+    # In case 1a no problem is reported. In case 1b the missing required column
+    # generates an error in _ReadCSV so this method should not report another
+    # problem. There is no way to tell the difference between cases 1b and 2
+    # so case 2 is ignored because making the feedvalidator pretty is more
+    # important than perfect validation when an API users makes a mistake.
+    self.ValidateStartDate(problems)
+    self.ValidateEndDate(problems)
+
+    self.ValidateEndDateAfterStartDate(problems)
+    self.ValidateDaysOfWeek(problems)
+    self.ValidateHasServiceAtLeastOnceAWeek(problems)
+    self.ValidateDates(problems)
+