@@ -50,6 +50,7 @@ def __init__(
5050 offline_search : bool = False ,
5151 offline_search_path : Optional [str ] = None ,
5252 days_of_the_week : Optional [Sequence [int ]] = None ,
53+ exact_windows : bool = False ,
5354 ** kwargs ,
5455 ) -> None :
5556 """
@@ -73,10 +74,18 @@ def __init__(
7374 When not specified, the filename will default to `camply_campsites.json`
7475 days_of_the_week: Optional[Sequence[int]]
7576 Days of the week (by weekday integer) to search for.
77+ exact_windows: bool
78+ When set to True, only availabilities exactly matching a passed
79+ search_window will be returned. Useful when you have multiple search
80+ windows with different numbers of nights, but only want to book a
81+ given window if all days are available.
7682 """
7783 self ._verbose = kwargs .get ("verbose" , True )
7884 self .campsite_finder : ProviderType = self .provider_class ()
79- self .search_window : List [SearchWindow ] = make_list (search_window )
85+ self .exact_windows : bool = exact_windows
86+ self ._original_search_windows : List [SearchWindow ] = self ._valid_search_windows (
87+ make_list (search_window )
88+ )
8089 self .days_of_the_week = set (
8190 days_of_the_week if days_of_the_week is not None else ()
8291 )
@@ -126,6 +135,13 @@ def search_days(self) -> List[datetime]:
126135 current_date = datetime .now ().date ()
127136 return [day for day in self ._original_search_days if day >= current_date ]
128137
138+ @property
139+ def search_windows (self ) -> List [SearchWindow ]:
140+ """
141+ Get the list of search windows that need to be searched
142+ """
143+ return self ._valid_search_windows (self ._original_search_windows )
144+
129145 @property
130146 def search_months (self ) -> List [datetime ]:
131147 """
@@ -181,6 +197,31 @@ def _get_intersection_date_overlap(
181197 else :
182198 return False
183199
200+ def _has_matching_window (
201+ self ,
202+ date : datetime ,
203+ periods : int ,
204+ ) -> bool :
205+ """
206+ Determine if there is a matching search window when using exact_windows
207+
208+ Parameters
209+ ----------
210+ date: datetime
211+ Start date of window to search for
212+ periods: int
213+ Number of days of window to search for
214+
215+ Returns
216+ -------
217+ bool
218+ """
219+ return any (
220+ window .start_date == date .date ()
221+ and (window .end_date - window .start_date ).days == periods
222+ for window in self .search_windows
223+ )
224+
184225 def _compare_date_overlap (self , campsite : AvailableCampsite ) -> bool :
185226 """
186227 See whether a campsite should be returned as found
@@ -193,11 +234,17 @@ def _compare_date_overlap(self, campsite: AvailableCampsite) -> bool:
193234 -------
194235 bool
195236 """
196- intersection = self ._get_intersection_date_overlap (
197- date = campsite .booking_date ,
198- periods = campsite .booking_nights ,
199- search_days = self .search_days ,
200- )
237+ if self .exact_windows :
238+ intersection = self ._has_matching_window (
239+ date = campsite .booking_date ,
240+ periods = campsite .booking_nights ,
241+ )
242+ else :
243+ intersection = self ._get_intersection_date_overlap (
244+ date = campsite .booking_date ,
245+ periods = campsite .booking_nights ,
246+ search_days = self .search_days ,
247+ )
201248 return intersection
202249
203250 def _filter_date_overlap (self , campsites : DataFrame ) -> pd .DataFrame :
@@ -605,7 +652,7 @@ def _get_search_days(self) -> List[datetime]:
605652 """
606653 current_date = datetime .now ().date ()
607654 search_nights = set ()
608- for window in self .search_window :
655+ for window in self .search_windows :
609656 generated_dates = {
610657 date for date in window .get_date_range () if date >= current_date
611658 }
@@ -639,9 +686,30 @@ def _get_search_days(self) -> List[datetime]:
639686 raise RuntimeError (SearchConfig .ERROR_MESSAGE )
640687 return sorted (search_nights )
641688
642- @classmethod
689+ def _valid_search_windows (self , windows : List [SearchWindow ]) -> List [SearchWindow ]:
690+ """
691+ Return the subset of windows which have not yet expired
692+
693+ Parameters
694+ ----------
695+ windows: List[SearchWindow]
696+
697+ Returns
698+ -------
699+ List[SearchWindow]
700+ """
701+ current_date = datetime .now ().date ()
702+ if self .exact_windows :
703+ # In this case we are only interested if no days of the window have
704+ # yet elapsed.
705+ return [w for w in windows if w .start_date >= current_date ]
706+ else :
707+ # In this case we are interested as long as there is still at least
708+ # one day that has not yet elapsed.
709+ return [w for w in windows if w .end_date >= current_date ]
710+
643711 def _consolidate_campsites (
644- cls , campsite_df : DataFrame , nights : int
712+ self , campsite_df : DataFrame , nights : int
645713 ) -> pd .DataFrame :
646714 """
647715 Consolidate Single Night Campsites into Multiple Night Campsites
@@ -679,14 +747,101 @@ def _consolidate_campsites(
679747 composed_grouping .drop (
680748 columns = [CampsiteContainerFields .CAMPSITE_GROUP ], inplace = True
681749 )
682- nightly_breakouts = cls . _find_consecutive_nights (
683- dataframe = composed_grouping , nights = nights
750+ nightly_breakouts = self . _find_night_groupings (
751+ dataframe = composed_grouping
684752 )
685753 composed_groupings .append (nightly_breakouts )
686754 if len (composed_groupings ) == 0 :
687755 composed_groupings = [DataFrame ()]
688756 return concat (composed_groupings , ignore_index = True )
689757
758+ def _find_night_groupings (self , dataframe : DataFrame ) -> DataFrame :
759+ """
760+ Find all matching night groupings in dataframe
761+
762+ Matching criteria depends on the value of self.exact_windows.
763+
764+ Parameters
765+ ----------
766+ dataframe: DataFrame
767+
768+ Returns
769+ -------
770+ DataFrame
771+ """
772+ if self .exact_windows :
773+ return self ._find_matching_windows (dataframe )
774+ else :
775+ return self ._find_consecutive_nights (dataframe , self .nights )
776+
777+ @staticmethod
778+ def _booking_in_window (booking : Series , window : SearchWindow ) -> bool :
779+ """
780+ Return true only if the dates of booking are completely inside window
781+
782+ Parameters
783+ ----------
784+ booking: Series
785+ AvailableCampsite converted to a Series
786+ window: SearchWindow
787+
788+ Returns
789+ -------
790+ bool
791+ """
792+ return (
793+ window .start_date <= booking ["booking_date" ].date ()
794+ and booking ["booking_end_date" ].date () <= window .end_date
795+ )
796+
797+ def _find_matching_windows (self , dataframe : DataFrame ) -> DataFrame :
798+ """
799+ Find all sub sequences of dataframe that exactly match a search window
800+
801+ Parameters
802+ ----------
803+ dataframe: DataFrame
804+ Each row contains a consecutive available night for the same
805+ campsite
806+
807+ Returns
808+ -------
809+ DataFrame
810+ """
811+ duplicate_subset = set (dataframe .columns ) - AvailableCampsite .__unhashable__
812+ matching_windows = []
813+ for window in self .search_windows :
814+ if (
815+ dataframe .booking_date .min ().date () <= window .start_date
816+ and window .end_date <= dataframe .booking_end_date .max ().date ()
817+ ):
818+ intersect_criteria = dataframe .apply (
819+ self ._booking_in_window , axis = 1 , window = window
820+ )
821+ window_intersection = dataframe [intersect_criteria ].copy ()
822+
823+ window_intersection .booking_date = (
824+ window_intersection .booking_date .min ()
825+ )
826+ window_intersection .booking_end_date = (
827+ window_intersection .booking_end_date .max ()
828+ )
829+ window_intersection .booking_url = window_intersection .booking_url .iloc [
830+ 0
831+ ]
832+ window_intersection .booking_nights = (
833+ window_intersection .booking_end_date
834+ - window_intersection .booking_date
835+ ).dt .days
836+ window_intersection .drop_duplicates (
837+ inplace = True , subset = duplicate_subset
838+ )
839+ matching_windows .append (window_intersection )
840+
841+ if len (matching_windows ) == 0 :
842+ matching_windows = [DataFrame ()]
843+ return concat (matching_windows , ignore_index = True )
844+
690845 @classmethod
691846 def _consecutive_subseq (cls , iterable : Iterable , length : int ) -> Generator :
692847 """
0 commit comments