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

Source Code for Module Gnumed.timelinelib.time.pytime

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