Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Base and utility classes for tseries type pandas objects. 

3""" 

4import operator 

5from typing import List, Optional, Set 

6 

7import numpy as np 

8 

9from pandas._libs import NaT, iNaT, join as libjoin, lib 

10from pandas._libs.algos import unique_deltas 

11from pandas._libs.tslibs import timezones 

12from pandas.compat.numpy import function as nv 

13from pandas.errors import AbstractMethodError 

14from pandas.util._decorators import Appender, cache_readonly 

15 

16from pandas.core.dtypes.common import ( 

17 ensure_int64, 

18 is_bool_dtype, 

19 is_categorical_dtype, 

20 is_dtype_equal, 

21 is_float, 

22 is_integer, 

23 is_list_like, 

24 is_period_dtype, 

25 is_scalar, 

26 needs_i8_conversion, 

27) 

28from pandas.core.dtypes.concat import concat_compat 

29from pandas.core.dtypes.generic import ABCIndex, ABCIndexClass, ABCSeries 

30from pandas.core.dtypes.missing import isna 

31 

32from pandas.core import algorithms 

33from pandas.core.accessor import PandasDelegate 

34from pandas.core.arrays import DatetimeArray, ExtensionArray, TimedeltaArray 

35from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin 

36import pandas.core.indexes.base as ibase 

37from pandas.core.indexes.base import Index, _index_shared_docs 

38from pandas.core.indexes.extension import ( 

39 ExtensionIndex, 

40 inherit_names, 

41 make_wrapped_arith_op, 

42) 

43from pandas.core.indexes.numeric import Int64Index 

44from pandas.core.ops import get_op_result_name 

45from pandas.core.tools.timedeltas import to_timedelta 

46 

47from pandas.tseries.frequencies import DateOffset, to_offset 

48 

49_index_doc_kwargs = dict(ibase._index_doc_kwargs) 

50 

51 

52def _join_i8_wrapper(joinf, with_indexers: bool = True): 

53 """ 

54 Create the join wrapper methods. 

55 """ 

56 

57 @staticmethod # type: ignore 

58 def wrapper(left, right): 

59 if isinstance(left, (np.ndarray, ABCIndex, ABCSeries, DatetimeLikeArrayMixin)): 

60 left = left.view("i8") 

61 if isinstance(right, (np.ndarray, ABCIndex, ABCSeries, DatetimeLikeArrayMixin)): 

62 right = right.view("i8") 

63 

64 results = joinf(left, right) 

65 if with_indexers: 

66 # dtype should be timedelta64[ns] for TimedeltaIndex 

67 # and datetime64[ns] for DatetimeIndex 

68 dtype = left.dtype.base 

69 

70 join_index, left_indexer, right_indexer = results 

71 join_index = join_index.view(dtype) 

72 return join_index, left_indexer, right_indexer 

73 return results 

74 

75 return wrapper 

76 

77 

78@inherit_names( 

79 ["inferred_freq", "_isnan", "_resolution", "resolution"], 

80 DatetimeLikeArrayMixin, 

81 cache=True, 

82) 

83@inherit_names( 

84 ["__iter__", "mean", "freq", "freqstr", "_ndarray_values", "asi8", "_box_values"], 

85 DatetimeLikeArrayMixin, 

86) 

87class DatetimeIndexOpsMixin(ExtensionIndex): 

88 """ 

89 Common ops mixin to support a unified interface datetimelike Index. 

90 """ 

91 

92 _data: ExtensionArray 

93 freq: Optional[DateOffset] 

94 freqstr: Optional[str] 

95 _resolution: int 

96 _bool_ops: List[str] = [] 

97 _field_ops: List[str] = [] 

98 

99 hasnans = cache_readonly(DatetimeLikeArrayMixin._hasnans.fget) # type: ignore 

100 _hasnans = hasnans # for index / array -agnostic code 

101 

102 @property 

103 def is_all_dates(self) -> bool: 

104 return True 

105 

106 # ------------------------------------------------------------------------ 

107 # Abstract data attributes 

108 

109 @property 

110 def values(self): 

111 # Note: PeriodArray overrides this to return an ndarray of objects. 

112 return self._data._data 

113 

114 def __array_wrap__(self, result, context=None): 

115 """ 

