"""test to_datetime"""

import calendar
from collections import deque
from datetime import (
    UTC,
    date,
    datetime,
    timedelta,
    timezone,
)
from decimal import Decimal
import locale
import re
import zoneinfo

from dateutil.parser import parse
import numpy as np
import pytest

from pandas._libs import tslib
from pandas._libs.tslibs import (
    iNaT,
    parsing,
)
from pandas.compat import (
    PY314,
    PY315,
    WASM,
)
from pandas.errors import (
    OutOfBoundsDatetime,
    OutOfBoundsTimedelta,
    Pandas4Warning,
)
import pandas.util._test_decorators as td

from pandas.core.dtypes.common import is_datetime64_ns_dtype

import pandas as pd
import pandas._testing as tm
from pandas.core.arrays import (
    ArrowExtensionArray,
    DatetimeArray,
)
from pandas.core.tools import datetimes as tools
from pandas.core.tools.datetimes import start_caching_at

PARSING_ERR_MSG = (
    r"You might want to try:\n"
    r"    - passing `format` if your strings have a consistent format;\n"
    r"    - passing `format=\'ISO8601\'` if your strings are all ISO8601 "
    r"but not necessarily in exactly the same format;\n"
    r"    - passing `format=\'mixed\'`, and the format will be inferred "
    r"for each element individually. You might want to use `dayfirst` "
    r"alongside this."
)

if PY314:
    NOT_99 = ", not 99"
    DAY_IS_OUT_OF_RANGE = (
        r"day \d{1,2} must be in range 1\.\.\d{1,2} for "
        r"month \d{1,2} in year \d{4}"
    )
else:
    NOT_99 = ""
    DAY_IS_OUT_OF_RANGE = "day is out of range for month"


