Added TODO and DEVELOPMENT.
[faces-project.git] / faces / pcalendar.py
blob2efbdce227792022daa333d1d2e334d89a80f0cf
1 #@+leo-ver=4
2 #@+node:@file pcalendar.py
3 #@@language python
4 #@<< Copyright >>
5 #@+node:<< Copyright >>
6 ############################################################################
7 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
8 # mreithinger@web.de
10 # This file is part of faces.
12 # faces is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # faces is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the
24 # Free Software Foundation, Inc.,
25 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ############################################################################
28 #@-node:<< Copyright >>
29 #@nl
30 """
31 This module contains all classes and functions for the project plan calendar
32 """
33 #@<< Imports >>
34 #@+node:<< Imports >>
35 from string import *
36 import datetime
37 import time
38 import re
39 import locale
40 import bisect
41 import sys
43 TIME_RANGE_PATTERN = re.compile("(\\d+):(\\d+)\\s*-\\s*(\\d+):(\\d+)")
44 TIME_DELTA_PATTERN = re.compile("([-+]?\\d+(\\.\\d+)?)([dwmyMH])")
46 DEFAULT_MINIMUM_TIME_UNIT = 15
47 DEFAULT_WORKING_DAYS_PER_WEEK = 5
48 DEFAULT_WORKING_DAYS_PER_MONTH = 20
49 DEFAULT_WORKING_DAYS_PER_YEAR = 200
50 DEFAULT_WORKING_HOURS_PER_DAY = 8
52 DEFAULT_WORKING_TIMES = ( (8 * 60, 12 * 60 ),
53 (13 * 60, 17 * 60 ) )
54 DEFAULT_WORKING_DAYS = { 0 : DEFAULT_WORKING_TIMES,
55 1 : DEFAULT_WORKING_TIMES,
56 2 : DEFAULT_WORKING_TIMES,
57 3 : DEFAULT_WORKING_TIMES,
58 4 : DEFAULT_WORKING_TIMES,
59 5 : (),
60 6 : () }
62 #@-node:<< Imports >>
63 #@nl
64 #@+others
65 #@+node:to_time_range
66 def to_time_range(src):
67 """
68 converts a string to a timerange, i.e
69 (from, to)
70 from, to are ints, specifing the minutes since midnight
71 """
73 if not src: return ()
75 mo = TIME_RANGE_PATTERN.match(src)
76 if not mo:
77 raise ValueError("%s is no time range" % src)
79 from_time = int(mo.group(1)) * 60 + int(mo.group(2))
80 to_time = int(mo.group(3)) * 60 + int(mo.group(4))
81 return from_time, to_time
82 #@-node:to_time_range
83 #@+node:to_datetime
84 def to_datetime(src):
85 """
86 a tolerant conversion function to convert different strings
87 to a datetime.dateime
88 """
90 #to get the original value for wrappers
91 new = getattr(src, "_value", src)
92 while new is not src:
93 src = new
94 new = getattr(src, "_value", src)
96 if isinstance(src, _WorkingDateBase):
97 src = src.to_datetime()
99 if isinstance(src, datetime.datetime):
100 return src
102 src = str(src)
104 formats = [ "%x %H:%M",
105 "%x",
106 "%Y-%m-%d %H:%M",
107 "%y-%m-%d %H:%M",
108 "%d.%m.%Y %H:%M",
109 "%d.%m.%y %H:%M",
110 "%Y%m%d %H:%M",
111 "%d/%m/%y %H:%M",
112 "%d/%m/%Y %H:%M",
113 "%d/%m/%Y",
114 "%d/%m/%y",
115 "%Y-%m-%d",
116 "%y-%m-%d",
117 "%d.%m.%Y",
118 "%d.%m.%y",
119 "%Y%m%d" ]
120 for f in formats:
121 try:
122 conv = time.strptime(src, f)
123 return datetime.datetime(*conv[0:-3])
124 except Exception, e:
125 pass
127 raise TypeError("'%s' (%s) is not a datetime" % (src, str(type(src))))
128 #@-node:to_datetime
129 #@+node:_to_days
130 def _to_days(src):
132 converts a string of the day abreviations mon, tue, wed,
133 thu, fri, sat, sun to a dir with correct weekday indices.
134 For Example
135 convert_to_days('mon, tue, thu') results in
136 { 0:1, 1:1, 3:1 }
139 tokens = src.split(",")
140 result = { }
141 for t in tokens:
142 try:
143 index = { "mon" : 0,
144 "tue" : 1,
145 "wed" : 2,
146 "thu" : 3,
147 "fri" : 4,
148 "sat" : 5,
149 "sun" : 6 } [ lower(t.strip()) ]
150 result[index] = 1
151 except:
152 raise ValueError("%s is not a day" % (t))
154 return result
155 #@-node:_to_days
156 #@+node:_add_to_time_spans
157 def _add_to_time_spans(src, to_add, is_free):
158 if not isinstance(to_add, (tuple, list)):
159 to_add = (to_add,)
161 tmp = []
162 for start, end, f in src:
163 tmp.append((start, True, f))
164 tmp.append((end, False, f))
166 for v in to_add:
167 if isinstance(v, (tuple, list)):
168 start = to_datetime(v[0])
169 end = to_datetime(v[1])
170 else:
171 start = to_datetime(v)
172 end = start.replace(hour=0, minute=0) + datetime.timedelta(1)
174 tmp.append((start, start <= end, is_free))
175 tmp.append((end, start > end, is_free))
177 tmp.sort()
179 # 0: date
180 # 1: is_start
181 # 2: is_free
182 sequence = []
183 free_count = 0
184 work_count = 0
185 last = None
186 for date, is_start, is_free in tmp:
187 if is_start:
188 if is_free:
189 if not free_count and not work_count:
190 last = date
192 free_count += 1
193 else:
194 if not work_count:
195 if free_count: sequence.append((last, date, True))
196 last = date
197 work_count += 1
198 else:
199 if is_free:
200 assert(free_count > 0)
201 free_count -= 1
202 if not free_count and not work_count:
203 sequence.append((last, date, True))
204 else:
205 assert(work_count > 0)
206 work_count -= 1
207 if not work_count: sequence.append((last, date, False))
208 if free_count: last = date
210 return tuple(sequence)
211 #@-node:_add_to_time_spans
212 #@+node:to_timedelta
213 def to_timedelta(src, cal=None, is_duration=False):
215 converts a string to a datetime.timedelta. If cal is specified
216 it will be used for getting the working times. if is_duration=True
217 working times will not be considered. Valid units are
218 d for Days
219 w for Weeks
220 m for Months
221 y for Years
222 H for Hours
223 M for Minutes
226 cal = cal or _default_calendar
227 if isinstance(src, datetime.timedelta):
228 return datetime.timedelta(src.days, seconds=src.seconds, calendar=cal)
230 if isinstance(src, (long, int, float)):
231 src = "%sM" % str(src)
233 if not isinstance(src, basestring):
234 raise ValueError("%s is not a duration" % (repr(src)))
236 src = src.strip()
238 if is_duration:
239 d_p_w = 7
240 d_p_m = 30
241 d_p_y = 360
242 d_w_h = 24
243 else:
244 d_p_w = cal.working_days_per_week
245 d_p_m = cal.working_days_per_month
246 d_p_y = cal.working_days_per_year
247 d_w_h = cal.working_hours_per_day
249 def convert_minutes(minutes):
250 minutes = int(minutes)
251 hours = minutes / 60
252 minutes = minutes % 60
253 days = hours / d_w_h
254 hours = hours % d_w_h
255 return [ days, 0, 0, 0, minutes, hours ]
257 def convert_days(value):
258 days = int(value)
259 value -= days
260 value *= d_w_h
261 hours = int(value)
262 value -= hours
263 value *= 60
264 minutes = round(value)
265 return [ days, 0, 0, 0, minutes, hours ]
267 sum_args = [ 0, 0, 0, 0, 0, 0 ]
269 split = src.split(" ")
270 for s in split:
271 mo = TIME_DELTA_PATTERN.match(s)
272 if not mo:
273 raise ValueError(src +
274 " is not a valid duration: valid"
275 " units are: d w m y M H")
277 unit = mo.group(3)
278 val = float(mo.group(1))
280 if unit == 'd':
281 args = convert_days(val)
282 elif unit == 'w':
283 args = convert_days(val * d_p_w)
284 elif unit == 'm':
285 args = convert_days(val * d_p_m)
286 elif unit == 'y':
287 args = convert_days(val * d_p_y)
288 elif unit == 'M':
289 args = convert_minutes(val)
290 elif unit == 'H':
291 args = convert_minutes(val * 60)
293 sum_args = [ a + b for a, b in zip(sum_args, args) ]
295 sum_args = tuple(sum_args)
296 return datetime.timedelta(*sum_args)
297 #@-node:to_timedelta
298 #@+node:timedelta_to_str
299 def timedelta_to_str(delta, format, cal=None, is_duration=False):
300 cal = cal or _default_calendar
301 if is_duration:
302 d_p_w = 7
303 d_p_m = 30
304 d_p_y = 365
305 d_w_h = 24
306 else:
307 d_p_w = cal.working_days_per_week
308 d_p_m = cal.working_days_per_month
309 d_p_y = cal.working_days_per_year
310 d_w_h = cal.working_hours_per_day
312 has_years = format.find("%y") > -1
313 has_minutes = format.find("%M") > -1
314 has_hours = format.find("%H") > -1 or has_minutes
315 has_days = format.find("%d") > -1
316 has_weeks = format.find("%w") > -1
317 has_months = format.find("%m") > -1
319 result = format
320 days = delta.days
322 d_r = (days, format)
323 minutes = delta.seconds / 60
325 def rebase(d_r, cond1, cond2, letter, divisor):
326 #rebase the days
327 if not cond1: return d_r
329 days, result = d_r
331 if cond2:
332 val = days / divisor
333 if not val:
334 result = re.sub("{[^{]*?%" + letter + "[^}]*?}", "", result)
336 result = result.replace("%" + letter, str(val))
337 days %= divisor
338 else:
339 result = result.replace("%" + letter,
340 locale.format("%.2f",
341 (float(days) / divisor)))
343 return (days, result)
345 d_r = rebase(d_r, has_years, has_months or has_weeks or has_days, "y", d_p_y)
346 d_r = rebase(d_r, has_months, has_weeks or has_days, "m", d_p_m)
347 d_r = rebase(d_r, has_weeks, has_days, "w", d_p_w)
348 days, result = d_r
350 if not has_days:
351 minutes += days * d_w_h * 60
352 days = 0
354 if has_hours:
355 if not days:
356 result = re.sub("{[^{]*?%d[^}]*?}", "", result)
358 result = result.replace("%d", str(days))
359 else:
360 result = result.replace("%d",
361 "%.2f" % (days + float(minutes)
362 / (d_w_h * 60)))
364 if has_hours:
365 if has_minutes:
366 val = minutes / 60
367 if not val:
368 result = re.sub("{[^{]*?%H[^}]*?}", "", result)
370 result = result.replace("%H", str(val))
371 minutes %= 60
372 else:
373 result = result.replace("%H", "%.2f" % (float(minutes) / 60))
375 if not minutes:
376 result = re.sub("{[^{]*?%M[^}]*?}", "", result)
378 result = result.replace("%M", str(minutes))
380 result = result.replace("{", "")
381 result = result.replace("}", "")
382 return result.strip()
383 #@-node:timedelta_to_str
384 #@+node:strftime
385 def strftime(dt, format):
387 an extended version of strftime, that introduces some new
388 directives:
389 %IW iso week number
390 %IY iso year
391 %IB full month name appropriate to iso week
392 %ib abbreviated month name appropriate to iso week
393 %im month as decimal number appropriate to iso week
395 iso = dt.isocalendar()
396 if iso[0] != dt.year:
397 iso_date = dt.replace(day=1, month=1)
398 format = format \
399 .replace("%IB", iso_date.strftime("%B"))\
400 .replace("%ib", iso_date.strftime("%b"))\
401 .replace("%im", iso_date.strftime("%m"))
402 else:
403 format = format \
404 .replace("%IB", "%B")\
405 .replace("%ib", "%b")\
406 .replace("%im", "%m")
408 format = format \
409 .replace("%IW", str(iso[1]))\
410 .replace("%IY", str(iso[0]))\
412 return dt.strftime(format)
413 #@-node:strftime
414 #@+node:union
415 def union(*calendars):
416 """
417 returns a calendar that unifies all working times
419 #@ << check arguments >>
420 #@+node:<< check arguments >>
421 if len(calendars) == 1:
422 calendars = calendars[0]
423 #@nonl
424 #@-node:<< check arguments >>
425 #@nl
426 #@ << intersect vacations >>
427 #@+node:<< intersect vacations >>
428 free_time = []
429 for c in calendars:
430 for start, end, is_free in c.time_spans:
431 if is_free:
432 free_time.append((start, False))
433 free_time.append((end, True))
435 count = len(calendars)
436 open = 0
437 time_spans = []
438 free_time.sort()
439 for date, is_end in free_time:
440 if is_end:
441 if open == count:
442 time_spans.append((start, date, True))
443 open -= 1
444 else:
445 open += 1
446 start = date
447 #@-node:<< intersect vacations >>
448 #@nl
449 #@ << unify extra worktime >>
450 #@+node:<< unify extra worktime >>
451 for c in calendars:
452 for start, end, is_free in c.time_spans:
453 if not is_free:
454 time_spans = _add_to_time_spans(time_spans, start, end)
455 #@nonl
456 #@-node:<< unify extra worktime >>
457 #@nl
458 #@ << unify working times >>
459 #@+node:<< unify working times >>
460 working_times = {}
461 for d in range(0, 7):
462 times = []
463 for c in calendars:
464 for start, end in c.working_times.get(d, []):
465 times.append((start, False))
466 times.append((end, True))
468 times.sort()
469 open = 0
470 ti = []
471 start = None
472 for time, is_end in times:
473 if not is_end:
474 if not start: start = time
475 open += 1
476 else:
477 open -= 1
478 if not open:
479 ti.append((start, time))
480 start = None
482 if ti:
483 working_times[d] = ti
484 #@-node:<< unify working times >>
485 #@nl
486 #@ << create result calendar >>
487 #@+node:<< create result calendar >>
488 result = Calendar()
489 result.working_times = working_times
490 result.time_spans = time_spans
491 result._recalc_working_time()
492 result._build_mapping()
493 #@nonl
494 #@-node:<< create result calendar >>
495 #@nl
496 return result
497 #@nonl
498 #@-node:union
499 #@+node:class _CalendarItem
500 class _CalendarItem(int):
501 #@ << class _CalendarItem declarations >>
502 #@+node:<< class _CalendarItem declarations >>
503 __slots__ = ()
504 calender = None
507 #@-node:<< class _CalendarItem declarations >>
508 #@nl
509 #@ @+others
510 #@+node:__new__
511 def __new__(cls, val):
512 try:
513 return int.__new__(cls, val)
514 except OverflowError:
515 return int.__new__(cls, sys.maxint)
516 #@-node:__new__
517 #@+node:round
518 def round(self, round_up=True):
519 m_t_u = self.calendar.minimum_time_unit
521 minutes = int(self)
522 base = (minutes / m_t_u) * m_t_u
523 minutes %= m_t_u
525 round_up = round_up and minutes > 0 or minutes > m_t_u / 2
526 if round_up: base += m_t_u
527 return self.__class__(base)
528 #@-node:round
529 #@-others
530 #@-node:class _CalendarItem
531 #@+node:class _Minutes
532 class _Minutes(_CalendarItem):
533 #@ << class _Minutes declarations >>
534 #@+node:<< class _Minutes declarations >>
535 __slots__ = ()
536 STR_FORMAT = "{%dd}{ %HH}{ %MM}"
539 #@-node:<< class _Minutes declarations >>
540 #@nl
541 #@ @+others
542 #@+node:__new__
543 def __new__(cls, src=0, is_duration=False):
545 converts a timedelta in working minutes.
547 if isinstance(src, cls) or type(src) is int:
548 return _CalendarItem.__new__(cls, src)
550 cal = cls.calendar
551 if not isinstance(src, datetime.timedelta):
552 src = to_timedelta(src, cal, is_duration)
554 d_w_h = is_duration and 24 or cal.working_hours_per_day
555 src = src.days * d_w_h * 60 + src.seconds / 60
556 return _CalendarItem.__new__(cls, src)
557 #@-node:__new__
558 #@+node:__cmp__
559 def __cmp__(self, other):
560 return cmp(int(self), int(self.__class__(other)))
561 #@-node:__cmp__
562 #@+node:__add__
563 def __add__(self, other):
564 try:
565 return self.__class__(int(self) + int(self.__class__(other)))
566 except:
567 return NotImplemented
568 #@-node:__add__
569 #@+node:__sub__
570 def __sub__(self, other):
571 try:
572 return self.__class__(int(self) - int(self.__class__(other)))
573 except:
574 return NotImplemented
575 #@-node:__sub__
576 #@+node:to_timedelta
577 def to_timedelta(self, is_duration=False):
578 d_w_h = is_duration and 24 or self.calendar.working_hours_per_day
579 minutes = int(self)
580 hours = minutes / 60
581 minutes = minutes % 60
582 days = hours / d_w_h
583 hours = hours % d_w_h
584 return datetime.timedelta(days, hours=hours, minutes=minutes)
585 #@nonl
586 #@-node:to_timedelta
587 #@+node:strftime
588 def strftime(self, format=None, is_duration=False):
589 td = self.to_timedelta(is_duration)
590 return timedelta_to_str(td, format or self.STR_FORMAT,
591 self.calendar, is_duration)
592 #@nonl
593 #@-node:strftime
594 #@-others
595 #@-node:class _Minutes
596 #@+node:class _WorkingDateBase
597 class _WorkingDateBase(_CalendarItem):
599 A daytetime which has only valid values within the
600 workingtimes of a specific calendar
602 #@ << class _WorkingDateBase declarations >>
603 #@+node:<< class _WorkingDateBase declarations >>
604 timetuple = True
605 STR_FORMAT = "%x %H:%M"
606 _minutes = _Minutes
607 __slots__ = ()
610 #@-node:<< class _WorkingDateBase declarations >>
611 #@nl
612 #@ @+others
613 #@+node:__new__
614 def __new__(cls, src):
615 #cls.__bases__[0] is the base of
616 #the calendar specific StartDate and EndDate
618 if isinstance(src, cls.__bases__[0]) or type(src) in (int, float):
619 return _CalendarItem.__new__(cls, src)
621 src = cls.calendar.from_datetime(to_datetime(src))
622 return _CalendarItem.__new__(cls, src)
623 #@-node:__new__
624 #@+node:__repr__
625 def __repr__(self):
626 return self.strftime()
627 #@-node:__repr__
628 #@+node:to_datetime
629 def to_datetime(self):
630 return self.to_starttime()
631 #@-node:to_datetime
632 #@+node:to_starttime
633 def to_starttime(self):
634 return self.calendar.to_starttime(self)
635 #@-node:to_starttime
636 #@+node:to_endtime
637 def to_endtime(self):
638 return self.calendar.to_endtime(self)
639 #@-node:to_endtime
640 #@+node:__cmp__
641 def __cmp__(self, other):
642 return cmp(int(self), int(self.__class__(other)))
643 #@-node:__cmp__
644 #@+node:__add__
645 def __add__(self, other):
646 try:
647 return self.__class__(int(self) + int(self._minutes(other)))
648 except ValueError, e:
649 raise e
650 except:
651 return NotImplemented
652 #@-node:__add__
653 #@+node:__sub__
654 def __sub__(self, other):
655 if isinstance(other, (datetime.timedelta, str, _Minutes)):
656 try:
657 other = self._minutes(other)
658 except:
659 pass
661 if isinstance(other, self._minutes):
662 return self.__class__(int(self) - int(other))
664 try:
665 return self._minutes(int(self) - int(self.__class__(other)))
666 except:
667 return NotImplemented
668 #@-node:__sub__
669 #@+node:strftime
670 def strftime(self, format=None):
671 return strftime(self.to_datetime(), format or self.STR_FORMAT)
672 #@-node:strftime
673 #@-others
674 #@-node:class _WorkingDateBase
675 #@+node:class Calendar
676 class Calendar(object):
678 A calendar to specify working times and vacations.
679 The calendars epoch start at 1.1.1979
681 #@ << declarations >>
682 #@+node:<< declarations >>
683 # january the first must be a monday
684 EPOCH = datetime.datetime(1979, 1, 1)
685 minimum_time_unit = DEFAULT_MINIMUM_TIME_UNIT
686 working_days_per_week = DEFAULT_WORKING_DAYS_PER_WEEK
687 working_days_per_month = DEFAULT_WORKING_DAYS_PER_MONTH
688 working_days_per_year = DEFAULT_WORKING_DAYS_PER_YEAR
689 working_hours_per_day = DEFAULT_WORKING_HOURS_PER_DAY
690 now = EPOCH
693 #@-node:<< declarations >>
694 #@nl
695 #@ @+others
696 #@+node:__init__
697 def __init__(self):
698 self.time_spans = ()
699 self._dt_num_can = ()
700 self._num_dt_can = ()
701 self.working_times = { }
702 self._recalc_working_time()
703 self._make_classes()
704 #@-node:__init__
705 #@+node:__or__
706 def __or__(self, other):
707 if isinstance(other, Calendar):
708 return union(self, other)
710 return NotImplemented
711 #@nonl
712 #@-node:__or__
713 #@+node:clone
714 def clone(self):
715 result = Calendar()
716 result.working_times = self.working_times.copy()
717 result.time_spans = self.time_spans
718 result._recalc_working_time()
719 result._build_mapping()
720 return result
721 #@nonl
722 #@-node:clone
723 #@+node:set_working_days
724 def set_working_days(self, day_range, trange, *further_tranges):
726 Sets the working days of an calendar
727 day_range is a string of day abbreviations like 'mon, tue'
728 trange and further_tranges is a time range string like
729 '8:00-10:00'
731 time_ranges = [ trange ] + list(further_tranges)
732 time_ranges = filter(bool, map(to_time_range, time_ranges))
733 days = _to_days(day_range)
735 for k in days.keys():
736 self.working_times[k] = time_ranges
738 self._recalc_working_time()
739 self._build_mapping()
740 #@-node:set_working_days
741 #@+node:set_vacation
742 def set_vacation(self, value):
744 Sets vacation time.
745 value is either a datetime literal or
746 a sequence of items that can be
747 a datetime literals and or pair of datetime literals
749 self.time_spans = _add_to_time_spans(self.time_spans, value, True)
750 self._build_mapping()
751 #@-node:set_vacation
752 #@+node:set_extra_work
753 def set_extra_work(self, value):
755 Sets extra working time
756 value is either a datetime literal or
757 a sequence of items that can be
758 a datetime literals and or pair of datetime literals
760 self.time_spans = _add_to_time_spans(self.time_spans, value, False)
761 self._build_mapping()
762 #@-node:set_extra_work
763 #@+node:from_datetime
764 def from_datetime(self, value):
765 assert(isinstance(value, datetime.datetime))
767 delta = value - self.EPOCH
768 days = delta.days
769 minutes = delta.seconds / 60
771 #calculate the weektime
772 weeks = days / 7
773 wtime = self.week_time * weeks
775 #calculate the daytime
776 days %= 7
777 dtime = sum(self.day_times[:days])
779 #calculate the minute time
780 slots = self.working_times.get(days, DEFAULT_WORKING_DAYS[days])
781 mtime = 0
782 for start, end in slots:
783 if minutes > end:
784 mtime += end - start
785 else:
786 if minutes > start:
787 mtime += minutes - start
788 break
790 result = wtime + dtime + mtime
792 #map exceptional timespans
793 dt_num_can = self._dt_num_can
794 pos = bisect.bisect(dt_num_can, (value,)) - 1
795 if pos >= 0:
796 start, end, nstart, nend, cend = dt_num_can[pos]
797 if value < end:
798 if nstart < nend:
799 delta = value - start
800 delta = delta.days * 24 * 60 + delta.seconds / 60
801 result = nstart + delta
802 else:
803 result = nstart
804 else:
805 result += (nend - cend) # == (result - cend) + nend
807 return result
808 #@-node:from_datetime
809 #@+node:split_time
810 def split_time(self, value):
811 #map exceptional timespans
812 num_dt_can = self._num_dt_can
813 pos = bisect.bisect(num_dt_can, (value, sys.maxint)) - 1
814 if pos >= 0:
815 nstart, nend, start, end, cend = num_dt_can[pos]
816 if value < nend:
817 value = start + datetime.timedelta(minutes=value - nstart)
818 delta = value - self.EPOCH
819 return delta.days / 7, delta.days % 7, delta.seconds / 60, -1
820 else:
821 value += (cend - nend) # (value - nend + cend)
823 #calculate the weeks since the epoch
824 weeks = value / self.week_time
825 value %= self.week_time
827 #calculate the remaining days
828 days = 0
829 for day_time in self.day_times:
830 if value < day_time: break
831 value -= day_time
832 days += 1
834 #calculate the remaining minutes
835 minutes = 0
836 slots = self.working_times.get(days, DEFAULT_WORKING_DAYS[days])
837 index = 0
838 for start, end in slots:
839 delta = end - start
840 if delta > value:
841 minutes = start + value
842 break
843 else:
844 value -= delta
846 index += 1
848 return weeks, days, minutes, index
849 #@-node:split_time
850 #@+node:to_starttime
851 def to_starttime(self, value):
852 weeks, days, minutes, index = self.split_time(value)
853 return self.EPOCH + datetime.timedelta(weeks=weeks,
854 days=days,
855 minutes=minutes)
856 #@-node:to_starttime
857 #@+node:to_endtime
858 def to_endtime(self, value):
859 return self.to_starttime(value - 1) + datetime.timedelta(minutes=1)
860 #@-node:to_endtime
861 #@+node:get_working_times
862 def get_working_times(self, day):
863 return self.working_times.get(day, DEFAULT_WORKING_DAYS[day])
864 #@-node:get_working_times
865 #@+node:_build_mapping
866 def _build_mapping(self):
867 self._dt_num_can = self._num_dt_can = ()
868 dt_num_can = []
869 num_dt_can = []
871 delta = self.Minutes()
872 for start, end, is_free in self.time_spans:
873 cstart = self.StartDate(start)
874 cend = self.EndDate(end)
875 nstart = cstart + delta
877 if not is_free:
878 d = end - start
879 d = d.days * 24 * 60 + d.seconds / 60
880 nend = nstart + d
881 else:
882 nend = nstart
884 delta += (nend - nstart) - (cend - cstart)
885 dt_num_can.append((start, end, nstart, nend, cend))
886 num_dt_can.append((nstart, nend, start, end, cend))
888 self._dt_num_can = tuple(dt_num_can)
889 self._num_dt_can = tuple(num_dt_can)
890 #@-node:_build_mapping
891 #@+node:_recalc_working_time
892 def _recalc_working_time(self):
893 def slot_sum_time(day):
894 slots = self.working_times.get(day, DEFAULT_WORKING_DAYS[day])
895 return sum(map(lambda slot: slot[1] - slot[0], slots))
897 self.day_times = map(slot_sum_time, range(0, 7))
898 self.week_time = sum(self.day_times)
899 #@-node:_recalc_working_time
900 #@+node:_make_classes
901 def _make_classes(self):
902 #ensure that the clases are instance specific
903 class minutes(_Minutes):
904 calendar = self
905 __slots__ = ()
907 class db(_WorkingDateBase):
908 calendar = self
909 _minutes = minutes
910 __slots__ = ()
912 class wdt(db): __slots__ = ()
913 class edt(db):
914 __slots__ = ()
916 def to_datetime(self):
917 return self.to_endtime()
919 self.Minutes, self.StartDate, self.EndDate = minutes, wdt, edt
920 self.WorkingDate = self.StartDate
921 #@-node:_make_classes
922 #@-others
925 _default_calendar = Calendar()
927 WorkingDate = _default_calendar.WorkingDate
928 StartDate = _default_calendar.StartDate
929 EndDate = _default_calendar.EndDate
930 Minutes = _default_calendar.Minutes
931 #@-node:class Calendar
932 #@-others
934 if __name__ == '__main__':
935 cal = Calendar()
937 start = EndDate("10.1.2005")
938 print "start", start.strftime(), type(start)
940 delay = Minutes("4H")
941 print "delay", delay, delay.strftime()
943 print "Start", cal.StartDate is StartDate
944 print "base", cal.StartDate.__bases__[0] == StartDate.__bases__[0]
945 print "type", type(start)
947 print "convert start"
948 start2 = cal.StartDate(start)
949 print "convert end"
951 start3 = cal.StartDate("10.1.2005")
952 print "start2", start2.strftime(), type(start2)
953 #@-node:@file pcalendar.py
954 #@-leo