116 Gets called after a ufunc. 

117 """ 

118 result = lib.item_from_zerodim(result) 

119 if is_bool_dtype(result) or lib.is_scalar(result): 

120 return result 

121 

122 attrs = self._get_attributes_dict() 

123 if not is_period_dtype(self) and attrs["freq"]: 

124 # no need to infer if freq is None 

125 attrs["freq"] = "infer" 

126 return Index(result, **attrs) 

127 

128 # ------------------------------------------------------------------------ 

129 

130 def equals(self, other) -> bool: 

131 """ 

132 Determines if two Index objects contain the same elements. 

133 """ 

134 if self.is_(other): 

135 return True 

136 

137 if not isinstance(other, ABCIndexClass): 

138 return False 

139 elif not isinstance(other, type(self)): 

140 try: 

141 other = type(self)(other) 

142 except (ValueError, TypeError, OverflowError): 

143 # e.g. 

144 # ValueError -> cannot parse str entry, or OutOfBoundsDatetime 

145 # TypeError -> trying to convert IntervalIndex to DatetimeIndex 

146 # OverflowError -> Index([very_large_timedeltas]) 

147 return False 

148 

149 if not is_dtype_equal(self.dtype, other.dtype): 

150 # have different timezone 

151 return False 

152 

153 return np.array_equal(self.asi8, other.asi8) 

154 

155 @Appender(_index_shared_docs["contains"] % _index_doc_kwargs) 

156 def __contains__(self, key): 

157 try: 

158 res = self.get_loc(key) 

159 return ( 

160 is_scalar(res) 

161 or isinstance(res, slice) 

162 or (is_list_like(res) and len(res)) 

163 ) 

164 except (KeyError, TypeError, ValueError): 

165 return False 

166 

167 def sort_values(self, return_indexer=False, ascending=True): 

168 """ 

169 Return sorted copy of Index. 

170 """ 

171 if return_indexer: 

172 _as = self.argsort() 

173 if not ascending: 

174 _as = _as[::-1] 

175 sorted_index = self.take(_as) 

176 return sorted_index, _as 

177 else: 

178 # NB: using asi8 instead of _ndarray_values matters in numpy 1.18 

179 # because the treatment of NaT has been changed to put NaT last 

180 # instead of first. 

181 sorted_values = np.sort(self.asi8) 

182 attribs = self._get_attributes_dict() 

183 freq = attribs["freq"] 

184 

185 if freq is not None and not is_period_dtype(self): 

186 if freq.n > 0 and not ascending: 

187 freq = freq * -1 

188 elif freq.n < 0 and ascending: 

189 freq = freq * -1 

190 attribs["freq"] = freq 

191 

192 if not ascending: 

193 sorted_values = sorted_values[::-1] 

194 

195 return self._simple_new(sorted_values, **attribs) 

196 

197 @Appender(_index_shared_docs["take"] % _index_doc_kwargs) 

198 def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs): 

199 nv.validate_take(tuple(), kwargs) 

200 indices = ensure_int64(indices) 

201 

202 maybe_slice = lib.maybe_indices_to_slice(indices, len(self)) 

203 if isinstance(maybe_slice, slice): 

204 return self[maybe_slice] 

205 

206 return ExtensionIndex.take( 

207 self, indices, axis, allow_fill, fill_value, **kwargs 

208 ) 

209 

210 _can_hold_na = True 

211 

212 _na_value = NaT 

213 """The expected NA value to use with this index.""" 

214 

215 def _convert_tolerance(self, tolerance, target): 

216 tolerance = np.asarray(to_timedelta(tolerance).to_numpy()) 

217 

218 if target.size != tolerance.size and tolerance.size > 1: 

219 raise ValueError("list-like tolerance size must match target index size") 

220 return tolerance 

221 

222 def tolist(self) -> List: 

223 """ 