class TestTimeConversionFormats:
    def test_to_datetime_readonly(self, writable):
        # GH#34857
        arr = np.array([], dtype=object)
        arr.setflags(write=writable)
        result = pd.to_datetime(arr)
        expected = pd.to_datetime([])
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "format, expected",
        [
            [
                "%d/%m/%Y",
                [
                    pd.Timestamp("20000101"),
                    pd.Timestamp("20000201"),
                    pd.Timestamp("20000301"),
                ],
            ],
            [
                "%m/%d/%Y",
                [
                    pd.Timestamp("20000101"),
                    pd.Timestamp("20000102"),
                    pd.Timestamp("20000103"),
                ],
            ],
        ],
    )
    def test_to_datetime_format(self, cache, index_or_series, format, expected):
        values = index_or_series(["1/1/2000", "1/2/2000", "1/3/2000"])
        result = pd.to_datetime(values, format=format, cache=cache)
        expected = index_or_series(expected)
        tm.assert_equal(result, expected)

    @pytest.mark.parametrize(
        "arg, expected, format",
        [
            ["1/1/2000", "20000101", "%d/%m/%Y"],
            ["1/1/2000", "20000101", "%m/%d/%Y"],
            ["1/2/2000", "20000201", "%d/%m/%Y"],
            ["1/2/2000", "20000102", "%m/%d/%Y"],
            ["1/3/2000", "20000301", "%d/%m/%Y"],
            ["1/3/2000", "20000103", "%m/%d/%Y"],
        ],
    )
    def test_to_datetime_format_scalar(self, cache, arg, expected, format):
        result = pd.to_datetime(arg, format=format, cache=cache)
        expected = pd.Timestamp(expected)
        assert result == expected

    def test_to_datetime_format_YYYYMMDD(self, cache):
        ser = pd.Series([19801222, 19801222] + [19810105] * 5)
        expected = pd.Series([pd.Timestamp(x) for x in ser.apply(str)])

        # GH#55663 - int with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m%d", cache=cache)
        tm.assert_series_equal(result, expected)

        result = pd.to_datetime(ser.apply(str), format="%Y%m%d", cache=cache)
        tm.assert_series_equal(result, expected)

    def test_to_datetime_format_YYYYMMDD_with_nat(self, cache):
        # Explicit cast to float to explicit cast when setting np.nan
        ser = pd.Series([19801222, 19801222] + [19810105] * 5, dtype="float")
        # with NaT
        expected = pd.Series(
            [pd.Timestamp("19801222"), pd.Timestamp("19801222")]
            + [pd.Timestamp("19810105")] * 5,
            dtype="M8[us]",
        )
        expected[2] = np.nan
        ser[2] = np.nan

        # GH#55663 - float with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m%d", cache=cache)
        tm.assert_series_equal(result, expected)

        # string with NaT
        ser2 = ser.apply(str)
        ser2[2] = "nat"
        with pytest.raises(
            ValueError,
            match=(
                'unconverted data remains when parsing with format "%Y%m%d": ".0". '
            ),
        ):
            # https://github.com/pandas-dev/pandas/issues/50051
            pd.to_datetime(ser2, format="%Y%m%d", cache=cache)

    def test_to_datetime_format_YYYYMM_with_nat(self, cache):
        # https://github.com/pandas-dev/pandas/issues/50237
        # Explicit cast to float to explicit cast when setting np.nan
        ser = pd.Series([198012, 198012] + [198101] * 5, dtype="float")
        expected = pd.Series(
            [pd.Timestamp("19801201"), pd.Timestamp("19801201")]
            + [pd.Timestamp("19810101")] * 5,
            dtype="M8[us]",
        )
        expected[2] = np.nan
        ser[2] = np.nan
        # GH#55663 - float with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m", cache=cache)
        tm.assert_series_equal(result, expected)

    def test_to_datetime_format_YYYYMMDD_oob_for_ns(self, cache):
        # coercion
        # GH 7930, GH 14487
        ser = pd.Series([20121231, 20141231, 99991231])
        # GH#55663 - int with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m%d", errors="raise", cache=cache)
        expected = pd.Series(
            np.array(["2012-12-31", "2014-12-31", "9999-12-31"], dtype="M8[s]"),
            dtype="M8[us]",
        )
        tm.assert_series_equal(result, expected)

    def test_to_datetime_format_YYYYMMDD_coercion(self, cache):
        # coercion
        # GH 7930
        ser = pd.Series([20121231, 20141231, 999999999999999999999999999991231])
        # GH#55663 - int with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m%d", errors="coerce", cache=cache)
        expected = pd.Series(["20121231", "20141231", "NaT"], dtype="M8[us]")
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "input_s",
        [
            # Null values with Strings
            ["19801222", "20010112", None],
            ["19801222", "20010112", np.nan],
            ["19801222", "20010112", pd.NaT],
            ["19801222", "20010112", "NaT"],
            # Null values with Integers
            [19801222, 20010112, None],
            [19801222, 20010112, np.nan],
            [19801222, 20010112, pd.NaT],
            [19801222, 20010112, "NaT"],
        ],
    )
    def test_to_datetime_format_YYYYMMDD_with_none(self, input_s):
        # GH 30011
        # format='%Y%m%d'
        # with None
        expected = pd.Series(
            [pd.Timestamp("19801222"), pd.Timestamp("20010112"), pd.NaT]
        )
        # GH#55663 - int/float with format is deprecated (NaN doesn't count)
        has_numeric = any(
            isinstance(val, (int, float)) and val == val for val in input_s
        )
        warn = Pandas4Warning if has_numeric else None
        with tm.assert_produces_warning(warn, match="integer or float"):
            result = pd.Series(pd.to_datetime(input_s, format="%Y%m%d"))
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "input_s, expected",
        [
            # NaN before strings with invalid date values
            [
                ["19801222", np.nan, "20010012", "10019999"],
                [pd.Timestamp("19801222"), np.nan, np.nan, np.nan],
            ],
            # NaN after strings with invalid date values
            [
                ["19801222", "20010012", "10019999", np.nan],
                [pd.Timestamp("19801222"), np.nan, np.nan, np.nan],
            ],
            # NaN before integers with invalid date values
            [
                [20190813, np.nan, 20010012, 20019999],
                [pd.Timestamp("20190813"), np.nan, np.nan, np.nan],
            ],
            # NaN after integers with invalid date values
            [
                [20190813, 20010012, np.nan, 20019999],
                [pd.Timestamp("20190813"), np.nan, np.nan, np.nan],
            ],
        ],
    )
    def test_to_datetime_format_YYYYMMDD_overflow(self, input_s, expected):
        # GH 25512
        # format='%Y%m%d', errors='coerce'
        # GH#55663 - int/float with format is deprecated (NaN doesn't count)
        has_numeric = any(
            isinstance(val, (int, float)) and val == val for val in input_s
        )
        input_s = pd.Series(input_s)
        warn = Pandas4Warning if has_numeric else None
        with tm.assert_produces_warning(warn, match="integer or float"):
            result = pd.to_datetime(input_s, format="%Y%m%d", errors="coerce")
        expected = pd.Series(expected)
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "data, format, expected",
        [
            ([pd.NA], "%Y%m%d%H%M%S", ["NaT"]),
            ([pd.NA], None, ["NaT"]),
            (
                [pd.NA, "20210202202020"],
                "%Y%m%d%H%M%S",
                ["NaT", "2021-02-02 20:20:20"],
            ),
            (["201010", pd.NA], "%y%m%d", ["2020-10-10", "NaT"]),
            (["201010", pd.NA], "%d%m%y", ["2010-10-20", "NaT"]),
            ([None, np.nan, pd.NA], None, ["NaT", "NaT", "NaT"]),
            ([None, np.nan, pd.NA], "%Y%m%d", ["NaT", "NaT", "NaT"]),
        ],
    )
    def test_to_datetime_with_NA(self, data, format, expected):
        # GH#42957
        result = pd.to_datetime(data, format=format)
        expected = pd.DatetimeIndex(expected)
        tm.assert_index_equal(result, expected)

    def test_to_datetime_with_NA_with_warning(self):
        # GH#42957
        result = pd.to_datetime(["201010", pd.NA])
        expected = pd.DatetimeIndex(["2010-10-20", "NaT"])
        tm.assert_index_equal(result, expected)

    def test_to_datetime_format_integer(self, cache):
        # GH 10178
        # GH#55663 - int with format is deprecated
        ser = pd.Series([2000, 2001, 2002])
        expected = pd.Series([pd.Timestamp(x) for x in ser.apply(str)])

        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y", cache=cache)
        tm.assert_series_equal(result, expected)

        ser = pd.Series([200001, 200105, 200206])
        expected = pd.Series(
            [pd.Timestamp(x[:4] + "-" + x[4:]) for x in ser.apply(str)]
        )

        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            result = pd.to_datetime(ser, format="%Y%m", cache=cache)
        tm.assert_series_equal(result, expected)

    def test_to_datetime_format_microsecond(self, cache):
        month_abbr = calendar.month_abbr[4]
        val = f"01-{month_abbr}-2011 00:00:01.978"

        format = "%d-%b-%Y %H:%M:%S.%f"
        result = pd.to_datetime(val, format=format, cache=cache)
        exp = datetime.strptime(val, format)
        assert result == exp

    @pytest.mark.parametrize(
        "value, format, dt",
        [
            ["01/10/2010 15:20", "%m/%d/%Y %H:%M", pd.Timestamp("2010-01-10 15:20")],
            ["01/10/2010 05:43", "%m/%d/%Y %I:%M", pd.Timestamp("2010-01-10 05:43")],
            [
                "01/10/2010 13:56:01",
                "%m/%d/%Y %H:%M:%S",
                pd.Timestamp("2010-01-10 13:56:01"),
            ],
            pytest.param(
                "01/10/2010 08:14 PM",
                "%m/%d/%Y %I:%M %p",
                pd.Timestamp("2010-01-10 20:14"),
            ),
            pytest.param(
                "01/10/2010 07:40 AM",
                "%m/%d/%Y %I:%M %p",
                pd.Timestamp("2010-01-10 07:40"),
            ),
            pytest.param(
                "01/10/2010 09:12:56 AM",
                "%m/%d/%Y %I:%M:%S %p",
                pd.Timestamp("2010-01-10 09:12:56"),
            ),
        ],
    )
    def test_to_datetime_format_time(self, cache, value, format, dt):
        with tm.set_locale("C", locale.LC_TIME):
            assert pd.to_datetime(value, format=format, cache=cache) == dt

    def test_to_datetime_with_non_exact(self, cache):
        with tm.set_locale("C", locale.LC_TIME):
            # GH 10834
            # 8904
            # exact kw
            ser = pd.Series(
                ["19MAY11", "foobar19MAY11", "19MAY11:00:00:00", "19MAY11 00:00:00Z"]
            )
            result = pd.to_datetime(ser, format="%d%b%y", exact=False, cache=cache)
            expected = pd.to_datetime(
                ser.str.extract(r"(\d+\w+\d+)", expand=False),
                format="%d%b%y",
                cache=cache,
            )
            tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "format, expected",
        [
            ("%Y-%m-%d", pd.Timestamp(2000, 1, 3)),
            ("%Y-%d-%m", pd.Timestamp(2000, 3, 1)),
            ("%Y-%m-%d %H", pd.Timestamp(2000, 1, 3, 12)),
            ("%Y-%d-%m %H", pd.Timestamp(2000, 3, 1, 12)),
            ("%Y-%m-%d %H:%M", pd.Timestamp(2000, 1, 3, 12, 34)),
            ("%Y-%d-%m %H:%M", pd.Timestamp(2000, 3, 1, 12, 34)),
            ("%Y-%m-%d %H:%M:%S", pd.Timestamp(2000, 1, 3, 12, 34, 56)),
            ("%Y-%d-%m %H:%M:%S", pd.Timestamp(2000, 3, 1, 12, 34, 56)),
            ("%Y-%m-%d %H:%M:%S.%f", pd.Timestamp(2000, 1, 3, 12, 34, 56, 123456)),
            ("%Y-%d-%m %H:%M:%S.%f", pd.Timestamp(2000, 3, 1, 12, 34, 56, 123456)),
            (
                "%Y-%m-%d %H:%M:%S.%f%z",
                pd.Timestamp(2000, 1, 3, 12, 34, 56, 123456, tz="UTC+01:00"),
            ),
            (
                "%Y-%d-%m %H:%M:%S.%f%z",
                pd.Timestamp(2000, 3, 1, 12, 34, 56, 123456, tz="UTC+01:00"),
            ),
        ],
    )
    def test_non_exact_doesnt_parse_whole_string(self, cache, format, expected):
        # https://github.com/pandas-dev/pandas/issues/50412
        # the formats alternate between ISO8601 and non-ISO8601 to check both paths
        result = pd.to_datetime(
            "2000-01-03 12:34:56.123456+01:00", format=format, exact=False
        )
        assert result == expected

    @pytest.mark.parametrize(
        "arg",
        [
            "2012-01-01 09:00:00.000000001",
            "2012-01-01 09:00:00.000001",
            "2012-01-01 09:00:00.001",
            "2012-01-01 09:00:00.001000",
            "2012-01-01 09:00:00.001000000",
        ],
    )
    def test_parse_nanoseconds_with_formula(self, cache, arg):
        # GH8989
        # truncating the nanoseconds when a format was provided
        expected = pd.to_datetime(arg, cache=cache)
        result = pd.to_datetime(arg, format="%Y-%m-%d %H:%M:%S.%f", cache=cache)
        assert result == expected

    @pytest.mark.parametrize(
        "value,fmt,expected",
        [
            ["2009324", "%Y%W%w", "2009-08-13"],
            ["2013020", "%Y%U%w", "2013-01-13"],
        ],
    )
    def test_to_datetime_format_weeks(self, value, fmt, expected, cache):
        assert pd.to_datetime(value, format=fmt, cache=cache) == pd.Timestamp(expected)

    @pytest.mark.parametrize(
        "fmt,dates,expected_dates",
        [
            [
                "%Y-%m-%d %H:%M:%S %Z",
                ["2010-01-01 12:00:00 UTC"] * 2,
                [pd.Timestamp("2010-01-01 12:00:00", tz="UTC")] * 2,
            ],
            [
                "%Y-%m-%d %H:%M:%S%z",
                ["2010-01-01 12:00:00+0100"] * 2,
                [
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=60))
                    )
                ]
                * 2,
            ],
            [
                "%Y-%m-%d %H:%M:%S %z",
                ["2010-01-01 12:00:00 +0100"] * 2,
                [
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=60))
                    )
                ]
                * 2,
            ],
            [
                "%Y-%m-%d %H:%M:%S %z",
                ["2010-01-01 12:00:00 Z", "2010-01-01 12:00:00 Z"],
                [
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=0))
                    ),
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=0))
                    ),
                ],
            ],
        ],
    )
    def test_to_datetime_parse_tzname_or_tzoffset(self, fmt, dates, expected_dates):
        # GH 13486
        result = pd.to_datetime(dates, format=fmt)
        expected = pd.Index(expected_dates)
        tm.assert_equal(result, expected)

    @pytest.mark.parametrize(
        "fmt,dates,expected_dates",
        [
            [
                "%Y-%m-%d %H:%M:%S %Z",
                [
                    "2010-01-01 12:00:00 UTC",
                    "2010-01-01 12:00:00 GMT",
                    "2010-01-01 12:00:00 US/Pacific",
                ],
                [
                    pd.Timestamp("2010-01-01 12:00:00", tz="UTC"),
                    pd.Timestamp("2010-01-01 12:00:00", tz="GMT"),
                    pd.Timestamp("2010-01-01 12:00:00", tz="US/Pacific"),
                ],
            ],
            [
                "%Y-%m-%d %H:%M:%S %z",
                ["2010-01-01 12:00:00 +0100", "2010-01-01 12:00:00 -0100"],
                [
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=60))
                    ),
                    pd.Timestamp(
                        "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=-60))
                    ),
                ],
            ],
        ],
    )
    def test_to_datetime_parse_tzname_or_tzoffset_utc_false_removed(
        self, fmt, dates, expected_dates
    ):
        # GH#13486, GH#50887, GH#57275
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(dates, format=fmt)

    def test_to_datetime_parse_tzname_or_tzoffset_different_tz_to_utc(self):
        # GH 32792
        dates = [
            "2010-01-01 12:00:00 +0100",
            "2010-01-01 12:00:00 -0100",
            "2010-01-01 12:00:00 +0300",
            "2010-01-01 12:00:00 +0400",
        ]
        expected_dates = [
            "2010-01-01 11:00:00+00:00",
            "2010-01-01 13:00:00+00:00",
            "2010-01-01 09:00:00+00:00",
            "2010-01-01 08:00:00+00:00",
        ]
        fmt = "%Y-%m-%d %H:%M:%S %z"

        result = pd.to_datetime(dates, format=fmt, utc=True)
        expected = pd.DatetimeIndex(expected_dates)
        tm.assert_index_equal(result, expected)

    @pytest.mark.xfail(
        not PY315, reason="%:z directive not supported prior to 3.15", raises=ValueError
    )
    def test_to_datetime_colon_z_offset(self):
        dates = [
            "2010-01-01 12:00:00+04:00",
            "2010-01-01 12:00:00+04:30",
            "2010-01-01 12:00:00-05:00",
        ]
        expected_dates = [
            "2010-01-01 08:00:00+00:00",
            "2010-01-01 07:30:00+00:00",
            "2010-01-01 17:00:00+00:00",
        ]
        fmt = "%Y-%m-%d %H:%M:%S%:z"

        result = pd.to_datetime(dates, format=fmt, utc=True)
        expected = pd.DatetimeIndex(expected_dates)
        tm.assert_index_equal(result, expected)

    def test_to_datetime_missing_colon_z_offset(self):
        # test adapted from python/cpython#136961
        dates = ["+04:0030"]
        fmt = "%:z"

        if PY315:
            msg = r"Missing colon in %:z before '30', got '\+04:0030'"
        else:
            msg = "':' is a bad directive in format '%:z'"

        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(dates, format=fmt, utc=True)

    @pytest.mark.parametrize(
        "offset", ["+0", "-1foo", "UTCbar", ":10", "+01:000:01", ""]
    )
    def test_to_datetime_parse_timezone_malformed(self, offset):
        fmt = "%Y-%m-%d %H:%M:%S %z"
        date = "2010-01-01 12:00:00 " + offset

        msg = "|".join(
            [
                r'^time data ".*" doesn\'t match format ".*". ' f"{PARSING_ERR_MSG}$",
                r'^unconverted data remains when parsing with format ".*": ".*". '
                f"{PARSING_ERR_MSG}$",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([date], format=fmt)

    def test_to_datetime_parse_timezone_keeps_name(self):
        # GH 21697
        fmt = "%Y-%m-%d %H:%M:%S %z"
        arg = pd.Index(["2010-01-01 12:00:00 Z"], name="foo")
        result = pd.to_datetime(arg, format=fmt)
        expected = pd.DatetimeIndex(["2010-01-01 12:00:00"], tz="UTC", name="foo")
        tm.assert_index_equal(result, expected)


class TestToDatetime:
    def test_to_datetime_invalid_errors(self):
        # GH#66542
        msg = "errors must be one of"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(["2024-01-01"], errors="never")

    def test_to_datetime_mixed_string_resos(self):
        # GH#62801
        vals = [
            "2016-01-01 01:02:03",
            "2016-01-01 01:02:03.001",
            "2016-01-01 01:02:03.001002",
            "2016-01-01 01:02:03.001002003",
        ]
        expected = pd.DatetimeIndex([pd.Timestamp(x).as_unit("ns") for x in vals])

        result1 = pd.DatetimeIndex(vals)
        tm.assert_index_equal(result1, expected)

        result2 = pd.to_datetime(vals, format="ISO8601")
        tm.assert_index_equal(result2, expected)

        result3 = pd.to_datetime(vals, format="mixed")
        tm.assert_index_equal(result3, expected)

    def test_to_datetime_none(self):
        # GH#23055
        assert pd.to_datetime(None) is pd.NaT

    def test_to_datetime_overflow(self):
        # we should get an OutOfBoundsDatetime, NOT OverflowError
        # TODO: Timestamp raises ValueError("could not convert string to Timestamp")
        #  can we make these more consistent?
        arg = "08335394550"
        msg = 'Parsing "08335394550" to datetime overflows'
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arg)

        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime([arg])

        res = pd.to_datetime(arg, errors="coerce")
        assert res is pd.NaT
        res = pd.to_datetime([arg], errors="coerce")
        exp = pd.Index([pd.NaT], dtype="M8[s]")
        tm.assert_index_equal(res, exp)

    def test_to_datetime_mixed_datetime_and_string(self):
        # GH#47018 adapted old doctest with new behavior
        d1 = datetime(2020, 1, 1, 17, tzinfo=timezone(-timedelta(hours=1)))
        d2 = datetime(2020, 1, 1, 18, tzinfo=timezone(-timedelta(hours=1)))
        res = pd.to_datetime(["2020-01-01 17:00 -0100", d2])
        expected = pd.to_datetime([d1, d2]).tz_convert(timezone(timedelta(minutes=-60)))
        tm.assert_index_equal(res, expected)

    def test_to_datetime_mixed_string_and_numeric(self):
        # GH#55780 np.array(vals) would incorrectly cast the number to str
        vals = ["2016-01-01", 0]
        expected = pd.DatetimeIndex([pd.Timestamp(x) for x in vals])
        result = pd.to_datetime(vals, format="mixed")
        result2 = pd.to_datetime(vals[::-1], format="mixed")[::-1]
        result3 = pd.DatetimeIndex(vals)
        result4 = pd.DatetimeIndex(vals[::-1])[::-1]

        tm.assert_index_equal(result, expected)
        tm.assert_index_equal(result2, expected)
        tm.assert_index_equal(result3, expected)
        tm.assert_index_equal(result4, expected)

    @pytest.mark.parametrize(
        "format", ["%Y-%m-%d", "%Y-%d-%m"], ids=["ISO8601", "non-ISO8601"]
    )
    def test_to_datetime_mixed_date_and_string(self, format):
        # https://github.com/pandas-dev/pandas/issues/50108
        d1 = date(2020, 1, 2)
        res = pd.to_datetime(["2020-01-01", d1], format=format)
        expected = pd.DatetimeIndex(["2020-01-01", "2020-01-02"], dtype="M8[us]")
        tm.assert_index_equal(res, expected)

    @pytest.mark.parametrize(
        "fmt",
        ["%Y-%d-%m %H:%M:%S%z", "%Y-%m-%d %H:%M:%S%z"],
        ids=["non-ISO8601 format", "ISO8601 format"],
    )
    @pytest.mark.parametrize(
        "utc, args, expected",
        [
            pytest.param(
                True,
                ["2000-01-01 01:00:00-08:00", "2000-01-01 02:00:00-08:00"],
                pd.DatetimeIndex(
                    ["2000-01-01 09:00:00+00:00", "2000-01-01 10:00:00+00:00"],
                    dtype="datetime64[us, UTC]",
                ),
                id="all tz-aware, with utc",
            ),
            pytest.param(
                False,
                ["2000-01-01 01:00:00+00:00", "2000-01-01 02:00:00+00:00"],
                pd.DatetimeIndex(
                    ["2000-01-01 01:00:00+00:00", "2000-01-01 02:00:00+00:00"],
                ).as_unit("us"),
                id="all tz-aware, without utc",
            ),
            pytest.param(
                True,
                ["2000-01-01 01:00:00-08:00", "2000-01-01 02:00:00+00:00"],
                pd.DatetimeIndex(
                    ["2000-01-01 09:00:00+00:00", "2000-01-01 02:00:00+00:00"],
                    dtype="datetime64[us, UTC]",
                ),
                id="all tz-aware, mixed offsets, with utc",
            ),
            pytest.param(
                True,
                ["2000-01-01 01:00:00", "2000-01-01 02:00:00+00:00"],
                pd.DatetimeIndex(
                    ["2000-01-01 01:00:00+00:00", "2000-01-01 02:00:00+00:00"],
                    dtype="datetime64[us, UTC]",
                ),
                id="tz-aware string, naive pydatetime, with utc",
            ),
        ],
    )
    @pytest.mark.parametrize(
        "constructor",
        [pd.Timestamp, lambda x: pd.Timestamp(x).to_pydatetime()],
    )
    def test_to_datetime_mixed_datetime_and_string_with_format(
        self, fmt, utc, args, expected, constructor
    ):
        # https://github.com/pandas-dev/pandas/issues/49298
        # https://github.com/pandas-dev/pandas/issues/50254
        # note: ISO8601 formats go down a fastpath, so we need to check both
        # an ISO8601 format and a non-ISO8601 one
        ts1 = constructor(args[0])
        ts2 = args[1]
        result = pd.to_datetime([ts1, ts2], format=fmt, utc=utc)
        if constructor is pd.Timestamp:
            expected = expected.as_unit("us")
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "fmt",
        ["%Y-%d-%m %H:%M:%S%z", "%Y-%m-%d %H:%M:%S%z"],
        ids=["non-ISO8601 format", "ISO8601 format"],
    )
    @pytest.mark.parametrize(
        "constructor",
        [pd.Timestamp, lambda x: pd.Timestamp(x).to_pydatetime()],
    )
    def test_to_datetime_mixed_dt_and_str_with_format_mixed_offsets_utc_false_removed(
        self, fmt, constructor
    ):
        # https://github.com/pandas-dev/pandas/issues/49298
        # https://github.com/pandas-dev/pandas/issues/50254
        # GH#57275
        # note: ISO8601 formats go down a fastpath, so we need to check both
        # an ISO8601 format and a non-ISO8601 one
        args = ["2000-01-01 01:00:00", "2000-01-01 02:00:00+00:00"]
        ts1 = constructor(args[0])
        ts2 = args[1]
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"

        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([ts1, ts2], format=fmt, utc=False)

    @pytest.mark.parametrize(
        "fmt, expected",
        [
            pytest.param(
                "%Y-%m-%d %H:%M:%S%z",
                [
                    pd.Timestamp("2000-01-01 09:00:00+0100", tz="UTC+01:00"),
                    pd.Timestamp("2000-01-02 02:00:00+0200", tz="UTC+02:00"),
                    pd.NaT,
                ],
                id="ISO8601, non-UTC",
            ),
            pytest.param(
                "%Y-%d-%m %H:%M:%S%z",
                [
                    pd.Timestamp("2000-01-01 09:00:00+0100", tz="UTC+01:00"),
                    pd.Timestamp("2000-02-01 02:00:00+0200", tz="UTC+02:00"),
                    pd.NaT,
                ],
                id="non-ISO8601, non-UTC",
            ),
        ],
    )
    def test_to_datetime_mixed_offsets_with_none_tz_utc_false_removed(
        self, fmt, expected
    ):
        # https://github.com/pandas-dev/pandas/issues/50071
        # GH#57275
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"

        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(
                ["2000-01-01 09:00:00+01:00", "2000-01-02 02:00:00+02:00", None],
                format=fmt,
                utc=False,
            )

    @pytest.mark.parametrize(
        "fmt, expected",
        [
            pytest.param(
                "%Y-%m-%d %H:%M:%S%z",
                pd.DatetimeIndex(
                    ["2000-01-01 08:00:00+00:00", "2000-01-02 00:00:00+00:00", "NaT"],
                    dtype="datetime64[us, UTC]",
                ),
                id="ISO8601, UTC",
            ),
            pytest.param(
                "%Y-%d-%m %H:%M:%S%z",
                pd.DatetimeIndex(
                    ["2000-01-01 08:00:00+00:00", "2000-02-01 00:00:00+00:00", "NaT"],
                    dtype="datetime64[us, UTC]",
                ),
                id="non-ISO8601, UTC",
            ),
        ],
    )
    def test_to_datetime_mixed_offsets_with_none(self, fmt, expected):
        # https://github.com/pandas-dev/pandas/issues/50071
        result = pd.to_datetime(
            ["2000-01-01 09:00:00+01:00", "2000-01-02 02:00:00+02:00", None],
            format=fmt,
            utc=True,
        )
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "fmt",
        ["%Y-%d-%m %H:%M:%S%z", "%Y-%m-%d %H:%M:%S%z"],
        ids=["non-ISO8601 format", "ISO8601 format"],
    )
    @pytest.mark.parametrize(
        "args",
        [
            pytest.param(
                ["2000-01-01 01:00:00-08:00", "2000-01-01 02:00:00-07:00"],
                id="all tz-aware, mixed timezones, without utc",
            ),
        ],
    )
    @pytest.mark.parametrize(
        "constructor",
        [pd.Timestamp, lambda x: pd.Timestamp(x).to_pydatetime()],
    )
    def test_to_datetime_mixed_datetime_and_string_with_format_raises(
        self, fmt, args, constructor
    ):
        # https://github.com/pandas-dev/pandas/issues/49298
        # note: ISO8601 formats go down a fastpath, so we need to check both
        # an ISO8601 format and a non-ISO8601 one
        ts1 = constructor(args[0])
        ts2 = constructor(args[1])
        with pytest.raises(
            ValueError, match="cannot be converted to datetime64 unless utc=True"
        ):
            pd.to_datetime([ts1, ts2], format=fmt, utc=False)

    def test_to_datetime_np_str(self):
        # GH#32264
        # GH#48969
        value = np.str_("2019-02-04 10:18:46.297000+0000")

        ser = pd.Series([value])

        exp = pd.Timestamp("2019-02-04 10:18:46.297000", tz="UTC")

        assert pd.to_datetime(value) == exp
        assert pd.to_datetime(ser.iloc[0]) == exp

        res = pd.to_datetime([value])
        expected = pd.Index([exp])
        tm.assert_index_equal(res, expected)

        res = pd.to_datetime(ser)
        expected = pd.Series(expected)
        tm.assert_series_equal(res, expected)

    @pytest.mark.parametrize(
        "s, _format, dt",
        [
            ["2015-1-1", "%G-%V-%u", datetime(2014, 12, 29, 0, 0)],
            ["2015-1-4", "%G-%V-%u", datetime(2015, 1, 1, 0, 0)],
            ["2015-1-7", "%G-%V-%u", datetime(2015, 1, 4, 0, 0)],
            ["2024-52-1", "%G-%V-%u", datetime(2024, 12, 23, 0, 0)],
            ["2024-52-7", "%G-%V-%u", datetime(2024, 12, 29, 0, 0)],
            ["2025-1-1", "%G-%V-%u", datetime(2024, 12, 30, 0, 0)],
            ["2020-53-1", "%G-%V-%u", datetime(2020, 12, 28, 0, 0)],
        ],
    )
    def test_to_datetime_iso_week_year_format(self, s, _format, dt):
        # See GH#16607
        assert pd.to_datetime(s, format=_format) == dt

    @pytest.mark.parametrize(
        "msg, s, _format",
        [
            [
                "Week 53 does not exist in ISO year 2024",
                "2024 53 1",
                "%G %V %u",
            ],
            [
                "Week 53 does not exist in ISO year 2023",
                "2023 53 1",
                "%G %V %u",
            ],
        ],
    )
    def test_invalid_iso_week_53(self, msg, s, _format):
        # See GH#60885
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(s, format=_format)

    @pytest.mark.parametrize(
        "msg, s, _format",
        [
            [
                "ISO week directive '%V' is incompatible with the year directive "
                "'%Y'. Use the ISO year '%G' instead.",
                "1999 50",
                "%Y %V",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 51",
                "%G %V",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 Monday",
                "%G %A",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 Mon",
                "%G %a",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 6",
                "%G %w",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 6",
                "%G %u",
            ],
            [
                "ISO year directive '%G' must be used with the ISO week directive "
                "'%V' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "2051",
                "%G",
            ],
            [
                "Day of the year directive '%j' is not compatible with ISO year "
                "directive '%G'. Use '%Y' instead.",
                "1999 51 6 256",
                "%G %V %u %j",
            ],
            [
                "ISO week directive '%V' is incompatible with the year directive "
                "'%Y'. Use the ISO year '%G' instead.",
                "1999 51 Sunday",
                "%Y %V %A",
            ],
            [
                "ISO week directive '%V' is incompatible with the year directive "
                "'%Y'. Use the ISO year '%G' instead.",
                "1999 51 Sun",
                "%Y %V %a",
            ],
            [
                "ISO week directive '%V' is incompatible with the year directive "
                "'%Y'. Use the ISO year '%G' instead.",
                "1999 51 1",
                "%Y %V %w",
            ],
            [
                "ISO week directive '%V' is incompatible with the year directive "
                "'%Y'. Use the ISO year '%G' instead.",
                "1999 51 1",
                "%Y %V %u",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "20",
                "%V",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 51 Sunday",
                "%V %A",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 51 Sun",
                "%V %a",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 51 1",
                "%V %w",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "1999 51 1",
                "%V %u",
            ],
            [
                "Day of the year directive '%j' is not compatible with ISO year "
                "directive '%G'. Use '%Y' instead.",
                "1999 50",
                "%G %j",
            ],
            [
                "ISO week directive '%V' must be used with the ISO year directive "
                "'%G' and a weekday directive '%A', '%a', '%w', or '%u'.",
                "20 Monday",
                "%V %A",
            ],
        ],
    )
    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_error_iso_week_year(self, msg, s, _format, errors):
        # See GH#16607, GH#50308
        # These formats are rejected before any parsing happens, so the message
        #  does not depend on the locale (GH#44625).
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(s, format=_format, errors=errors)

    @pytest.mark.parametrize("tz", [None, "US/Central"])
    def test_to_datetime_dtarr(self, tz):
        # DatetimeArray
        dti = pd.date_range("1965-04-03", periods=19, freq="2W", tz=tz)
        arr = dti._data

        result = pd.to_datetime(arr)
        assert result is arr

    # Doesn't work on Windows since tzpath not set correctly
    @td.skip_if_windows
    @pytest.mark.parametrize("utc", [True, False])
    @pytest.mark.parametrize("tz", [None, "US/Central"])
    def test_to_datetime_arrow(self, tz, utc, index_or_series):
        pa = pytest.importorskip("pyarrow")

        dti = pd.date_range("1965-04-03", periods=19, freq="2W", tz=tz)
        dti = index_or_series(dti)

        dti_arrow = dti.astype(pd.ArrowDtype(pa.timestamp(unit="ns", tz=tz)))

        result = pd.to_datetime(dti_arrow, utc=utc)
        expected = pd.to_datetime(dti, utc=utc).astype(
            pd.ArrowDtype(pa.timestamp(unit="ns", tz=tz if not utc else "UTC"))
        )
        if not utc and index_or_series is not pd.Series:
            # Doesn't hold for utc=True, since that will astype
            # to_datetime also returns a new object for series
            assert result is dti_arrow
        if index_or_series is pd.Series:
            tm.assert_series_equal(result, expected)
        else:
            tm.assert_index_equal(result, expected)

    def test_to_datetime_pydatetime(self):
        actual = pd.to_datetime(datetime(2008, 1, 15))
        assert actual == datetime(2008, 1, 15)

    def test_to_datetime_YYYYMMDD(self):
        actual = pd.to_datetime("20080115")
        assert actual == datetime(2008, 1, 15)

    @td.skip_if_windows  # `tm.set_timezone` does not work in windows
    @pytest.mark.skipif(WASM, reason="tzset is not available on WASM")
    def test_to_datetime_now(self):
        # See GH#18666
        with tm.set_timezone("US/Eastern"):
            # GH#18705
            now = pd.Timestamp("now")  # current-time-ok
            pdnow = pd.to_datetime("now")  # current-time-ok
            pdnow2 = pd.to_datetime(["now"])[0]  # current-time-ok

            # These should all be equal with infinite perf; this gives
            # a generous margin of 10 seconds
            assert abs(pdnow._value - now._value) < 1e10
            assert abs(pdnow2._value - now._value) < 1e10

            assert pdnow.tzinfo is None
            assert pdnow2.tzinfo is None

    @td.skip_if_windows  # `tm.set_timezone` does not work on Windows
    @pytest.mark.skipif(WASM, reason="tzset is not available on WASM")
    @pytest.mark.parametrize("tz", ["Pacific/Auckland", "US/Samoa"])
    def test_to_datetime_today(self, tz):
        # See GH#18666
        # Test with one timezone far ahead of UTC and another far behind, so
        # one of these will _almost_ always be in a different day from UTC.
        # Unfortunately this test between 12 and 1 AM Samoa time
        # this both of these timezones _and_ UTC will all be in the same day,
        # so this test will not detect the regression introduced in #18666.
        with tm.set_timezone(tz):
            today64 = np.datetime64("today")  # current-time-ok
            nptoday = today64.astype("datetime64[us]").astype(np.int64)
            pdtoday = pd.to_datetime("today")  # current-time-ok
            pdtoday2 = pd.to_datetime(["today"])[0]  # current-time-ok

            tstoday = pd.Timestamp("today")  # current-time-ok
            tstoday2 = pd.Timestamp.today()  # noqa: TID251

            # These should all be equal with infinite perf; this gives
            # a generous margin of 10 seconds
            assert abs(pdtoday.normalize()._value - nptoday) < 1e10
            assert abs(pdtoday2.normalize()._value - nptoday) < 1e10
            assert abs(pdtoday._value - tstoday._value) < 1e10
            assert abs(pdtoday._value - tstoday2._value) < 1e10

            assert pdtoday.tzinfo is None
            assert pdtoday2.tzinfo is None

    @pytest.mark.parametrize("arg", ["now", "today"])
    def test_to_datetime_today_now_unicode_bytes(self, arg):
        pd.to_datetime([arg])

    @pytest.mark.filterwarnings(
        "ignore:Timestamp.utcnow is deprecated:DeprecationWarning"
    )
    @pytest.mark.skipif(WASM, reason="tzset is not available on WASM")
    @pytest.mark.parametrize(
        "format, expected_ds",
        [
            ("%Y-%m-%d %H:%M:%S%z", "2020-01-03"),
            ("%Y-%d-%m %H:%M:%S%z", "2020-03-01"),
            (None, "2020-01-03"),
        ],
    )
    @pytest.mark.parametrize(
        "string, attribute",
        [
            ("now", "utcnow"),
            ("today", "today"),
        ],
    )
    def test_to_datetime_now_with_format(self, format, expected_ds, string, attribute):
        # https://github.com/pandas-dev/pandas/issues/50359
        result = pd.to_datetime(
            ["2020-01-03 00:00:00Z", string], format=format, utc=True
        )
        expected = pd.DatetimeIndex(
            [expected_ds, getattr(pd.Timestamp, attribute)()],
            dtype="datetime64[s, UTC]",
        )
        assert (expected - result).max().total_seconds() < 1

    @pytest.mark.parametrize(
        "dt", [np.datetime64("2000-01-01"), np.datetime64("2000-01-02")]
    )
    def test_to_datetime_dt64s(self, cache, dt):
        assert pd.to_datetime(dt, cache=cache) == pd.Timestamp(dt)

    @pytest.mark.parametrize(
        "arg, format",
        [
            ("2001-01-01", "%Y-%m-%d"),
            ("01-01-2001", "%d-%m-%Y"),
        ],
    )
    def test_to_datetime_dt64s_and_str(self, arg, format):
        # https://github.com/pandas-dev/pandas/issues/50036
        result = pd.to_datetime([arg, np.datetime64("2020-01-01")], format=format)
        expected = pd.DatetimeIndex(["2001-01-01", "2020-01-01"])
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "dt", [np.datetime64("1000-01-01"), np.datetime64("5000-01-02")]
    )
    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_to_datetime_dt64s_out_of_ns_bounds(self, cache, dt, errors):
        # GH#50369 We cast to the nearest supported reso, i.e. "s"
        ts = pd.to_datetime(dt, errors=errors, cache=cache)
        assert isinstance(ts, pd.Timestamp)
        assert ts.unit == "s"
        assert ts.asm8 == dt

        ts = pd.Timestamp(dt)
        assert ts.unit == "s"
        assert ts.asm8 == dt

    def test_to_datetime_dt64d_out_of_bounds(self, cache):
        dt64 = np.datetime64(np.iinfo(np.int64).max, "D")

        msg = "Out of bounds second timestamp: 25252734927768524-07-27"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.Timestamp(dt64)
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(dt64, errors="raise", cache=cache)

        assert pd.to_datetime(dt64, errors="coerce", cache=cache) is pd.NaT

    @pytest.mark.parametrize("unit", ["s", "D"])
    def test_to_datetime_array_of_dt64s(self, cache, unit):
        # https://github.com/pandas-dev/pandas/issues/31491
        # Need at least 50 to ensure cache is used.
        dts = [
            np.datetime64("2000-01-01", unit),
            np.datetime64("2000-01-02", unit),
        ] * 30
        # Assuming all datetimes are in bounds, to_datetime() returns
        # an array that is equal to Timestamp() parsing
        result = pd.to_datetime(dts, cache=cache)
        expected = pd.DatetimeIndex([pd.Timestamp(x).asm8 for x in dts], dtype="M8[s]")

        tm.assert_index_equal(result, expected)

        # A list of datetimes where the last one is out of bounds
        dts_with_oob = [*dts, np.datetime64("9999-01-01")]

        # As of GH#51978 we do not raise in this case
        pd.to_datetime(dts_with_oob, errors="raise")

        result = pd.to_datetime(dts_with_oob, errors="coerce", cache=cache)
        expected = pd.DatetimeIndex(np.array(dts_with_oob, dtype="M8[s]"))
        tm.assert_index_equal(result, expected)

    def test_to_datetime_tz(self, cache):
        # xref 8260
        # uniform returns a DatetimeIndex
        arr = [
            pd.Timestamp("2013-01-01 13:00:00-0800", tz="US/Pacific"),
            pd.Timestamp("2013-01-02 14:00:00-0800", tz="US/Pacific"),
        ]
        result = pd.to_datetime(arr, cache=cache)
        expected = pd.DatetimeIndex(
            ["2013-01-01 13:00:00", "2013-01-02 14:00:00"], tz="US/Pacific"
        )
        tm.assert_index_equal(result, expected)

    def test_to_datetime_tz_mixed(self, cache):
        # mixed tzs will raise if errors='raise'
        # https://github.com/pandas-dev/pandas/issues/50585
        arr = [
            pd.Timestamp("2013-01-01 13:00:00", tz="US/Pacific"),
            pd.Timestamp("2013-01-02 14:00:00", tz="US/Eastern"),
        ]
        msg = (
            "Tz-aware datetime.datetime cannot be "
            "converted to datetime64 unless utc=True"
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(arr, cache=cache)

        result = pd.to_datetime(arr, cache=cache, errors="coerce")
        expected = pd.DatetimeIndex(
            ["2013-01-01 13:00:00-08:00", "NaT"], dtype="datetime64[us, US/Pacific]"
        )
        tm.assert_index_equal(result, expected)

    def test_to_datetime_different_offsets_removed(self, cache):
        # inspired by asv timeseries.ToDatetimeNONISO8601 benchmark
        # see GH-26097 for more
        # GH#57275
        ts_string_1 = "March 1, 2018 12:00:00+0400"
        ts_string_2 = "March 1, 2018 12:00:00+0500"
        arr = [ts_string_1] * 5 + [ts_string_2] * 5
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(arr, cache=cache)

    def test_to_datetime_tz_pytz(self, cache):
        # see gh-8260
        pytz = pytest.importorskip("pytz")
        us_eastern = pytz.timezone("US/Eastern")
        arr = np.array(
            [
                us_eastern.localize(
                    datetime(year=2000, month=1, day=1, hour=3, minute=0)
                ),
                us_eastern.localize(
                    datetime(year=2000, month=6, day=1, hour=3, minute=0)
                ),
            ],
            dtype=object,
        )
        result = pd.to_datetime(arr, utc=True, cache=cache)
        expected = pd.DatetimeIndex(
            ["2000-01-01 08:00:00+00:00", "2000-06-01 07:00:00+00:00"],
            dtype="datetime64[us, UTC]",
            freq=None,
        )
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "init_constructor, end_constructor",
        [
            (pd.Index, pd.DatetimeIndex),
            (list, pd.DatetimeIndex),
            (np.array, pd.DatetimeIndex),
            (pd.Series, pd.Series),
        ],
    )
    def test_to_datetime_utc_true(self, cache, init_constructor, end_constructor):
        # See gh-11934 & gh-6415
        data = ["20100102 121314", "20100102 121315"]
        expected_data = [
            pd.Timestamp("2010-01-02 12:13:14", tz="utc"),
            pd.Timestamp("2010-01-02 12:13:15", tz="utc"),
        ]

        result = pd.to_datetime(
            init_constructor(data), format="%Y%m%d %H%M%S", utc=True, cache=cache
        )
        expected = end_constructor(expected_data)
        tm.assert_equal(result, expected)

    @pytest.mark.parametrize(
        "scalar, expected",
        [
            ["20100102 121314", pd.Timestamp("2010-01-02 12:13:14", tz="utc")],
            ["20100102 121315", pd.Timestamp("2010-01-02 12:13:15", tz="utc")],
        ],
    )
    def test_to_datetime_utc_true_scalar(self, cache, scalar, expected):
        # Test scalar case as well
        result = pd.to_datetime(scalar, format="%Y%m%d %H%M%S", utc=True, cache=cache)
        assert result == expected

    def test_to_datetime_utc_true_with_series_single_value(self, cache):
        # GH 15760 UTC=True with Series
        ts = 1.5e18
        result = pd.to_datetime(pd.Series([ts]), utc=True, cache=cache)
        expected = pd.Series([pd.Timestamp(ts, tz="utc")])
        tm.assert_series_equal(result, expected)

    def test_to_datetime_utc_true_with_series_tzaware_string(self, cache):
        ts = "2013-01-01 00:00:00-01:00"
        expected_ts = "2013-01-01 01:00:00"
        data = pd.Series([ts] * 3)
        result = pd.to_datetime(data, utc=True, cache=cache)
        expected = pd.Series([pd.Timestamp(expected_ts, tz="utc")] * 3)
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "date, dtype",
        [
            ("2013-01-01 01:00:00", "datetime64[ns]"),
            ("2013-01-01 01:00:00", "datetime64[ns, UTC]"),
        ],
    )
    def test_to_datetime_utc_true_with_series_datetime_ns(self, cache, date, dtype):
        expected = pd.Series(
            [pd.Timestamp("2013-01-01 01:00:00", tz="UTC")], dtype="M8[ns, UTC]"
        )
        result = pd.to_datetime(pd.Series([date], dtype=dtype), utc=True, cache=cache)
        tm.assert_series_equal(result, expected)

    def test_to_datetime_tz_psycopg2(self, request, cache):
        # xref 8260
        psycopg2_tz = pytest.importorskip("psycopg2.tz")

        # misc cases
        tz1 = psycopg2_tz.FixedOffsetTimezone(offset=-300, name=None)
        tz2 = psycopg2_tz.FixedOffsetTimezone(offset=-240, name=None)
        arr = np.array(
            [
                datetime(2000, 1, 1, 3, 0, tzinfo=tz1),
                datetime(2000, 6, 1, 3, 0, tzinfo=tz2),
            ],
            dtype=object,
        )

        result = pd.to_datetime(arr, errors="coerce", utc=True, cache=cache)
        expected = pd.DatetimeIndex(
            ["2000-01-01 08:00:00+00:00", "2000-06-01 07:00:00+00:00"],
            dtype="datetime64[us, UTC]",
            freq=None,
        )
        tm.assert_index_equal(result, expected)

        # dtype coercion
        i = pd.DatetimeIndex(
            ["2000-01-01 08:00:00"],
            tz=psycopg2_tz.FixedOffsetTimezone(offset=-300, name=None),
        ).as_unit("us")
        assert not is_datetime64_ns_dtype(i)

        # tz coercion
        result = pd.to_datetime(i, errors="coerce", cache=cache)
        tm.assert_index_equal(result, i)

        result = pd.to_datetime(i, errors="coerce", utc=True, cache=cache)
        expected = pd.DatetimeIndex(
            ["2000-01-01 13:00:00"], dtype="datetime64[us, UTC]"
        )
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize("arg", [True, False])
    def test_datetime_bool(self, cache, arg):
        # GH13176
        msg = r"dtype bool cannot be converted to datetime64\[ns\]"
        with pytest.raises(TypeError, match=msg):
            pd.to_datetime(arg)
        assert pd.to_datetime(arg, errors="coerce", cache=cache) is pd.NaT

    def test_datetime_bool_arrays_mixed(self, cache):
        msg = f"{type(cache)} is not convertible to datetime"
        with pytest.raises(TypeError, match=msg):
            pd.to_datetime([False, datetime(2011, 1, 1)], cache=cache)
        with pytest.raises(
            ValueError,
            match=(
                r'^time data "True" doesn\'t match format "%Y%m%d". '
                f"{PARSING_ERR_MSG}$"
            ),
        ):
            pd.to_datetime(["20130101", True], cache=cache)
        tm.assert_index_equal(
            pd.to_datetime([0, False, pd.NaT, 0.0], errors="coerce", cache=cache),
            pd.DatetimeIndex(
                [
                    pd.to_datetime(0, cache=cache),
                    pd.NaT,
                    pd.NaT,
                    pd.to_datetime(0, cache=cache),
                ]
            ),
        )

    @pytest.mark.parametrize("arg", [bool, pd.to_datetime])
    def test_datetime_invalid_datatype(self, arg):
        # GH13176
        msg = "is not convertible to datetime"
        with pytest.raises(TypeError, match=msg):
            pd.to_datetime(arg)

    @pytest.mark.parametrize("errors", ["coerce", "raise"])
    def test_invalid_format_raises(self, errors):
        # https://github.com/pandas-dev/pandas/issues/50255
        if PY315:
            msg = r"':M' is a bad directive in format 'H%:M%:S%"
        else:
            msg = "':' is a bad directive in format 'H%:M%:S%"

        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(["00:00:00"], format="H%:M%:S%", errors=errors)

    @pytest.mark.parametrize("value", ["a", "00:01:99"])
    @pytest.mark.parametrize("format", [None, "%H:%M:%S"])
    def test_datetime_invalid_scalar(self, value, format):
        # GH24763
        res = pd.to_datetime(value, errors="coerce", format=format)
        assert res is pd.NaT

        msg = "|".join(
            [
                r'^time data "a" doesn\'t match format "%H:%M:%S". '
                f"{PARSING_ERR_MSG}$",
                r'^Given date string "a" not likely a datetime$',
                r'^unconverted data remains when parsing with format "%H:%M:%S": "9". '
                f"{PARSING_ERR_MSG}$",
                rf"^second must be in 0..59{NOT_99}: 00:01:99$",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(value, errors="raise", format=format)

    @pytest.mark.parametrize("value", ["3000/12/11 00:00:00"])
    @pytest.mark.parametrize("format", [None, "%H:%M:%S"])
    def test_datetime_outofbounds_scalar(self, value, format):
        # GH24763
        res = pd.to_datetime(value, errors="coerce", format=format)
        if format is None:
            assert isinstance(res, pd.Timestamp)
            assert res == pd.Timestamp(value)
        else:
            assert res is pd.NaT

        if format is not None:
            msg = r'^time data ".*" doesn\'t match format ".*"'
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(value, errors="raise", format=format)
        else:
            res = pd.to_datetime(value, errors="raise", format=format)
            assert isinstance(res, pd.Timestamp)
            assert res == pd.Timestamp(value)

    @pytest.mark.parametrize(
        ("values"), [(["a"]), (["00:01:99"]), (["a", "b", "99:00:00"])]
    )
    @pytest.mark.parametrize("format", [(None), ("%H:%M:%S")])
    def test_datetime_invalid_index(self, values, format):
        # GH24763
        # Not great to have logic in tests, but this one's hard to
        # parametrise over
        if format is None and len(values) > 1:
            warn = UserWarning
        else:
            warn = None

        with tm.assert_produces_warning(
            warn, match="Could not infer format", raise_on_extra_warnings=False
        ):
            res = pd.to_datetime(values, errors="coerce", format=format)
        tm.assert_index_equal(res, pd.DatetimeIndex([pd.NaT] * len(values)))

        msg = "|".join(
            [
                r'^Given date string "a" not likely a datetime$',
                r'^time data "a" doesn\'t match format "%H:%M:%S". '
                f"{PARSING_ERR_MSG}$",
                r'^unconverted data remains when parsing with format "%H:%M:%S": "9". '
                f"{PARSING_ERR_MSG}$",
                rf"^second must be in 0..59{NOT_99}: 00:01:99$",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            with tm.assert_produces_warning(
                warn, match="Could not infer format", raise_on_extra_warnings=False
            ):
                pd.to_datetime(values, errors="raise", format=format)

    @pytest.mark.parametrize("utc", [True, None])
    @pytest.mark.parametrize("format", ["%Y%m%d %H:%M:%S", None])
    @pytest.mark.parametrize("constructor", [list, tuple, np.array, pd.Index, deque])
    def test_to_datetime_cache(self, utc, format, constructor):
        date = "20130101 00:00:00"
        test_dates = [date] * 10**5
        data = constructor(test_dates)

        result = pd.to_datetime(data, utc=utc, format=format, cache=True)
        expected = pd.to_datetime(data, utc=utc, format=format, cache=False)

        tm.assert_index_equal(result, expected)

    def test_to_datetime_from_deque(self):
        # GH 29403
        result = pd.to_datetime(deque([pd.Timestamp("2010-06-02 09:30:00")] * 51))
        expected = pd.to_datetime([pd.Timestamp("2010-06-02 09:30:00")] * 51)
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize("utc", [True, None])
    @pytest.mark.parametrize("format", ["%Y%m%d %H:%M:%S", None])
    def test_to_datetime_cache_series(self, utc, format):
        date = "20130101 00:00:00"
        test_dates = [date] * 10**5
        data = pd.Series(test_dates)
        result = pd.to_datetime(data, utc=utc, format=format, cache=True)
        expected = pd.to_datetime(data, utc=utc, format=format, cache=False)
        tm.assert_series_equal(result, expected)

    def test_to_datetime_cache_scalar(self):
        date = "20130101 00:00:00"
        result = pd.to_datetime(date, cache=True)
        expected = pd.Timestamp("20130101 00:00:00")
        assert result == expected

    @pytest.mark.parametrize(
        "datetimelikes,expected_values,exp_unit",
        (
            (
                (None, np.nan) + (pd.NaT,) * start_caching_at,
                (pd.NaT,) * (start_caching_at + 2),
                "s",
            ),
            (
                (None, pd.Timestamp("2012-07-26").as_unit("s"))
                + (pd.NaT,) * start_caching_at,
                (pd.NaT, pd.Timestamp("2012-07-26").as_unit("s"))
                + (pd.NaT,) * start_caching_at,
                "s",
            ),
            (
                (None,)
                + (pd.NaT,) * start_caching_at
                + ("2012 July 26", pd.Timestamp("2012-07-26")),
                (pd.NaT,) * (start_caching_at + 1)
                + (pd.Timestamp("2012-07-26"), pd.Timestamp("2012-07-26")),
                "us",
            ),
        ),
    )
    def test_convert_object_to_datetime_with_cache(
        self, datetimelikes, expected_values, exp_unit
    ):
        # GH#39882
        ser = pd.Series(
            datetimelikes,
            dtype="object",
        )
        result_series = pd.to_datetime(ser, errors="coerce")
        expected_series = pd.Series(
            expected_values,
            dtype=f"datetime64[{exp_unit}]",
        )
        tm.assert_series_equal(result_series, expected_series)

    @pytest.mark.parametrize(
        "input",
        [
            pd.Series([pd.NaT] * 20 + [None] * 20, dtype="object"),
            pd.Series([pd.NaT] * 60 + [None] * 60, dtype="object"),
            pd.Series([None] * 20),
            pd.Series([None] * 60),
            pd.Series([""] * 20),
            pd.Series([""] * 60),
            pd.Series([pd.NA] * 20),
            pd.Series([pd.NA] * 60),
            pd.Series([np.nan] * 20),
            pd.Series([np.nan] * 60),
        ],
    )
    def test_to_datetime_converts_null_like_to_nat(self, cache, input):
        # GH35888
        expected = pd.Series([pd.NaT] * len(input), dtype="M8[s]")
        result = pd.to_datetime(input, cache=cache)
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "date, format",
        [
            ("2017-20", "%Y-%W"),
            ("20 Sunday", "%W %A"),
            ("20 Sun", "%W %a"),
            ("2017-21", "%Y-%U"),
            ("20 Sunday", "%U %A"),
            ("20 Sun", "%U %a"),
        ],
    )
    def test_week_without_day_and_calendar_year(self, date, format):
        # GH16774

        msg = "Cannot use '%W' or '%U' without day and year"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(date, format=format)

    def test_to_datetime_coerce(self):
        # GH#26122, GH#57275
        ts_strings = [
            "March 1, 2018 12:00:00+0400",
            "March 1, 2018 12:00:00+0500",
            "20100240",
        ]
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(ts_strings, errors="coerce")

    @pytest.mark.parametrize(
        "string_arg, format",
        [("March 1, 2018", "%B %d, %Y"), ("2018-03-01", "%Y-%m-%d")],
    )
    @pytest.mark.parametrize(
        "outofbounds",
        [
            datetime(9999, 1, 1),
            date(9999, 1, 1),
            np.datetime64("9999-01-01"),
            "January 1, 9999",
            "9999-01-01",
        ],
    )
    def test_to_datetime_coerce_oob(self, string_arg, format, outofbounds):
        # https://github.com/pandas-dev/pandas/issues/50255
        ts_strings = [string_arg, outofbounds]
        result = pd.to_datetime(ts_strings, errors="coerce", format=format)
        if isinstance(outofbounds, str) and (
            format.startswith("%B") ^ outofbounds.startswith("J")
        ):
            # the strings don't match the given format, so they raise and we coerce
            expected = pd.DatetimeIndex([datetime(2018, 3, 1), pd.NaT], dtype="M8[us]")
        elif isinstance(outofbounds, datetime):
            expected = pd.DatetimeIndex(
                [datetime(2018, 3, 1), outofbounds], dtype="M8[us]"
            )
        else:
            expected = pd.DatetimeIndex(
                [datetime(2018, 3, 1), outofbounds], dtype="M8[us]"
            )
        tm.assert_index_equal(result, expected)

    def test_to_datetime_malformed_no_raise(self):
        # GH 28299
        # GH 48633
        ts_strings = ["200622-12-31", "111111-24-11"]
        with tm.assert_produces_warning(
            UserWarning, match="Could not infer format", raise_on_extra_warnings=False
        ):
            result = pd.to_datetime(ts_strings, errors="coerce")
        # TODO: should Index get "s" by default here?
        exp = pd.Index([pd.NaT, pd.NaT], dtype="M8[s]")
        tm.assert_index_equal(result, exp)

    def test_to_datetime_malformed_raise(self):
        # GH 48633
        ts_strings = ["200622-12-31", "111111-24-11"]
        msg = (
            'Parsed string "200622-12-31" gives an invalid tzoffset, which must '
            r"be between -timedelta\(hours=24\) and timedelta\(hours=24\)"
        )
        with pytest.raises(
            ValueError,
            match=msg,
        ):
            with tm.assert_produces_warning(
                UserWarning, match="Could not infer format"
            ):
                pd.to_datetime(
                    ts_strings,
                    errors="raise",
                )

    def test_iso_8601_strings_with_same_offset(self):
        # GH 17697, 11736
        ts_str = "2015-11-18 15:30:00+05:30"
        result = pd.to_datetime(ts_str)
        expected = pd.Timestamp(ts_str)
        assert result == expected

        expected = pd.DatetimeIndex([pd.Timestamp(ts_str)] * 2)
        result = pd.to_datetime([ts_str] * 2)
        tm.assert_index_equal(result, expected)

        result = pd.DatetimeIndex([ts_str] * 2)
        tm.assert_index_equal(result, expected)

    def test_iso_8601_strings_with_different_offsets_removed(self):
        # GH#17697, GH#11736, GH#50887, GH#57275
        ts_strings = ["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30", pd.NaT]
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(ts_strings)

    def test_iso_8601_strings_with_different_offsets_utc(self):
        ts_strings = ["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30", pd.NaT]
        result = pd.to_datetime(ts_strings, utc=True)
        expected = pd.DatetimeIndex(
            [pd.Timestamp(2015, 11, 18, 10), pd.Timestamp(2015, 11, 18, 10), pd.NaT],
            tz="UTC",
        )
        tm.assert_index_equal(result, expected)

    def test_mixed_offsets_with_native_datetime_utc_false_raises(self):
        # GH#25978, GH#57275

        vals = [
            "nan",
            pd.Timestamp("1990-01-01"),
            "2015-03-14T16:15:14.123-08:00",
            "2019-03-04T21:56:32.620-07:00",
            None,
            "today",
            "now",
        ]
        ser = pd.Series(vals)
        assert all(ser[i] is vals[i] for i in range(len(vals)))  # GH#40111

        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(ser)

    def test_non_iso_strings_with_tz_offset(self):
        result = pd.to_datetime(["March 1, 2018 12:00:00+0400"] * 2)
        expected = pd.DatetimeIndex(
            [datetime(2018, 3, 1, 12, tzinfo=timezone(timedelta(minutes=240)))] * 2
        )
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "ts, expected",
        [
            (pd.Timestamp("2018-01-01"), pd.Timestamp("2018-01-01", tz="UTC")),
            (
                pd.Timestamp("2018-01-01", tz="US/Pacific"),
                pd.Timestamp("2018-01-01 08:00", tz="UTC"),
            ),
        ],
    )
    def test_timestamp_utc_true(self, ts, expected):
        # GH 24415
        result = pd.to_datetime(ts, utc=True)
        assert result == expected

    @pytest.mark.parametrize("dt_str", ["00010101", "13000101", "30000101", "99990101"])
    def test_to_datetime_with_format_out_of_bounds(self, dt_str):
        # GH 9107
        res = pd.to_datetime(dt_str, format="%Y%m%d")
        dtobj = datetime.strptime(dt_str, "%Y%m%d")
        expected = pd.Timestamp(dtobj)
        assert res == expected
        assert res.unit == expected.unit

    def test_to_datetime_utc(self):
        arr = np.array([parse("2012-06-13T01:39:00Z")], dtype=object)

        result = pd.to_datetime(arr, utc=True)
        assert result.tz is UTC

    def test_to_datetime_fixed_offset(self):
        from pandas.tests.indexes.datetimes.test_timezones import FixedOffset

        fixed_off = FixedOffset(-420, "-07:00")

        dates = [
            datetime(2000, 1, 1, tzinfo=fixed_off),
            datetime(2000, 1, 2, tzinfo=fixed_off),
            datetime(2000, 1, 3, tzinfo=fixed_off),
        ]
        result = pd.to_datetime(dates)
        assert result.tz == fixed_off

    @pytest.mark.parametrize(
        "date",
        [
            ["2020-10-26 00:00:00+06:00", "2020-10-26 00:00:00+01:00"],
            ["2020-10-26 00:00:00+06:00", pd.Timestamp("2018-01-01", tz="US/Pacific")],
            [
                "2020-10-26 00:00:00+06:00",
                datetime(2020, 1, 1, 18).astimezone(
                    zoneinfo.ZoneInfo("Australia/Melbourne")
                ),
            ],
        ],
    )
    def test_to_datetime_mixed_offsets_with_utc_false_removed(self, date):
        # GH#50887, GH#57275
        msg = "Mixed timezones detected. Pass utc=True in to_datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(date, utc=False)


class TestToDatetimeUnit:
    @pytest.mark.parametrize("unit", ["Y", "M"])
    @pytest.mark.parametrize(
        "item", [150, float(150), np.int64(150), np.int32(150), np.int8(15)]
    )
    def test_to_datetime_month_or_year_unit_int(self, cache, unit, item, request):
        # GH#50870 Note we have separate tests that pd.Timestamp gets these right
        # GH#56996 np.datetime64() rejects *any* NumPy integer scalar, so the
        #  object path used to raise ValueError for these until cast_from_unit
        #  started widening to a Python int first
        ts = pd.Timestamp(item, unit=unit)
        expected = pd.DatetimeIndex([ts], dtype="M8[us]")

        result = pd.to_datetime([item], unit=unit, cache=cache)
        tm.assert_index_equal(result, expected)

        result = pd.to_datetime(np.array([item], dtype=object), unit=unit, cache=cache)
        tm.assert_index_equal(result, expected)

        result = pd.to_datetime(np.array([item]), unit=unit, cache=cache)
        tm.assert_index_equal(result, expected)

        # with a nan!
        result = pd.to_datetime(np.array([item, np.nan]), unit=unit, cache=cache)
        assert result.isna()[1]
        tm.assert_index_equal(result[:1], expected)

    @pytest.mark.parametrize("unit", ["Y", "M"])
    def test_to_datetime_month_or_year_unit_non_round_float(self, cache, unit):
        # GH#50301
        # Match Timestamp behavior in disallowing non-round floats with
        #  Y or M unit
        msg = f"Conversion of non-round float with unit={unit} is ambiguous"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([1.5], unit=unit, errors="raise")
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(np.array([1.5]), unit=unit, errors="raise")

        msg = r"Given date string \"1.5\" not likely a datetime"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(["1.5"], unit=unit, errors="raise")

        res = pd.to_datetime([1.5], unit=unit, errors="coerce")
        expected = pd.Index([pd.NaT], dtype="M8[ns]")
        tm.assert_index_equal(res, expected)

        # In 3.0, the string "1.5" is parsed as it would be without unit,
        #  which fails. With errors="coerce" this becomes NaT.
        res = pd.to_datetime(["1.5"], unit=unit, errors="coerce")
        expected = pd.to_datetime([pd.NaT])
        tm.assert_index_equal(res, expected)

        # round floats are OK; treated like integers
        res = pd.to_datetime([1.0], unit=unit)
        expected = pd.to_datetime([1], unit=unit)
        tm.assert_index_equal(res, expected)

    def test_unit(self, cache):
        # GH 11758
        # test proper behavior with errors
        msg = "cannot specify both format and unit"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([1], unit="D", format="%Y%m%d", cache=cache)

    def test_unit_array_mixed_nans(self, cache):
        values = [11111111111111111, 1, 1.0, iNaT, pd.NaT, np.nan, "NaT", ""]

        result = pd.to_datetime(values, unit="D", errors="coerce", cache=cache)
        expected = pd.DatetimeIndex(
            ["NaT", "1970-01-02", "1970-01-02", "NaT", "NaT", "NaT", "NaT", "NaT"],
            dtype="M8[us]",
        )
        tm.assert_index_equal(result, expected)

        msg = "cannot convert input 11111111111111111 with the unit 'D'"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(values, unit="D", errors="raise", cache=cache)

    def test_unit_array_mixed_nans_large_int(self, cache):
        values = [1420043460000000000000000, iNaT, pd.NaT, np.nan, "NaT"]

        result = pd.to_datetime(values, errors="coerce", unit="s", cache=cache)
        expected = pd.DatetimeIndex(["NaT", "NaT", "NaT", "NaT", "NaT"], dtype="M8[us]")
        tm.assert_index_equal(result, expected)

        msg = "cannot convert input 1420043460000000000000000 with the unit 's'"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(values, errors="raise", unit="s", cache=cache)

    def test_to_datetime_invalid_str_not_out_of_bounds_valuerror(self, cache):
        # if we have a string, then we raise a ValueError
        # and NOT an OutOfBoundsDatetime
        msg = "Unknown datetime string format, unable to parse: foo"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime("foo", errors="raise", unit="s", cache=cache)

    @pytest.mark.parametrize("error", ["raise", "coerce"])
    def test_unit_consistency(self, cache, error):
        # consistency of conversions
        expected = pd.Timestamp("1970-05-09 14:25:11")
        result = pd.to_datetime(11111111, unit="s", errors=error, cache=cache)
        assert result == expected
        assert isinstance(result, pd.Timestamp)

    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    @pytest.mark.parametrize("dtype", ["float64", "int64"])
    def test_unit_with_numeric(self, cache, errors, dtype):
        # GH 13180
        # coercions from floats/ints are ok
        expected = pd.DatetimeIndex(
            ["2015-06-19 05:33:20", "2015-05-27 22:33:20"], dtype="M8[ns]"
        )
        arr = np.array([1.434692e18, 1.432766e18]).astype(dtype)
        result = pd.to_datetime(arr, errors=errors, cache=cache)
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "dtype",
        [np.int8, np.int16, np.int32, np.uint8, np.uint16, np.uint32, np.int64],
    )
    def test_unit_subint64_scalars(self, dtype, cache):
        # GH#56996 cast_from_unit did base/frac arithmetic in the input's own
        #  narrow dtype, so `frac * m` overflowed and raised OutOfBoundsDatetime
        #  on the object path below. The scalar constructor casts to int64 up
        #  front so it never hit this, but is checked here to keep it that way.
        expected = pd.Timestamp("1970-01-02")
        assert pd.Timestamp(dtype(1), unit="D") == expected

        result = pd.to_datetime([dtype(1)], unit="D", cache=cache)
        tm.assert_index_equal(result, pd.DatetimeIndex([expected], dtype="M8[us]"))

        # mixing in a nanosecond value forces "ns", so the unit factor is
        #  86400 * 10**9 and the 32-bit dtypes overflow it too
        other = pd.Timestamp("1970-01-01 00:00:00.000000001")
        result = pd.to_datetime([dtype(1), other], unit="D", cache=cache)
        tm.assert_index_equal(
            result, pd.DatetimeIndex([expected, other], dtype="M8[ns]")
        )

    @pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64])
    def test_unit_narrow_float_scalars(self, dtype, cache):
        # GH#56996 np.float32(1.5) with unit="D" picked up bogus sub-second
        #  digits from float32-precision arithmetic; np.float16 overflowed
        expected = pd.Timestamp("1970-01-02 12:00:00")
        assert pd.Timestamp(dtype(1.5), unit="D") == expected

        result = pd.to_datetime([dtype(1.5)], unit="D", cache=cache)
        tm.assert_index_equal(result, pd.DatetimeIndex([expected], dtype="M8[ns]"))

    @pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64])
    def test_unit_float_neg_inf_raises(self, dtype, cache):
        # GH#56996 NPY_NAT (-2**63) saturates to -inf when cast down to float16,
        #  so the NaT-sentinel check matched and np.float16("-inf") silently
        #  became NaT instead of raising like the wider float dtypes
        with pytest.raises(OutOfBoundsDatetime, match="cannot convert input"):
            pd.Timestamp(dtype("-inf"), unit="D")

        with pytest.raises(OutOfBoundsDatetime, match="cannot convert input"):
            pd.to_datetime([dtype("-inf")], unit="D", cache=cache)

    @pytest.mark.parametrize(
        "exp, arr, warning",
        [
            [
                ["NaT", "2015-06-19 05:33:20", "2015-05-27 22:33:20"],
                ["foo", 1.434692e18, 1.432766e18],
                UserWarning,
            ],
            [
                ["2015-06-19 05:33:20", "2015-05-27 22:33:20", "NaT", "NaT"],
                [1.434692e18, 1.432766e18, "foo", "NaT"],
                None,
            ],
        ],
    )
    def test_unit_with_numeric_coerce(self, cache, exp, arr, warning):
        # but we want to make sure that we are coercing
        # if we have ints/strings
        expected = pd.DatetimeIndex(exp, dtype="M8[ns]")
        with tm.assert_produces_warning(warning, match="Could not infer format"):
            result = pd.to_datetime(arr, errors="coerce", cache=cache)
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "arr",
        [
            [pd.Timestamp("20130101"), 1.434692e18, 1.432766e18],
            [1.434692e18, 1.432766e18, pd.Timestamp("20130101")],
        ],
    )
    def test_unit_mixed(self, cache, arr):
        # GH#50453 pre-2.0 with mixed numeric/datetimes and errors="coerce"
        #  the numeric entries would be coerced to NaT, was never clear exactly
        #  why.
        # mixed integers/datetimes
        expected = pd.Index([pd.Timestamp(x) for x in arr], dtype="M8[ns]")
        result = pd.to_datetime(arr, errors="coerce", cache=cache)
        tm.assert_index_equal(result, expected)

        # GH#49037 pre-2.0 this raised, but it always worked with Series,
        #  was never clear why it was disallowed
        result = pd.to_datetime(arr, errors="raise", cache=cache)
        tm.assert_index_equal(result, expected)

        result = pd.DatetimeIndex(arr)
        tm.assert_index_equal(result, expected)

    def test_unit_rounding(self, cache):
        # GH 14156 & GH 20445: argument will incur floating point errors
        # but no premature rounding
        value = 1434743731.8770001
        result = pd.to_datetime(value, unit="s", cache=cache)
        expected = pd.Timestamp("2015-06-19 19:55:31.877000093")
        assert result == expected

        alt = pd.Timestamp(value, unit="s")
        assert alt == result

    @pytest.mark.parametrize("dtype", [int, float, "Int64", "Float64"])
    def test_to_datetime_unit(self, dtype):
        epoch = 1370745748
        ser = pd.Series([epoch + t for t in range(20)], dtype=dtype)
        result = pd.to_datetime(ser, unit="s")
        expected = pd.Series(
            [
                pd.Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t)
                for t in range(20)
            ],
            dtype="M8[us]",
        )
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "null,dtype",
        [(iNaT, "int64"), (iNaT, "Int64"), (np.nan, "float64"), (np.nan, "Float64")],
    )
    def test_to_datetime_unit_with_nulls(self, null, dtype):
        epoch = 1370745748
        ser = pd.Series([epoch + t for t in range(20)] + [null], dtype=dtype)
        result = pd.to_datetime(ser, unit="s")
        expected = pd.Series(
            [
                pd.Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t)
                for t in range(20)
            ]
            + [pd.NaT],
            dtype="M8[us]",
        )
        tm.assert_series_equal(result, expected)

    def test_to_datetime_unit_fractional_seconds(self):
        # GH13834
        epoch = 1370745748
        ser = pd.Series([epoch + t for t in np.arange(0, 2, 0.25)] + [iNaT]).astype(
            float
        )
        result = pd.to_datetime(ser, unit="s")
        expected = pd.Series(
            [
                pd.Timestamp("2013-06-09 02:42:28") + timedelta(seconds=t)
                for t in np.arange(0, 2, 0.25)
            ]
            + [pd.NaT],
            dtype="M8[ns]",
        )
        # GH20455 argument will incur floating point errors but no premature rounding
        result = result.dt.round("ms")
        tm.assert_series_equal(result, expected)

    def test_to_datetime_unit_na_values(self):
        result = pd.to_datetime([1, 2, "NaT", pd.NaT, np.nan], unit="D")
        expected = pd.DatetimeIndex(
            [pd.Timestamp("1970-01-02"), pd.Timestamp("1970-01-03")] + ["NaT"] * 3,
            dtype="M8[us]",
        )
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize("bad_val", ["foo", 111111111111111])
    def test_to_datetime_unit_invalid(self, bad_val):
        if bad_val == "foo":
            msg = f"Unknown datetime string format, unable to parse: {bad_val}"
        else:
            msg = "cannot convert input 111111111111111 with the unit 'D'"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([1, 2, bad_val], unit="D")

    @pytest.mark.parametrize("bad_val", ["foo", 111111111111111])
    def test_to_timestamp_unit_coerce(self, bad_val):
        # coerce we can process
        expected = pd.DatetimeIndex(
            [pd.Timestamp("1970-01-02"), pd.Timestamp("1970-01-03")] + ["NaT"] * 1,
            dtype="M8[us]",
        )
        result = pd.to_datetime([1, 2, bad_val], unit="D", errors="coerce")
        tm.assert_index_equal(result, expected)

    def test_float_to_datetime_raise_near_bounds(self):
        # GH50183
        msg = "cannot convert input with unit 'D'"
        oneday_in_ns = 1e9 * 60 * 60 * 24
        tsmax_in_days = 2**63 / oneday_in_ns  # 2**63 ns, in days
        # just in bounds
        should_succeed = pd.Series(
            [0, tsmax_in_days - 0.005, -tsmax_in_days + 0.005], dtype=float
        )
        expected = (should_succeed * oneday_in_ns).astype(np.int64)
        for error_mode in ["raise", "coerce"]:
            result1 = pd.to_datetime(should_succeed, unit="D", errors=error_mode)
            # Cast to `np.float64` so that `rtol` and inexact checking kick in
            # (`check_exact` doesn't take place for integer dtypes)
            tm.assert_almost_equal(
                result1.astype(np.int64).astype(np.float64),
                expected.astype(np.float64),
                rtol=1e-10,
            )

        # just out of bounds
        should_fail1 = pd.Series([0, tsmax_in_days + 0.005], dtype=float)
        should_fail2 = pd.Series([0, -tsmax_in_days - 0.005], dtype=float)
        msg2 = "cannot convert input 106751.99616730065 with the unit 'D'"
        msg3 = "cannot convert input -106751.99616730065 with the unit 'D'"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(should_fail1, unit="D", errors="raise")
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(should_fail1[1], unit="D", errors="raise")
        with pytest.raises(OutOfBoundsDatetime, match=msg2):
            pd.Timestamp(should_fail1[1], unit="D")

        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(should_fail2, unit="D", errors="raise")
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(should_fail2[1], unit="D", errors="raise")
        with pytest.raises(OutOfBoundsDatetime, match=msg3):
            pd.Timestamp(should_fail2[1], unit="D")

    def test_float_to_datetime_near_int64_boundary(self):
        # GH#57366 within a few ULP of +/-2**63 ns the float bound check must be
        # exact. A negative float whose ns value lands just past int64 min must
        # raise instead of silently wrapping to a positive timestamp...
        wrapped = np.array([-(2**63 + 200) / 1e9], dtype="float64")
        with pytest.raises(OutOfBoundsDatetime, match="cannot convert input"):
            pd.to_datetime(wrapped, unit="s", errors="raise")
        assert pd.to_datetime(wrapped, unit="s", errors="coerce")[0] is pd.NaT

        # ...and an in-bounds float just shy of int64 max must not spuriously
        # raise (it did for units D/W, diverging from the scalar path).
        oneday_in_ns = 1e9 * 60 * 60 * 24
        val = (2**63 - 1 - 100) / oneday_in_ns
        arr = np.array([val], dtype="float64")
        assert pd.to_datetime(arr, unit="D")[0] == pd.Timestamp(val, unit="D")

    def test_float_to_datetime_raise_oob_ns(self):
        value = np.float64(2**63)
        arr = np.array([value], dtype=np.float64)

        msg = "cannot convert input with unit 'ns'"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arr, unit="ns", errors="raise")
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(value, unit="ns", errors="raise")

        msg = "Out of bounds timestamp"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.Timestamp(value, unit="ns")

    @pytest.mark.parametrize("unit", ["Y", "M"])
    def test_float_to_datetime_oob_year_month(self, unit):
        # GH#68640 units Y/M skipped the int64-domain check, so a float below
        # int64 min aliased to the NaT sentinel and came back as NaT.
        arr = np.array([-1e19], dtype="float64")
        with pytest.raises(OutOfBoundsDatetime, match="cannot convert input"):
            pd.to_datetime(arr, unit=unit)
        assert pd.to_datetime(arr, unit=unit, errors="coerce")[0] is pd.NaT

    @pytest.mark.parametrize("unit", ["Y", "M"])
    @pytest.mark.parametrize("value", [np.inf, -np.inf])
    def test_inf_to_datetime_oob_year_month(self, unit, value):
        # GH#68640 inf is unrepresentable, not "ambiguous": units Y/M raised
        # ValueError where every other unit raised OutOfBoundsDatetime.
        msg = "cannot convert input"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(np.array([value]), unit=unit)
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime([value], unit=unit)
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.Timestamp(value, unit=unit)

    def test_year_month_non_round_beats_oob(self):
        # GH#68640 the int64-domain check must not preempt the ambiguity error,
        # which an out-of-bounds neighbour would otherwise mask.
        msg = "Conversion of non-round float with unit=Y is ambiguous"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(np.array([1.5, 1e19]), unit="Y")

    @pytest.mark.parametrize("unit", ["foo", "", "YM"])
    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_unrecognized_unit_beats_oob(self, unit, errors):
        # GH#68640 an out-of-bounds value must not mask a misspelled unit
        with pytest.raises(ValueError, match="Unrecognized unit"):
            pd.to_datetime(np.array([1e19, 1.5]), unit=unit, errors=errors)

    @pytest.mark.parametrize("unit", ["Y", "M", "D", "s", "ns"])
    def test_round_float_to_datetime_oob_object_path(self, unit):
        # GH#68640 a round float went through a cast at the call site, outside
        # the guard that turns the overflow into OutOfBoundsDatetime.
        with pytest.raises(OutOfBoundsDatetime, match="cannot convert input"):
            pd.to_datetime([1e30], unit=unit)
        assert pd.to_datetime([1e30], unit=unit, errors="coerce")[0] is pd.NaT

    def test_uint64_to_datetime_raise_oob(self):
        # GH#60677 uint64 values > int64 max overflow silently
        uint64_max = np.iinfo(np.uint64).max

        msg = "cannot convert input with unit 'ns'"

        # arrays via to_datetime
        arr = np.array([uint64_max], dtype=np.uint64)
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arr, unit="ns", errors="raise")
        for con in [pd.array, pd.Series, pd.Index]:
            arr = con([uint64_max, None], dtype="UInt64")
            with pytest.raises(OutOfBoundsDatetime, match=msg):
                pd.to_datetime(arr, unit="ns", errors="raise")

        # scalar via to_datetime
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(uint64_max, unit="ns", errors="raise")
        # scalar via Timestamp constructor
        msg = "Out of bounds timestamp"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.Timestamp(uint64_max, unit="ns")

    def test_uint64_to_datetime_coerce(self):
        # GH#60677
        uint64_max = np.iinfo(np.uint64).max

        arr = np.array([uint64_max], dtype=np.uint64)
        result = pd.to_datetime(arr, unit="ns", errors="coerce")
        expected = pd.DatetimeIndex(["NaT"], dtype="datetime64[ns]")
        tm.assert_index_equal(result, expected)

        for con in [pd.array, pd.Series, pd.Index]:
            arr = con([uint64_max, None], dtype="UInt64")
            result = pd.to_datetime(arr, unit="ns", errors="coerce")
            expected = pd.DatetimeIndex(["NaT", "NaT"], dtype="datetime64[ns]")
            if con is pd.Series:
                expected = pd.Series(expected)
            tm.assert_equal(result, expected)

        # scalar
        result = pd.to_datetime(uint64_max, unit="ns", errors="coerce")
        assert result is pd.NaT

    def test_uint64_to_datetime_valid(self):
        # GH#60677 valid uint64 values should still work
        arr = np.array([1_000_000, 2_000_000], dtype=np.uint64)
        result = pd.to_datetime(arr, unit="ns")
        expected = pd.to_datetime(arr.astype(np.int64), unit="ns")
        tm.assert_index_equal(result, expected)

        arr = pd.array([1_000_000, 2_000_000, None], dtype="UInt64")
        result = pd.to_datetime(arr, unit="ns")
        expected = expected.append(pd.to_datetime([pd.NaT]))
        tm.assert_index_equal(result, expected)

    def test_nullable_int_to_datetime_no_float_precision_loss(
        self, index_or_series_or_array
    ):
        # GH#66988 a nullable int array with a value that is not exactly
        #  representable as float64 should not lose precision by going
        #  through a float64 intermediate when it contains NA values
        value = 2**60 + 1
        arr = index_or_series_or_array([value, None], dtype="Int64")
        result = pd.to_datetime(arr, unit="ns")
        expected = pd.DatetimeIndex([pd.Timestamp(value, unit="ns"), pd.NaT])
        if isinstance(arr, pd.Series):
            expected = pd.Series(expected)
        tm.assert_equal(result, expected)

        uarr = index_or_series_or_array([value, None], dtype="UInt64")
        result = pd.to_datetime(uarr, unit="ns")
        tm.assert_equal(result, expected)

    def test_int64_oob_raise(self):
        # GH#66988 an int64 can be out of bounds when the unit is not supported
        # and we have to cast
        value = 2**60 - 1

        # arrays via to_datetime
        arr = np.array([value], dtype=np.int64)
        msg = "Out of bounds microsecond timestamp"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arr, unit="D", errors="raise")
        arr = pd.array([value, None], dtype="Int64")
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arr, unit="D", errors="raise")
        # scalar via to_datetime
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(value, unit="D", errors="raise")
        # scalar via Timestamp constructor
        msg = "cannot convert input"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.Timestamp(value, unit="D")

    def test_int64_oob_coerce(self):
        # GH#66988 an int64 can be out of bounds when the unit is not supported
        # and we have to cast
        value = 2**60 - 1

        arr = np.array([value], dtype=np.int64)
        result = pd.to_datetime(arr, unit="D", errors="coerce")
        expected = pd.DatetimeIndex(["NaT"], dtype="datetime64[us]")
        tm.assert_index_equal(result, expected)

        arr = pd.array([value, None], dtype="Int64")
        expected = pd.DatetimeIndex(["NaT", "NaT"], dtype="datetime64[us]")
        result = pd.to_datetime(arr, unit="D", errors="coerce")
        tm.assert_index_equal(result, expected)

        # scalar
        result = pd.to_datetime(value, unit="D", errors="coerce")
        assert result is pd.NaT

    @pytest.mark.parametrize("typ,frac", [(int, 0), (float, 0), (float, 0.1)])
    @pytest.mark.parametrize("unit", ["ps", "fs", "as"])
    def test_sub_nano_unit(self, typ, frac, unit):
        value = np.datetime64(1, "ns").astype(f"M8[{unit}]").item()
        value = typ(value)
        if frac != 0:
            # this fractional part gets discarded, but it still works
            value = value + frac

        result = pd.to_datetime([value], unit=unit)
        expected = pd.DatetimeIndex([pd.Timestamp(1, unit="ns")], dtype="M8[ns]")
        tm.assert_index_equal(result, expected)

        if frac != 0:
            # TODO typed array code path fails this case - internal error
            with pytest.raises(ValueError, match=None):  # noqa: PT011
                pd.to_datetime(np.array([value]), unit=unit)

            with pytest.raises(ValueError, match=None):  # noqa: PT011
                pd.to_datetime(pd.array([value, None]), unit=unit)

            with pytest.raises(ValueError, match=None):  # noqa: PT011
                pd.to_datetime(value, unit=unit)
        else:
            result = pd.to_datetime(np.array([value]), unit=unit)
            tm.assert_index_equal(result, expected)

            result = pd.to_datetime(pd.array([value, None]), unit=unit)
            expected = pd.DatetimeIndex(
                [pd.Timestamp(1, unit="ns"), pd.NaT], dtype="M8[ns]"
            )
            tm.assert_index_equal(result, expected)

            # scalar via to_datetime
            result = pd.to_datetime(value, unit=unit)
            assert result == pd.Timestamp(1, unit="ns")

        # scalar via Timestamp constructor
        result = pd.Timestamp(value, unit=unit)
        assert result == pd.Timestamp(1, unit="ns")


