1111from typing import (
1212 TYPE_CHECKING ,
1313 Any ,
14- Final ,
1514 cast ,
1615)
1716
3029 Timestamp ,
3130 to_offset ,
3231)
33- from pandas ._libs .tslibs .dtypes import FreqGroup
34- from pandas ._typing import F
32+ from pandas ._libs .tslibs .dtypes import (
33+ FreqGroup ,
34+ periods_per_day ,
35+ )
36+ from pandas ._typing import (
37+ F ,
38+ npt ,
39+ )
3540
3641from pandas .core .dtypes .common import (
3742 is_float ,
6065
6166 from pandas ._libs .tslibs .offsets import BaseOffset
6267
63- # constants
64- HOURS_PER_DAY : Final = 24.0
65- MIN_PER_HOUR : Final = 60.0
66- SEC_PER_MIN : Final = 60.0
67-
68- SEC_PER_HOUR : Final = SEC_PER_MIN * MIN_PER_HOUR
69- SEC_PER_DAY : Final = SEC_PER_HOUR * HOURS_PER_DAY
70-
71- MUSEC_PER_DAY : Final = 10 ** 6 * SEC_PER_DAY
7268
7369_mpl_units = {} # Cache for units overwritten by us
7470
@@ -495,7 +491,7 @@ def _get_default_annual_spacing(nyears) -> tuple[int, int]:
495491 return (min_spacing , maj_spacing )
496492
497493
498- def period_break (dates : PeriodIndex , period : str ) -> np .ndarray :
494+ def _period_break (dates : PeriodIndex , period : str ) -> npt . NDArray [ np .intp ] :
499495 """
500496 Returns the indices where the given period changes.
501497
@@ -506,12 +502,17 @@ def period_break(dates: PeriodIndex, period: str) -> np.ndarray:
506502 period : str
507503 Name of the period to monitor.
508504 """
505+ mask = _period_break_mask (dates , period )
506+ return np .nonzero (mask )[0 ]
507+
508+
509+ def _period_break_mask (dates : PeriodIndex , period : str ) -> npt .NDArray [np .bool_ ]:
509510 current = getattr (dates , period )
510511 previous = getattr (dates - 1 * dates .freq , period )
511- return np . nonzero ( current - previous )[ 0 ]
512+ return current != previous
512513
513514
514- def has_level_label (label_flags : np .ndarray , vmin : float ) -> bool :
515+ def has_level_label (label_flags : npt . NDArray [ np .intp ] , vmin : float ) -> bool :
515516 """
516517 Returns true if the ``label_flags`` indicate there is at least one label
517518 for this level.
@@ -527,54 +528,59 @@ def has_level_label(label_flags: np.ndarray, vmin: float) -> bool:
527528 return True
528529
529530
530- def _daily_finder ( vmin , vmax , freq : BaseOffset ):
531+ def _get_periods_per_ymd ( freq : BaseOffset ) -> tuple [ int , int , int ] :
531532 # error: "BaseOffset" has no attribute "_period_dtype_code"
532533 dtype_code = freq ._period_dtype_code # type: ignore[attr-defined]
533534 freq_group = FreqGroup .from_period_dtype_code (dtype_code )
534535
535- periodsperday = - 1
536+ ppd = - 1 # placeholder for above-day freqs
536537
537538 if dtype_code >= FreqGroup .FR_HR .value :
538- if freq_group == FreqGroup .FR_NS :
539- periodsperday = 24 * 60 * 60 * 1000000000
540- elif freq_group == FreqGroup .FR_US :
541- periodsperday = 24 * 60 * 60 * 1000000
542- elif freq_group == FreqGroup .FR_MS :
543- periodsperday = 24 * 60 * 60 * 1000
544- elif freq_group == FreqGroup .FR_SEC :
545- periodsperday = 24 * 60 * 60
546- elif freq_group == FreqGroup .FR_MIN :
547- periodsperday = 24 * 60
548- elif freq_group == FreqGroup .FR_HR :
549- periodsperday = 24
550- else : # pragma: no cover
551- raise ValueError (f"unexpected frequency: { dtype_code } " )
552- periodsperyear = 365 * periodsperday
553- periodspermonth = 28 * periodsperday
554-
539+ # error: "BaseOffset" has no attribute "_creso"
540+ ppd = periods_per_day (freq ._creso ) # type: ignore[attr-defined]
541+ ppm = 28 * ppd
542+ ppy = 365 * ppd
555543 elif freq_group == FreqGroup .FR_BUS :
556- periodsperyear = 261
557- periodspermonth = 19
544+ ppm = 19
545+ ppy = 261
558546 elif freq_group == FreqGroup .FR_DAY :
559- periodsperyear = 365
560- periodspermonth = 28
547+ ppm = 28
548+ ppy = 365
561549 elif freq_group == FreqGroup .FR_WK :
562- periodsperyear = 52
563- periodspermonth = 3
564- else : # pragma: no cover
565- raise ValueError ("unexpected frequency" )
550+ ppm = 3
551+ ppy = 52
552+ elif freq_group == FreqGroup .FR_MTH :
553+ ppm = 1
554+ ppy = 12
555+ elif freq_group == FreqGroup .FR_QTR :
556+ ppm = - 1 # placerholder
557+ ppy = 4
558+ elif freq_group == FreqGroup .FR_ANN :
559+ ppm = - 1 # placeholder
560+ ppy = 1
561+ else :
562+ raise NotImplementedError (f"Unsupported frequency: { dtype_code } " )
563+
564+ return ppd , ppm , ppy
565+
566+
567+ def _daily_finder (vmin , vmax , freq : BaseOffset ) -> np .ndarray :
568+ # error: "BaseOffset" has no attribute "_period_dtype_code"
569+ dtype_code = freq ._period_dtype_code # type: ignore[attr-defined]
570+
571+ periodsperday , periodspermonth , periodsperyear = _get_periods_per_ymd (freq )
566572
567573 # save this for later usage
568574 vmin_orig = vmin
575+ (vmin , vmax ) = (int (vmin ), int (vmax ))
576+ span = vmax - vmin + 1
569577
570- (vmin , vmax ) = (
571- Period (ordinal = int (vmin ), freq = freq ),
572- Period (ordinal = int (vmax ), freq = freq ),
578+ dates_ = period_range (
579+ start = Period (ordinal = vmin , freq = freq ),
580+ end = Period (ordinal = vmax , freq = freq ),
581+ freq = freq ,
573582 )
574- assert isinstance (vmin , Period )
575- assert isinstance (vmax , Period )
576- span = vmax .ordinal - vmin .ordinal + 1
577- dates_ = period_range (start = vmin , end = vmax , freq = freq )
583+
578584 # Initialize the output
579585 info = np .zeros (
580586 span , dtype = [("val" , np .int64 ), ("maj" , bool ), ("min" , bool ), ("fmt" , "|S20" )]
@@ -595,45 +601,38 @@ def first_label(label_flags):
595601
596602 # Case 1. Less than a month
597603 if span <= periodspermonth :
598- day_start = period_break (dates_ , "day" )
599- month_start = period_break (dates_ , "month" )
604+ day_start = _period_break (dates_ , "day" )
605+ month_start = _period_break (dates_ , "month" )
606+ year_start = _period_break (dates_ , "year" )
600607
601- def _hour_finder (label_interval , force_year_start ) -> None :
602- _hour = dates_ .hour
603- _prev_hour = (dates_ - 1 * dates_ .freq ).hour
604- hour_start = (_hour - _prev_hour ) != 0
608+ def _hour_finder (label_interval : int , force_year_start : bool ) -> None :
609+ target = dates_ .hour
610+ mask = _period_break_mask (dates_ , "hour" )
605611 info_maj [day_start ] = True
606- info_min [hour_start & (_hour % label_interval == 0 )] = True
607- year_start = period_break (dates_ , "year" )
608- info_fmt [hour_start & (_hour % label_interval == 0 )] = "%H:%M"
612+ info_min [mask & (target % label_interval == 0 )] = True
613+ info_fmt [mask & (target % label_interval == 0 )] = "%H:%M"
609614 info_fmt [day_start ] = "%H:%M\n %d-%b"
610615 info_fmt [year_start ] = "%H:%M\n %d-%b\n %Y"
611616 if force_year_start and not has_level_label (year_start , vmin_orig ):
612617 info_fmt [first_label (day_start )] = "%H:%M\n %d-%b\n %Y"
613618
614- def _minute_finder (label_interval ) -> None :
615- hour_start = period_break (dates_ , "hour" )
616- _minute = dates_ .minute
617- _prev_minute = (dates_ - 1 * dates_ .freq ).minute
618- minute_start = (_minute - _prev_minute ) != 0
619+ def _minute_finder (label_interval : int ) -> None :
620+ target = dates_ .minute
621+ hour_start = _period_break (dates_ , "hour" )
622+ mask = _period_break_mask (dates_ , "minute" )
619623 info_maj [hour_start ] = True
620- info_min [minute_start & (_minute % label_interval == 0 )] = True
621- year_start = period_break (dates_ , "year" )
622- info_fmt = info ["fmt" ]
623- info_fmt [minute_start & (_minute % label_interval == 0 )] = "%H:%M"
624+ info_min [mask & (target % label_interval == 0 )] = True
625+ info_fmt [mask & (target % label_interval == 0 )] = "%H:%M"
624626 info_fmt [day_start ] = "%H:%M\n %d-%b"
625627 info_fmt [year_start ] = "%H:%M\n %d-%b\n %Y"
626628
627- def _second_finder (label_interval ) -> None :
628- minute_start = period_break (dates_ , "minute" )
629- _second = dates_ .second
630- _prev_second = (dates_ - 1 * dates_ .freq ).second
631- second_start = (_second - _prev_second ) != 0
632- info ["maj" ][minute_start ] = True
633- info ["min" ][second_start & (_second % label_interval == 0 )] = True
634- year_start = period_break (dates_ , "year" )
635- info_fmt = info ["fmt" ]
636- info_fmt [second_start & (_second % label_interval == 0 )] = "%H:%M:%S"
629+ def _second_finder (label_interval : int ) -> None :
630+ target = dates_ .second
631+ minute_start = _period_break (dates_ , "minute" )
632+ mask = _period_break_mask (dates_ , "second" )
633+ info_maj [minute_start ] = True
634+ info_min [mask & (target % label_interval == 0 )] = True
635+ info_fmt [mask & (target % label_interval == 0 )] = "%H:%M:%S"
637636 info_fmt [day_start ] = "%H:%M:%S\n %d-%b"
638637 info_fmt [year_start ] = "%H:%M:%S\n %d-%b\n %Y"
639638
@@ -672,8 +671,6 @@ def _second_finder(label_interval) -> None:
672671 else :
673672 info_maj [month_start ] = True
674673 info_min [day_start ] = True
675- year_start = period_break (dates_ , "year" )
676- info_fmt = info ["fmt" ]
677674 info_fmt [day_start ] = "%d"
678675 info_fmt [month_start ] = "%d\n %b"
679676 info_fmt [year_start ] = "%d\n %b\n %Y"
@@ -685,15 +682,15 @@ def _second_finder(label_interval) -> None:
685682
686683 # Case 2. Less than three months
687684 elif span <= periodsperyear // 4 :
688- month_start = period_break (dates_ , "month" )
685+ month_start = _period_break (dates_ , "month" )
689686 info_maj [month_start ] = True
690687 if dtype_code < FreqGroup .FR_HR .value :
691688 info ["min" ] = True
692689 else :
693- day_start = period_break (dates_ , "day" )
690+ day_start = _period_break (dates_ , "day" )
694691 info ["min" ][day_start ] = True
695- week_start = period_break (dates_ , "week" )
696- year_start = period_break (dates_ , "year" )
692+ week_start = _period_break (dates_ , "week" )
693+ year_start = _period_break (dates_ , "year" )
697694 info_fmt [week_start ] = "%d"
698695 info_fmt [month_start ] = "\n \n %b"
699696 info_fmt [year_start ] = "\n \n %b\n %Y"
@@ -704,9 +701,9 @@ def _second_finder(label_interval) -> None:
704701 info_fmt [first_label (month_start )] = "\n \n %b\n %Y"
705702 # Case 3. Less than 14 months ...............
706703 elif span <= 1.15 * periodsperyear :
707- year_start = period_break (dates_ , "year" )
708- month_start = period_break (dates_ , "month" )
709- week_start = period_break (dates_ , "week" )
704+ year_start = _period_break (dates_ , "year" )
705+ month_start = _period_break (dates_ , "month" )
706+ week_start = _period_break (dates_ , "week" )
710707 info_maj [month_start ] = True
711708 info_min [week_start ] = True
712709 info_min [year_start ] = False
@@ -717,17 +714,17 @@ def _second_finder(label_interval) -> None:
717714 info_fmt [first_label (month_start )] = "%b\n %Y"
718715 # Case 4. Less than 2.5 years ...............
719716 elif span <= 2.5 * periodsperyear :
720- year_start = period_break (dates_ , "year" )
721- quarter_start = period_break (dates_ , "quarter" )
722- month_start = period_break (dates_ , "month" )
717+ year_start = _period_break (dates_ , "year" )
718+ quarter_start = _period_break (dates_ , "quarter" )
719+ month_start = _period_break (dates_ , "month" )
723720 info_maj [quarter_start ] = True
724721 info_min [month_start ] = True
725722 info_fmt [quarter_start ] = "%b"
726723 info_fmt [year_start ] = "%b\n %Y"
727724 # Case 4. Less than 4 years .................
728725 elif span <= 4 * periodsperyear :
729- year_start = period_break (dates_ , "year" )
730- month_start = period_break (dates_ , "month" )
726+ year_start = _period_break (dates_ , "year" )
727+ month_start = _period_break (dates_ , "month" )
731728 info_maj [year_start ] = True
732729 info_min [month_start ] = True
733730 info_min [year_start ] = False
@@ -738,15 +735,15 @@ def _second_finder(label_interval) -> None:
738735 info_fmt [year_start ] = "%b\n %Y"
739736 # Case 5. Less than 11 years ................
740737 elif span <= 11 * periodsperyear :
741- year_start = period_break (dates_ , "year" )
742- quarter_start = period_break (dates_ , "quarter" )
738+ year_start = _period_break (dates_ , "year" )
739+ quarter_start = _period_break (dates_ , "quarter" )
743740 info_maj [year_start ] = True
744741 info_min [quarter_start ] = True
745742 info_min [year_start ] = False
746743 info_fmt [year_start ] = "%Y"
747744 # Case 6. More than 12 years ................
748745 else :
749- year_start = period_break (dates_ , "year" )
746+ year_start = _period_break (dates_ , "year" )
750747 year_break = dates_ [year_start ].year
751748 nyears = span / periodsperyear
752749 (min_anndef , maj_anndef ) = _get_default_annual_spacing (nyears )
@@ -759,8 +756,8 @@ def _second_finder(label_interval) -> None:
759756 return info
760757
761758
762- def _monthly_finder (vmin , vmax , freq ) :
763- periodsperyear = 12
759+ def _monthly_finder (vmin , vmax , freq : BaseOffset ) -> np . ndarray :
760+ _ , _ , periodsperyear = _get_periods_per_ymd ( freq )
764761
765762 vmin_orig = vmin
766763 (vmin , vmax ) = (int (vmin ), int (vmax ))
@@ -795,6 +792,7 @@ def _monthly_finder(vmin, vmax, freq):
795792 quarter_start = (dates_ % 3 == 0 ).nonzero ()
796793 info_maj [year_start ] = True
797794 # TODO: Check the following : is it really info['fmt'] ?
795+ # 2023-09-15 this is reached in test_finder_monthly
798796 info ["fmt" ][quarter_start ] = True
799797 info ["min" ] = True
800798
@@ -829,8 +827,8 @@ def _monthly_finder(vmin, vmax, freq):
829827 return info
830828
831829
832- def _quarterly_finder (vmin , vmax , freq ) :
833- periodsperyear = 4
830+ def _quarterly_finder (vmin , vmax , freq : BaseOffset ) -> np . ndarray :
831+ _ , _ , periodsperyear = _get_periods_per_ymd ( freq )
834832 vmin_orig = vmin
835833 (vmin , vmax ) = (int (vmin ), int (vmax ))
836834 span = vmax - vmin + 1
@@ -876,7 +874,8 @@ def _quarterly_finder(vmin, vmax, freq):
876874 return info
877875
878876
879- def _annual_finder (vmin , vmax , freq ):
877+ def _annual_finder (vmin , vmax , freq : BaseOffset ) -> np .ndarray :
878+ # Note: small difference here vs other finders in adding 1 to vmax
880879 (vmin , vmax ) = (int (vmin ), int (vmax + 1 ))
881880 span = vmax - vmin + 1
882881
@@ -889,8 +888,9 @@ def _annual_finder(vmin, vmax, freq):
889888
890889 (min_anndef , maj_anndef ) = _get_default_annual_spacing (span )
891890 major_idx = dates_ % maj_anndef == 0
891+ minor_idx = dates_ % min_anndef == 0
892892 info ["maj" ][major_idx ] = True
893- info ["min" ][( dates_ % min_anndef == 0 ) ] = True
893+ info ["min" ][minor_idx ] = True
894894 info ["fmt" ][major_idx ] = "%Y"
895895
896896 return info
@@ -1087,7 +1087,7 @@ def format_timedelta_ticks(x, pos, n_decimals: int) -> str:
10871087 """
10881088 Convert seconds to 'D days HH:MM:SS.F'
10891089 """
1090- s , ns = divmod (x , 10 ** 9 )
1090+ s , ns = divmod (x , 10 ** 9 ) # TODO(non-nano): this looks like it assumes ns
10911091 m , s = divmod (s , 60 )
10921092 h , m = divmod (m , 60 )
10931093 d , h = divmod (h , 24 )
0 commit comments