Gnumed.timelinelib.calendar.pharaonic.timetype

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

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