224 Return a list of the underlying data. 

225 """ 

226 return list(self.astype(object)) 

227 

228 def min(self, axis=None, skipna=True, *args, **kwargs): 

229 """ 

230 Return the minimum value of the Index or minimum along 

231 an axis. 

232 

233 See Also 

234 -------- 

235 numpy.ndarray.min 

236 Series.min : Return the minimum value in a Series. 

237 """ 

238 nv.validate_min(args, kwargs) 

239 nv.validate_minmax_axis(axis) 

240 

241 if not len(self): 

242 return self._na_value 

243 

244 i8 = self.asi8 

245 try: 

246 # quick check 

247 if len(i8) and self.is_monotonic: 

248 if i8[0] != iNaT: 

249 return self._box_func(i8[0]) 

250 

251 if self.hasnans: 

252 if skipna: 

253 min_stamp = self[~self._isnan].asi8.min() 

254 else: 

255 return self._na_value 

256 else: 

257 min_stamp = i8.min() 

258 return self._box_func(min_stamp) 

259 except ValueError: 

260 return self._na_value 

261 

262 def argmin(self, axis=None, skipna=True, *args, **kwargs): 

263 """ 

264 Returns the indices of the minimum values along an axis. 

265 

266 See `numpy.ndarray.argmin` for more information on the 

267 `axis` parameter. 

268 

269 See Also 

270 -------- 

271 numpy.ndarray.argmin 

272 """ 

273 nv.validate_argmin(args, kwargs) 

274 nv.validate_minmax_axis(axis) 

275 

276 i8 = self.asi8 

277 if self.hasnans: 

278 mask = self._isnan 

279 if mask.all() or not skipna: 

280 return -1 

281 i8 = i8.copy() 

282 i8[mask] = np.iinfo("int64").max 

283 return i8.argmin() 

284 

285 def max(self, axis=None, skipna=True, *args, **kwargs): 

286 """ 

287 Return the maximum value of the Index or maximum along 

288 an axis. 

289 

290 See Also 

291 -------- 

292 numpy.ndarray.max 

293 Series.max : Return the maximum value in a Series. 

294 """ 

295 nv.validate_max(args, kwargs) 

296 nv.validate_minmax_axis(axis) 

297 

298 if not len(self): 

299 return self._na_value 

300 

301 i8 = self.asi8 

302 try: 

303 # quick check 

304 if len(i8) and self.is_monotonic: 

305 if i8[-1] != iNaT: 

306 return self._box_func(i8[-1]) 

307 

308 if self.hasnans: 

309 if skipna: 

310 max_stamp = self[~self._isnan].asi8.max() 

311 else: 

312 return self._na_value 

313 else: 

314 max_stamp = i8.max() 

315 return self._box_func(max_stamp) 

316 except ValueError: 

317 return self._na_value 

318 

319 def argmax(self, axis=None, skipna=True, *args, **kwargs): 

320 """ 

321 Returns the indices of the maximum values along an axis. 

322 

323 See `numpy.ndarray.argmax` for more information on the 

324 `axis` parameter. 

325 

326 See Also 

327 -------- 

328 numpy.ndarray.argmax 

329 """ 

330 nv.validate_argmax(args, kwargs) 

331 nv.validate_minmax_axis(axis) 

332 

333 i8 = self.asi8 

334 if self.hasnans: 

335 mask = self._isnan 

336 if mask.all() or not skipna: 

337 return -1 

338 i8 = i8.copy() 

339 i8[mask] = 0 

340 return i8.argmax() 

341 

342 # -------------------------------------------------------------------- 

343 # Rendering Methods 

344 

345 def _format_with_header(self, header, na_rep="NaT", **kwargs): 

346 return header + list(self._format_native_types(na_rep, **kwargs)) 

347 

348 @property 

349 def _formatter_func(self): 

350 raise AbstractMethodError(self) 

351 

352 def _format_attrs(self): 

353 """ 

