Package Gnumed :: Package timelinelib :: Package time :: Module pytime |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.time.pytime

      1  # Copyright (C) 2009, 2010, 2011  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  from datetime import datetime
     20  from datetime import time
     21  from datetime import timedelta
     22  import calendar
     23  import re
     24
     25  from timelinelib.calendar.monthnames import abbreviated_name_of_month
     26  from timelinelib.calendar.weekdaynames import abbreviated_name_of_weekday
     27  from timelinelib.db.objects import TimeOutOfRangeLeftError
     28  from timelinelib.db.objects import TimeOutOfRangeRightError
     29  from timelinelib.db.objects import TimePeriod
     30  from timelinelib.db.objects import time_period_center
     31  from timelinelib.drawing.interface import Strip
     32  from timelinelib.drawing.utils import get_default_font
     33  from timelinelib.time.typeinterface import TimeType
     34
     35
     36  # To save computation power (used by `delta_to_microseconds`)
     37  US_PER_SEC = 1000000
     38  US_PER_DAY = 24 * 60 * 60 * US_PER_SEC
     39
     40
     41 -class PyTimeType(TimeType):
     42
     43 -    def __eq__(self, other):
     44          return isinstance(other, PyTimeType)
     45
     46 -    def __ne__(self, other):
     47          return not (self == other)
     48
     49 -    def time_string(self, time):
     50          return "%s-%s-%s %s:%s:%s" % (time.year, time.month, time.day,
     51                                        time.hour, time.minute, time.second)
     52
     53 -    def parse_time(self, time_string):
     54          match = re.search(r"^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$", time_string)
     55          if match:
     56              year = int(match.group(1))
     57              month = int(match.group(2))
     58              day = int(match.group(3))
     59              hour = int(match.group(4))
     60              minute = int(match.group(5))
     61              second = int(match.group(6))
     62              try:
     63                  return datetime(year, month, day, hour, minute, second)
     64              except ValueError:
     65                  raise ValueError("Invalid time, time string = '%s'" % time_string)
     66          else:
     67              raise ValueError("Time not on correct format = '%s'" % time_string)
     68
     91
     92 -    def is_date_time_type(self):
     93          return True
     94
     95 -    def format_period(self, time_period):
     96          """Returns a unicode string describing the time period."""
     97          def label_with_time(time):
     98              return u"%s %s" % (label_without_time(time), time_label(time))
     99          def label_without_time(time):
    100              return u"%s %s %s" % (time.day, abbreviated_name_of_month(time.month), time.year)
    101          def time_label(time):
    102              return time.time().isoformat()[0:5]
    103          if time_period.is_period():
    104              if time_period.has_nonzero_time():
    105                  label = u"%s to %s" % (label_with_time(time_period.start_time),
    106                                        label_with_time(time_period.end_time))
    107              else:
    108                  label = u"%s to %s" % (label_without_time(time_period.start_time),
    109                                        label_without_time(time_period.end_time))
    110          else:
    111              if time_period.has_nonzero_time():
    112                  label = u"%s" % label_with_time(time_period.start_time)
    113              else:
    114                  label = u"%s" % label_without_time(time_period.start_time)
    115          return label
    116
    117 -    def format_delta(self, delta):
    118          days = delta.days
    119          hours = delta.seconds / 3600
    120          minutes = (delta.seconds - hours * 3600) / 60
    121          collector = []
    122          if days == 1:
    123              collector.append(u"1 %s" % _("day"))
    124          elif days > 1:
    125              collector.append(u"%d %s" % (days, _("days")))
    126          if hours == 1:
    127              collector.append(u"1 %s" % _("hour"))
    128          elif hours > 1:
    129              collector.append(u"%d %s" % (hours, _("hours")))
    130          if minutes == 1:
    131              collector.append(u"1 %s" % _("minute"))
    132          elif minutes > 1:
    133              collector.append(u"%d %s" % (minutes, _("minutes")))
    134          delta_string = u" ".join(collector)
    135          if delta_string == "":
    136              delta_string = "0"
    137          return delta_string
    138
    139 -    def get_min_time(self):
    140          min_time = datetime(10, 1, 1)
    141          return (min_time, _("can't be before year 10"))
    142
    143 -    def get_max_time(self):
    144          max_time = datetime(9990, 1, 1)
    145          return (max_time, _("can't be after year 9989"))
    146
    147 -    def choose_strip(self, metrics, config):
    148          """
    149          Return a tuple (major_strip, minor_strip) for current time period and
    150          window size.
    151          """
    152          today = datetime.now()
    153          tomorrow = today + timedelta(days=1)
    154          day_period = TimePeriod(self, today, tomorrow)
    155          one_day_width = metrics.calc_exact_width(day_period)
    156          if one_day_width > 600:
    157              return (StripDay(), StripHour())
    158          elif one_day_width > 45:
    159              return (StripWeek(config), StripWeekday())
    160          elif one_day_width > 25:
    161              return (StripMonth(), StripDay())
    162          elif one_day_width > 1.5:
    163              return (StripYear(), StripMonth())
    164          elif one_day_width > 0.12:
    165              return (StripDecade(), StripYear())
    166          elif one_day_width > 0.012:
    167              return (StripCentury(), StripDecade())
    168          else:
    169              return (StripCentury(), StripCentury())
    170
    171 -    def mult_timedelta(self, delta, num):
    172          """Return a new timedelta that is `num` times larger than `delta`."""
    173          days = delta.days * num
    174          seconds = delta.seconds * num
    175          microseconds = delta.microseconds * num
    176          return timedelta(days, seconds, microseconds)
    177
    178 -    def get_default_time_period(self):
    179          return time_period_center(self, datetime.now(), timedelta(days=30))
    180
    181 -    def now(self):
    182          return datetime.now()
    183
    184 -    def get_time_at_x(self, time_period, x_percent_of_width):
    185          """Return the time at pixel `x`."""
    186          microsecs = delta_to_microseconds(time_period.delta())
    187          microsecs = microsecs * x_percent_of_width
    188          return time_period.start_time + microseconds_to_delta(microsecs)
    189
    190 -    def div_timedeltas(self, delta1, delta2):
    191          """Return how many times delta2 fit in delta1."""
    192          # Since Python can handle infinitely large numbers, this solution works. It
    193          # might however not be optimal. If you are clever, you should be able to
    194          # treat the different parts individually. But this is simple.
    195          total_us1 = delta_to_microseconds(delta1)
    196          total_us2 = delta_to_microseconds(delta2)
    197          # Make sure that the result is a floating point number
    198          return total_us1 / float(total_us2)
    199
    200 -    def get_max_zoom_delta(self):
    201          return (timedelta(days=1200*365),
    202                  _("Can't zoom wider than 1200 years"))
    203
    204 -    def get_min_zoom_delta(self):
    205          return (timedelta(hours=1), _("Can't zoom deeper than 1 hour"))
    206
    207 -    def get_zero_delta(self):
    208          return timedelta(0)
    209
    210 -    def time_period_has_nonzero_time(self, time_period):
    211          nonzero_time = (time_period.start_time.time() != time(0, 0, 0) or
    212                          time_period.end_time.time()   != time(0, 0, 0))
    213          return nonzero_time
    214
    215 -    def get_name(self):
    216          return u"pytime"
    217
    218 -    def get_duplicate_functions(self):
    219          return [
    220              (_("Day"), move_period_num_days),
    221              (_("Week"), move_period_num_weeks),
    222              (_("Month"), move_period_num_months),
    223              (_("Year"), move_period_num_years),
    224          ]
    225
    226 -    def zoom_is_ok(self, delta):
    227          return (delta.seconds > 3600) or (delta.days > 0)
    228
    229 -    def half_delta(self, delta):
    230          return delta / 2
    231
    232 -    def margin_delta(self, delta):
    233          return delta / 24
    234
    235 -    def event_date_string(self, time):
    236          return "%04d-%02d-%02d" % (time.year, time.month, time.day)
    237
    238 -    def event_time_string(self, time):
    239          return "%02d:%02d" % (time.hour, time.minute)
    240
    241 -    def eventtimes_equals(self, time1, time2):
    242          s1 = "%s %s" % (self.event_date_string(time1),
    243                          self.event_date_string(time1))
    244          s2 = "%s %s" % (self.event_date_string(time2),
    245                          self.event_date_string(time2))
    246          return s1 == s2
    247
    248 -    def adjust_for_bc_years(self, time):
    249          return time
    250
    251 -def go_to_today_fn(main_frame, current_period, navigation_fn):
    252      navigation_fn(lambda tp: tp.center(datetime.now()))
    253
    254
    255 -def go_to_date_fn(main_frame, current_period, navigation_fn):
    256      def navigate_to(time):
    257          navigation_fn(lambda tp: tp.center(time))
    258      main_frame.display_time_editor_dialog(
    259          PyTimeType(), current_period.mean_time(), navigate_to, _("Go to Date"))
    260
    261
    262 -def backward_fn(main_frame, current_period, navigation_fn):
    263      _move_page_smart(current_period, navigation_fn, -1)
    264
    265
    266 -def forward_fn(main_frame, current_period, navigation_fn):
    267      _move_page_smart(current_period, navigation_fn, 1)
    268
    269
    270 -def _move_page_smart(current_period, navigation_fn, direction):
    271      if _whole_number_of_years(current_period):
    272          _move_page_years(current_period, navigation_fn, direction)
    273      elif _whole_number_of_months(current_period):
    274          _move_page_months(current_period, navigation_fn, direction)
    275      else:
    276          navigation_fn(lambda tp: tp.move_delta(direction*current_period.delta()))
    277
    278
    279 -def _whole_number_of_years(period):
    280      start, end = period.start_time, period.end_time
    281      year_diff = _calculate_year_diff(period)
    282      whole_years = start.replace(year=start.year+year_diff) == end
    283      return whole_years and year_diff > 0
    284
    285
    286 -def _move_page_years(curret_period, navigation_fn, direction):
    287      def navigate(tp):
    288          year_delta = direction * _calculate_year_diff(curret_period)
    289          new_start_year = curret_period.start_time.year + year_delta
    290          new_end_year = curret_period.end_time.year + year_delta
    291          try:
    292              new_start = curret_period.start_time.replace(year=new_start_year)
    293              new_end = curret_period.end_time.replace(year=new_end_year)
    294          except ValueError:
    295              if direction < 0:
    296                  raise TimeOutOfRangeLeftError()
    297              else:
    298                  raise TimeOutOfRangeRightError()
    299          return tp.update(new_start, new_end)
    300      navigation_fn(navigate)
    301
    302
    303 -def _calculate_year_diff(period):
    304      return period.end_time.year - period.start_time.year
    305
    306
    307 -def _whole_number_of_months(period):
    308      start, end = period.start_time, period.end_time
    309      start_months = start.year * 12 + start.month
    310      end_months = end.year * 12 + end.month
    311      month_diff = end_months - start_months
    312      whole_months = start.day == 1 and end.day == 1
    313      return whole_months and month_diff > 0
    314
    315
    316 -def _move_page_months(curret_period, navigation_fn, direction):
    317      def navigate(tp):
    318          start_months = curret_period.start_time.year * 12 + curret_period.start_time.month
    319          end_months = curret_period.end_time.year * 12 + curret_period.end_time.month
    320          month_diff = end_months - start_months
    321          month_delta = month_diff * direction
    322          new_start_year, new_start_month = _months_to_year_and_month(start_months + month_delta)
    323          new_end_year, new_end_month = _months_to_year_and_month(end_months + month_delta)
    324          try:
    325              new_start = curret_period.start_time.replace(year=new_start_year, month=new_start_month)
    326              new_end = curret_period.end_time.replace(year=new_end_year, month=new_end_month)
    327          except ValueError:
    328              if direction < 0:
    329                  raise TimeOutOfRangeLeftError()
    330              else:
    331                  raise TimeOutOfRangeRightError()
    332          return tp.update(new_start, new_end)
    333      navigation_fn(navigate)
    334
    335
    336 -def _months_to_year_and_month(months):
    337      years = int(months / 12)
    338      month = months - years * 12
    339      if month == 0:
    340          month = 12
    341          years -= 1
    342      return years, month
    343
    344
    345 -def forward_one_week_fn(main_frame, current_period, navigation_fn):
    346      wk = timedelta(days=7)
    347      navigation_fn(lambda tp: tp.move_delta(wk))
    348
    349
    350 -def backward_one_week_fn(main_frame, current_period, navigation_fn):
    351      wk = timedelta(days=7)
    352      navigation_fn(lambda tp: tp.move_delta(-1*wk))
    353
    354
    355 -def navigate_month_step(current_period, navigation_fn, direction):
    356      """
    357      Currently does notice leap years.
    358      """
    359      tm = current_period.mean_time()
    360      if direction > 0:
    361          if tm.month == 2:
    362              d = 28
    363          elif tm.month in (4,6,9,11):
    364              d = 30
    365          else:
    366              d = 31
    367      else:
    368          if tm.month == 3:
    369              d = 28
    370          elif tm.month in (5,7,10,12):
    371              d = 30
    372          else:
    373              d = 31
    374      mv = timedelta(days=d)
    375      navigation_fn(lambda tp: tp.move_delta(direction*mv))
    376
    377
    378 -def forward_one_month_fn(main_frame, current_period, navigation_fn):
    379      navigate_month_step(current_period, navigation_fn, 1)
    380
    381
    382 -def backward_one_month_fn(main_frame, current_period, navigation_fn):
    383      navigate_month_step(current_period, navigation_fn, -1)
    384
    385
    386 -def forward_one_year_fn(main_frame, current_period, navigation_fn):
    387      yr = timedelta(days=365)
    388      navigation_fn(lambda tp: tp.move_delta(yr))
    389
    390
    391 -def backward_one_year_fn(main_frame, current_period, navigation_fn):
    392      yr = timedelta(days=365)
    393      navigation_fn(lambda tp: tp.move_delta(-1*yr))
    394
    395
    396 -def fit_millennium_fn(main_frame, current_period, navigation_fn):
    397      mean = current_period.mean_time()
    398      if mean.year > get_millenium_max_year():
    399          year = get_millenium_max_year()
    400      else:
    401          year = max(get_min_year(), int(mean.year/1000)*1000)
    402      start = datetime(year, 1, 1)
    403      end = datetime(year + 1000, 1, 1)
    404      navigation_fn(lambda tp: tp.update(start, end))
    405
    406
    407 -def get_min_year():
    408      return PyTimeType().get_min_time()[0].year
    409
    410
    411 -def get_millenium_max_year():
    412      return PyTimeType().get_max_time()[0].year - 1000
    413
    414
    415 -def get_century_max_year():
    416      return PyTimeType().get_max_time()[0].year - 100
    417
    418
    419 -def fit_century_fn(main_frame, current_period, navigation_fn):
    420      mean = current_period.mean_time()
    421      if mean.year > get_century_max_year():
    422          year = get_century_max_year()
    423      else:
    424          year = max(get_min_year(), int(mean.year/100)*100)
    425      start = datetime(year, 1, 1)
    426      end = datetime(year + 100, 1, 1)
    427      navigation_fn(lambda tp: tp.update(start, end))
    428
    429
    430 -def fit_decade_fn(main_frame, current_period, navigation_fn):
    431      mean = current_period.mean_time()
    432      start = datetime(int(mean.year/10)*10, 1, 1)
    433      end = datetime(int(mean.year/10)*10+10, 1, 1)
    434      navigation_fn(lambda tp: tp.update(start, end))
    435
    436
    437 -def fit_year_fn(main_frame, current_period, navigation_fn):
    438      mean = current_period.mean_time()
    439      start = datetime(mean.year, 1, 1)
    440      end = datetime(mean.year + 1, 1, 1)
    441      navigation_fn(lambda tp: tp.update(start, end))
    442
    443
    444 -def fit_month_fn(main_frame, current_period, navigation_fn):
    445      mean = current_period.mean_time()
    446      start = datetime(mean.year, mean.month, 1)
    447      if mean.month == 12:
    448          end = datetime(mean.year + 1, 1, 1)
    449      else:
    450          end = datetime(mean.year, mean.month + 1, 1)
    451      navigation_fn(lambda tp: tp.update(start, end))
    452
    453
    454 -def fit_day_fn(main_frame, current_period, navigation_fn):
    455      mean = current_period.mean_time()
    456      start = datetime(mean.year, mean.month, mean.day)
    457      end = start + timedelta(days=1)
    458      navigation_fn(lambda tp: tp.update(start, end))
    459
    460
    461 -def fit_week_fn(main_frame, current_period, navigation_fn):
    462      mean = current_period.mean_time()
    463      start = datetime(mean.year, mean.month, mean.day)
    464      weekday = datetime.weekday(start)
    465      start = start - timedelta(days=weekday)
    466      if not main_frame.week_starts_on_monday():
    467          start = start - timedelta(days=1)
    468      end = start + timedelta(days=7)
    469      navigation_fn(lambda tp: tp.update(start, end))
    470
    471
    472 -class StripCentury(Strip):
    473
    474 -    def label(self, time, major=False):
    475          if major:
    476              # TODO: This only works for English. Possible to localize?
    477              start_year = self._century_start_year(time.year)
    478              next_start_year = start_year + 100
    479              return str(next_start_year / 100) + " century"
    480          return ""
    481
    482 -    def start(self, time):
    483          return datetime(max(self._century_start_year(time.year), 10), 1, 1)
    484
    485 -    def increment(self, time):
    486          return time.replace(year=time.year + 100)
    487
    488 -    def get_font(self, time_period):
    489          return get_default_font(8)
    490
    491 -    def _century_start_year(self, year):
    492          year = (int(year) / 100) * 100
    493          #if year > get_century_max_year():
    494          #    year = get_century_max_year
    495          return year
    496
    497
    498 -class StripDecade(Strip):
    499
    500 -    def label(self, time, major=False):
    501          # TODO: This only works for English. Possible to localize?
    502          return str(self._decade_start_year(time.year)) + "s"
    503
    504 -    def start(self, time):
    505          return datetime(self._decade_start_year(time.year), 1, 1)
    506
    507 -    def increment(self, time):
    508          return time.replace(year=time.year+10)
    509
    510 -    def _decade_start_year(self, year):
    511          return (int(year) / 10) * 10
    512
    513 -    def get_font(self, time_period):
    514          return get_default_font(8)
    515
    516
    517 -class StripYear(Strip):
    518
    519 -    def label(self, time, major=False):
    520          return str(time.year)
    521
    522 -    def start(self, time):
    523          return datetime(time.year, 1, 1)
    524
    525 -    def increment(self, time):
    526          return time.replace(year=time.year+1)
    527
    528 -    def get_font(self, time_period):
    529          return get_default_font(8)
    530
    531
    532 -class StripMonth(Strip):
    533
    534 -    def label(self, time, major=False):
    535          if major:
    536              return "%s %s" % (abbreviated_name_of_month(time.month), time.year)
    537          return abbreviated_name_of_month(time.month)
    538
    539 -    def start(self, time):
    540          return datetime(time.year, time.month, 1)
    541
    542 -    def increment(self, time):
    543          return time + timedelta(calendar.monthrange(time.year, time.month)[1])
    544
    545 -    def get_font(self, time_period):
    546          return get_default_font(8)
    547
    548
    549 -class StripDay(Strip):
    550
    551 -    def label(self, time, major=False):
    552          if major:
    553              return "%s %s %s" % (time.day, abbreviated_name_of_month(time.month), time.year)
    554          return str(time.day)
    555
    556 -    def start(self, time):
    557          return datetime(time.year, time.month, time.day)
    558
    559 -    def increment(self, time):
    560          return time + timedelta(1)
    561
    562 -    def get_font(self, time_period):
    563          if (time_period.start_time.weekday() in (5, 6)):
    564                  return get_default_font(8, True)
    565          else:
    566              return get_default_font(8)
    567
    568
    569 -class StripWeek(Strip):
    570
    571 -    def __init__(self, config):
    572          Strip.__init__(self)
    573          self.config = config
    574
    575 -    def label(self, time, major=False):
    576          if major:
    577              # Example: Week 23 (1-7 Jan 2009)
    578              first_weekday = self.start(time)
    579              next_first_weekday = self.increment(first_weekday)
    580              last_weekday = next_first_weekday - timedelta(days=1)
    581              range_string = self._time_range_string(first_weekday, last_weekday)
    582              if self.config.week_start == "monday":
    583                  return (_("Week") + " %s (%s)") % (time.isocalendar()[1], range_string)
    584              else:
    585                  # It is sunday (don't know what to do about week numbers here)
    586                  return range_string
    587          # This strip should never be used as minor
    588          return ""
    589
    590 -    def start(self, time):
    591          stripped_date = datetime(time.year, time.month, time.day)
    592          if self.config.week_start == "monday":
    593              days_to_subtract = stripped_date.weekday()
    594          else:
    595              # It is sunday
    596              days_to_subtract = (stripped_date.weekday() + 1) % 7
    597          return stripped_date - timedelta(days=days_to_subtract)
    598
    599 -    def increment(self, time):
    600          return time + timedelta(7)
    601
    602 -    def get_font(self, time_period):
    603          return get_default_font(8)
    604
    605 -    def _time_range_string(self, time1, time2):
    606          """
    607          Examples:
    608
    609          * 1-7 Jun 2009
    610          * 28 Jun-3 Jul 2009
    611          * 28 Jun 08-3 Jul 2009
    612          """
    613          if time1.year == time2.year:
    614              if time1.month == time2.month:
    615                  return "%s-%s %s %s" % (time1.day, time2.day,
    616                                          abbreviated_name_of_month(time1.month),
    617                                          time1.year)
    618              return "%s %s-%s %s %s" % (time1.day,
    619                                         abbreviated_name_of_month(time1.month),
    620                                         time2.day,
    621                                         abbreviated_name_of_month(time2.month),
    622                                         time1.year)
    623          return "%s %s %s-%s %s %s" % (time1.day,
    624                                        abbreviated_name_of_month(time1.month),
    625                                        time1.year,
    626                                        time2.day,
    627                                        abbreviated_name_of_month(time2.month),
    628                                        time2.year)
    629
    630
    631 -class StripWeekday(Strip):
    632
    633 -    def label(self, time, major=False):
    634          if major:
    635              return "%s %s %s %s" % (abbreviated_name_of_weekday(time.weekday()),
    636                                      time.day,
    637                                      abbreviated_name_of_month(time.month),
    638                                      time.year)
    639          return abbreviated_name_of_weekday(time.weekday())
    640
    641 -    def start(self, time):
    642          return datetime(time.year, time.month, time.day)
    643
    644 -    def increment(self, time):
    645          return time + timedelta(1)
    646
    647 -    def get_font(self, time_period):
    648          return get_default_font(8)
    649
    650
    651 -class StripHour(Strip):
    652
    653 -    def label(self, time, major=False):
    654          if major:
    655              return "%s %s %s %s" % (time.day, abbreviated_name_of_month(time.month),
    656                                      time.year, time.hour)
    657          return str(time.hour)
    658
    659 -    def start(self, time):
    660          return datetime(time.year, time.month, time.day, time.hour)
    661
    662 -    def increment(self, time):
    663          return time + timedelta(hours=1)
    664
    665 -    def get_font(self, time_period):
    666          return get_default_font(8)
    667
    668
    669 -def microseconds_to_delta(microsecs):
    670      """Return a timedelta representing the given number of microseconds."""
    671      return timedelta(microseconds=microsecs)
    672
    673
    674 -def delta_to_microseconds(delta):
    675      """Return the number of microseconds that the timedelta represents."""
    676      return (delta.days * US_PER_DAY +
    677              delta.seconds * US_PER_SEC +
    678              delta.microseconds)
    679
    680
    681 -def move_period_num_days(period, num):
    682      delta = timedelta(days=1) * num
    683      start_time = period.start_time + delta
    684      end_time = period.end_time + delta
    685      return TimePeriod(period.time_type, start_time, end_time)
    686
    687
    688 -def move_period_num_weeks(period, num):
    689      delta = timedelta(weeks=1) * num
    690      start_time = period.start_time + delta
    691      end_time = period.end_time + delta
    692      return TimePeriod(period.time_type, start_time, end_time)
    693
    694
    695 -def move_period_num_months(period, num):
    696      try:
    697          delta = num
    698          years = abs(delta) / 12
    699          if num < 0:
    700              years = -years
    701          delta = delta - 12 * years
    702          if delta < 0:
    703              start_month = period.start_time.month + 12 + delta
    704              end_month = period.end_time.month + 12 + delta
    705              if start_month > 12:
    706                  start_month -=12
    707                  end_month -=12
    708              if start_month > period.start_time.month:
    709                  years -= 1
    710          else:
    711              start_month = period.start_time.month + delta
    712              end_month = period.start_time.month + delta
    713              if start_month > 12:
    714                  start_month -=12
    715                  end_month -=12
    716                  years += 1
    717          start_year = period.start_time.year + years
    718          end_year = period.start_time.year + years
    719          start_time = period.start_time.replace(year=start_year, month=start_month)
    720          end_time = period.end_time.replace(year=end_year, month=end_month)
    721          return TimePeriod(period.time_type, start_time, end_time)
    722      except ValueError:
    723          return None
    724
    725
    726 -def move_period_num_years(period, num):
    727      try:
    728          delta = num
    729          start_year = period.start_time.year
    730          end_year = period.end_time.year
    731          start_time = period.start_time.replace(year=start_year + delta)
    732          end_time = period.end_time.replace(year=end_year + delta)
    733          return TimePeriod(period.time_type, start_time, end_time)
    734      except ValueError:
    735          return None
    736