@@ -579,9 +579,28 @@ class BusinessHourMixin(BusinessMixin):
579
579
580
580
def __init__ (self , start = '09:00' , end = '17:00' , offset = timedelta (0 )):
581
581
# must be validated here to equality check
582
- start = liboffsets ._validate_business_time (start )
582
+ if not isinstance (start , (tuple , list )):
583
+ start = (start ,)
584
+ if not isinstance (end , (tuple , list )):
585
+ end = (end ,)
586
+ start = tuple (map (liboffsets ._validate_business_time , start ))
587
+ end = tuple (map (liboffsets ._validate_business_time , end ))
588
+
589
+ # Validation of input
590
+ if len (start ) != len (end ):
591
+ raise ValueError ('number of starting time and ending time must be the same' )
592
+ num_openings = len (start )
593
+ openings = sorted (zip (start , end ))
594
+ start = tuple (x for x , _ in openings )
595
+ end = tuple (x for _ , x in openings )
596
+ total_secs = 0
597
+ for i in range (num_openings ):
598
+ total_secs += self ._get_business_hours_by_sec (start [i ], end [i ])
599
+ total_secs += self ._get_business_hours_by_sec (end [i ], start [(i + 1 ) % num_openings ])
600
+ if total_secs != 24 * 60 * 60 :
601
+ raise ValueError ('invalid starting and ending time(s)' )
602
+
583
603
object .__setattr__ (self , "start" , start )
584
- end = liboffsets ._validate_business_time (end )
585
604
object .__setattr__ (self , "end" , end )
586
605
object .__setattr__ (self , "_offset" , offset )
587
606
@@ -603,14 +622,8 @@ def next_bday(self):
603
622
else :
604
623
return BusinessDay (n = nb_offset )
605
624
606
- @cache_readonly
607
- def _get_daytime_flag (self ):
608
- if self .start == self .end :
609
- raise ValueError ('start and end must not be the same' )
610
- elif self .start < self .end :
611
- return True
612
- else :
613
- return False
625
+ def _get_daytime_flag (self , start , end ):
626
+ return start < end
614
627
615
628
def _next_opening_time (self , other ):
616
629
"""
@@ -620,44 +633,83 @@ def _next_opening_time(self, other):
620
633
Opening time always locates on BusinessDay.
621
634
Otherwise, closing time may not if business hour extends over midnight.
622
635
"""
636
+ earliest_start = self .start [0 ]
637
+ latest_start = self .start [- 1 ]
623
638
if not self .next_bday .onOffset (other ):
624
639
other = other + self .next_bday
640
+ if self .n >= 0 :
641
+ return datetime (other .year , other .month , other .day ,
642
+ earliest_start .hour , earliest_start .minute )
643
+ else :
644
+ return datetime (other .year , other .month , other .day ,
645
+ latest_start .hour , latest_start .minute )
625
646
else :
626
- if self .n >= 0 and self . start < other .time ():
647
+ if self .n >= 0 and latest_start < other .time ():
627
648
other = other + self .next_bday
628
- elif self .n < 0 and other .time () < self .start :
649
+ return datetime (other .year , other .month , other .day ,
650
+ earliest_start .hour , earliest_start .minute )
651
+ elif self .n < 0 and other .time () < earliest_start :
629
652
other = other + self .next_bday
630
- return datetime (other .year , other .month , other .day ,
631
- self .start .hour , self .start .minute )
653
+ return datetime (other .year , other .month , other .day ,
654
+ latest_start .hour , latest_start .minute )
655
+ if self .n >= 0 :
656
+ for st in self .start :
657
+ if other .time () <= st :
658
+ return datetime (other .year , other .month , other .day ,
659
+ st .hour , st .minute )
660
+ else :
661
+ for st in reversed (self .start ):
662
+ if other .time () >= st :
663
+ return datetime (other .year , other .month , other .day ,
664
+ st .hour , st .minute )
632
665
633
666
def _prev_opening_time (self , other ):
634
667
"""
635
668
If n is positive, return yesterday's business day opening time.
636
669
Otherwise yesterday business day's opening time.
637
670
"""
671
+ earliest_start = self .start [0 ]
672
+ latest_start = self .start [- 1 ]
638
673
if not self .next_bday .onOffset (other ):
639
674
other = other - self .next_bday
675
+ if self .n < 0 :
676
+ return datetime (other .year , other .month , other .day ,
677
+ earliest_start .hour , earliest_start .minute )
678
+ else :
679
+ return datetime (other .year , other .month , other .day ,
680
+ latest_start .hour , latest_start .minute )
640
681
else :
641
- if self .n >= 0 and other .time () < self . start :
682
+ if self .n < 0 and latest_start < other .time ():
642
683
other = other - self .next_bday
643
- elif self .n < 0 and other .time () > self .start :
684
+ return datetime (other .year , other .month , other .day ,
685
+ earliest_start .hour , earliest_start .minute )
686
+ elif self .n >= 0 and other .time () < earliest_start :
644
687
other = other - self .next_bday
645
- return datetime (other .year , other .month , other .day ,
646
- self .start .hour , self .start .minute )
688
+ return datetime (other .year , other .month , other .day ,
689
+ latest_start .hour , latest_start .minute )
690
+ if self .n < 0 :
691
+ for st in self .start :
692
+ if other .time () <= st :
693
+ return datetime (other .year , other .month , other .day ,
694
+ st .hour , st .minute )
695
+ else :
696
+ for st in reversed (self .start ):
697
+ if other .time () >= st :
698
+ return datetime (other .year , other .month , other .day ,
699
+ st .hour , st .minute )
647
700
648
- @cache_readonly
649
- def _get_business_hours_by_sec (self ):
701
+ def _get_business_hours_by_sec (self , start , end ):
650
702
"""
651
703
Return business hours in a day by seconds.
652
704
"""
653
- if self ._get_daytime_flag :
705
+ if self ._get_daytime_flag ( start , end ) :
654
706
# create dummy datetime to calculate businesshours in a day
655
- dtstart = datetime (2014 , 4 , 1 , self . start .hour , self . start .minute )
656
- until = datetime (2014 , 4 , 1 , self . end .hour , self . end .minute )
707
+ dtstart = datetime (2014 , 4 , 1 , start .hour , start .minute )
708
+ until = datetime (2014 , 4 , 1 , end .hour , end .minute )
657
709
return (until - dtstart ).total_seconds ()
658
710
else :
659
- dtstart = datetime (2014 , 4 , 1 , self . start .hour , self . start .minute )
660
- until = datetime (2014 , 4 , 2 , self . end .hour , self . end .minute )
711
+ dtstart = datetime (2014 , 4 , 1 , start .hour , start .minute )
712
+ until = datetime (2014 , 4 , 2 , end .hour , end .minute )
661
713
return (until - dtstart ).total_seconds ()
662
714
663
715
@apply_wraps
@@ -666,13 +718,11 @@ def rollback(self, dt):
666
718
Roll provided date backward to next offset only if not on offset.
667
719
"""
668
720
if not self .onOffset (dt ):
669
- businesshours = self ._get_business_hours_by_sec
670
721
if self .n >= 0 :
671
- dt = self ._prev_opening_time (
672
- dt ) + timedelta (seconds = businesshours )
722
+ dt = self ._prev_opening_time (dt )
673
723
else :
674
- dt = self ._next_opening_time (
675
- dt ) + timedelta ( seconds = businesshours )
724
+ dt = self ._next_opening_time (dt )
725
+ return self . _get_closing_time ( dt )
676
726
return dt
677
727
678
728
@apply_wraps
@@ -687,33 +737,37 @@ def rollforward(self, dt):
687
737
return self ._prev_opening_time (dt )
688
738
return dt
689
739
740
+ def _get_closing_time (self , dt ):
741
+ for i , st in enumerate (self .start ):
742
+ if st .hour == dt .hour and st .minute == dt .minute :
743
+ return dt + timedelta (seconds = self ._get_business_hours_by_sec (st , self .end [i ]))
744
+ raise ValueError ("dt should be a starting time" )
745
+
690
746
@apply_wraps
691
747
def apply (self , other ):
692
- daytime = self ._get_daytime_flag
693
- businesshours = self ._get_business_hours_by_sec
694
- bhdelta = timedelta (seconds = businesshours )
695
-
696
748
if isinstance (other , datetime ):
697
749
# used for detecting edge condition
698
750
nanosecond = getattr (other , 'nanosecond' , 0 )
699
751
# reset timezone and nanosecond
700
752
# other may be a Timestamp, thus not use replace
701
753
other = datetime (other .year , other .month , other .day ,
702
- other .hour , other .minute ,
703
- other .second , other .microsecond )
754
+ other .hour , other .minute ,
755
+ other .second , other .microsecond )
704
756
n = self .n
705
757
if n >= 0 :
706
- if (other .time () == self .end or
707
- not self ._onOffset (other , businesshours )):
758
+ if (other .time () in self .end or
759
+ not self ._onOffset (other )):
708
760
other = self ._next_opening_time (other )
709
761
else :
710
- if other .time () == self .start :
762
+ if other .time () in self .start :
711
763
# adjustment to move to previous business day
712
764
other = other - timedelta (seconds = 1 )
713
- if not self ._onOffset (other , businesshours ):
765
+ if not self ._onOffset (other ):
714
766
other = self ._next_opening_time (other )
715
- other = other + bhdelta
767
+ other = self . _get_closing_time ( other )
716
768
769
+ businesshours = sum (self ._get_business_hours_by_sec (st , en )
770
+ for st , en in zip (self .start , self .end ))
717
771
bd , r = divmod (abs (n * 60 ), businesshours // 60 )
718
772
if n < 0 :
719
773
bd , r = - bd , - r
@@ -722,45 +776,50 @@ def apply(self, other):
722
776
skip_bd = BusinessDay (n = bd )
723
777
# midnight business hour may not on BusinessDay
724
778
if not self .next_bday .onOffset (other ):
725
- remain = other - self ._prev_opening_time (other )
726
- other = self ._next_opening_time (other + skip_bd ) + remain
779
+ prev_open = self ._prev_opening_time (other )
780
+ remain = other - prev_open
781
+ other = prev_open + skip_bd + remain
727
782
else :
728
783
other = other + skip_bd
729
784
730
785
hours , minutes = divmod (r , 60 )
731
- result = other + timedelta (hours = hours , minutes = minutes )
786
+ rem = timedelta (hours = hours , minutes = minutes )
732
787
733
788
# because of previous adjustment, time will be larger than start
734
- if ((daytime and (result .time () < self .start or
735
- self .end < result .time ())) or
736
- not daytime and (self .end < result .time () < self .start )):
737
- if n >= 0 :
738
- bday_edge = self ._prev_opening_time (other )
739
- bday_edge = bday_edge + bhdelta
740
- # calculate remainder
741
- bday_remain = result - bday_edge
742
- result = self ._next_opening_time (other )
743
- result += bday_remain
744
- else :
745
- bday_edge = self ._next_opening_time (other )
746
- bday_remain = result - bday_edge
747
- result = self ._next_opening_time (result ) + bhdelta
748
- result += bday_remain
789
+ if n >= 0 :
790
+ while rem != timedelta (0 ):
791
+ bhour_left = self ._get_closing_time (self ._prev_opening_time (other )) - other
792
+ if bhour_left >= rem :
793
+ other = other + rem
794
+ rem = timedelta (0 )
795
+ else :
796
+ rem = rem - bhour_left
797
+ other = self ._next_opening_time (other + bhour_left )
798
+ else :
799
+ while rem != timedelta (0 ):
800
+ bhour_left = self ._next_opening_time (other ) - other
801
+ if bhour_left <= rem :
802
+ other = other + rem
803
+ rem = timedelta (0 )
804
+ else :
805
+ rem = rem - bhour_left
806
+ other = self ._get_closing_time (
807
+ self ._next_opening_time (other + bhour_left - timedelta (seconds = 1 )))
808
+
749
809
# edge handling
750
810
if n >= 0 :
751
- if result .time () == self .end :
752
- result = self ._next_opening_time (result )
811
+ if other .time () in self .end :
812
+ other = self ._next_opening_time (other )
753
813
else :
754
- if result .time () == self .start and nanosecond == 0 :
814
+ if other .time () in self .start and nanosecond == 0 :
755
815
# adjustment to move to previous business day
756
- result = self ._next_opening_time (
757
- result - timedelta (seconds = 1 )) + bhdelta
816
+ other = self . _get_closing_time ( self ._next_opening_time (
817
+ other - timedelta (seconds = 1 )))
758
818
759
- return result
819
+ return other
760
820
else :
761
- # TODO: Figure out the end of this sente
762
821
raise ApplyTypeError (
763
- 'Only know how to combine business hour with ' )
822
+ 'Only know how to combine business hour with datetime ' )
764
823
765
824
def onOffset (self , dt ):
766
825
if self .normalize and not _is_normalized (dt ):
@@ -771,10 +830,9 @@ def onOffset(self, dt):
771
830
dt .minute , dt .second , dt .microsecond )
772
831
# Valid BH can be on the different BusinessDay during midnight
773
832
# Distinguish by the time spent from previous opening time
774
- businesshours = self ._get_business_hours_by_sec
775
- return self ._onOffset (dt , businesshours )
833
+ return self ._onOffset (dt )
776
834
777
- def _onOffset (self , dt , businesshours ):
835
+ def _onOffset (self , dt ):
778
836
"""
779
837
Slight speedups using calculated values.
780
838
"""
@@ -787,15 +845,19 @@ def _onOffset(self, dt, businesshours):
787
845
else :
788
846
op = self ._next_opening_time (dt )
789
847
span = (dt - op ).total_seconds ()
848
+ businesshours = 0
849
+ for i , st in enumerate (self .start ):
850
+ if op .hour == st .hour and op .minute == st .minute :
851
+ businesshours = self ._get_business_hours_by_sec (st , self .end [i ])
790
852
if span <= businesshours :
791
853
return True
792
854
else :
793
855
return False
794
856
795
857
def _repr_attrs (self ):
796
858
out = super ()._repr_attrs ()
797
- start = self . start .strftime ('%H:%M' )
798
- end = self . end .strftime ('%H:%M' )
859
+ start = ',' . join ( st .strftime ('%H:%M' ) for st in self . start )
860
+ end = ',' . join ( en .strftime ('%H:%M' ) for en in self . end )
799
861
attrs = ['{prefix}={start}-{end}' .format (prefix = self ._prefix ,
800
862
start = start , end = end )]
801
863
out += ': ' + ', ' .join (attrs )
0 commit comments