354 Return a list of tuples of the (attr,formatted_value). 

355 """ 

356 attrs = super()._format_attrs() 

357 for attrib in self._attributes: 

358 if attrib == "freq": 

359 freq = self.freqstr 

360 if freq is not None: 

361 freq = repr(freq) 

362 attrs.append(("freq", freq)) 

363 return attrs 

364 

365 # -------------------------------------------------------------------- 

366 

367 def _convert_scalar_indexer(self, key, kind=None): 

368 """ 

369 We don't allow integer or float indexing on datetime-like when using 

370 loc. 

371 

372 Parameters 

373 ---------- 

374 key : label of the slice bound 

375 kind : {'ix', 'loc', 'getitem', 'iloc'} or None 

376 """ 

377 

378 assert kind in ["ix", "loc", "getitem", "iloc", None] 

379 

380 # we don't allow integer/float indexing for loc 

381 # we don't allow float indexing for ix/getitem 

382 if is_scalar(key): 

383 is_int = is_integer(key) 

384 is_flt = is_float(key) 

385 if kind in ["loc"] and (is_int or is_flt): 

386 self._invalid_indexer("index", key) 

387 elif kind in ["ix", "getitem"] and is_flt: 

388 self._invalid_indexer("index", key) 

389 

390 return super()._convert_scalar_indexer(key, kind=kind) 

391 

392 __add__ = make_wrapped_arith_op("__add__") 

393 __radd__ = make_wrapped_arith_op("__radd__") 

394 __sub__ = make_wrapped_arith_op("__sub__") 

395 __rsub__ = make_wrapped_arith_op("__rsub__") 

396 __pow__ = make_wrapped_arith_op("__pow__") 

397 __rpow__ = make_wrapped_arith_op("__rpow__") 

398 __mul__ = make_wrapped_arith_op("__mul__") 

399 __rmul__ = make_wrapped_arith_op("__rmul__") 

400 __floordiv__ = make_wrapped_arith_op("__floordiv__") 

401 __rfloordiv__ = make_wrapped_arith_op("__rfloordiv__") 

402 __mod__ = make_wrapped_arith_op("__mod__") 

403 __rmod__ = make_wrapped_arith_op("__rmod__") 

404 __divmod__ = make_wrapped_arith_op("__divmod__") 

405 __rdivmod__ = make_wrapped_arith_op("__rdivmod__") 

406 __truediv__ = make_wrapped_arith_op("__truediv__") 

407 __rtruediv__ = make_wrapped_arith_op("__rtruediv__") 

408 

409 def isin(self, values, level=None): 

410 """ 

411 Compute boolean array of whether each index value is found in the 

412 passed set of values. 

413 

414 Parameters 

415 ---------- 

416 values : set or sequence of values 

417 

418 Returns 

419 ------- 

420 is_contained : ndarray (boolean dtype) 

421 """ 

422 if level is not None: 

423 self._validate_index_level(level) 

424 

425 if not isinstance(values, type(self)): 

426 try: 

427 values = type(self)(values) 

428 except ValueError: 

429 return self.astype(object).isin(values) 

430 

431 return algorithms.isin(self.asi8, values.asi8) 

432 

433 @Appender(_index_shared_docs["where"] % _index_doc_kwargs) 

434 def where(self, cond, other=None): 

435 values = self.view("i8") 

436 

437 if is_scalar(other) and isna(other): 

438 other = NaT.value 

439 

440 else: 

441 # Do type inference if necessary up front 

442 # e.g. we passed PeriodIndex.values and got an ndarray of Periods 

443 other = Index(other) 

444 

445 if is_categorical_dtype(other): 

446 # e.g. we have a Categorical holding self.dtype 

447 if needs_i8_conversion(other.categories): 

448 other = other._internal_get_values() 

449 

450 if not is_dtype_equal(self.dtype, other.dtype): 

451 raise TypeError(f"Where requires matching dtype, not {other.dtype}") 

452 

453 other = other.view("i8") 

454 

455 result = np.where(cond, values, other).astype("i8") 

456 return self._shallow_copy(result) 

457 

458 def _summary(self, name=None): 

459 """ 

