Package Gnumed :: Package timelinelib :: Package calendar :: Package gregorian :: Module timetype |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.calendar.gregorian.timetype

      1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  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  import re
     21
     22  from timelinelib.calendar.gregorian.gregorian import GregorianDateTime
     23  from timelinelib.calendar.gregorian.monthnames import abbreviated_name_of_month
     24  from timelinelib.calendar.gregorian.time import GregorianDelta
     25  from timelinelib.calendar.gregorian.time import GregorianTime
     26  from timelinelib.calendar.gregorian.time import SECONDS_IN_DAY
     27  from timelinelib.calendar.gregorian.weekdaynames import abbreviated_name_of_weekday
     28  from timelinelib.calendar.timetype import TimeType
     29  from timelinelib.canvas.data import TimeOutOfRangeLeftError
     30  from timelinelib.canvas.data import TimeOutOfRangeRightError
     31  from timelinelib.canvas.data import TimePeriod
     32  from timelinelib.canvas.data import time_period_center
     33  from timelinelib.canvas.drawing.interface import Strip
     34
     35
     36  BC = _("BC")
     37
     38
     39 -class GregorianTimeType(TimeType):
     40
     41 -    def __eq__(self, other):
     42          return isinstance(other, GregorianTimeType)
     43
     44 -    def __ne__(self, other):
     45          return not (self == other)
     46
     47 -    def time_string(self, time):
     48          return "%d-%02d-%02d %02d:%02d:%02d" % GregorianDateTime.from_time(time).to_tuple()
     49
     50 -    def parse_time(self, time_string):
     51          match = re.search(r"^(-?\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$", time_string)
     52          if match:
     53              year = int(match.group(1))
     54              month = int(match.group(2))
     55              day = int(match.group(3))
     56              hour = int(match.group(4))
     57              minute = int(match.group(5))
     58              second = int(match.group(6))
     59              try:
     60                  return GregorianDateTime(year, month, day, hour, minute, second).to_time()
     61              except ValueError:
     62                  raise ValueError("Invalid time, time string = '%s'" % time_string)
     63          else:
     64              raise ValueError("Time not on correct format = '%s'" % time_string)
     65
     88
     89 -    def format_period(self, time_period):
     90          """Returns a unicode string describing the time period."""
     91          def label_with_time(time):
     92              return u"%s %s" % (label_without_time(time), time_label(time))
     93
     94          def label_without_time(time):
     95              gregorian_datetime = GregorianDateTime.from_time(time)
     96              return u"%s %s %s" % (
     97                  gregorian_datetime.day,
     98                  abbreviated_name_of_month(gregorian_datetime.month),
     99                  format_year(gregorian_datetime.year)
    100              )
    101
    102          def time_label(time):
    103              return "%02d:%02d" % time.get_time_of_day()[:-1]
    104          if time_period.is_period():
    105              if has_nonzero_time(time_period):
    106                  label = u"%s to %s" % (label_with_time(time_period.start_time),
    107                                         label_with_time(time_period.end_time))
    108              else:
    109                  label = u"%s to %s" % (label_without_time(time_period.start_time),
    110                                         label_without_time(time_period.end_time))
    111          else:
    112              if has_nonzero_time(time_period):
    113                  label = u"%s" % label_with_time(time_period.start_time)
    114              else:
    115                  label = u"%s" % label_without_time(time_period.start_time)
    116          return label
    117
    118 -    def format_delta(self, delta):
    119          days = abs(delta.get_days())
    120          seconds = abs(delta.seconds) - days * SECONDS_IN_DAY
    121          delta_format = (YEARS, DAYS, HOURS, MINUTES, SECONDS)
    122          return DurationFormatter([days, seconds]).format(delta_format)
    123
    124 -    def get_min_time(self):
    125          return GregorianTime.min()
    126
    127 -    def get_max_time(self):
    128          return GregorianTime(5369833, 0)
    129
    130 -    def choose_strip(self, metrics, appearance):
    131          """
    132          Return a tuple (major_strip, minor_strip) for current time period and
    133          window size.
    134          """
    135          day_period = TimePeriod(GregorianTime(0, 0), GregorianTime(1, 0))
    136          one_day_width = metrics.calc_exact_width(day_period)
    137          if one_day_width > 20000:
    138              return (StripHour(), StripMinute())
    139          elif one_day_width > 600:
    140              return (StripDay(), StripHour())
    141          elif one_day_width > 45:
    142              return (StripWeek(appearance), StripWeekday())
    143          elif one_day_width > 25:
    144              return (StripMonth(), StripDay())
    145          elif one_day_width > 1.5:
    146              return (StripYear(), StripMonth())
    147          elif one_day_width > 0.12:
    148              return (StripDecade(), StripYear())
    149          elif one_day_width > 0.012:
    150              return (StripCentury(), StripDecade())
    151          else:
    152              return (StripCentury(), StripCentury())
    153
    154 -    def get_default_time_period(self):
    155          return time_period_center(self.now(), GregorianDelta.from_days(30))
    156
    157 -    def supports_saved_now(self):
    158          return False
    159
    160 -    def set_saved_now(self, time):
    161          ()
    162
    163 -    def now(self):
    164          py = datetime.now()
    165          gregorian = GregorianDateTime(
    166              py.year,
    167              py.month,
    168              py.day,
    169              py.hour,
    170              py.minute,
    171              py.second
    172          )
    173          return gregorian.to_time()
    174
    175 -    def get_min_zoom_delta(self):
    176          return (GregorianDelta.from_seconds(60), _("Can't zoom deeper than 1 minute"))
    177
    178 -    def get_name(self):
    179          return u"gregoriantime"
    180
    181 -    def get_duplicate_functions(self):
    182          return [
    183              (_("Day"), move_period_num_days),
    184              (_("Week"), move_period_num_weeks),
    185              (_("Month"), move_period_num_months),
    186              (_("Year"), move_period_num_years),
    187          ]
    188
    189 -    def is_special_day(self, time):
    190          return False
    191
    192 -    def is_weekend_day(self, time):
    193          return self.get_day_of_week(time) in (5, 6)
    194
    195 -    def get_day_of_week(self, time):
    196          return time.julian_day % 7
    197
    198 -    def create_time_picker(self, parent, *args, **kwargs):
    199          from timelinelib.calendar.gregorian.timepicker.datetime import GregorianDateTimePicker
    200          return GregorianDateTimePicker(parent, *args, **kwargs)
    201
    202 -    def create_period_picker(self, parent, *args, **kwargs):
    203          from timelinelib.calendar.gregorian.timepicker.period import GregorianPeriodPicker
    204          return GregorianPeriodPicker(parent, *args, **kwargs)
    205
    206
    207 -def go_to_today_fn(main_frame, current_period, navigation_fn):
    208      navigation_fn(lambda tp: tp.center(GregorianTimeType().now()))
    209
    210
    211 -def go_to_date_fn(main_frame, current_period, navigation_fn):
    212      def navigate_to(time):
    213          navigation_fn(lambda tp: tp.center(time))
    214      main_frame.display_time_editor_dialog(
    215          GregorianTimeType(), current_period.mean_time(), navigate_to, _("Go to Date"))
    216
    217
    218 -def backward_fn(main_frame, current_period, navigation_fn):
    219      _move_page_smart(current_period, navigation_fn, -1)
    220
    221
    222 -def forward_fn(main_frame, current_period, navigation_fn):
    223      _move_page_smart(current_period, navigation_fn, 1)
    224
    225
    226 -def _move_page_smart(current_period, navigation_fn, direction):
    227      if _whole_number_of_years(current_period):
    228          _move_page_years(current_period, navigation_fn, direction)
    229      elif _whole_number_of_months(current_period):
    230          _move_page_months(current_period, navigation_fn, direction)
    231      else:
    232          navigation_fn(lambda tp: tp.move_delta(direction * current_period.delta()))
    233
    234
    235 -def _whole_number_of_years(period):
    236      """
    237      >>> from timelinelib.test.utils import gregorian_period
    238
    239      >>> _whole_number_of_years(gregorian_period("1 Jan 2013", "1 Jan 2014"))
    240      True
    241
    242      >>> _whole_number_of_years(gregorian_period("1 Jan 2013", "1 Jan 2015"))
    243      True
    244
    245      >>> _whole_number_of_years(gregorian_period("1 Feb 2013", "1 Feb 2014"))
    246      False
    247
    248      >>> _whole_number_of_years(gregorian_period("1 Jan 2013", "1 Feb 2014"))
    249      False
    250      """
    251      return (GregorianDateTime.from_time(period.start_time).is_first_day_in_year() and
    252              GregorianDateTime.from_time(period.end_time).is_first_day_in_year() and
    253              _calculate_year_diff(period) > 0)
    254
    255
    256 -def _move_page_years(curret_period, navigation_fn, direction):
    257      def navigate(tp):
    258          year_delta = direction * _calculate_year_diff(curret_period)
    259          gregorian_start = GregorianDateTime.from_time(curret_period.start_time)
    260          gregorian_end = GregorianDateTime.from_time(curret_period.end_time)
    261          new_start_year = gregorian_start.year + year_delta
    262          new_end_year = gregorian_end.year + year_delta
    263          try:
    264              new_start = gregorian_start.replace(year=new_start_year).to_time()
    265              new_end = gregorian_end.replace(year=new_end_year).to_time()
    266              if new_end > GregorianTimeType().get_max_time():
    267                  raise ValueError()
    268              if new_start < GregorianTimeType().get_min_time():
    269                  raise ValueError()
    270          except ValueError:
    271              if direction < 0:
    272                  raise TimeOutOfRangeLeftError()
    273              else:
    274                  raise TimeOutOfRangeRightError()
    275          return tp.update(new_start, new_end)
    276      navigation_fn(navigate)
    277
    278
    279 -def _calculate_year_diff(period):
    280      return (GregorianDateTime.from_time(period.end_time).year -
    281              GregorianDateTime.from_time(period.start_time).year)
    282
    283
    284 -def _whole_number_of_months(period):
    285      """
    286      >>> from timelinelib.test.utils import gregorian_period
    287
    288      >>> _whole_number_of_months(gregorian_period("1 Jan 2013", "1 Jan 2014"))
    289      True
    290
    291      >>> _whole_number_of_months(gregorian_period("1 Jan 2013", "1 Mar 2014"))
    292      True
    293
    294      >>> _whole_number_of_months(gregorian_period("2 Jan 2013", "2 Mar 2014"))
    295      False
    296
    297      >>> _whole_number_of_months(gregorian_period("1 Jan 2013 12:00", "1 Mar 2014"))
    298      False
    299      """
    300      start, end = GregorianDateTime.from_time(period.start_time), GregorianDateTime.from_time(period.end_time)
    301      start_months = start.year * 12 + start.month
    302      end_months = end.year * 12 + end.month
    303      month_diff = end_months - start_months
    304      return (start.is_first_of_month() and
    305              end.is_first_of_month() and
    306              month_diff > 0)
    307
    308
    309 -def _move_page_months(curret_period, navigation_fn, direction):
    310      def navigate(tp):
    311          start = GregorianDateTime.from_time(curret_period.start_time)
    312          end = GregorianDateTime.from_time(curret_period.end_time)
    313          start_months = start.year * 12 + start.month
    314          end_months = end.year * 12 + end.month
    315          month_diff = end_months - start_months
    316          month_delta = month_diff * direction
    317          new_start_year, new_start_month = _months_to_year_and_month(start_months + month_delta)
    318          new_end_year, new_end_month = _months_to_year_and_month(end_months + month_delta)
    319          try:
    320              new_start = start.replace(year=new_start_year, month=new_start_month)
    321              new_end = end.replace(year=new_end_year, month=new_end_month)
    322              start = new_start.to_time()
    323              end = new_end.to_time()
    324              if end > GregorianTimeType().get_max_time():
    325                  raise ValueError()
    326              if start < GregorianTimeType().get_min_time():
    327                  raise ValueError()
    328          except ValueError:
    329              if direction < 0:
    330                  raise TimeOutOfRangeLeftError()
    331              else:
    332                  raise TimeOutOfRangeRightError()
    333          return tp.update(start, end)
    334      navigation_fn(navigate)
    335
    336
    337 -def _months_to_year_and_month(months):
    338      years = int(months / 12)
    339      month = months - years * 12
    340      if month == 0:
    341          month = 12
    342          years -= 1
    343      return years, month
    344
    345
    346 -def forward_one_week_fn(main_frame, current_period, navigation_fn):
    347      wk = GregorianDelta.from_days(7)
    348      navigation_fn(lambda tp: tp.move_delta(wk))
    349
    350
    351 -def backward_one_week_fn(main_frame, current_period, navigation_fn):
    352      wk = GregorianDelta.from_days(7)
    353      navigation_fn(lambda tp: tp.move_delta(-1 * wk))
    354
    355
    356 -def navigate_month_step(current_period, navigation_fn, direction):
    357      """
    358      Currently does notice leap years.
    359      """
    360      # TODO: NEW-TIME: (year, month, day, hour, minute, second) -> int (days in # month)
    361      tm = current_period.mean_time()
    362      gt = GregorianDateTime.from_time(tm)
    363      if direction > 0:
    364          if gt.month == 2:
    365              d = 28
    366          elif gt.month in (4, 6, 9, 11):
    367              d = 30
    368          else:
    369              d = 31
    370      else:
    371          if gt.month == 3:
    372              d = 28
    373          elif gt.month in (5, 7, 10, 12):
    374              d = 30
    375          else:
    376              d = 31
    377      mv = GregorianDelta.from_days(d)
    378      navigation_fn(lambda tp: tp.move_delta(direction * mv))
    379
    380
    381 -def forward_one_month_fn(main_frame, current_period, navigation_fn):
    382      navigate_month_step(current_period, navigation_fn, 1)
    383
    384
    385 -def backward_one_month_fn(main_frame, current_period, navigation_fn):
    386      navigate_month_step(current_period, navigation_fn, -1)
    387
    388
    389 -def forward_one_year_fn(main_frame, current_period, navigation_fn):
    390      yr = GregorianDelta.from_days(365)
    391      navigation_fn(lambda tp: tp.move_delta(yr))
    392
    393
    394 -def backward_one_year_fn(main_frame, current_period, navigation_fn):
    395      yr = GregorianDelta.from_days(365)
    396      navigation_fn(lambda tp: tp.move_delta(-1 * yr))
    397
    398
    399 -def fit_millennium_fn(main_frame, current_period, navigation_fn):
    400      mean = GregorianDateTime.from_time(current_period.mean_time())
    401      if mean.year > get_millenium_max_year():
    402          year = get_millenium_max_year()
    403      else:
    404          year = max(get_min_year_containing_jan_1(), int(mean.year / 1000) * 1000)
    405      start = GregorianDateTime.from_ymd(year, 1, 1).to_time()
    406      end = GregorianDateTime.from_ymd(year + 1000, 1, 1).to_time()
    407      navigation_fn(lambda tp: tp.update(start, end))
    408
    409
    410 -def get_min_year_containing_jan_1():
    411      return GregorianDateTime.from_time(GregorianTimeType().get_min_time()).year + 1
    412
    413
    414 -def get_millenium_max_year():
    415      return GregorianDateTime.from_time(GregorianTimeType().get_max_time()).year - 1000
    416
    417
    418 -def fit_week_fn(main_frame, current_period, navigation_fn):
    419      mean = GregorianDateTime.from_time(current_period.mean_time())
    420      start = GregorianDateTime.from_ymd(mean.year, mean.month, mean.day).to_time()
    421      weekday = GregorianTimeType().get_day_of_week(start)
    422      start = start - GregorianDelta.from_days(weekday)
    423      if not main_frame.week_starts_on_monday():
    424          start = start - GregorianDelta.from_days(1)
    425      end = start + GregorianDelta.from_days(7)
    426      navigation_fn(lambda tp: tp.update(start, end))
    427
    428
    429 -def create_strip_fitter(strip_cls):
    430      def fit(main_frame, current_period, navigation_fn):
    431          def navigate(time_period):
    432              strip = strip_cls()
    433              start = strip.start(current_period.mean_time())
    434              end = strip.increment(start)
    435              return time_period.update(start, end)
    436          navigation_fn(navigate)
    437      return fit
    438
    439
    440 -class StripCentury(Strip):
    441
    442      """
    443      Year Name | Year integer | Decade name
    444      ----------+--------------+------------
    445      ..        |  ..          |
    446      200 BC    | -199         | 200s BC (100 years)
    447      ----------+--------------+------------
    448      199 BC    | -198         |
    449      ...       | ...          | 100s BC (100 years)
    450      100 BC    | -99          |
    451      ----------+--------------+------------
    452      99  BC    | -98          |
    453      ...       |  ...         | 0s BC (only 99 years)
    454      1   BC    |  0           |
    455      ----------+--------------+------------
    456      1         |  1           |
    457      ...       |  ...         | 0s (only 99 years)
    458      99        |  99          |
    459      ----------+--------------+------------
    460      100       |  100         |
    461      ..        |  ..          | 100s (100 years)
    462      199       |  199         |
    463      ----------+--------------+------------
    464      200       |  200         | 200s (100 years)
    465      ..        |  ..          |
    466      """
    467
    468 -    def label(self, time, major=False):
    469          if major:
    470              gregorian_time = GregorianDateTime.from_time(time)
    471              return self._format_century(
    472                  self._century_number(
    473                      self._century_start_year(gregorian_time.year)
    474                  ),
    475                  gregorian_time.is_bc()
    476              )
    477          else:
    478              return ""
    479
    480 -    def start(self, time):
    481          return GregorianDateTime.from_ymd(
    482              self._century_start_year(GregorianDateTime.from_time(time).year),
    483              1,
    484              1
    485          ).to_time()
    486
    487 -    def increment(self, time):
    488          gregorian_time = GregorianDateTime.from_time(time)
    489          return gregorian_time.replace(
    490              year=self._next_century_start_year(gregorian_time.year)
    491          ).to_time()
    492
    493 -    def _century_number(self, century_start_year):
    494          if century_start_year > 99:
    495              return century_start_year
    496          elif century_start_year >= -98:
    497              return 0
    498          else:  # century_start_year < -98:
    499              return self._century_number(-century_start_year - 98)
    500
    501 -    def _next_century_start_year(self, start_year):
    502          return start_year + self._century_year_len(start_year)
    503
    504 -    def _century_year_len(self, start_year):
    505          if start_year in [-98, 1]:
    506              return 99
    507          else:
    508              return 100
    509
    510 -    def _format_century(self, century_number, is_bc):
    511          if is_bc:
    512              return u"{century}s {bc}".format(century=century_number, bc=BC)
    513          else:
    514              return u"{century}s".format(century=century_number)
    515
    516 -    def _century_start_year(self, year):
    517          if year > 99:
    518              return year - int(year) % 100
    519          elif year >= 1:
    520              return 1
    521          elif year >= -98:
    522              return -98
    523          else:  # year < -98
    524              return -self._century_start_year(-year + 1) - 98
    525
    526
    527 -class StripDecade(Strip):
    528
    529      """
    530      Year Name | Year integer | Decade name
    531      ----------+--------------+------------
    532      ..        |  ..          |
    533      20 BC     | -19          | 20s BC (10 years)
    534      ----------+--------------+------------
    535      19 BC     | -18          |
    536      18 BC     | -17          |
    537      17 BC     | -16          |
    538      16 BC     | -15          |
    539      15 BC     | -14          | 10s BC (10 years)
    540      14 BC     | -13          |
    541      13 BC     | -12          |
    542      12 BC     | -11          |
    543      11 BC     | -10          |
    544      10 BC     | -9           |
    545      ----------+--------------+------------
    546      9  BC     | -8           |
    547      8  BC     | -7           |
    548      7  BC     | -6           |
    549      6  BC     | -5           |
    550      5  BC     | -4           | 0s BC (only 9 years)
    551      4  BC     | -3           |
    552      3  BC     | -2           |
    553      2  BC     | -1           |
    554      1  BC     |  0           |
    555      ----------+--------------+------------
    556      1         |  1           |
    557      2         |  2           |
    558      3         |  3           |
    559      4         |  4           |
    560      5         |  5           |  0s (only 9 years)
    561      6         |  6           |
    562      7         |  7           |
    563      8         |  8           |
    564      9         |  9           |
    565      ----------+--------------+------------
    566      10        |  10          |
    567      11        |  11          |
    568      12        |  12          |
    569      13        |  13          |
    570      14        |  14          |
    571      15        |  15          |  10s (10 years)
    572      16        |  16          |
    573      17        |  17          |
    574      18        |  18          |
    575      19        |  19          |
    576      ----------+--------------+------------
    577      20        |  20          |  20s (10 years)
    578      ..        |  ..          |
    579      """
    580
    581 -    def __init__(self):
    582          self.skip_s_in_decade_text = False
    583
    584 -    def label(self, time, major=False):
    585          gregorian_time = GregorianDateTime.from_time(time)
    586          return self._format_decade(
    587              self._decade_number(self._decade_start_year(gregorian_time.year)),
    588              gregorian_time.is_bc()
    589          )
    590
    591 -    def start(self, time):
    592          return GregorianDateTime.from_ymd(
    593              self._decade_start_year(GregorianDateTime.from_time(time).year),
    594              1,
    595              1
    596          ).to_time()
    597
    598 -    def increment(self, time):
    599          gregorian_time = GregorianDateTime.from_time(time)
    600          return gregorian_time.replace(
    601              year=self._next_decacde_start_year(gregorian_time.year)
    602          ).to_time()
    603
    604 -    def set_skip_s_in_decade_text(self, value):
    605          self.skip_s_in_decade_text = value
    606
    607 -    def _format_decade(self, decade_number, is_bc):
    608          parts = []
    609          parts.append("{0}".format(decade_number))
    610          if not self.skip_s_in_decade_text:
    611              parts.append("s")
    612          if is_bc:
    613              parts.append(" ")
    614              parts.append(BC)
    615          return "".join(parts)
    616
    617 -    def _decade_start_year(self, year):
    618          if year > 9:
    619              return int(year) - (int(year) % 10)
    620          elif year >= 1:
    621              return 1
    622          elif year >= -8:
    623              return -8
    624          else:  # year < -8
    625              return -self._decade_start_year(-year + 1) - 8
    626
    627 -    def _next_decacde_start_year(self, start_year):
    628          return start_year + self._decade_year_len(start_year)
    629
    630 -    def _decade_year_len(self, start_year):
    631          if self._decade_number(start_year) == 0:
    632              return 9
    633          else:
    634              return 10
    635
    636 -    def _decade_number(self, start_year):
    637          if start_year > 9:
    638              return start_year
    639          elif start_year >= -8:
    640              return 0
    641          else:  # start_year < -8
    642              return self._decade_number(-start_year - 8)
    643
    644
    645 -class StripYear(Strip):
    646
    647 -    def label(self, time, major=False):
    648          return format_year(GregorianDateTime.from_time(time).year)
    649
    650 -    def start(self, time):
    651          gregorian_time = GregorianDateTime.from_time(time)
    652          new_gregorian = GregorianDateTime.from_ymd(gregorian_time.year, 1, 1)
    653          return new_gregorian.to_time()
    654
    655 -    def increment(self, time):
    656          gregorian_time = GregorianDateTime.from_time(time)
    657          return gregorian_time.replace(year=gregorian_time.year + 1).to_time()
    658
    659
    660 -class StripMonth(Strip):
    661
    662 -    def label(self, time, major=False):
    663          time = GregorianDateTime.from_time(time)
    664          if major:
    665              return "%s %s" % (abbreviated_name_of_month(time.month),
    666                                format_year(time.year))
    667          return abbreviated_name_of_month(time.month)
    668
    669 -    def start(self, time):
    670          gregorian_time = GregorianDateTime.from_time(time)
    671          return GregorianDateTime.from_ymd(
    672              gregorian_time.year,
    673              gregorian_time.month,
    674              1
    675          ).to_time()
    676
    677 -    def increment(self, time):
    678          return time + GregorianDelta.from_days(
    679              GregorianDateTime.from_time(time).days_in_month()
    680          )
    681
    682
    683 -class StripDay(Strip):
    684
    685 -    def label(self, time, major=False):
    686          time = GregorianDateTime.from_time(time)
    687          if major:
    688              return "%s %s %s" % (time.day,
    689                                   abbreviated_name_of_month(time.month),
    690                                   format_year(time.year))
    691          return str(time.day)
    692
    693 -    def start(self, time):
    694          gregorian_time = GregorianDateTime.from_time(time)
    695          return GregorianDateTime.from_ymd(
    696              gregorian_time.year,
    697              gregorian_time.month,
    698              gregorian_time.day
    699          ).to_time()
    700
    701 -    def increment(self, time):
    702          return time + GregorianDelta.from_days(1)
    703
    704 -    def is_day(self):
    705          return True
    706
    707
    708 -class StripWeek(Strip):
    709
    710 -    def __init__(self, appearance):
    711          Strip.__init__(self)
    712          self.appearance = appearance
    713
    714 -    def label(self, time, major=False):
    715          if major:
    716              first_weekday = self.start(time)
    717              next_first_weekday = self.increment(first_weekday)
    718              last_weekday = next_first_weekday - GregorianDelta.from_days(1)
    719              range_string = self._time_range_string(first_weekday, last_weekday)
    720              if self.appearance.get_week_start() == "monday":
    721                  return (_("Week") + " %s (%s)") % (
    722                      GregorianDateTime.from_time(time).week_number,
    723                      range_string
    724                  )
    725              else:
    726                  # It is sunday (don't know what to do about week numbers here)
    727                  return range_string
    728          # This strip should never be used as minor
    729          return ""
    730
    731 -    def _time_range_string(self, start, end):
    732          start = GregorianDateTime.from_time(start)
    733          end = GregorianDateTime.from_time(end)
    734          if start.year == end.year:
    735              if start.month == end.month:
    736                  return "%s-%s %s %s" % (start.day, end.day,
    737                                          abbreviated_name_of_month(start.month),
    738                                          format_year(start.year))
    739              return "%s %s-%s %s %s" % (start.day,
    740                                         abbreviated_name_of_month(start.month),
    741                                         end.day,
    742                                         abbreviated_name_of_month(end.month),
    743                                         format_year(start.year))
    744          return "%s %s %s-%s %s %s" % (start.day,
    745                                        abbreviated_name_of_month(start.month),
    746                                        format_year(start.year),
    747                                        end.day,
    748                                        abbreviated_name_of_month(end.month),
    749                                        format_year(end.year))
    750
    751 -    def start(self, time):
    752          if self.appearance.get_week_start() == "monday":
    753              days_to_subtract = GregorianTimeType().get_day_of_week(time)
    754          else:
    755              # It is sunday
    756              days_to_subtract = (GregorianTimeType().get_day_of_week(time) + 1) % 7
    757          return GregorianTime(time.julian_day - days_to_subtract, 0)
    758
    759 -    def increment(self, time):
    760          return time + GregorianDelta.from_days(7)
    761
    762
    763 -class StripWeekday(Strip):
    764
    765 -    def label(self, time, major=False):
    766          day_of_week = GregorianTimeType().get_day_of_week(time)
    767          if major:
    768              time = GregorianDateTime.from_time(time)
    769              return "%s %s %s %s" % (abbreviated_name_of_weekday(day_of_week),
    770                                      time.day,
    771                                      abbreviated_name_of_month(time.month),
    772                                      format_year(time.year))
    773          return (abbreviated_name_of_weekday(day_of_week) +
    774                  " %s" % GregorianDateTime.from_time(time).day)
    775
    776 -    def start(self, time):
    777          gregorian_time = GregorianDateTime.from_time(time)
    778          new_gregorian = GregorianDateTime.from_ymd(gregorian_time.year, gregorian_time.month, gregorian_time.day)
    779          return new_gregorian.to_time()
    780
    781 -    def increment(self, time):
    782          return time + GregorianDelta.from_days(1)
    783
    784 -    def is_day(self):
    785          return True
    786
    787
    788 -class StripHour(Strip):
    789
    790 -    def label(self, time, major=False):
    791          time = GregorianDateTime.from_time(time)
    792          if major:
    793              return "%s %s %s: %sh" % (time.day, abbreviated_name_of_month(time.month),
    794                                        format_year(time.year), time.hour)
    795          return str(time.hour)
    796
    797 -    def start(self, time):
    798          (hours, _, _) = time.get_time_of_day()
    799          return GregorianTime(time.julian_day, hours * 60 * 60)
    800
    801 -    def increment(self, time):
    802          return time + GregorianDelta.from_seconds(60 * 60)
    803
    804
    805 -class StripMinute(Strip):
    806
    807 -    def label(self, time, major=False):
    808          time = GregorianDateTime.from_time(time)
    809          if major:
    810              return "%s %s %s: %s:%s" % (time.day, abbreviated_name_of_month(time.month),
    811                                          format_year(time.year), time.hour, time.minute)
    812          return str(time.minute)
    813
    814 -    def start(self, time):
    815          (hours, minutes, _) = time.get_time_of_day()
    816          return GregorianTime(time.julian_day, minutes * 60 + hours * 60 * 60)
    817
    818 -    def increment(self, time):
    819          return time + GregorianDelta.from_seconds(60)
    820
    821
    822 -def format_year(year):
    823      if year <= 0:
    824          return "%d %s" % ((1 - year), BC)
    825      else:
    826          return str(year)
    827
    828
    829 -def move_period_num_days(period, num):
    830      delta = GregorianDelta.from_days(1) * num
    831      start_time = period.start_time + delta
    832      end_time = period.end_time + delta
    833      return TimePeriod(start_time, end_time)
    834
    835
    836 -def move_period_num_weeks(period, num):
    837      delta = GregorianDelta.from_days(7) * num
    838      start_time = period.start_time + delta
    839      end_time = period.end_time + delta
    840      return TimePeriod(start_time, end_time)
    841
    842
    843 -def move_period_num_months(period, num):
    844      def move_time(time):
    845          gregorian_time = GregorianDateTime.from_time(time)
    846          new_month = gregorian_time.month + num
    847          new_year = gregorian_time.year
    848          while new_month < 1:
    849              new_month += 12
    850              new_year -= 1
    851          while new_month > 12:
    852              new_month -= 12
    853              new_year += 1
    854          return gregorian_time.replace(year=new_year, month=new_month).to_time()
    855      try:
    856          return TimePeriod(
    857              move_time(period.start_time),
    858              move_time(period.end_time)
    859          )
    860      except ValueError:
    861          return None
    862
    863
    864 -def move_period_num_years(period, num):
    865      try:
    866          delta = num
    867          start_year = GregorianDateTime.from_time(period.start_time).year
    868          end_year = GregorianDateTime.from_time(period.end_time).year
    869          start_time = GregorianDateTime.from_time(period.start_time).replace(year=start_year + delta)
    870          end_time = GregorianDateTime.from_time(period.end_time).replace(year=end_year + delta)
    871          return TimePeriod(start_time.to_time(), end_time.to_time())
    872      except ValueError:
    873          return None
    874
    875
    876 -def has_nonzero_time(time_period):
    877      return (time_period.start_time.seconds != 0 or
    878              time_period.end_time.seconds != 0)
    879
    880
    881 -class DurationType(object):
    882
    883 -    def __init__(self, name, single_name, value_fn, remainder_fn):
    884          self._name = name
    885          self._single_name = single_name
    886          self._value_fn = value_fn
    887          self._remainder_fn = remainder_fn
    888
    889      @property
    890 -    def name(self):
    891          return self._name
    892
    893      @property
    894 -    def single_name(self):
    895          return self._single_name
    896
    897      @property
    898 -    def value_fn(self):
    899          return self._value_fn
    900
    901      @property
    902 -    def remainder_fn(self):
    903          return self._remainder_fn
    904
    905
    906  YEARS = DurationType(_('years'), _('year'),
    907                       lambda ds: ds[0] / 365,
    908                       lambda ds: (ds[0] % 365, ds[1]))
    909  MONTHS = DurationType(_('months'), _('month'),
    910                        lambda ds: ds[0] / 30,
    911                        lambda ds: (ds[0] % 30, ds[1]))
    912  WEEKS = DurationType(_('weeks'), _('week'),
    913                       lambda ds: ds[0] / 7,
    914                       lambda ds: (ds[0] % 7, ds[1]))
    915  DAYS = DurationType(_('days'), _('day'),
    916                      lambda ds: ds[0],
    917                      lambda ds: (0, ds[1]))
    918  HOURS = DurationType(_('hours'), _('hour'),
    919                       lambda ds: ds[0] * 24 + ds[1] / 3600,
    920                       lambda ds: (0, ds[1] % 3600))
    921  MINUTES = DurationType(_('minutes'), _('minute'),
    922                         lambda ds: ds[0] * 1440 + ds[1] / 60,
    923                         lambda ds: (0, ds[1] % 60))
    924  SECONDS = DurationType(_('seconds'), _('second'),
    925                         lambda ds: ds[0] * 86400 + ds[1],
    926                         lambda ds: (0, 0))
    927
    928
    929 -class DurationFormatter(object):
    930
    931 -    def __init__(self, duration):
    932          """Duration is a list containing days and seconds."""
    933          self._duration = duration
    934
    935 -    def format(self, duration_parts):
    936          """
    937          Return a string describing a time duration. Such a string
    938          can look like::
    939
    940              2 years 1 month 3 weeks
    941
    942          The argument duration_parts is a tuple where each element
    943          describes a duration type like YEARS, WEEKS etc.
    944          """
    945          values = self._calc_duration_values(self._duration, duration_parts)
    946          return self._format_parts(zip(values, duration_parts))
    947
    948 -    def _calc_duration_values(self, duration, duration_parts):
    949          values = []
    950          for duration_part in duration_parts:
    951              value = duration_part.value_fn(duration)
    952              duration[0], duration[1] = duration_part.remainder_fn(duration)
    953              values.append(value)
    954          return values
    955
    956 -    def _format_parts(self, duration_parts):
    957          durations = self._remov_zero_value_parts(duration_parts)
    958          return " ". join(self._format_durations_parts(durations))
    959
    960 -    def _remov_zero_value_parts(self, duration_parts):
    961          return [duration for duration in duration_parts
    962                  if duration[0] > 0]
    963
    964 -    def _format_durations_parts(self, durations):
    965          return [self._format_part(duration_value, duration_type) for
    966                  duration_value, duration_type in durations]
    967
    968 -    def _format_part(self, value, duration_type):
    969          if value == 1:
    970              heading = duration_type.single_name
    971          else:
    972              heading = duration_type.name
    973          return '%d %s' % (value, heading)
    974