class TestToDatetimeDataFrame:
    @pytest.fixture
    def df(self):
        return pd.DataFrame(
            {
                "year": [2015, 2016],
                "month": [2, 3],
                "day": [4, 5],
                "hour": [6, 7],
                "minute": [58, 59],
                "second": [10, 11],
                "ms": [1, 1],
                "us": [2, 2],
                "ns": [3, 3],
            }
        )

    def test_dataframe(self, df, cache):
        result = pd.to_datetime(
            {"year": df["year"], "month": df["month"], "day": df["day"]}, cache=cache
        )
        expected = pd.Series(
            [pd.Timestamp("20150204 00:00:00"), pd.Timestamp("20160305 00:0:00")]
        )
        tm.assert_series_equal(result, expected)

        # dict-like
        result = pd.to_datetime(df[["year", "month", "day"]].to_dict(), cache=cache)
        expected.index = pd.Index([0, 1])
        tm.assert_series_equal(result, expected)

    def test_dataframe_dict_with_constructable(self, df, cache):
        # dict but with constructable
        df2 = df[["year", "month", "day"]].to_dict()
        df2["month"] = 2
        result = pd.to_datetime(df2, cache=cache)
        expected2 = pd.Series(
            [pd.Timestamp("20150204 00:00:00"), pd.Timestamp("20160205 00:0:00")],
            index=pd.Index([0, 1]),
        )
        tm.assert_series_equal(result, expected2)

    @pytest.mark.parametrize(
        "unit",
        [
            {
                "year": "years",
                "month": "months",
                "day": "days",
                "hour": "hours",
                "minute": "minutes",
                "second": "seconds",
            },
            {
                "year": "year",
                "month": "month",
                "day": "day",
                "hour": "hour",
                "minute": "minute",
                "second": "second",
            },
        ],
    )
    def test_dataframe_field_aliases_column_subset(self, df, cache, unit):
        # unit mappings
        result = pd.to_datetime(df[list(unit.keys())].rename(columns=unit), cache=cache)
        expected = pd.Series(
            [pd.Timestamp("20150204 06:58:10"), pd.Timestamp("20160305 07:59:11")],
            dtype="M8[us]",
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_field_aliases(self, df, cache):
        d = {
            "year": "year",
            "month": "month",
            "day": "day",
            "hour": "hour",
            "minute": "minute",
            "second": "second",
            "ms": "ms",
            "us": "us",
            "ns": "ns",
        }

        result = pd.to_datetime(df.rename(columns=d), cache=cache)
        expected = pd.Series(
            [
                pd.Timestamp("20150204 06:58:10.001002003"),
                pd.Timestamp("20160305 07:59:11.001002003"),
            ]
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_str_dtype(self, df, cache):
        # coerce back to int
        result = pd.to_datetime(df.astype(str), cache=cache)
        expected = pd.Series(
            [
                pd.Timestamp("20150204 06:58:10.001002003"),
                pd.Timestamp("20160305 07:59:11.001002003"),
            ]
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_float32_dtype(self, df, cache):
        # GH#60506
        # coerce to float64
        result = pd.to_datetime(df.astype(np.float32), cache=cache)
        expected = pd.Series(
            [
                pd.Timestamp("20150204 06:58:10.001002003"),
                pd.Timestamp("20160305 07:59:11.001002003"),
            ]
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_coerce(self, cache):
        # passing coerce
        df2 = pd.DataFrame({"year": [2015, 2016], "month": [2, 20], "day": [4, 5]})

        msg = (
            "^cannot assemble the datetimes: "
            'invalid or out-of-bounds date "2016-20-05"$'
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df2, cache=cache)

        result = pd.to_datetime(df2, errors="coerce", cache=cache)
        expected = pd.Series([pd.Timestamp("20150204 00:00:00"), pd.NaT])
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_dataframe_float_out_of_int64_range(self, errors, cache):
        # GH#55663 a float year far out of datetime range (10**16 fits in
        #  int64 but is not a valid year) is flagged out-of-bounds by the
        #  vectorized field assembly, so it raises / coerces to NaT like any
        #  invalid date; filterwarnings=error guards the no-warning path
        df2 = pd.DataFrame({"year": [10**16, np.nan], "month": [1, 1], "day": [1, 1]})
        if errors == "raise":
            msg = "cannot assemble the datetimes"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(df2, errors=errors, cache=cache)
        else:
            result = pd.to_datetime(df2, errors=errors, cache=cache)
            expected = pd.Series([pd.NaT, pd.NaT], dtype="datetime64[us]")
            tm.assert_series_equal(result, expected)

    def test_dataframe_extra_keys_raises(self, df, cache):
        # extra columns
        msg = r"extra keys have been passed to the datetime assemblage: \[foo\]"
        df2 = df.copy()
        df2["foo"] = 1
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df2, cache=cache)

    @pytest.mark.parametrize(
        "cols",
        [
            ["year"],
            ["year", "month"],
            ["year", "month", "second"],
            ["month", "day"],
            ["year", "day", "second"],
        ],
    )
    def test_dataframe_missing_keys_raises(self, df, cache, cols):
        # not enough
        msg = (
            r"to assemble mappings requires at least that \[year, month, "
            r"day\] be specified: \[.+\] is missing"
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df[cols], cache=cache)

    def test_dataframe_duplicate_columns_raises(self, cache):
        # duplicates
        msg = "cannot assemble with duplicate keys"
        df2 = pd.DataFrame({"year": [2015, 2016], "month": [2, 20], "day": [4, 5]})
        df2.columns = ["year", "year", "day"]
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df2, cache=cache)

        df2 = pd.DataFrame(
            {"year": [2015, 2016], "month": [2, 20], "day": [4, 5], "hour": [4, 5]}
        )
        df2.columns = ["year", "month", "day", "day"]
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df2, cache=cache)

    def test_dataframe_int16(self, cache):
        # GH#13451
        df = pd.DataFrame({"year": [2015, 2016], "month": [2, 3], "day": [4, 5]})

        # int16
        result = pd.to_datetime(df.astype("int16"), cache=cache)
        expected = pd.Series(
            [pd.Timestamp("20150204 00:00:00"), pd.Timestamp("20160305 00:00:00")]
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_mixed(self, cache):
        # mixed dtypes
        df = pd.DataFrame({"year": [2015, 2016], "month": [2, 3], "day": [4, 5]})
        df["month"] = df["month"].astype("int8")
        df["day"] = df["day"].astype("int8")
        result = pd.to_datetime(df, cache=cache)
        expected = pd.Series(
            [pd.Timestamp("20150204 00:00:00"), pd.Timestamp("20160305 00:00:00")]
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_float(self, cache):
        # float
        df = pd.DataFrame({"year": [2000, 2001], "month": [1.5, 1], "day": [1, 1]})
        msg = (
            r"^cannot assemble the datetimes: column 'month' contains "
            r"fractional values$"
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df, cache=cache)

    def test_dataframe_utc_true(self):
        # GH#23760
        df = pd.DataFrame({"year": [2015, 2016], "month": [2, 3], "day": [4, 5]})
        result = pd.to_datetime(df, utc=True)
        expected = pd.Series(
            np.array(["2015-02-04", "2016-03-05"], dtype="datetime64[us]")
        ).dt.tz_localize("UTC")
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "data,expected_ts",
        [
            # Feb 29 in leap year (valid)
            (
                {"year": [2000], "month": [2], "day": [29]},
                [pd.Timestamp("2000-02-29")],
            ),
            # Feb 28 in non-leap year (valid)
            (
                {"year": [2001], "month": [2], "day": [28]},
                [pd.Timestamp("2001-02-28")],
            ),
        ],
    )
    def test_dataframe_leap_year_valid(self, data, expected_ts):
        result = pd.to_datetime(pd.DataFrame(data))
        expected = pd.Series(expected_ts)
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "data",
        [
            {"year": [2001], "month": [2], "day": [29]},  # Feb 29 non-leap
            {"year": [2020], "month": [4], "day": [31]},  # Apr 31
            {"year": [2020], "month": [1], "day": [0]},  # day 0
        ],
    )
    def test_dataframe_invalid_day_raises(self, data):
        msg = r"cannot assemble the datetimes: invalid or out-of-bounds date"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(pd.DataFrame(data))

    @pytest.mark.parametrize(
        "data",
        [
            {"year": [2001], "month": [2], "day": [29]},  # Feb 29 non-leap
            {"year": [2020], "month": [4], "day": [31]},  # Apr 31
        ],
    )
    def test_dataframe_invalid_day_coerce(self, data):
        result = pd.to_datetime(pd.DataFrame(data), errors="coerce")
        expected = pd.Series([pd.NaT], dtype="datetime64[us]")
        tm.assert_series_equal(result, expected)

    def test_dataframe_fractional_float_coerce(self):
        # Fractional float with errors="coerce" should produce NaT
        df = pd.DataFrame({"year": [2000, 2001], "month": [1.5, 1], "day": [1, 1]})
        result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series([pd.NaT, pd.Timestamp("2001-01-01")])
        tm.assert_series_equal(result, expected)

    def test_dataframe_empty(self):
        # Empty DataFrame should produce empty Series
        df = pd.DataFrame({"year": [], "month": [], "day": []})
        result = pd.to_datetime(df)
        expected = pd.Series([], dtype="datetime64[us]")
        tm.assert_series_equal(result, expected)

    def test_dataframe_utc_with_time_fields(self):
        df = pd.DataFrame(
            {
                "year": [2020],
                "month": [6],
                "day": [15],
                "hour": [12],
                "minute": [30],
                "second": [45],
            }
        )
        result = pd.to_datetime(df, utc=True)
        expected = pd.Series(
            [pd.Timestamp("2020-06-15 12:30:45")], dtype="datetime64[us, UTC]"
        )
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "field,unit", [("hour", "h"), ("minute", "m"), ("second", "s")]
    )
    def test_dataframe_fractional_time_fields(self, field, unit):
        # fractional hours/minutes/seconds are valid and interpreted as
        #  timedeltas, e.g. hour=1.5 -> 01:30:00
        df = pd.DataFrame({"year": [2000, 2000], "month": [1, 1], "day": [1, 1]})
        df[field] = [1.5, np.nan]
        result = pd.to_datetime(df)
        expected = pd.Series(
            [pd.Timestamp("2000-01-01") + pd.Timedelta(1.5, unit=unit), pd.NaT],
            dtype="datetime64[ns]",
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_year_outside_4_digits(self):
        # years outside the range 1000-9999 previously raised because the
        #  fields went through "%Y%m%d" string parsing
        df = pd.DataFrame({"year": [99, 12345], "month": [1, 6], "day": [2, 15]})
        result = pd.to_datetime(df)
        expected = pd.Series(
            np.array(["0099-01-02", "12345-06-15"], dtype="datetime64[us]")
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_year_out_of_bounds_raises(self):
        # year too large for datetime64[us]
        df = pd.DataFrame({"year": [2000, 3_000_000], "month": [1, 1], "day": [1, 1]})
        msg = (
            "cannot assemble the datetimes: "
            'invalid or out-of-bounds date "3000000-01-01"'
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df)

    def test_dataframe_year_out_of_bounds_coerce(self):
        df = pd.DataFrame({"year": [2000, 3_000_000], "month": [1, 1], "day": [1, 1]})
        result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series(
            [pd.Timestamp("2000-01-01"), pd.NaT], dtype="datetime64[us]"
        )
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_dataframe_hour_outside_int32(self, errors):
        # hour values outside int32 range fall back to to_timedelta, but should
        # still correctly raise / coerce to NaT
        df = pd.DataFrame(
            {"year": [2000, 2000], "month": [1, 1], "day": [1, 1], "hour": [1, 2**32]}
        )
        if errors == "raise":
            msg = r"cannot assemble the datetimes \[hour\]"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(df, errors=errors)
        else:
            result = pd.to_datetime(df, errors=errors)
            expected = pd.Series([pd.Timestamp("2000-01-01 01:00:00"), pd.NaT])
            tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "year,field,value,exp_str",
        [
            # post-2038 epoch seconds exceed int32 but are valid datetimes
            (1970, "second", 2**31 + 100, "2038-01-19T03:15:48"),
            (2000, "hour", 2_200_000_000, "252974-10-22T16:00:00"),
        ],
    )
    def test_dataframe_time_field_outside_int32_valid(
        self, year, field, value, exp_str
    ):
        # values outside int32 range that still produce an in-bounds
        #  datetime go through the to_timedelta fallback
        df = pd.DataFrame({"year": [year], "month": [1], "day": [1], field: [value]})
        result = pd.to_datetime(df)
        expected = pd.Series(np.array([exp_str], dtype="datetime64[us]"))
        tm.assert_series_equal(result, expected)

    def test_dataframe_infinite_float(self):
        # +/-inf cannot be cast to int64; previously produced garbage values
        df = pd.DataFrame({"year": [2000.0, np.inf], "month": [1, 1], "day": [1, 1]})
        msg = "cannot assemble the datetimes: column 'year' contains out-of-bounds"
        with pytest.raises(ValueError, match=msg):
            with tm.assert_produces_warning(None):
                pd.to_datetime(df)

        with tm.assert_produces_warning(None):
            result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series(
            [pd.Timestamp("2000-01-01"), pd.NaT], dtype="datetime64[us]"
        )
        tm.assert_series_equal(result, expected)

    def test_dataframe_infinite_float_time_field(self):
        # an inf hour goes through to_timedelta, which raises under errors="raise"
        #  and coerces to NaT under errors="coerce" (GH#66823)
        df = pd.DataFrame(
            {
                "year": [2000.0, 2000.0],
                "month": [1, 1],
                "day": [1, 1],
                "hour": [0.0, np.inf],
            }
        )
        msg = r"cannot assemble the datetimes \[hour\]: cannot convert input inf"
        with pytest.raises(ValueError, match=msg):
            with tm.assert_produces_warning(None):
                pd.to_datetime(df)

        with tm.assert_produces_warning(None):
            result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series(
            [pd.Timestamp("2000-01-01"), pd.NaT], dtype="datetime64[ns]"
        )
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("dtype", ["bool", "boolean"])
    def test_dataframe_bool_column(self, dtype):
        # bool year/month/day columns raise instead of being read as 0/1
        df = pd.DataFrame({"year": [True, False], "month": [1, 1], "day": [1, 1]})
        df["year"] = df["year"].astype(dtype)
        msg = "cannot assemble the datetimes: column 'year' has dtype bool"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df)

        result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series([pd.NaT, pd.NaT], dtype="datetime64[us]")
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_dataframe_bool_time_field(self, errors):
        # bool hour goes through to_timedelta, which raises for bool dtype
        df = pd.DataFrame({"year": [2000], "month": [1], "day": [1], "hour": [True]})
        msg = r"cannot assemble the datetimes \[hour\]: dtype bool cannot be converted"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df, errors=errors)

    def test_dataframe_nan_hour_does_not_mask_invalid_date(self):
        # NaN in the hour column must not mask an invalid month in the
        #  same row
        df = pd.DataFrame({"year": [2000], "month": [13], "day": [1], "hour": [np.nan]})
        msg = (
            'cannot assemble the datetimes: invalid or out-of-bounds date "2000-13-01"'
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df)

        result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series([pd.NaT], dtype="datetime64[us]")
        tm.assert_series_equal(result, expected)

    def test_dataframe_nan_year_does_not_mask_oob_hour(self):
        # NaN in the year column must not mask an out-of-bounds hour in
        #  the same row
        df = pd.DataFrame({"year": [np.nan], "month": [1], "day": [1], "hour": [2**62]})
        msg = r"cannot assemble the datetimes \[hour\]"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(df)

        result = pd.to_datetime(df, errors="coerce")
        expected = pd.Series([pd.NaT], dtype="datetime64[us]")
        tm.assert_series_equal(result, expected)

    def test_dataframe_nan_in_time_field(self):
        # NaN in a float hour column gives NaT for that row only
        df = pd.DataFrame(
            {
                "year": [2000, 2000],
                "month": [1, 1],
                "day": [1, 1],
                "hour": [np.nan, 5.0],
            }
        )
        result = pd.to_datetime(df)
        expected = pd.Series(
            [pd.NaT, pd.Timestamp("2000-01-01 05:00:00")], dtype="datetime64[us]"
        )
        tm.assert_series_equal(result, expected)