460 Return a summarized representation. 

461 

462 Parameters 

463 ---------- 

464 name : str 

465 Name to use in the summary representation. 

466 

467 Returns 

468 ------- 

469 str 

470 Summarized representation of the index. 

471 """ 

472 formatter = self._formatter_func 

473 if len(self) > 0: 

474 index_summary = f", {formatter(self[0])} to {formatter(self[-1])}" 

475 else: 

476 index_summary = "" 

477 

478 if name is None: 

479 name = type(self).__name__ 

480 result = f"{name}: {len(self)} entries{index_summary}" 

481 if self.freq: 

482 result += f"\nFreq: {self.freqstr}" 

483 

484 # display as values, not quoted 

485 result = result.replace("'", "") 

486 return result 

487 

488 def _concat_same_dtype(self, to_concat, name): 

489 """ 

490 Concatenate to_concat which has the same class. 

491 """ 

492 attribs = self._get_attributes_dict() 

493 attribs["name"] = name 

494 # do not pass tz to set because tzlocal cannot be hashed 

495 if len({str(x.dtype) for x in to_concat}) != 1: 

496 raise ValueError("to_concat must have the same tz") 

497 

498 new_data = type(self._values)._concat_same_type(to_concat).asi8 

499 

500 # GH 3232: If the concat result is evenly spaced, we can retain the 

501 # original frequency 

502 is_diff_evenly_spaced = len(unique_deltas(new_data)) == 1 

503 if not is_period_dtype(self) and not is_diff_evenly_spaced: 

504 # reset freq 

505 attribs["freq"] = None 

506 

507 return self._simple_new(new_data, **attribs) 

508 

509 def shift(self, periods=1, freq=None): 

510 """ 

511 Shift index by desired number of time frequency increments. 

512 

513 This method is for shifting the values of datetime-like indexes 

514 by a specified time increment a given number of times. 

515 

516 Parameters 

517 ---------- 

518 periods : int, default 1 

519 Number of periods (or increments) to shift by, 

520 can be positive or negative. 

521 

522 .. versionchanged:: 0.24.0 

523 

524 freq : pandas.DateOffset, pandas.Timedelta or string, optional 

525 Frequency increment to shift by. 

526 If None, the index is shifted by its own `freq` attribute. 

527 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc. 

528 

529 Returns 

530 ------- 

531 pandas.DatetimeIndex 

532 Shifted index. 

533 

534 See Also 

535 -------- 

536 Index.shift : Shift values of Index. 

537 PeriodIndex.shift : Shift values of PeriodIndex. 

538 """ 

539 result = self._data._time_shift(periods, freq=freq) 

540 return type(self)(result, name=self.name) 

541 

542 # -------------------------------------------------------------------- 

543 # List-like Methods 

544 

545 def delete(self, loc): 

546 new_i8s = np.delete(self.asi8, loc) 

547 

548 freq = None 

549 if is_period_dtype(self): 

550 freq = self.freq 

551 elif is_integer(loc): 

552 if loc in (0, -len(self), -1, len(self) - 1): 

553 freq = self.freq 

554 else: 

555 if is_list_like(loc): 

556 loc = lib.maybe_indices_to_slice(ensure_int64(np.array(loc)), len(self)) 

557 if isinstance(loc, slice) and loc.step in (1, None): 

558 if loc.start in (0, None) or loc.stop in (len(self), None): 

559 freq = self.freq 

560 

561 return self._shallow_copy(new_i8s, freq=freq) 

562 

563 

564class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, Int64Index): 

565 """ 

