Gnumed.timelinelib.calendar.coptic.timetype

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

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