class TestToDatetimeMisc:
    def test_to_datetime_barely_out_of_bounds(self):
        # GH#19529
        # GH#19382 close enough to bounds that dropping nanos would result
        # in an in-bounds datetime
        arr = np.array(["2262-04-11 23:47:16.854775808"], dtype=object)

        msg = "^Out of bounds nanosecond timestamp: .*"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(arr)

    @pytest.mark.parametrize(
        "arg, exp_str",
        [
            ["2012-01-01 00:00:00", "2012-01-01 00:00:00"],
            ["20121001", "2012-10-01"],  # bad iso 8601
        ],
    )
    def test_to_datetime_iso8601(self, cache, arg, exp_str):
        result = pd.to_datetime([arg], cache=cache)
        exp = pd.Timestamp(exp_str)
        assert result[0] == exp

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2012", "%Y-%m"),
            ("2012-01", "%Y-%m-%d"),
            ("2012-01-01", "%Y-%m-%d %H"),
            ("2012-01-01 10", "%Y-%m-%d %H:%M"),
            ("2012-01-01 10:00", "%Y-%m-%d %H:%M:%S"),
            ("2012-01-01 10:00:00", "%Y-%m-%d %H:%M:%S.%f"),
            ("2012-01-01 10:00:00.123", "%Y-%m-%d %H:%M:%S.%f%z"),
            (0, "%Y-%m-%d"),
        ],
    )
    @pytest.mark.parametrize("exact", [True, False])
    def test_to_datetime_iso8601_fails(self, input, format, exact):
        # https://github.com/pandas-dev/pandas/issues/12649
        # `format` is longer than the string, so this fails regardless of `exact`
        # GH#55663 - int with format is deprecated
        warn = Pandas4Warning if isinstance(input, (int, float)) else None
        with tm.assert_produces_warning(warn, match="integer or float"):
            with pytest.raises(
                ValueError,
                match=(
                    rf"time data \"{input}\" doesn't match format " rf"\"{format}\""
                ),
            ):
                pd.to_datetime(input, format=format, exact=exact)

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2012-01-01", "%Y-%m"),
            ("2012-01-01 10", "%Y-%m-%d"),
            ("2012-01-01 10:00", "%Y-%m-%d %H"),
            ("2012-01-01 10:00:00", "%Y-%m-%d %H:%M"),
            (0, "%Y-%m-%d"),
        ],
    )
    def test_to_datetime_iso8601_exact_fails(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/12649
        # `format` is shorter than the date string, so only fails with `exact=True`
        msg = "|".join(
            [
                '^unconverted data remains when parsing with format ".*": ".*". '
                f"{PARSING_ERR_MSG}$",
                f'^time data ".*" doesn\'t match format ".*". {PARSING_ERR_MSG}$',
            ]
        )
        # GH#55663 - int with format is deprecated
        warn = Pandas4Warning if isinstance(input, (int, float)) else None
        with tm.assert_produces_warning(warn, match="integer or float"):
            with pytest.raises(
                ValueError,
                match=(msg),
            ):
                pd.to_datetime(input, format=format)

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2012-01-01", "%Y-%m"),
            ("2012-01-01 00", "%Y-%m-%d"),
            ("2012-01-01 00:00", "%Y-%m-%d %H"),
            ("2012-01-01 00:00:00", "%Y-%m-%d %H:%M"),
        ],
    )
    def test_to_datetime_iso8601_non_exact(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/12649
        expected = pd.Timestamp(2012, 1, 1)
        result = pd.to_datetime(input, format=format, exact=False)
        assert result == expected

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2020-01", "%Y/%m"),
            ("2020-01-01", "%Y/%m/%d"),
            ("2020-01-01 00", "%Y/%m/%dT%H"),
            ("2020-01-01T00", "%Y/%m/%d %H"),
            ("2020-01-01 00:00", "%Y/%m/%dT%H:%M"),
            ("2020-01-01T00:00", "%Y/%m/%d %H:%M"),
            ("2020-01-01 00:00:00", "%Y/%m/%dT%H:%M:%S"),
            ("2020-01-01T00:00:00", "%Y/%m/%d %H:%M:%S"),
        ],
    )
    def test_to_datetime_iso8601_separator(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/12649
        with pytest.raises(
            ValueError,
            match=(rf"time data \"{input}\" doesn\'t match format " rf"\"{format}\""),
        ):
            pd.to_datetime(input, format=format)

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2020-01", "%Y-%m"),
            ("2020-01-01", "%Y-%m-%d"),
            ("2020-01-01 00", "%Y-%m-%d %H"),
            ("2020-01-01T00", "%Y-%m-%dT%H"),
            ("2020-01-01 00:00", "%Y-%m-%d %H:%M"),
            ("2020-01-01T00:00", "%Y-%m-%dT%H:%M"),
            ("2020-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"),
            ("2020-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"),
            ("2020-01-01T00:00:00.000", "%Y-%m-%dT%H:%M:%S.%f"),
            ("2020-01-01T00:00:00.000000", "%Y-%m-%dT%H:%M:%S.%f"),
            ("2020-01-01T00:00:00.000000000", "%Y-%m-%dT%H:%M:%S.%f"),
        ],
    )
    def test_to_datetime_iso8601_valid(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/12649
        expected = pd.Timestamp(2020, 1, 1)
        result = pd.to_datetime(input, format=format)
        assert result == expected

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2020-1", "%Y-%m"),
            ("2020-1-1", "%Y-%m-%d"),
            ("2020-1-1 0", "%Y-%m-%d %H"),
            ("2020-1-1T0", "%Y-%m-%dT%H"),
            ("2020-1-1 0:0", "%Y-%m-%d %H:%M"),
            ("2020-1-1T0:0", "%Y-%m-%dT%H:%M"),
            ("2020-1-1 0:0:0", "%Y-%m-%d %H:%M:%S"),
            ("2020-1-1T0:0:0", "%Y-%m-%dT%H:%M:%S"),
            ("2020-1-1T0:0:0.000", "%Y-%m-%dT%H:%M:%S.%f"),
            ("2020-1-1T0:0:0.000000", "%Y-%m-%dT%H:%M:%S.%f"),
            ("2020-1-1T0:0:0.000000000", "%Y-%m-%dT%H:%M:%S.%f"),
        ],
    )
    def test_to_datetime_iso8601_non_padded(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/21422
        expected = pd.Timestamp(2020, 1, 1)
        result = pd.to_datetime(input, format=format)
        assert result == expected

    @pytest.mark.parametrize(
        "input, format",
        [
            ("2020-01-01T00:00:00.000000000+00:00", "%Y-%m-%dT%H:%M:%S.%f%z"),
            ("2020-01-01T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"),
            ("2020-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
        ],
    )
    def test_to_datetime_iso8601_with_timezone_valid(self, input, format):
        # https://github.com/pandas-dev/pandas/issues/12649
        expected = pd.Timestamp(2020, 1, 1, tzinfo=UTC)
        result = pd.to_datetime(input, format=format)
        assert result == expected

    def test_to_datetime_default(self, cache):
        rs = pd.to_datetime("2001", cache=cache)
        xp = datetime(2001, 1, 1)
        assert rs == xp

    @pytest.mark.xfail(reason="fails to enforce dayfirst=True, which would raise")
    def test_to_datetime_respects_dayfirst(self, cache):
        # dayfirst is essentially broken

        # The msg here is not important since it isn't actually raised yet.
        msg = "Invalid date specified"
        with pytest.raises(ValueError, match=msg):
            # if dayfirst is respected, then this would parse as month=13, which
            #  would raise
            with tm.assert_produces_warning(UserWarning, match="Provide format"):
                pd.to_datetime("01-13-2012", dayfirst=True, cache=cache)

    def test_to_datetime_on_datetime64_series(self, cache):
        # #2699
        ser = pd.Series(pd.date_range("1/1/2000", periods=10))

        result = pd.to_datetime(ser, cache=cache)
        assert result[0] == ser[0]

    def test_to_datetime_with_space_in_series(self, cache):
        # GH 6428
        ser = pd.Series(["10/18/2006", "10/18/2008", " "])
        msg = (
            r'^time data " " doesn\'t match format "%m/%d/%Y". ' rf"{PARSING_ERR_MSG}$"
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(ser, errors="raise", cache=cache)
        result_coerce = pd.to_datetime(ser, errors="coerce", cache=cache)
        expected_coerce = pd.Series(
            [datetime(2006, 10, 18), datetime(2008, 10, 18), pd.NaT]
        )
        tm.assert_series_equal(result_coerce, expected_coerce)

    def test_to_datetime_with_apply(self, cache):
        with tm.set_locale("C", locale.LC_TIME):
            # this is only locale tested with US/None locales
            # GH 5195
            # with a format and coerce a single item to_datetime fails
            td = pd.Series(["May 04", "Jun 02", "Dec 11"], index=[1, 2, 3])
            expected = pd.to_datetime(td, format="%b %y", cache=cache)
            result = td.apply(pd.to_datetime, format="%b %y", cache=cache)
            tm.assert_series_equal(result, expected)

    def test_to_datetime_timezone_name(self):
        # https://github.com/pandas-dev/pandas/issues/49748
        result = pd.to_datetime("2020-01-01 00:00:00UTC", format="%Y-%m-%d %H:%M:%S%Z")
        expected = pd.Timestamp(2020, 1, 1).tz_localize("UTC")
        assert result == expected

    @pytest.mark.parametrize("errors", ["raise", "coerce"])
    def test_to_datetime_with_apply_with_empty_str(self, cache, errors):
        with tm.set_locale("C", locale.LC_TIME):
            # this is only locale tested with US/None locales
            # GH 5195, GH50251
            # with a format and coerce a single item to_datetime fails
            td = pd.Series(["May 04", "Jun 02", ""], index=[1, 2, 3])
            expected = pd.to_datetime(td, format="%b %y", errors=errors, cache=cache)

            result = td.apply(
                lambda x: pd.to_datetime(
                    x, format="%b %y", errors="coerce", cache=cache
                )
            )
            tm.assert_series_equal(result, expected)

    def test_to_datetime_empty_stt(self, cache):
        # empty string
        result = pd.to_datetime("", cache=cache)
        assert result is pd.NaT

    def test_to_datetime_empty_str_list(self, cache):
        result = pd.to_datetime(["", ""], cache=cache)
        assert pd.isna(result).all()

    def test_to_datetime_zero(self, cache):
        # ints
        result = pd.Timestamp(0)
        expected = pd.to_datetime(0, cache=cache)
        assert result == expected

    def test_to_datetime_strings(self, cache):
        # GH 3888 (strings)
        expected = pd.to_datetime(["2012"], cache=cache)[0]
        result = pd.to_datetime("2012", cache=cache)
        assert result == expected

    def test_to_datetime_strings_variation(self, cache):
        array = ["2012", "20120101", "20120101 12:01:01"]
        expected = [pd.to_datetime(dt_str, cache=cache) for dt_str in array]
        result = [pd.Timestamp(date_str) for date_str in array]
        tm.assert_almost_equal(result, expected)

    @pytest.mark.parametrize("result", [pd.Timestamp("2012"), pd.to_datetime("2012")])
    def test_to_datetime_strings_vs_constructor(self, result):
        expected = pd.Timestamp(2012, 1, 1)
        assert result == expected

    def test_to_datetime_unprocessable_input(self, cache):
        # GH 4928
        # GH 21864
        msg = '^Given date string "1" not likely a datetime$'
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime([1, "1"], errors="raise", cache=cache)

    def test_to_datetime_other_datetime64_units(self):
        # 5/25/2012
        scalar = np.int64(1337904000000000).view("M8[us]")
        as_obj = scalar.astype("O")

        index = pd.DatetimeIndex([scalar])
        assert index[0] == scalar.astype("O")

        value = pd.Timestamp(scalar)
        assert value == as_obj

    def test_to_datetime_list_of_integers(self):
        rng = pd.date_range("1/1/2000", periods=20, unit="ns")
        rng = pd.DatetimeIndex(rng.values)

        ints = list(rng.asi8)

        result = pd.DatetimeIndex(ints)

        tm.assert_index_equal(rng, result)

    def test_to_datetime_overflow(self):
        # gh-17637
        # we are overflowing Timedelta range here
        msg = "Cannot cast 139999 days 00:00:00 to unit='ns' without overflow"
        with pytest.raises(OutOfBoundsTimedelta, match=msg):
            pd.date_range(start="1/1/1700", freq="B", periods=100000, unit="ns")

    def test_to_datetime_float_with_nans_floating_point_error(self):
        # GH#58419
        ser = pd.Series([np.nan] * 1000 + [1712219033.0], dtype=np.float64)
        result = pd.to_datetime(ser, unit="s", errors="coerce")
        expected = pd.Series(
            [pd.NaT] * 1000 + [pd.Timestamp("2024-04-04 08:23:53")],
            dtype="datetime64[us]",
        )
        tm.assert_series_equal(result, expected)

    def test_string_invalid_operation(self, cache):
        invalid = np.array(["87156549591102612381000001219H5"], dtype=object)
        # GH #51084

        with pytest.raises(ValueError, match="Unknown datetime string format"):
            pd.to_datetime(invalid, errors="raise", cache=cache)

    def test_string_na_nat_conversion(self, cache):
        # GH #999, #858

        strings = np.array(["1/1/2000", "1/2/2000", np.nan, "1/4/2000"], dtype=object)

        expected = np.empty(4, dtype="M8[us]")
        for i, val in enumerate(strings):
            if pd.isna(val):
                expected[i] = iNaT
            else:
                expected[i] = parse(val)

        result = tslib.array_to_datetime(strings)[0]
        tm.assert_almost_equal(result, expected)

        result2 = pd.to_datetime(strings, cache=cache)
        assert isinstance(result2, pd.DatetimeIndex)
        tm.assert_numpy_array_equal(result, result2.values)

    def test_string_na_nat_conversion_malformed(self, cache):
        malformed = np.array(["1/100/2000", np.nan], dtype=object)

        # GH 10636, default is now 'raise'
        msg = r"Unknown datetime string format"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(malformed, errors="raise", cache=cache)

        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(malformed, errors="raise", cache=cache)

    def test_string_na_nat_conversion_with_name(self, cache):
        idx = ["a", "b", "c", "d", "e"]
        series = pd.Series(
            ["1/1/2000", np.nan, "1/3/2000", np.nan, "1/5/2000"], index=idx, name="foo"
        )
        dseries = pd.Series(
            [
                pd.to_datetime("1/1/2000", cache=cache),
                np.nan,
                pd.to_datetime("1/3/2000", cache=cache),
                np.nan,
                pd.to_datetime("1/5/2000", cache=cache),
            ],
            index=idx,
            name="foo",
        )

        result = pd.to_datetime(series, cache=cache)
        dresult = pd.to_datetime(dseries, cache=cache)

        expected = pd.Series(np.empty(5, dtype="M8[us]"), index=idx)
        for i in range(5):
            x = series.iloc[i]
            if pd.isna(x):
                expected.iloc[i] = pd.NaT
            else:
                expected.iloc[i] = pd.to_datetime(x, cache=cache)

        tm.assert_series_equal(result, expected, check_names=False)
        assert result.name == "foo"

        tm.assert_series_equal(dresult, expected, check_names=False)
        assert dresult.name == "foo"

    @pytest.mark.parametrize(
        "unit",
        ["h", "m", "s", "ms", "us", "ns"],
    )
    def test_dti_constructor_numpy_timeunits(self, cache, unit):
        # GH 9114
        dtype = np.dtype(f"M8[{unit}]")
        base = pd.to_datetime(
            ["2000-01-01T00:00", "2000-01-02T00:00", "NaT"], cache=cache
        )

        values = base.values.astype(dtype)

        if unit in ["h", "m"]:
            # we cast to closest supported unit
            unit = "s"
        exp_dtype = np.dtype(f"M8[{unit}]")
        expected = pd.DatetimeIndex(base.astype(exp_dtype))
        assert expected.dtype == exp_dtype

        tm.assert_index_equal(pd.DatetimeIndex(values), expected)
        tm.assert_index_equal(pd.to_datetime(values, cache=cache), expected)

    def test_dayfirst(self, cache):
        # GH 5917
        arr = ["10/02/2014", "11/02/2014", "12/02/2014"]
        expected = pd.DatetimeIndex(
            [datetime(2014, 2, 10), datetime(2014, 2, 11), datetime(2014, 2, 12)]
        )
        depr_msg = "The 'dayfirst' keyword in DatetimeIndex is deprecated"
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            idx1 = pd.DatetimeIndex(arr, dayfirst=True)
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            idx2 = pd.DatetimeIndex(np.array(arr), dayfirst=True)
        idx3 = pd.to_datetime(arr, dayfirst=True, cache=cache)
        idx4 = pd.to_datetime(np.array(arr), dayfirst=True, cache=cache)
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            idx5 = pd.DatetimeIndex(pd.Index(arr), dayfirst=True)
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            idx6 = pd.DatetimeIndex(pd.Series(arr), dayfirst=True)
        tm.assert_index_equal(expected, idx1)
        tm.assert_index_equal(expected, idx2)
        tm.assert_index_equal(expected, idx3)
        tm.assert_index_equal(expected, idx4)
        tm.assert_index_equal(expected, idx5)
        tm.assert_index_equal(expected, idx6)

    def test_dayfirst_warnings_valid_input(self):
        # GH 12585
        warning_msg = (
            "Parsing dates in .* format when dayfirst=.* was specified. "
            "Pass `dayfirst=.*` or specify a format to silence this warning."
        )

        # CASE 1: valid input
        arr = ["31/12/2014", "10/03/2011"]
        expected = pd.DatetimeIndex(
            ["2014-12-31", "2011-03-10"], dtype="datetime64[us]", freq=None
        )

        # A. dayfirst arg correct, no warning
        res1 = pd.to_datetime(arr, dayfirst=True)
        tm.assert_index_equal(expected, res1)

        # B. dayfirst arg incorrect, warning
        with tm.assert_produces_warning(UserWarning, match=warning_msg):
            res2 = pd.to_datetime(arr, dayfirst=False)
        tm.assert_index_equal(expected, res2)

    def test_dayfirst_warnings_invalid_input(self):
        # CASE 2: invalid input
        # cannot consistently process with single format
        # ValueError *always* raised

        # first in DD/MM/YYYY, second in MM/DD/YYYY
        arr = ["31/12/2014", "03/30/2011"]

        with pytest.raises(
            ValueError,
            match=(
                r'^time data "03/30/2011" doesn\'t match format '
                rf'"%d/%m/%Y". {PARSING_ERR_MSG}$'
            ),
        ):
            pd.to_datetime(arr, dayfirst=True)

    @pytest.mark.parametrize("klass", [pd.DatetimeIndex, DatetimeArray._from_sequence])
    def test_to_datetime_dta_tz(self, klass):
        # GH#27733
        dti = pd.date_range("2015-04-05", periods=3).rename("foo")
        expected = dti.tz_localize("UTC")

        obj = klass(dti)
        expected = klass(expected)

        result = pd.to_datetime(obj, utc=True)
        tm.assert_equal(result, expected)


class TestGuessDatetimeFormat:
    @pytest.mark.parametrize(
        "test_list",
        [
            [
                "2011-12-30 00:00:00.000000",
                "2011-12-30 00:00:00.000000",
                "2011-12-30 00:00:00.000000",
            ],
            [np.nan, np.nan, "2011-12-30 00:00:00.000000"],
            ["", "2011-12-30 00:00:00.000000"],
            ["NaT", "2011-12-30 00:00:00.000000"],
            ["2011-12-30 00:00:00.000000", "random_string"],
            ["now", "2011-12-30 00:00:00.000000"],
            ["today", "2011-12-30 00:00:00.000000"],
        ],
    )
    def test_guess_datetime_format_for_array(self, test_list):
        expected_format = "%Y-%m-%d %H:%M:%S.%f"
        test_array = np.array(test_list, dtype=object)
        assert tools._guess_datetime_format_for_array(test_array) == expected_format

    def test_guess_datetime_format_for_array_all_nans(self):
        format_for_string_of_nans = tools._guess_datetime_format_for_array(
            np.array([np.nan, np.nan, np.nan], dtype="O")
        )
        assert format_for_string_of_nans is None


class TestToDatetimeInferFormat:
    @pytest.mark.parametrize(
        "test_format", ["%m-%d-%Y", "%m/%d/%Y %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S.%f"]
    )
    def test_to_datetime_infer_datetime_format_consistent_format(
        self, cache, test_format
    ):
        ser = pd.Series(pd.date_range("20000101", periods=50, freq="h"))

        s_as_dt_strings = ser.apply(lambda x: x.strftime(test_format))

        with_format = pd.to_datetime(s_as_dt_strings, format=test_format, cache=cache)
        without_format = pd.to_datetime(s_as_dt_strings, cache=cache)

        # Whether the format is explicitly passed, or
        # it is inferred, the results should all be the same
        tm.assert_series_equal(with_format, without_format)

    def test_to_datetime_inconsistent_format(self, cache):
        data = ["01/01/2011 00:00:00", "01-02-2011 00:00:00", "2011-01-03T00:00:00"]
        ser = pd.Series(np.array(data))
        msg = (
            r'^time data "01-02-2011 00:00:00" doesn\'t match format '
            rf'"%m/%d/%Y %H:%M:%S". {PARSING_ERR_MSG}$'
        )
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(ser, cache=cache)

    def test_to_datetime_consistent_format(self, cache):
        data = ["Jan/01/2011", "Feb/01/2011", "Mar/01/2011"]
        ser = pd.Series(np.array(data))
        result = pd.to_datetime(ser, cache=cache)
        expected = pd.Series(
            ["2011-01-01", "2011-02-01", "2011-03-01"], dtype="datetime64[us]"
        )
        tm.assert_series_equal(result, expected)

    def test_to_datetime_series_with_nans(self, cache):
        ser = pd.Series(
            np.array(
                ["01/01/2011 00:00:00", np.nan, "01/03/2011 00:00:00", np.nan],
                dtype=object,
            )
        )
        result = pd.to_datetime(ser, cache=cache)
        expected = pd.Series(
            ["2011-01-01", pd.NaT, "2011-01-03", pd.NaT], dtype="datetime64[us]"
        )
        tm.assert_series_equal(result, expected)

    def test_to_datetime_series_start_with_nans(self, cache):
        ser = pd.Series(
            np.array(
                [
                    np.nan,
                    np.nan,
                    "01/01/2011 00:00:00",
                    "01/02/2011 00:00:00",
                    "01/03/2011 00:00:00",
                ],
                dtype=object,
            )
        )

        result = pd.to_datetime(ser, cache=cache)
        expected = pd.Series(
            [pd.NaT, pd.NaT, "2011-01-01", "2011-01-02", "2011-01-03"],
            dtype="datetime64[us]",
        )
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "tz_name, offset",
        [("UTC", 0), ("GMT", 0), ("UTC-3", 180), ("UTC+3", -180)],
    )
    def test_infer_datetime_format_tz_name(self, tz_name, offset):
        # GH 33133
        ser = pd.Series([f"2019-02-02 08:07:13 {tz_name}"])
        result = pd.to_datetime(ser)
        tz = timezone(timedelta(minutes=offset))
        expected = pd.Series([pd.Timestamp("2019-02-02 08:07:13").tz_localize(tz)])
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "ts,zero_tz",
        [
            ("2019-02-02 08:07:13", "Z"),
            ("2019-02-02 08:07:13", ""),
            ("2019-02-02 08:07:13.012345", "Z"),
            ("2019-02-02 08:07:13.012345", ""),
        ],
    )
    def test_infer_datetime_format_zero_tz(self, ts, zero_tz):
        # GH 41047
        ser = pd.Series([ts + zero_tz])
        result = pd.to_datetime(ser)
        tz = UTC if zero_tz == "Z" else None
        expected = pd.Series([pd.Timestamp(ts, tz=tz)])
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("format", [None, "%Y-%m-%d"])
    def test_to_datetime_iso8601_noleading_0s(self, cache, format):
        # GH 11871
        ser = pd.Series(["2014-1-1", "2014-2-2", "2015-3-3"])
        expected = pd.Series(
            [
                pd.Timestamp("2014-01-01"),
                pd.Timestamp("2014-02-02"),
                pd.Timestamp("2015-03-03"),
            ]
        )
        result = pd.to_datetime(ser, format=format, cache=cache)
        tm.assert_series_equal(result, expected)


class TestDaysInMonth:
    # tests for issue #10154

    @pytest.mark.parametrize(
        "arg, format",
        [
            ["2015-02-29", None],
            ["2015-02-29", "%Y-%m-%d"],
            ["2015-02-32", "%Y-%m-%d"],
            ["2015-04-31", "%Y-%m-%d"],
        ],
    )
    def test_day_not_in_month_coerce(self, cache, arg, format):
        assert pd.isna(pd.to_datetime(arg, errors="coerce", format=format, cache=cache))

    def test_day_not_in_month_raise(self, cache):
        if PY314:
            msg = "day 29 must be in range 1..28 for month 2 in year 2015: 2015-02-29"
        else:
            msg = "day is out of range for month: 2015-02-29"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime("2015-02-29", errors="raise", cache=cache)

    @pytest.mark.parametrize(
        "arg, format, msg",
        [
            (
                "2015-02-29",
                "%Y-%m-%d",
                f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$",
            ),
            (
                "2015-29-02",
                "%Y-%d-%m",
                f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$",
            ),
            (
                "2015-02-32",
                "%Y-%m-%d",
                '^unconverted data remains when parsing with format "%Y-%m-%d": "2". '
                f"{PARSING_ERR_MSG}$",
            ),
            (
                "2015-32-02",
                "%Y-%d-%m",
                '^time data "2015-32-02" doesn\'t match format "%Y-%d-%m". '
                f"{PARSING_ERR_MSG}$",
            ),
            (
                "2015-04-31",
                "%Y-%m-%d",
                f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$",
            ),
            (
                "2015-31-04",
                "%Y-%d-%m",
                f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$",
            ),
        ],
    )
    def test_day_not_in_month_raise_value(self, cache, arg, format, msg):
        # https://github.com/pandas-dev/pandas/issues/50462
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(arg, errors="raise", format=format, cache=cache)


class TestDatetimeParsingWrappers:
    @pytest.mark.parametrize(
        "date_str, expected",
        [
            ("2011-01-01", datetime(2011, 1, 1)),
            ("2Q2005", datetime(2005, 4, 1)),
            ("2Q05", datetime(2005, 4, 1)),
            ("2005Q1", datetime(2005, 1, 1)),
            ("05Q1", datetime(2005, 1, 1)),
            ("2011Q3", datetime(2011, 7, 1)),
            ("11Q3", datetime(2011, 7, 1)),
            ("3Q2011", datetime(2011, 7, 1)),
            ("3Q11", datetime(2011, 7, 1)),
            # quarterly without space
            ("2000Q4", datetime(2000, 10, 1)),
            ("00Q4", datetime(2000, 10, 1)),
            ("4Q2000", datetime(2000, 10, 1)),
            ("4Q00", datetime(2000, 10, 1)),
            ("2000q4", datetime(2000, 10, 1)),
            ("2000-Q4", datetime(2000, 10, 1)),
            ("00-Q4", datetime(2000, 10, 1)),
            ("4Q-2000", datetime(2000, 10, 1)),
            ("4Q-00", datetime(2000, 10, 1)),
            ("00q4", datetime(2000, 10, 1)),
            ("2005", datetime(2005, 1, 1)),
            ("2005-11", datetime(2005, 11, 1)),
            ("2005 11", datetime(2005, 11, 1)),
            ("11-2005", datetime(2005, 11, 1)),
            ("11 2005", datetime(2005, 11, 1)),
            ("200511", datetime(2020, 5, 11)),
            ("20051109", datetime(2005, 11, 9)),
            ("20051109 10:15", datetime(2005, 11, 9, 10, 15)),
            ("20051109 08H", datetime(2005, 11, 9, 8, 0)),
            ("2005-11-09 10:15", datetime(2005, 11, 9, 10, 15)),
            ("2005-11-09 08H", datetime(2005, 11, 9, 8, 0)),
            ("2005/11/09 10:15", datetime(2005, 11, 9, 10, 15)),
            ("2005/11/09 10:15:32", datetime(2005, 11, 9, 10, 15, 32)),
            ("2005/11/09 10:15:32 AM", datetime(2005, 11, 9, 10, 15, 32)),
            ("2005/11/09 10:15:32 PM", datetime(2005, 11, 9, 22, 15, 32)),
            ("2005/11/09 08H", datetime(2005, 11, 9, 8, 0)),
            ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28)),
            ("Thu Sep 25 2003", datetime(2003, 9, 25)),
            ("Sep 25 2003", datetime(2003, 9, 25)),
            ("January 1 2014", datetime(2014, 1, 1)),
            # GH#10537
            ("2014-06", datetime(2014, 6, 1)),
            ("06-2014", datetime(2014, 6, 1)),
            ("2014-6", datetime(2014, 6, 1)),
            ("6-2014", datetime(2014, 6, 1)),
            ("20010101 12", datetime(2001, 1, 1, 12)),
            ("20010101 1234", datetime(2001, 1, 1, 12, 34)),
            ("20010101 123456", datetime(2001, 1, 1, 12, 34, 56)),
        ],
    )
    def test_parsers(self, date_str, expected, cache):
        # dateutil >= 2.5.0 defaults to yearfirst=True
        # https://github.com/dateutil/dateutil/issues/217
        yearfirst = True

        # GH#50907
        is_quarter = "Q" in date_str.upper()
        warn = Pandas4Warning if is_quarter else None
        msg = "quarterly string is deprecated" if is_quarter else ""

        with tm.assert_produces_warning(warn, match=msg):
            result1, reso_attrname = parsing.parse_datetime_string_with_reso(
                date_str, yearfirst=yearfirst
            )

        reso = {
            "nanosecond": "ns",
        }.get(reso_attrname, "us")
        with tm.assert_produces_warning(warn, match=msg):
            result2 = pd.to_datetime(date_str, yearfirst=yearfirst)
        with tm.assert_produces_warning(warn, match=msg):
            result3 = pd.to_datetime([date_str], yearfirst=yearfirst)
        # result5 is used below
        with tm.assert_produces_warning(warn, match=msg):
            result4 = pd.to_datetime(
                np.array([date_str], dtype=object),
                yearfirst=yearfirst,
                cache=cache,
            )
        depr_msg = "keyword in DatetimeIndex is deprecated"
        if is_quarter:
            depr_msg = f"{depr_msg}|{msg}"
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            result6 = pd.DatetimeIndex([date_str], yearfirst=yearfirst)
        # result7 is used below
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            result8 = pd.DatetimeIndex(pd.Index([date_str]), yearfirst=yearfirst)
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            result9 = pd.DatetimeIndex(pd.Series([date_str]), yearfirst=yearfirst)

        for res in [result1, result2]:
            assert res == expected
        for res in [result3, result4, result6, result8, result9]:
            exp = pd.DatetimeIndex([pd.Timestamp(expected)]).as_unit(reso)
            tm.assert_index_equal(res, exp)

        # these really need to have yearfirst, but we don't support
        if not yearfirst:
            result5 = pd.Timestamp(date_str)
            assert result5 == expected
            result7 = pd.date_range(date_str, freq="S", periods=1, yearfirst=yearfirst)
            assert result7 == expected

    def test_na_values_with_cache(
        self, cache, unique_nulls_fixture, unique_nulls_fixture2
    ):
        # GH22305
        expected = pd.Index([pd.NaT, pd.NaT], dtype="datetime64[s]")
        result = pd.to_datetime(
            [unique_nulls_fixture, unique_nulls_fixture2], cache=cache
        )
        tm.assert_index_equal(result, expected)

    def test_parsers_nat(self):
        # Test that each of several string-accepting methods return pd.NaT
        result1, _ = parsing.parse_datetime_string_with_reso("NaT")
        result2 = pd.to_datetime("NaT")
        result3 = pd.Timestamp("NaT")
        result4 = pd.DatetimeIndex(["NaT"])[0]
        assert result1 is pd.NaT
        assert result2 is pd.NaT
        assert result3 is pd.NaT
        assert result4 is pd.NaT

    @pytest.mark.parametrize(
        "date_str, dayfirst, yearfirst, expected",
        [
            ("10-11-12", False, False, datetime(2012, 10, 11)),
            ("10-11-12", True, False, datetime(2012, 11, 10)),
            ("10-11-12", False, True, datetime(2010, 11, 12)),
            ("10-11-12", True, True, datetime(2010, 12, 11)),
            ("20/12/21", False, False, datetime(2021, 12, 20)),
            ("20/12/21", True, False, datetime(2021, 12, 20)),
            ("20/12/21", False, True, datetime(2020, 12, 21)),
            ("20/12/21", True, True, datetime(2020, 12, 21)),
            # GH 58859
            ("20201012", True, False, datetime(2020, 12, 10)),
        ],
    )
    def test_parsers_dayfirst_yearfirst(
        self, cache, date_str, dayfirst, yearfirst, expected
    ):
        # OK
        # 2.5.1 10-11-12   [dayfirst=0, yearfirst=0] -> 2012-10-11 00:00:00
        # 2.5.2 10-11-12   [dayfirst=0, yearfirst=1] -> 2012-10-11 00:00:00
        # 2.5.3 10-11-12   [dayfirst=0, yearfirst=0] -> 2012-10-11 00:00:00

        # OK
        # 2.5.1 10-11-12   [dayfirst=0, yearfirst=1] -> 2010-11-12 00:00:00
        # 2.5.2 10-11-12   [dayfirst=0, yearfirst=1] -> 2010-11-12 00:00:00
        # 2.5.3 10-11-12   [dayfirst=0, yearfirst=1] -> 2010-11-12 00:00:00

        # bug fix in 2.5.2
        # 2.5.1 10-11-12   [dayfirst=1, yearfirst=1] -> 2010-11-12 00:00:00
        # 2.5.2 10-11-12   [dayfirst=1, yearfirst=1] -> 2010-12-11 00:00:00
        # 2.5.3 10-11-12   [dayfirst=1, yearfirst=1] -> 2010-12-11 00:00:00

        # OK
        # 2.5.1 10-11-12   [dayfirst=1, yearfirst=0] -> 2012-11-10 00:00:00
        # 2.5.2 10-11-12   [dayfirst=1, yearfirst=0] -> 2012-11-10 00:00:00
        # 2.5.3 10-11-12   [dayfirst=1, yearfirst=0] -> 2012-11-10 00:00:00

        # OK
        # 2.5.1 20/12/21   [dayfirst=0, yearfirst=0] -> 2021-12-20 00:00:00
        # 2.5.2 20/12/21   [dayfirst=0, yearfirst=0] -> 2021-12-20 00:00:00
        # 2.5.3 20/12/21   [dayfirst=0, yearfirst=0] -> 2021-12-20 00:00:00

        # OK
        # 2.5.1 20/12/21   [dayfirst=0, yearfirst=1] -> 2020-12-21 00:00:00
        # 2.5.2 20/12/21   [dayfirst=0, yearfirst=1] -> 2020-12-21 00:00:00
        # 2.5.3 20/12/21   [dayfirst=0, yearfirst=1] -> 2020-12-21 00:00:00

        # revert of bug in 2.5.2
        # 2.5.1 20/12/21   [dayfirst=1, yearfirst=1] -> 2020-12-21 00:00:00
        # 2.5.2 20/12/21   [dayfirst=1, yearfirst=1] -> month must be in 1..12
        # 2.5.3 20/12/21   [dayfirst=1, yearfirst=1] -> 2020-12-21 00:00:00

        # OK
        # 2.5.1 20/12/21   [dayfirst=1, yearfirst=0] -> 2021-12-20 00:00:00
        # 2.5.2 20/12/21   [dayfirst=1, yearfirst=0] -> 2021-12-20 00:00:00
        # 2.5.3 20/12/21   [dayfirst=1, yearfirst=0] -> 2021-12-20 00:00:00

        # str : dayfirst, yearfirst, expected

        # compare with dateutil result
        dateutil_result = parse(date_str, dayfirst=dayfirst, yearfirst=yearfirst)
        assert dateutil_result == expected

        result1, _ = parsing.parse_datetime_string_with_reso(
            date_str, dayfirst=dayfirst, yearfirst=yearfirst
        )

        # we don't support dayfirst/yearfirst here:
        if not dayfirst and not yearfirst:
            result2 = pd.Timestamp(date_str)
            assert result2 == expected

        result3 = pd.to_datetime(
            date_str, dayfirst=dayfirst, yearfirst=yearfirst, cache=cache
        )

        depr_msg = "keyword in DatetimeIndex is deprecated"
        with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
            result4 = pd.DatetimeIndex(
                [date_str], dayfirst=dayfirst, yearfirst=yearfirst
            )[0]

        assert result1 == expected
        assert result3 == expected
        assert result4 == expected

    @pytest.mark.parametrize(
        "date_str, exp_def",
        [["10:15", datetime(1, 1, 1, 10, 15)], ["9:05", datetime(1, 1, 1, 9, 5)]],
    )
    def test_parsers_timestring(self, date_str, exp_def):
        # must be the same as dateutil result
        exp_now = parse(date_str)

        result1, _ = parsing.parse_datetime_string_with_reso(date_str)
        result2 = pd.to_datetime(date_str)
        result3 = pd.to_datetime([date_str])
        result4 = pd.Timestamp(date_str)
        result5 = pd.DatetimeIndex([date_str])[0]
        # parse time string return time string based on default date
        # others are not, and can't be changed because it is used in
        # time series plot
        assert result1 == exp_def
        assert result2 == exp_now
        assert result3 == exp_now
        assert result4 == exp_now
        assert result5 == exp_now

    @pytest.mark.parametrize(
        "dt_string, tz, dt_string_repr",
        [
            (
                "2013-01-01 05:45+0545",
                timezone(timedelta(minutes=345)),
                "Timestamp('2013-01-01 05:45:00+0545', tz='UTC+05:45')",
            ),
            (
                "2013-01-01 05:30+0530",
                timezone(timedelta(minutes=330)),
                "Timestamp('2013-01-01 05:30:00+0530', tz='UTC+05:30')",
            ),
        ],
    )
    def test_parsers_timezone_minute_offsets_roundtrip(
        self, cache, dt_string, tz, dt_string_repr
    ):
        # GH11708
        base = pd.to_datetime("2013-01-01 00:00:00", cache=cache)
        base = base.tz_localize("UTC").tz_convert(tz)
        dt_time = pd.to_datetime(dt_string, cache=cache)
        assert base == dt_time
        assert dt_string_repr == repr(dt_time)