566 Mixin class for methods shared by DatetimeIndex and TimedeltaIndex, 

567 but not PeriodIndex 

568 """ 

569 

570 # Compat for frequency inference, see GH#23789 

571 _is_monotonic_increasing = Index.is_monotonic_increasing 

572 _is_monotonic_decreasing = Index.is_monotonic_decreasing 

573 _is_unique = Index.is_unique 

574 

575 def _set_freq(self, freq): 

576 """ 

577 Set the _freq attribute on our underlying DatetimeArray. 

578 

579 Parameters 

580 ---------- 

581 freq : DateOffset, None, or "infer" 

582 """ 

583 # GH#29843 

584 if freq is None: 

585 # Always valid 

586 pass 

587 elif len(self) == 0 and isinstance(freq, DateOffset): 

588 # Always valid. In the TimedeltaIndex case, we assume this 

589 # is a Tick offset. 

590 pass 

591 else: 

592 # As an internal method, we can ensure this assertion always holds 

593 assert freq == "infer" 

594 freq = to_offset(self.inferred_freq) 

595 

596 self._data._freq = freq 

597 

598 def _shallow_copy(self, values=None, **kwargs): 

599 if values is None: 

600 values = self._data 

601 if isinstance(values, type(self)): 

602 values = values._data 

603 

604 attributes = self._get_attributes_dict() 

605 

606 if "freq" not in kwargs and self.freq is not None: 

607 if isinstance(values, (DatetimeArray, TimedeltaArray)): 

608 if values.freq is None: 

609 del attributes["freq"] 

610 

611 attributes.update(kwargs) 

612 return self._simple_new(values, **attributes) 

613 

614 # -------------------------------------------------------------------- 

615 # Set Operation Methods 

616 

617 @Appender(Index.difference.__doc__) 

618 def difference(self, other, sort=None): 

619 new_idx = super().difference(other, sort=sort) 

620 new_idx._set_freq(None) 

621 return new_idx 

622 

623 def intersection(self, other, sort=False): 

624 """ 

625 Specialized intersection for DatetimeIndex/TimedeltaIndex. 

626 

627 May be much faster than Index.intersection 

628 

629 Parameters 

630 ---------- 

631 other : Same type as self or array-like 

632 sort : False or None, default False 

633 Sort the resulting index if possible. 

634 

635 .. versionadded:: 0.24.0 

636 

637 .. versionchanged:: 0.24.1 

638 

639 Changed the default to ``False`` to match the behaviour 

640 from before 0.24.0. 

641 

642 .. versionchanged:: 0.25.0 

643 

644 The `sort` keyword is added 

645 

646 Returns 

647 ------- 

648 y : Index or same type as self 

