@@ -71,6 +71,34 @@ def effective_collection_type(self, subcollection_type):
7171 if subcollection_type == "single_datasets" :
7272 return self .collection_type
7373
74+ normalized = _normalize_collection_type (self .collection_type )
75+ normalized_sub = _normalize_collection_type (subcollection_type )
76+
77+ if subcollection_type == "paired_or_unpaired" :
78+ if self .collection_type .endswith (":paired" ):
79+ # paired_or_unpaired consumes the :paired suffix
80+ return self .collection_type [: - len (":paired" )]
81+ elif normalized .endswith ("list" ):
82+ # paired_or_unpaired acts like single_datasets for collections
83+ # whose innermost type is list (each element wrapped as unpaired)
84+ return self .collection_type
85+ else :
86+ # strip last rank (paired_or_unpaired consumes it)
87+ return self .collection_type [: self .collection_type .rfind (":" )]
88+
89+ if normalized_sub .endswith (":paired_or_unpaired" ):
90+ # Compound :paired_or_unpaired suffix — iterative peel-off matching
91+ # TS effectiveMapOver logic. Strip ranks from both sides, then
92+ # optionally strip one more if :paired was consumed.
93+ current = self .collection_type
94+ current_other = subcollection_type
95+ while ":" in current_other :
96+ current_other = current_other [: current_other .rfind (":" )]
97+ current = current [: current .rfind (":" )]
98+ if normalized .endswith (":paired" ):
99+ current = current [: current .rfind (":" )]
100+ return current
101+
74102 return self .collection_type [: - (len (subcollection_type ) + 1 )]
75103
76104 def has_subcollections_of_type (self , other_collection_type ) -> bool :
@@ -88,12 +116,23 @@ def has_subcollections_of_type(self, other_collection_type) -> bool:
88116 other_collection_type = _normalize_collection_type (other_collection_type )
89117 if collection_type == other_collection_type :
90118 return False
91- if collection_type .endswith (other_collection_type ):
119+ if collection_type .endswith (f": { other_collection_type } " ):
92120 return True
93121 if other_collection_type == "paired_or_unpaired" :
94122 # this can be thought of as a subcollection of anything except a pair
95123 # since it would match a pair exactly
96124 return collection_type != "paired"
125+ if other_collection_type .endswith (":paired_or_unpaired" ):
126+ # Compound :paired_or_unpaired suffix — e.g. list:list can map over
127+ # list:paired_or_unpaired. Strip the :paired_or_unpaired to get the
128+ # required higher ranks, optionally strip :paired from self (since
129+ # paired_or_unpaired consumes paired), then check alignment.
130+ higher_ranks_required = other_collection_type [: other_collection_type .rfind (":" )]
131+ if collection_type .endswith (":paired" ):
132+ higher_ranks = collection_type [: collection_type .rfind (":" )]
133+ else :
134+ higher_ranks = collection_type
135+ return higher_ranks .endswith (higher_ranks_required ) and higher_ranks != higher_ranks_required
97136 if other_collection_type == "single_datasets" :
98137 # effectively any collection has unpaired subcollections
99138 return True
0 commit comments