@pytest.fixture(params=["D", "s", "ms", "us", "ns"])
def units(request):
    """Day and some time units.

    * D
    * s
    * ms
    * us
    * ns
    """
    return request.param


@pytest.fixture
def julian_dates():
    return pd.date_range("2014-1-1", periods=10).to_julian_date().values


class TestOrigin:
    def test_origin_and_unit(self):
        # GH#42624
        ts = pd.to_datetime(1, unit="s", origin=1)
        expected = pd.Timestamp("1970-01-01 00:00:02")
        assert ts == expected

        ts = pd.to_datetime(1, unit="s", origin=1_000_000_000)
        expected = pd.Timestamp("2001-09-09 01:46:41")
        assert ts == expected

    def test_julian(self, julian_dates):
        # gh-11276, gh-11745
        # for origin as julian

        result = pd.Series(pd.to_datetime(julian_dates, unit="D", origin="julian"))
        expected = pd.Series(
            pd.to_datetime(julian_dates - pd.Timestamp(0).to_julian_date(), unit="D")
        )
        tm.assert_series_equal(result, expected)

    def test_unix(self):
        result = pd.Series(pd.to_datetime([0, 1, 2], unit="D", origin="unix"))
        expected = pd.Series(
            [
                pd.Timestamp("1970-01-01"),
                pd.Timestamp("1970-01-02"),
                pd.Timestamp("1970-01-03"),
            ],
            dtype="M8[us]",
        )
        tm.assert_series_equal(result, expected)

    def test_julian_round_trip(self):
        result = pd.to_datetime(2456658, origin="julian", unit="D")
        assert result.to_julian_date() == 2456658

        # out-of-bounds
        msg = "1 is Out of Bounds for origin='julian'"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(1, origin="julian", unit="D")

    def test_invalid_unit(self, units, julian_dates):
        # checking for invalid combination of origin='julian' and unit != D
        if units != "D":
            msg = "unit must be 'D' for origin='julian'"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(julian_dates, unit=units, origin="julian")

    @pytest.mark.parametrize("unit", ["ns", "D"])
    def test_invalid_origin(self, unit):
        # need to have a numeric specified
        msg = "it must be numeric with a unit specified"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime("2005-01-01", origin="1960-01-01", unit=unit)

    @pytest.mark.parametrize(
        "epochs, origin_unit",
        [
            (pd.Timestamp(1960, 1, 1), "us"),
            (datetime(1960, 1, 1), "us"),
            ("1960-01-01", "us"),
            (np.datetime64("1960-01-01"), "s"),
        ],
    )
    def test_epoch(self, units, epochs, origin_unit):
        # GH 63419: after v3.0.0 default resolution is microseconds
        epoch_1960 = pd.Timestamp(1960, 1, 1)
        units_from_epochs = np.arange(5, dtype=np.int64)
        exp_unit = units if units == "ns" else "us"
        expected = pd.Series(
            [pd.Timedelta(x, unit=units) + epoch_1960 for x in units_from_epochs],
            dtype=f"M8[{exp_unit}]",
        )

        result = pd.Series(pd.to_datetime(units_from_epochs, unit=units, origin=epochs))
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize(
        "origin, exc",
        [
            ("random_string", ValueError),
            ("epoch", ValueError),
            ("13-24-1990", ValueError),
        ],
    )
    def test_invalid_origins(self, origin, exc, units):
        # GH 63419
        msg = f"origin {origin} cannot be converted to a Timestamp"
        with pytest.raises(exc, match=msg):
            pd.to_datetime(list(range(5)), unit=units, origin=origin)

    def test_invalid_origins_tzinfo(self):
        # GH16842
        with pytest.raises(ValueError, match="must be tz-naive"):
            pd.to_datetime(1, unit="D", origin=datetime(2000, 1, 1, tzinfo=UTC))

    def test_incorrect_value_exception(self):
        # GH47495
        msg = "Unknown datetime string format, unable to parse: yesterday"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(["today", "yesterday"])  # current-time-ok

    @pytest.mark.parametrize(
        "format, warning",
        [
            (None, UserWarning),
            ("%Y-%m-%d %H:%M:%S", None),
            ("%Y-%d-%m %H:%M:%S", None),
        ],
    )
    def test_to_datetime_out_of_bounds_with_format_arg(self, format, warning):
        # see gh-23830
        if format is None:
            res = pd.to_datetime("2417-10-10 00:00:00.00", format=format)
            assert isinstance(res, pd.Timestamp)
            assert res.year == 2417
            assert res.month == 10
            assert res.day == 10
        else:
            msg = "unconverted data remains when parsing with format.*"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime("2417-10-10 00:00:00.00", format=format)

    @pytest.mark.parametrize(
        "arg, origin, expected_str",
        [
            [200 * 365, "unix", "2169-11-13 00:00:00"],
            [200 * 365, "1870-01-01", "2069-11-13 00:00:00"],
            [300 * 365, "1870-01-01", "2169-10-20 00:00:00"],
        ],
    )
    def test_processing_order(self, arg, origin, expected_str):
        # make sure we handle out-of-bounds *before*
        # constructing the dates

        result = pd.to_datetime(arg, unit="D", origin=origin)
        expected = pd.Timestamp(expected_str)
        assert result == expected

        result = pd.to_datetime(200 * 365, unit="D", origin="1870-01-01")
        expected = pd.Timestamp("2069-11-13 00:00:00")
        assert result == expected

        result = pd.to_datetime(300 * 365, unit="D", origin="1870-01-01")
        expected = pd.Timestamp("2169-10-20 00:00:00")
        assert result == expected

    @pytest.mark.parametrize(
        "offset,utc,exp",
        [
            ["Z", True, "2019-01-01T00:00:00.000Z"],
            ["Z", None, "2019-01-01T00:00:00.000Z"],
            ["-01:00", True, "2019-01-01T01:00:00.000Z"],
            ["-01:00", None, "2019-01-01T00:00:00.000-01:00"],
        ],
    )
    def test_arg_tz_ns_unit(self, offset, utc, exp):
        # GH 25546
        arg = "2019-01-01T00:00:00.000" + offset
        result = pd.to_datetime([arg], unit="ns", utc=utc)
        expected = pd.to_datetime([exp]).as_unit("us")
        tm.assert_index_equal(result, expected)

    def test_invalid_origins_oob(self):
        # GH 63419 datetime(1, 1, 1) overflows only for unit='ns'
        msg = "Cannot cast .* to unit='ns' without overflow"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime(list(range(5)), unit="ns", origin=datetime(1, 1, 1))

    def test_preserve_origin_time_resolution(self):
        # GH 63419
        ts = pd.Timestamp("2016-01-01 00:00:00.000001")
        result = pd.to_datetime([1, 2, 3], unit="D", origin=ts)
        expected = pd.DatetimeIndex(
            [
                "2016-01-02 00:00:00.000001",
                "2016-01-03 00:00:00.000001",
                "2016-01-04 00:00:00.000001",
            ],
            dtype="datetime64[us]",
        )
        tm.assert_index_equal(result, expected)

    def test_preserve_series_and_name(self):
        # GH 63419
        origin = pd.Timestamp("2016-01-01")
        data = ["2016-01-02", "2016-01-03", "2016-01-04"]
        arg = pd.Series([1, 2, 3], name="foo")
        result = pd.to_datetime(arg, unit="D", origin=origin)
        assert isinstance(result, pd.Series)
        expected = pd.Series(
            [pd.Timestamp(x) for x in data], dtype="datetime64[us]", name="foo"
        )
        tm.assert_series_equal(result, expected)

        arg = pd.Index([1, 2, 3], name="foo")
        result = pd.to_datetime(arg, unit="D", origin=origin)
        assert isinstance(result, pd.DatetimeIndex)
        expected = pd.DatetimeIndex(data, dtype="datetime64[us]", name="foo")
        tm.assert_index_equal(result, expected)

    def test_origin_errors_coerce_overflow(self):
        # GH#63419: errors="coerce" should turn out-of-bounds offsets into NaT
        # rather than raising.  10**8 days is in bounds; 10**9 overflows the
        # us-resolution result; 10**18 overflows to_timedelta itself
        origin = pd.Timestamp("2016-01-01")
        vals = [1, 2, 10**8, 10**9, 10**18]
        result = pd.to_datetime(vals, unit="D", origin=origin, errors="coerce")
        expected = pd.DatetimeIndex(
            [
                origin + pd.Timedelta(days=1),
                origin + pd.Timedelta(days=2),
                origin + pd.Timedelta(days=10**8),
                pd.NaT,
                pd.NaT,
            ],
            dtype="datetime64[us]",
        )
        tm.assert_index_equal(result, expected)

        result = pd.to_datetime(
            pd.Series(vals), unit="D", origin=origin, errors="coerce"
        )
        tm.assert_series_equal(result, pd.Series(expected))

        assert pd.to_datetime(10**9, unit="D", origin=origin, errors="coerce") is pd.NaT

        # errors="raise" still raises
        msg = "Cannot cast .* without overflow"
        with pytest.raises(OutOfBoundsTimedelta, match=msg):
            pd.to_datetime([1, 2, 10**18], unit="D", origin=origin)

        msg = "cannot add values to origin"
        with pytest.raises(OutOfBoundsDatetime, match=msg):
            pd.to_datetime([1, 2, 10**8], unit="D", origin=origin.as_unit("ns"))

    def test_origin_errors_coerce_overflow_boundary(self):
        # GH#63419 exact boundary: with origin 16801 days past the epoch, the
        # largest value representable in the us-resolution result is
        # (2**63 - 1) // (86400 * 10**6) - 16801 days
        origin = pd.Timestamp("2016-01-01")
        bound = (2**63 - 1) // (86400 * 10**6) - 16801
        result = pd.to_datetime(
            [bound, bound + 1], unit="D", origin=origin, errors="coerce"
        )
        assert result[0] == origin + pd.Timedelta(days=bound)
        assert result[1] is pd.NaT

        # values just past the boundary raise OutOfBoundsDatetime (not a bare
        # OverflowError) under errors="raise"
        with pytest.raises(OutOfBoundsDatetime, match="cannot add values"):
            pd.to_datetime([bound + 1], unit="D", origin=origin)

    def test_origin_oob_for_unit_coerce(self):
        # GH#63419 origin not representable in the result resolution; under
        # errors="coerce" all entries become NaT instead of raising
        result = pd.to_datetime(
            list(range(5)), unit="ns", origin=datetime(1, 1, 1), errors="coerce"
        )
        expected = pd.DatetimeIndex([pd.NaT] * 5, dtype="datetime64[ns]")
        tm.assert_index_equal(result, expected)