649 """ 

650 self._validate_sort_keyword(sort) 

651 self._assert_can_do_setop(other) 

652 

653 if self.equals(other): 

654 return self._get_reconciled_name_object(other) 

655 

656 if len(self) == 0: 

657 return self.copy() 

658 if len(other) == 0: 

659 return other.copy() 

660 

661 if not isinstance(other, type(self)): 

662 result = Index.intersection(self, other, sort=sort) 

663 if isinstance(result, type(self)): 

664 if result.freq is None: 

665 result._set_freq("infer") 

666 return result 

667 

668 elif ( 

669 other.freq is None 

670 or self.freq is None 

671 or other.freq != self.freq 

672 or not other.freq.is_anchored() 

673 or (not self.is_monotonic or not other.is_monotonic) 

674 ): 

675 result = Index.intersection(self, other, sort=sort) 

676 

677 # Invalidate the freq of `result`, which may not be correct at 

678 # this point, depending on the values. 

679 

680 result._set_freq(None) 

681 result = self._shallow_copy( 

682 result._data, name=result.name, dtype=result.dtype, freq=None 

683 ) 

684 if result.freq is None: 

685 result._set_freq("infer") 

686 return result 

687 

688 # to make our life easier, "sort" the two ranges 

689 if self[0] <= other[0]: 

690 left, right = self, other 

691 else: 

692 left, right = other, self 

693 

694 # after sorting, the intersection always starts with the right index 

695 # and ends with the index of which the last elements is smallest 

696 end = min(left[-1], right[-1]) 

697 start = right[0] 

698 

699 if end < start: 

700 return type(self)(data=[]) 

701 else: 

702 lslice = slice(*left.slice_locs(start, end)) 

703 left_chunk = left.values[lslice] 

704 return self._shallow_copy(left_chunk) 

705 

706 def _can_fast_union(self, other) -> bool: 

707 if not isinstance(other, type(self)): 

708 return False 

709 

710 freq = self.freq 

711 

712 if freq is None or freq != other.freq: 

713 return False 

714 

715 if not self.is_monotonic or not other.is_monotonic: 

716 return False 

717 

718 if len(self) == 0 or len(other) == 0: 

719 return True 

720 

721 # to make our life easier, "sort" the two ranges 

722 if self[0] <= other[0]: 

723 left, right = self, other 

724 else: 

725 left, right = other, self 

726 

727 right_start = right[0] 

728 left_end = left[-1] 

729 

730 # Only need to "adjoin", not overlap 

731 try: 

732 return (right_start == left_end + freq) or right_start in left 

733 except ValueError: 

734 # if we are comparing a freq that does not propagate timezones 

735 # this will raise 

736 return False 

737 

738 def _fast_union(self, other, sort=None): 

739 if len(other) == 0: 

740 return self.view(type(self)) 

741 

742 if len(self) == 0: 

743 return other.view(type(self)) 

744 

745 # to make our life easier, "sort" the two ranges 

746 if self[0] <= other[0]: 

747 left, right = self, other 

748 elif sort is False: 

749 # TDIs are not in the "correct" order and we don't want 

750 # to sort but want to remove overlaps 

751 left, right = self, other 

752 left_start = left[0] 

753 loc = right.searchsorted(left_start, side="left") 

754 right_chunk = right.values[:loc] 

755 dates = concat_compat((left.values, right_chunk)) 

756 return self._shallow_copy(dates) 

757 else: 

758 left, right = other, self 

759 

760 left_end = left[-1] 

761 right_end = right[-1] 

762 

763 # concatenate 

764 if left_end < right_end: 

765 loc = right.searchsorted(left_end, side="right") 

766 right_chunk = right.values[loc:] 

767 dates = concat_compat((left.values, right_chunk)) 

768 return self._shallow_copy(dates) 

769 else: 

770 return left 

771 

772 def _union(self, other, sort): 

773 if not len(other) or self.equals(other) or not len(self): 

774 return super()._union(other, sort=sort) 

775 

776 # We are called by `union`, which is responsible for this validation 

777 assert isinstance(other, type(self)) 

778 

779 this, other = self._maybe_utc_convert(other) 

780 

781 if this._can_fast_union(other): 

782 return this._fast_union(other, sort=sort) 

783 else: 

784 result = Index._union(this, other, sort=sort) 

785 if isinstance(result, type(self)): 

786 assert result._data.dtype == this.dtype 

787 if result.freq is None: 

788 result._set_freq("infer") 

789 return result 

790 

791 # -------------------------------------------------------------------- 

792 # Join Methods 

793 _join_precedence = 10 

794 

795 _inner_indexer = _join_i8_wrapper(libjoin.inner_join_indexer) 

796 _outer_indexer = _join_i8_wrapper(libjoin.outer_join_indexer) 

797 _left_indexer = _join_i8_wrapper(libjoin.left_join_indexer) 

798 _left_indexer_unique = _join_i8_wrapper( 

799 libjoin.left_join_indexer_unique, with_indexers=False 

800 ) 

801 

802 def join( 

803 self, other, how: str = "left", level=None, return_indexers=False, sort=False 

804 ): 

805 """ 