class TestShouldCache:
    @pytest.mark.parametrize(
        "listlike,do_caching",
        [
            ([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], False),
            ([1, 1, 1, 1, 4, 5, 6, 7, 8, 9], True),
        ],
    )
    def test_should_cache(self, listlike, do_caching):
        assert (
            tools.should_cache(listlike, check_count=len(listlike), unique_share=0.7)
            == do_caching
        )

    @pytest.mark.parametrize(
        "unique_share,check_count, err_message",
        [
            (0.5, 11, r"check_count must be in next bounds: \[0; len\(arg\)\]"),
            (10, 2, r"unique_share must be in next bounds: \(0; 1\)"),
        ],
    )
    def test_should_cache_errors(self, unique_share, check_count, err_message):
        arg = [5] * 10

        with pytest.raises(AssertionError, match=err_message):
            tools.should_cache(arg, unique_share, check_count)

    @pytest.mark.parametrize(
        "listlike",
        [
            (deque([pd.Timestamp("2010-06-02 09:30:00")] * 51)),
            ([pd.Timestamp("2010-06-02 09:30:00")] * 51),
            (tuple([pd.Timestamp("2010-06-02 09:30:00")] * 51)),
        ],
    )
    def test_no_slicing_errors_in_should_cache(self, listlike):
        # GH#29403
        assert tools.should_cache(listlike) is True