806 See Index.join 

807 """ 

808 if self._is_convertible_to_index_for_join(other): 

809 try: 

810 other = type(self)(other) 

811 except (TypeError, ValueError): 

812 pass 

813 

814 this, other = self._maybe_utc_convert(other) 

815 return Index.join( 

816 this, 

817 other, 

818 how=how, 

819 level=level, 

820 return_indexers=return_indexers, 

821 sort=sort, 

822 ) 

823 

824 def _maybe_utc_convert(self, other): 

825 this = self 

826 if not hasattr(self, "tz"): 

827 return this, other 

828 

829 if isinstance(other, type(self)): 

830 if self.tz is not None: 

831 if other.tz is None: 

832 raise TypeError("Cannot join tz-naive with tz-aware DatetimeIndex") 

833 elif other.tz is not None: 

834 raise TypeError("Cannot join tz-naive with tz-aware DatetimeIndex") 

835 

836 if not timezones.tz_compare(self.tz, other.tz): 

837 this = self.tz_convert("UTC") 

838 other = other.tz_convert("UTC") 

839 return this, other 

840 

841 @classmethod 

842 def _is_convertible_to_index_for_join(cls, other: Index) -> bool: 

843 """ 

844 return a boolean whether I can attempt conversion to a 

845 DatetimeIndex/TimedeltaIndex 

846 """ 

847 if isinstance(other, cls): 

848 return False 

849 elif len(other) > 0 and other.inferred_type not in ( 

850 "floating", 

851 "mixed-integer", 

852 "integer", 

853 "integer-na", 

854 "mixed-integer-float", 

855 "mixed", 

856 ): 

857 return True 

858 return False 

859 

860 def _wrap_joined_index(self, joined: np.ndarray, other): 

861 assert other.dtype == self.dtype, (other.dtype, self.dtype) 

862 name = get_op_result_name(self, other) 

863 

864 freq = self.freq if self._can_fast_union(other) else None 

865 new_data = type(self._data)._simple_new( # type: ignore 

866 joined, dtype=self.dtype, freq=freq 

867 ) 

868 

869 return type(self)._simple_new(new_data, name=name) 

870 

871 

872class DatetimelikeDelegateMixin(PandasDelegate): 

873 """ 

874 Delegation mechanism, specific for Datetime, Timedelta, and Period types. 

875 

876 Functionality is delegated from the Index class to an Array class. A 

877 few things can be customized 

878 

879 * _delegated_methods, delegated_properties : List 

880 The list of property / method names being delagated. 

881 * raw_methods : Set 

882 The set of methods whose results should should *not* be 

883 boxed in an index, after being returned from the array 

884 * raw_properties : Set 

885 The set of properties whose results should should *not* be 

886 boxed in an index, after being returned from the array 

887 """ 

888 

889 # raw_methods : dispatch methods that shouldn't be boxed in an Index 

890 _raw_methods: Set[str] = set() 

891 # raw_properties : dispatch properties that shouldn't be boxed in an Index 

892 _raw_properties: Set[str] = set() 

893 _data: ExtensionArray 

894 

895 def _delegate_property_get(self, name, *args, **kwargs): 

896 result = getattr(self._data, name) 

897 if name not in self._raw_properties: 

898 result = Index(result, name=self.name) 

899 return result 

900 

901 def _delegate_property_set(self, name, value, *args, **kwargs): 

902 setattr(self._data, name, value) 

903 

904 def _delegate_method(self, name, *args, **kwargs): 

905 result = operator.methodcaller(name, *args, **kwargs)(self._data) 

906 if name not in self._raw_methods: 

907 result = Index(result, name=self.name) 

908 return result