class TestShouldCacheEarlyBail:
    # GH#65380 should_cache returns False in O(1) for
    # inputs where caching cannot help.

    @pytest.mark.parametrize(
        "arg, kwargs",
        [
            # unit is not None
            (np.arange(100, dtype="int64"), {"unit": "s"}),
            # arg.dtype is np.datetime64
            (
                pd.date_range("2020-01-01", periods=100, freq="s").to_numpy(),
                {},
            ),
            # arg.dtype is DatetimeTZDtype
            (
                pd.date_range("2020-01-01", periods=100, freq="s", tz="US/Eastern"),
                {},
            ),
            # ISO 8601 format, handled by the vectorized C parser
            (["2020-01-01"] * 100, {"format": "%Y-%m-%d"}),
            (["20200101"] * 100, {"format": "%Y%m%d"}),
            (["2020-01-01T00:00:00-0800"] * 100, {"format": "%Y-%m-%dT%H:%M:%S%z"}),
        ],
    )
    def test_should_cache_returns_false(self, arg, kwargs):
        assert tools.should_cache(arg, **kwargs) is False

    @td.skip_if_no("pyarrow")
    def test_should_cache_returns_false_arrow(self):
        # ArrowDtype with Timestamp type
        import pyarrow as pa

        arr = ArrowExtensionArray(
            pa.array(
                pd.date_range("2020-01-01", periods=100, freq="s").to_numpy(),
                type=pa.timestamp("ns"),
            )
        )
        idx = pd.Index(arr)
        assert tools.should_cache(idx) is False

    @pytest.mark.parametrize(
        "arg, fmt",
        [
            (["19MAY11"] * 100, "%d%b%y"),
            (["10/11/2018 00:00:00.045-07:00"] * 100, "%m/%d/%Y %H:%M:%S.%f%z"),
            (["2020-01-01", "01/02/2020"] * 50, "mixed"),
        ],
    )
    def test_should_cache_non_iso_format_not_skipped(self, arg, fmt):
        # GH#65380, asv-runner#137: a non-ISO ``format`` must NOT disable
        # caching. Highly-duplicated strings still benefit from caching with
        # a (slow-parsing) strptime format, so should_cache returns True.
        assert tools.should_cache(pd.Index(arg), format=fmt) is True


def test_nullable_integer_to_datetime():
    # Test for #30050
    ser = pd.Series([1, 2, None, 2**61, None], dtype="Int64")
    ser_copy = ser.copy()

    res = pd.to_datetime(ser, unit="ns")

    expected = pd.Series(
        [
            np.datetime64("1970-01-01 00:00:00.000000001"),
            np.datetime64("1970-01-01 00:00:00.000000002"),
            np.datetime64("NaT", "ns"),
            np.datetime64("2043-01-25 23:56:49.213693952"),
            np.datetime64("NaT", "ns"),
        ]
    )
    tm.assert_series_equal(res, expected)
    # Check that ser isn't mutated
    tm.assert_series_equal(ser, ser_copy)


@pytest.mark.parametrize("klass", [np.array, list])
def test_na_to_datetime(nulls_fixture, klass):
    if isinstance(nulls_fixture, Decimal):
        with pytest.raises(TypeError, match="not convertible to datetime"):
            pd.to_datetime(klass([nulls_fixture]))

    else:
        result = pd.to_datetime(klass([nulls_fixture]))

        assert result[0] is pd.NaT


@pytest.mark.parametrize("errors", ["raise", "coerce"])
@pytest.mark.parametrize(
    "args, format",
    [
        (["03/24/2016", "03/25/2016", ""], "%m/%d/%Y"),
        (["2016-03-24", "2016-03-25", ""], "%Y-%m-%d"),
    ],
    ids=["non-ISO8601", "ISO8601"],
)
def test_empty_string_datetime(errors, args, format):
    # GH13044, GH50251
    td = pd.Series(args)

    # coerce empty string to pd.NaT
    result = pd.to_datetime(td, format=format, errors=errors)
    expected = pd.Series(["2016-03-24", "2016-03-25", pd.NaT], dtype="datetime64[us]")
    tm.assert_series_equal(expected, result)


def test_empty_string_datetime_coerce__unit():
    # GH13044
    # coerce empty string to pd.NaT
    result = pd.to_datetime([1, ""], unit="s", errors="coerce")
    expected = pd.DatetimeIndex(["1970-01-01 00:00:01", "NaT"], dtype="datetime64[us]")
    tm.assert_index_equal(expected, result)

    # verify that no exception is raised even when errors='raise' is set
    result = pd.to_datetime([1, ""], unit="s", errors="raise")
    tm.assert_index_equal(expected, result)


@pytest.mark.parametrize(
    "kwargs, expected",
    [
        ({}, "2012-10-11"),
        ({"dayfirst": True}, "2012-11-10"),
        ({"yearfirst": True}, "2010-11-12"),
        ({"dayfirst": True, "yearfirst": True}, "2010-12-11"),
    ],
)
def test_to_datetime_unit_dayfirst_yearfirst(kwargs, expected, cache):
    # GH#63472 dayfirst/yearfirst were silently ignored for string entries
    #  when 'unit' was passed
    arg = ["10/11/12"]
    result = pd.to_datetime(arg, unit="s", cache=cache, **kwargs)

    tm.assert_index_equal(result, pd.DatetimeIndex([expected], dtype="datetime64[us]"))
    # passing a unit gives the same result as not passing one
    tm.assert_index_equal(result, pd.to_datetime(arg, cache=cache, **kwargs))


@pytest.mark.parametrize(
    "kwargs, parsed",
    [
        ({"dayfirst": True}, "2012-11-10"),
        ({"yearfirst": True}, "2010-11-12"),
    ],
)
def test_to_datetime_unit_dayfirst_yearfirst_mixed_numeric(kwargs, parsed, cache):
    # GH#63472 the unit still applies to the numeric entries
    result = pd.to_datetime(["10/11/12", 1], unit="s", cache=cache, **kwargs)

    expected = pd.DatetimeIndex([parsed, "1970-01-01 00:00:01"], dtype="datetime64[us]")
    tm.assert_index_equal(result, expected)


def test_to_datetime_monotonic_increasing_index(cache):
    # GH28238
    cstart = start_caching_at
    times = pd.date_range(pd.Timestamp("1980"), periods=cstart, freq="YS")
    times = times.to_frame(index=False, name="DT").sample(n=cstart, random_state=1)
    times.index = times.index.to_series().astype(float) / 1000
    result = pd.to_datetime(times.iloc[:, 0], cache=cache)
    expected = times.iloc[:, 0]
    tm.assert_series_equal(result, expected)


@pytest.mark.parametrize(
    "series_length",
    [40, start_caching_at, (start_caching_at + 1), (start_caching_at + 5)],
)
def test_to_datetime_cache_coerce_50_lines_outofbounds(series_length):
    # GH#45319
    ser = pd.Series(
        [datetime.fromisoformat("1446-04-12 00:00:00+00:00")]
        + ([datetime.fromisoformat("1991-10-20 00:00:00+00:00")] * series_length),
        dtype=object,
    )
    result1 = pd.to_datetime(ser, errors="coerce", utc=True)

    expected1 = pd.Series([pd.Timestamp(x) for x in ser])
    assert expected1.dtype == "M8[us, UTC]"
    tm.assert_series_equal(result1, expected1)

    result3 = pd.to_datetime(ser, errors="raise", utc=True)
    tm.assert_series_equal(result3, expected1)


@pytest.mark.parametrize(
    "categories, dtype",
    [
        (["2019-09-24 17:00:00", "2020-01-02 03:04:05"], "M8[us]"),
        (
            pd.DatetimeIndex(
                ["2019-09-24 17:00:00", "2020-01-02 03:04:05"], dtype="M8[us, UTC]"
            ),
            "M8[us, UTC]",
        ),
    ],
)
@pytest.mark.parametrize("length", [start_caching_at, start_caching_at + 1])
def test_to_datetime_categorical_container(categories, dtype, length):
    # GH#28629 the result dtype must not depend on whether the cache kicked in
    values = list(categories) * length
    cat = pd.Categorical(values[:length])
    expected = pd.DatetimeIndex(values[:length], dtype=dtype)

    tm.assert_index_equal(pd.to_datetime(cat), expected)
    tm.assert_index_equal(
        pd.to_datetime(pd.CategoricalIndex(cat, name="foo")), expected.rename("foo")
    )
    ser = pd.Series(cat, index=pd.RangeIndex(10, 10 + length), name="foo")
    tm.assert_series_equal(
        pd.to_datetime(ser), pd.Series(expected, index=ser.index, name="foo")
    )


@pytest.mark.parametrize("length", [start_caching_at, start_caching_at + 1])
def test_to_datetime_categorical_all_coerced(length):
    # GH#28629 categories that all coerce to NaT leave the lookup object dtype
    cat = pd.Categorical(["foo", "bar"] * length)[:length]
    expected = pd.DatetimeIndex([pd.NaT] * length, dtype="M8[s]")
    # a non-ISO format keeps the cache in play, unlike an ISO one
    kwargs = {"format": "%d/%m/%Y", "errors": "coerce"}

    tm.assert_index_equal(pd.to_datetime(cat, **kwargs), expected)
    tm.assert_series_equal(
        pd.to_datetime(pd.Series(cat), **kwargs), pd.Series(expected)
    )


def test_to_datetime_format_f_parse_nanos():
    # GH 48767
    timestamp = "15/02/2020 02:03:04.123456789"
    timestamp_format = "%d/%m/%Y %H:%M:%S.%f"
    result = pd.to_datetime(timestamp, format=timestamp_format)
    expected = pd.Timestamp(
        year=2020,
        month=2,
        day=15,
        hour=2,
        minute=3,
        second=4,
        microsecond=123456,
        nanosecond=789,
    )
    assert result == expected


def test_to_datetime_mixed_iso8601():
    # https://github.com/pandas-dev/pandas/issues/50411
    result = pd.to_datetime(["2020-01-01", "2020-01-01 05:00:00"], format="ISO8601")
    expected = pd.DatetimeIndex(["2020-01-01 00:00:00", "2020-01-01 05:00:00"])
    tm.assert_index_equal(result, expected)


def test_to_datetime_mixed_other():
    # https://github.com/pandas-dev/pandas/issues/50411
    result = pd.to_datetime(["01/11/2000", "12 January 2000"], format="mixed")
    expected = pd.DatetimeIndex(["2000-01-11", "2000-01-12"])
    tm.assert_index_equal(result, expected)


@pytest.mark.parametrize("exact", [True, False])
@pytest.mark.parametrize("format", ["ISO8601", "mixed"])
def test_to_datetime_mixed_or_iso_exact(exact, format):
    msg = "Cannot use 'exact' when 'format' is 'mixed' or 'ISO8601'"
    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(["2020-01-01"], exact=exact, format=format)


def test_to_datetime_mixed_not_necessarily_iso8601_raise():
    # https://github.com/pandas-dev/pandas/issues/50411
    with pytest.raises(ValueError, match="Time data 01-01-2000 is not ISO8601 format"):
        pd.to_datetime(["2020-01-01", "01-01-2000"], format="ISO8601")


def test_to_datetime_mixed_not_necessarily_iso8601_coerce():
    # https://github.com/pandas-dev/pandas/issues/50411
    result = pd.to_datetime(
        ["2020-01-01", "01-01-2000"], format="ISO8601", errors="coerce"
    )
    tm.assert_index_equal(result, pd.DatetimeIndex(["2020-01-01 00:00:00", pd.NaT]))


def test_to_datetime_iso8601_utc_single_naive():
    # GH#61389
    result = pd.to_datetime("2023-10-15T14:30:00", utc=True, format="ISO8601")
    expected = pd.Timestamp("2023-10-15 14:30:00+00:00")
    assert result == expected


def test_to_datetime_iso8601_utc_mixed_negative_offset():
    # GH#61389
    data = ["2023-10-15T10:30:00-12:00", "2023-10-15T14:30:00"]
    result = pd.to_datetime(data, utc=True, format="ISO8601")

    expected = pd.DatetimeIndex(
        [
            pd.Timestamp("2023-10-15 22:30:00+00:00"),
            pd.Timestamp("2023-10-15 14:30:00+00:00"),
        ]
    )
    tm.assert_index_equal(result, expected)


def test_to_datetime_iso8601_utc_mixed_positive_offset():
    # GH#61389
    data = ["2023-10-15T10:30:00+08:00", "2023-10-15T14:30:00"]
    result = pd.to_datetime(data, utc=True, format="ISO8601")

    expected = pd.DatetimeIndex(
        [
            pd.Timestamp("2023-10-15 02:30:00+00:00"),
            pd.Timestamp("2023-10-15 14:30:00+00:00"),
        ]
    )
    tm.assert_index_equal(result, expected)


def test_to_datetime_iso8601_utc_mixed_both_offsets():
    # GH#61389
    data = [
        "2023-10-15T10:30:00+08:00",
        "2023-10-15T12:30:00-05:00",
        "2023-10-15T14:30:00",
    ]
    result = pd.to_datetime(data, utc=True, format="ISO8601")

    expected = pd.DatetimeIndex(
        [
            pd.Timestamp("2023-10-15 02:30:00+00:00"),
            pd.Timestamp("2023-10-15 17:30:00+00:00"),
            pd.Timestamp("2023-10-15 14:30:00+00:00"),
        ]
    )
    tm.assert_index_equal(result, expected)


def test_unknown_tz_raises():
    # GH#18702, GH#51476
    dtstr = "2014 Jan 9 05:15 FAKE"
    msg = '.*unrecognized timezone "FAKE".'
    with pytest.raises(ValueError, match=msg):
        pd.Timestamp(dtstr)

    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(dtstr)
    with pytest.raises(ValueError, match=msg):
        pd.to_datetime([dtstr])


def test_unformatted_input_raises():
    valid, invalid = "2024-01-01", "N"
    ser = pd.Series([valid] * start_caching_at + [invalid])
    msg = 'time data "N" doesn\'t match format "%Y-%m-%d"'

    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(ser, format="%Y-%m-%d", exact=True, cache=True)


def test_from_numeric_arrow_dtype(any_numeric_ea_dtype):
    # GH 52425
    pytest.importorskip("pyarrow")
    ser = pd.Series([1, 2], dtype=f"{any_numeric_ea_dtype.lower()}[pyarrow]")
    result = pd.to_datetime(ser)
    expected = pd.Series([1, 2], dtype="datetime64[ns]")
    tm.assert_series_equal(result, expected)


def test_to_datetime_with_empty_str_utc_false_format_mixed():
    # GH 50887
    vals = ["2020-01-01 00:00+00:00", ""]
    result = pd.to_datetime(vals, format="mixed")
    expected = pd.Index(
        [pd.Timestamp("2020-01-01 00:00+00:00"), "NaT"], dtype="M8[us, UTC]"
    )
    tm.assert_index_equal(result, expected)

    # Check that a couple of other similar paths work the same way
    alt = pd.to_datetime(vals)
    tm.assert_index_equal(alt, expected)
    alt2 = pd.DatetimeIndex(vals)
    tm.assert_index_equal(alt2, expected)


def test_to_datetime_with_empty_str_utc_false_offsets_and_format_mixed():
    # GH#50887, GH#57275
    msg = "Mixed timezones detected. Pass utc=True in to_datetime"

    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(
            ["2020-01-01 00:00+00:00", "2020-01-01 00:00+02:00", ""], format="mixed"
        )


def test_to_datetime_mixed_tzs_mixed_types():
    # GH#55793, GH#55693 mismatched tzs but one is str and other is
    #  datetime object
    ts = pd.Timestamp("2016-01-02 03:04:05", tz="US/Pacific")
    dtstr = "2023-10-30 15:06+01"
    arr = [ts, dtstr]

    msg = (
        "Mixed timezones detected. Pass utc=True in to_datetime or tz='UTC' "
        "in DatetimeIndex to convert to a common timezone"
    )
    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(arr)
    with pytest.raises(ValueError, match=msg):
        pd.to_datetime(arr, format="mixed")
    with pytest.raises(ValueError, match=msg):
        pd.DatetimeIndex(arr)


def test_to_datetime_mixed_types_matching_tzs():
    # GH#55793
    dtstr = "2023-11-01 09:22:03-07:00"
    ts = pd.Timestamp(dtstr)
    arr = [ts, dtstr]
    res1 = pd.to_datetime(arr)
    res2 = pd.to_datetime(arr[::-1])[::-1]
    res3 = pd.to_datetime(arr, format="mixed")
    res4 = pd.DatetimeIndex(arr)

    expected = pd.DatetimeIndex([ts, ts])
    tm.assert_index_equal(res1, expected)
    tm.assert_index_equal(res2, expected)
    tm.assert_index_equal(res3, expected)
    tm.assert_index_equal(res4, expected)


dtstr = "2020-01-01 00:00+00:00"
ts = pd.Timestamp(dtstr)


@pytest.mark.parametrize(
    "aware_val",
    [dtstr, pd.Timestamp(dtstr)],
    ids=lambda x: type(x).__name__,
)
@pytest.mark.parametrize(
    "naive_val",
    [dtstr[:-6], ts.tz_localize(None), ts.date(), ts.asm8, ts.value, float(ts.value)],
    ids=lambda x: type(x).__name__,
)
@pytest.mark.parametrize("naive_first", [True, False])
def test_to_datetime_mixed_awareness_mixed_types(aware_val, naive_val, naive_first):
    # GH#55793, GH#55693, GH#57275
    # Empty string parses to NaT
    vals = [aware_val, naive_val, ""]

    vec = vals
    if naive_first:
        # alas, the behavior is order-dependent, so we test both ways
        vec = [naive_val, aware_val, ""]

    # both_strs-> paths that were previously already deprecated with warning
    #  issued in _array_to_datetime_object
    both_strs = isinstance(aware_val, str) and isinstance(naive_val, str)
    has_numeric = isinstance(naive_val, (int, float))
    both_datetime = isinstance(naive_val, datetime) and isinstance(aware_val, datetime)

    mixed_msg = (
        "Mixed timezones detected. Pass utc=True in to_datetime or tz='UTC' "
        "in DatetimeIndex to convert to a common timezone"
    )

    first_non_null = next(x for x in vec if x != "")
    # if first_non_null is a not a string, _guess_datetime_format_for_array
    #  doesn't guess a format so we don't go through array_strptime
    if not isinstance(first_non_null, str):
        # that case goes through array_strptime which has different behavior
        msg = mixed_msg
        if naive_first and isinstance(aware_val, pd.Timestamp):
            if isinstance(naive_val, pd.Timestamp):
                msg = "Tz-aware datetime.datetime cannot be converted to datetime64"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec)
        else:
            if not naive_first and both_datetime:
                msg = "Cannot mix tz-aware with tz-naive values"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec)

        # No warning/error with utc=True
        pd.to_datetime(vec, utc=True)

    elif has_numeric and vec.index(aware_val) < vec.index(naive_val):
        msg = "time data .* doesn't match format"
        # GH#55663 - int/float with format is deprecated
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec)
        with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec, utc=True)

    elif both_strs and vec.index(aware_val) < vec.index(naive_val):
        msg = r"time data \"2020-01-01 00:00\" doesn't match format"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec)
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec, utc=True)

    elif both_strs and vec.index(naive_val) < vec.index(aware_val):
        msg = "unconverted data remains when parsing with format"
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec)
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec, utc=True)

    else:
        msg = mixed_msg
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec)

        # No warning/error with utc=True
        pd.to_datetime(vec, utc=True)

    if both_strs:
        msg = mixed_msg
        with pytest.raises(ValueError, match=msg):
            pd.to_datetime(vec, format="mixed")
        with pytest.raises(ValueError, match=msg):
            pd.DatetimeIndex(vec)
    else:
        msg = mixed_msg
        if naive_first and isinstance(aware_val, pd.Timestamp):
            if isinstance(naive_val, pd.Timestamp):
                msg = "Tz-aware datetime.datetime cannot be converted to datetime64"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec, format="mixed")
            with pytest.raises(ValueError, match=msg):
                pd.DatetimeIndex(vec)
        else:
            if not naive_first and both_datetime:
                msg = "Cannot mix tz-aware with tz-naive values"
            with pytest.raises(ValueError, match=msg):
                pd.to_datetime(vec, format="mixed")
            with pytest.raises(ValueError, match=msg):
                pd.DatetimeIndex(vec)


def test_to_datetime_wrapped_datetime64_ps():
    # GH#60341
    result = pd.to_datetime([np.datetime64(1901901901901, "ps")])
    expected = pd.DatetimeIndex(
        ["1970-01-01 00:00:01.901901901"], dtype="datetime64[ns]", freq=None
    )
    tm.assert_index_equal(result, expected)


def test_to_datetime_lxml_elementunicoderesult_with_format(cache):
    etree = pytest.importorskip("lxml.etree")

    s = "2025-02-05 16:59:57"
    node = etree.XML(f"<date>{s}</date>")
    val = node.xpath("/date/node()")[0]  # _ElementUnicodeResult

    out = pd.to_datetime(pd.Series([val]), format="%Y-%m-%d %H:%M:%S", cache=cache)
    assert out.iloc[0] == pd.Timestamp(s)


def test_to_datetime_missing_component_no_runtime_warning():
    df = pd.DataFrame(
        {
            "year": [2023, 2023],
            "month": [12, 2],
            "day": [1, 2],
            "hour": [2, np.nan],
        }
    )

    with tm.assert_produces_warning(None):
        result = pd.to_datetime(df)

    assert result.iloc[1] is pd.NaT


def test_to_datetime_narrow_float_with_format_no_runtime_warning():
    # GH#56996 the NaT-sentinel check compared the raw np.float16 against the
    #  int64 sentinel, which cast the sentinel down to float16 and emitted a
    #  spurious RuntimeWarning before the value reached the parser
    arr = np.array([np.float16(1970)], dtype=object)
    with tm.assert_produces_warning(Pandas4Warning, match="integer or float"):
        result = pd.to_datetime(arr, format="%Y", errors="coerce")

    assert result[0] is pd.NaT


def test_to_datetime_format_N_directive():
    # GH 65863
    result = pd.to_datetime(
        "2024-05-01 12:00:00.123456789", format="%Y-%m-%d %H:%M:%S.%N"
    )
    expected = pd.Timestamp("2024-05-01 12:00:00.123456789")
    tm.assert_equal(result, expected)

    # %N must be exactly 9 digits
    msg = 'time data "2024-05-01 12:00:00.12345" doesn\'t match format'
    with pytest.raises(ValueError, match=msg):
        pd.to_datetime("2024-05-01 12:00:00.12345", format="%Y-%m-%d %H:%M:%S.%N")

    # While %f can be fewer:
    result_f = pd.to_datetime(
        "2024-05-01 12:00:00.12345", format="%Y-%m-%d %H:%M:%S.%f"
    )
    expected_f = pd.Timestamp("2024-05-01 12:00:00.123450")
    tm.assert_equal(result_f, expected_f)


def test_stringify_numeric_column_int64_boundary():
    # GH#55663 float64(2**63) is just above int64.max and must take the
    #  plain-str path, not an undefined float->int64 cast
    ser = pd.Series([float(2**63), float(2**63 - 2048), np.inf, np.nan, 20000101.0])
    result = tools.stringify_numeric_column(ser)
    expected = pd.Series(
        ["9.223372036854776e+18", "9223372036854773760", "inf", np.nan, "20000101"],
        dtype=object,
    )
    tm.assert_series_equal(result, expected)


def test_dataframe_assemble_duplicate_index_with_nan(cache):
    # GH#55663 duplicate index labels plus a NaN component must not trip
    #  label alignment in the numeric-to-string cast
    df = pd.DataFrame(
        {"year": [2000, 2001, np.nan], "month": [1, 1, 1], "day": [1, 1, 1]},
        index=[0, 0, 1],
    )
    result = pd.to_datetime(df, cache=cache)
    expected = pd.Series(
        [pd.Timestamp("2000-01-01"), pd.Timestamp("2001-01-01"), pd.NaT],
        index=[0, 0, 1],
        dtype="M8[us]",
    )
    tm.assert_series_equal(result, expected)


# GH#66510 iNaT == INT64_MIN, one below Timestamp.min. A value that renders or
#  tz-shifts onto it is not NaT but reads back as NaT once stored. `{}` takes the
#  last three digits: "192" is the sentinel, "193" is one nanosecond later and
#  must keep parsing. to_datetime has several parser legs, and the format alone
#  does not determine which one a value takes -- with format=None the "192"
#  values take the array_to_datetime route, because guess_datetime_format
#  validates its guess by round-tripping through array_strptime and discards it
#  when that raises, while the "193" values keep their guess and go through
#  array_strptime. Both are worth pinning; the labels below are for "192".
NAT_SENTINEL_PARSE_ROUTES = [
    # array_to_datetime -> convert_str_to_tsobject (format guess discarded)
    ("1677-09-21 00:12:43.145224{}", None),
    ("1677-09-21 01:12:43.145224{}+01:00", None),
    # array_strptime, ISO route (string_to_dts), naive
    ("1677-09-21 00:12:43.145224{}", "ISO8601"),
    # array_strptime, ISO route, embedded offset (checked_sub leg)
    ("1677-09-21 01:12:43.145224{}+01:00", "ISO8601"),
    # array_strptime, _parse_with_format route, naive
    ("21/09/1677 00:12:43.145224{}", "%d/%m/%Y %H:%M:%S.%f"),
    # array_strptime, _parse_with_format route, %z fixed offset (checked_sub leg)
    ("21/09/1677 01:12:43.145224{}+01:00", "%d/%m/%Y %H:%M:%S.%f%z"),
    # array_strptime, _parse_with_format route, named tz
    #  (tz_localize_to_utc_single leg)
    ("1677-09-21 01:12:43.145224{} Etc/GMT-1", "%Y-%m-%d %H:%M:%S.%f %Z"),
    # array_to_datetime -> convert_str_to_tsobject, explicitly
    ("1677-09-21 00:12:43.145224{}", "mixed"),
    ("1677-09-21 01:12:43.145224{}+01:00", "mixed"),
]


@pytest.mark.parametrize("template, format", NAT_SENTINEL_PARSE_ROUTES)
def test_to_datetime_hits_nat_sentinel(template, format):
    # GH#66510 every parser leg must reject the sentinel rather than silently
    #  producing NaT
    with pytest.raises(OutOfBoundsDatetime, match="Out of bounds nanosecond timestamp"):
        pd.to_datetime([template.format("192")], format=format)


@pytest.mark.parametrize("template, format", NAT_SENTINEL_PARSE_ROUTES)
def test_to_datetime_hits_nat_sentinel_coerce(template, format):
    # GH#66510 errors="coerce" still asks for NaT
    result = pd.to_datetime([template.format("192")], format=format, errors="coerce")
    assert result[0] is pd.NaT


@pytest.mark.parametrize("template, format", NAT_SENTINEL_PARSE_ROUTES)
def test_to_datetime_nat_sentinel_neighbour(template, format):
    # GH#66510 one nanosecond later is representable and must still parse
    result = pd.to_datetime([template.format("193")], format=format)
    assert result[0]._value == -(2**63) + 1


@pytest.mark.parametrize(
    "value, format, wall",
    [
        ("1677-09-21 00:12:43.145224192", "ISO8601", "1677-09-21 00:12:43.145224192"),
        (
            "1677-09-21 01:12:43.145224192 Etc/GMT-1",
            "%Y-%m-%d %H:%M:%S.%f %Z",
            "1677-09-21 01:12:43.145224192",
        ),
    ],
)
def test_to_datetime_nat_sentinel_message(value, format, wall):
    # GH#66510 the message names the sub-second digits -- truncating to seconds
    #  would name a representable value
    msg = re.escape(f"Out of bounds nanosecond timestamp: {wall}")
    with pytest.raises(OutOfBoundsDatetime, match=msg):
        pd.to_datetime([value], format=format)


@pytest.mark.parametrize(
    "format",
    [
        None,  # array_strptime, ISO route
        "ISO8601",  # array_strptime, ISO route
        "%Y-%m-%d %H:%M:%S.%f%z",  # array_strptime, _parse_with_format route
    ],
)
def test_to_datetime_offset_shifts_off_nat_sentinel(format):
    # GH#66510 to_datetime counterpart of
    #  test_constructor_offset_shifts_off_nat_sentinel: the wall time renders onto
    #  the sentinel but the westward shift moves it back in bounds, so rejecting
    #  before the shift would be wrong.
    # utc=True so the result is usable: keeping the +01:00 offset would leave a
    #  value that boxes back through check_dts_bounds on the sentinel wall time.
    result = pd.to_datetime(
        ["1677-09-21 00:12:43.145224192-01:00"], format=format, utc=True
    )
    assert result.asi8[0] == -(2**63) + 3600 * 10**9
    assert result[0] == pd.Timestamp("1677-09-21 01:12:43.145224192", tz="UTC")


@pytest.mark.parametrize("box", [list, np.array, pd.Index, pd.Series])
def test_to_datetime_quarterly_string_warns_once(box):
    # GH#50907 the deprecation is emitted once per call, not once per element
    values = box([f"{2000 + i % 50}Q{1 + i % 4}" for i in range(200)])
    with tm.assert_produces_warning(
        Pandas4Warning,
        match="quarterly string is deprecated",
        raise_on_extra_warnings=False,
    ) as record:
        pd.to_datetime(values)

    quarter_warnings = [
        rec for rec in record if "quarterly string is deprecated" in str(rec.message)
    ]
    assert len(quarter_warnings) == 1


def test_datetime_index_quarterly_string_warns_once():
    # GH#50907
    values = [f"{2000 + i % 50}Q{1 + i % 4}" for i in range(200)]
    with tm.assert_produces_warning(
        Pandas4Warning, match="quarterly string is deprecated"
    ) as record:
        pd.DatetimeIndex(values)

    quarter_warnings = [
        rec for rec in record if "quarterly string is deprecated" in str(rec.message)
    ]
    assert len(quarter_warnings) == 1


def test_to_datetime_quarterly_string_warns_once_tzaware():
    # GH#50907 the tz-aware array path goes through array_to_datetime_with_tz
    values = [f"{2000 + i % 50}Q{1 + i % 4}" for i in range(200)]
    with tm.assert_produces_warning(
        Pandas4Warning, match="quarterly string is deprecated"
    ) as record:
        pd.DatetimeIndex(values, tz="US/Pacific")

    quarter_warnings = [
        rec for rec in record if "quarterly string is deprecated" in str(rec.message)
    ]
    assert len(quarter_warnings) == 1


def test_to_datetime_quarterly_string_warns_once_mixed_resolution():
    # GH#50907 mismatched resolutions re-parse the whole array; the deprecation
    # must not be emitted again on the second pass
    values = ["2014Q2", "2015-01-01 00:00:00.000000001"]
    with tm.assert_produces_warning(
        Pandas4Warning,
        match="quarterly string is deprecated",
        raise_on_extra_warnings=False,
    ) as record:
        pd.to_datetime(values, cache=False)

    quarter_warnings = [
        rec for rec in record if "quarterly string is deprecated" in str(rec.message)
    ]
    assert len(quarter_warnings) == 1


def test_to_datetime_quarterly_string_warns_once_mixed_resolution_tzaware():
    # GH#50907 same as the above for the array_to_datetime_with_tz recursion
    values = ["2014Q2", "2015-01-01 00:00:00.000000001"]
    with tm.assert_produces_warning(
        Pandas4Warning, match="quarterly string is deprecated"
    ) as record:
        result = pd.DatetimeIndex(values, tz="UTC")

    quarter_warnings = [
        rec for rec in record if "quarterly string is deprecated" in str(rec.message)
    ]
    assert len(quarter_warnings) == 1
    assert result[0] == pd.Timestamp("2014-04-01", tz="UTC")
