""" common type operations """

import numpy as np
from pandas.compat import (string_types, text_type, binary_type,
                           PY3, PY36)
from pandas._libs import algos, lib
from .dtypes import (CategoricalDtype, CategoricalDtypeType,
                     DatetimeTZDtype, DatetimeTZDtypeType,
                     PeriodDtype, PeriodDtypeType,
                     IntervalDtype, IntervalDtypeType,
                     ExtensionDtype)
from .generic import (ABCCategorical, ABCPeriodIndex,
                      ABCDatetimeIndex, ABCSeries,
                      ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex,
                      ABCIndexClass)
from .inference import is_string_like
from .inference import *  # noqa


_POSSIBLY_CAST_DTYPES = set([np.dtype(t).name
                             for t in ['O', 'int8', 'uint8', 'int16', 'uint16',
                                       'int32', 'uint32', 'int64', 'uint64']])

_NS_DTYPE = np.dtype('M8[ns]')
_TD_DTYPE = np.dtype('m8[ns]')
_INT64_DTYPE = np.dtype(np.int64)

# oh the troubles to reduce import time
_is_scipy_sparse = None

_ensure_float64 = algos.ensure_float64
_ensure_float32 = algos.ensure_float32


def _ensure_float(arr):
    """
    Ensure that an array object has a float dtype if possible.

    Parameters
    ----------
    arr : array-like
        The array whose data type we want to enforce as float.

    Returns
    -------
    float_arr : The original array cast to the float dtype if
                possible. Otherwise, the original array is returned.
    """

    if issubclass(arr.dtype.type, (np.integer, np.bool_)):
        arr = arr.astype(float)
    return arr


_ensure_uint64 = algos.ensure_uint64
_ensure_int64 = algos.ensure_int64
_ensure_int32 = algos.ensure_int32
_ensure_int16 = algos.ensure_int16
_ensure_int8 = algos.ensure_int8
_ensure_platform_int = algos.ensure_platform_int
_ensure_object = algos.ensure_object


def _ensure_categorical(arr):
    """
    Ensure that an array-like object is a Categorical (if not already).

    Parameters
    ----------
    arr : array-like
        The array that we want to convert into a Categorical.

    Returns
    -------
    cat_arr : The original array cast as a Categorical. If it already
              is a Categorical, we return as is.
    """

    if not is_categorical(arr):
        from pandas import Categorical
        arr = Categorical(arr)
    return arr


def is_object_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the object dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is of the object dtype.

    Examples
    --------
    >>> is_object_dtype(object)
    True
    >>> is_object_dtype(int)
    False
    >>> is_object_dtype(np.array([], dtype=object))
    True
    >>> is_object_dtype(np.array([], dtype=int))
    False
    >>> is_object_dtype([1, 2, 3])
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, np.object_)


def is_sparse(arr):
    """
    Check whether an array-like is a pandas sparse array.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a pandas sparse array.

    Examples
    --------
    >>> is_sparse(np.array([1, 2, 3]))
    False
    >>> is_sparse(pd.SparseArray([1, 2, 3]))
    True
    >>> is_sparse(pd.SparseSeries([1, 2, 3]))
    True

    This function checks only for pandas sparse array instances, so
    sparse arrays from other libraries will return False.

    >>> from scipy.sparse import bsr_matrix
    >>> is_sparse(bsr_matrix([1, 2, 3]))
    False
    """

    return isinstance(arr, (ABCSparseArray, ABCSparseSeries))


def is_scipy_sparse(arr):
    """
    Check whether an array-like is a scipy.sparse.spmatrix instance.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a
              scipy.sparse.spmatrix instance.

    Notes
    -----
    If scipy is not installed, this function will always return False.

    Examples
    --------
    >>> from scipy.sparse import bsr_matrix
    >>> is_scipy_sparse(bsr_matrix([1, 2, 3]))
    True
    >>> is_scipy_sparse(pd.SparseArray([1, 2, 3]))
    False
    >>> is_scipy_sparse(pd.SparseSeries([1, 2, 3]))
    False
    """

    global _is_scipy_sparse

    if _is_scipy_sparse is None:
        try:
            from scipy.sparse import issparse as _is_scipy_sparse
        except ImportError:
            _is_scipy_sparse = lambda _: False

    return _is_scipy_sparse(arr)


def is_categorical(arr):
    """
    Check whether an array-like is a Categorical instance.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is of a Categorical instance.

    Examples
    --------
    >>> is_categorical([1, 2, 3])
    False

    Categoricals, Series Categoricals, and CategoricalIndex will return True.

    >>> cat = pd.Categorical([1, 2, 3])
    >>> is_categorical(cat)
    True
    >>> is_categorical(pd.Series(cat))
    True
    >>> is_categorical(pd.CategoricalIndex([1, 2, 3]))
    True
    """

    return isinstance(arr, ABCCategorical) or is_categorical_dtype(arr)


def is_datetimetz(arr):
    """
    Check whether an array-like is a datetime array-like with a timezone
    component in its dtype.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a datetime array-like with
              a timezone component in its dtype.

    Examples
    --------
    >>> is_datetimetz([1, 2, 3])
    False

    Although the following examples are both DatetimeIndex objects,
    the first one returns False because it has no timezone component
    unlike the second one, which returns True.

    >>> is_datetimetz(pd.DatetimeIndex([1, 2, 3]))
    False
    >>> is_datetimetz(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern"))
    True

    The object need not be a DatetimeIndex object. It just needs to have
    a dtype which has a timezone component.

    >>> dtype = DatetimeTZDtype("ns", tz="US/Eastern")
    >>> s = pd.Series([], dtype=dtype)
    >>> is_datetimetz(s)
    True
    """

    # TODO: do we need this function?
    # It seems like a repeat of is_datetime64tz_dtype.

    return ((isinstance(arr, ABCDatetimeIndex) and
             getattr(arr, 'tz', None) is not None) or
            is_datetime64tz_dtype(arr))


def is_period(arr):
    """
    Check whether an array-like is a periodical index.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a periodical index.

    Examples
    --------
    >>> is_period([1, 2, 3])
    False
    >>> is_period(pd.Index([1, 2, 3]))
    False
    >>> is_period(pd.PeriodIndex(["2017-01-01"], freq="D"))
    True
    """

    # TODO: do we need this function?
    # It seems like a repeat of is_period_arraylike.
    return isinstance(arr, ABCPeriodIndex) or is_period_arraylike(arr)


def is_datetime64_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the datetime64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is of
              the datetime64 dtype.

    Examples
    --------
    >>> is_datetime64_dtype(object)
    False
    >>> is_datetime64_dtype(np.datetime64)
    True
    >>> is_datetime64_dtype(np.array([], dtype=int))
    False
    >>> is_datetime64_dtype(np.array([], dtype=np.datetime64))
    True
    >>> is_datetime64_dtype([1, 2, 3])
    False
    """

    if arr_or_dtype is None:
        return False
    try:
        tipo = _get_dtype_type(arr_or_dtype)
    except TypeError:
        return False
    return issubclass(tipo, np.datetime64)


def is_datetime64tz_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of a DatetimeTZDtype dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is of
              a DatetimeTZDtype dtype.

    Examples
    --------
    >>> is_datetime64tz_dtype(object)
    False
    >>> is_datetime64tz_dtype([1, 2, 3])
    False
    >>> is_datetime64tz_dtype(pd.DatetimeIndex([1, 2, 3]))  # tz-naive
    False
    >>> is_datetime64tz_dtype(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern"))
    True

    >>> dtype = DatetimeTZDtype("ns", tz="US/Eastern")
    >>> s = pd.Series([], dtype=dtype)
    >>> is_datetime64tz_dtype(dtype)
    True
    >>> is_datetime64tz_dtype(s)
    True
    """

    if arr_or_dtype is None:
        return False
    return DatetimeTZDtype.is_dtype(arr_or_dtype)


def is_timedelta64_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the timedelta64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is
              of the timedelta64 dtype.

    Examples
    --------
    >>> is_timedelta64_dtype(object)
    False
    >>> is_timedelta64_dtype(np.timedelta64)
    True
    >>> is_timedelta64_dtype([1, 2, 3])
    False
    >>> is_timedelta64_dtype(pd.Series([], dtype="timedelta64[ns]"))
    True
    >>> is_timedelta64_dtype('0 days')
    False
    """

    if arr_or_dtype is None:
        return False
    try:
        tipo = _get_dtype_type(arr_or_dtype)
    except:
        return False
    return issubclass(tipo, np.timedelta64)


def is_period_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the Period dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is of the Period dtype.

    Examples
    --------
    >>> is_period_dtype(object)
    False
    >>> is_period_dtype(PeriodDtype(freq="D"))
    True
    >>> is_period_dtype([1, 2, 3])
    False
    >>> is_period_dtype(pd.Period("2017-01-01"))
    False
    >>> is_period_dtype(pd.PeriodIndex([], freq="A"))
    True
    """

    # TODO: Consider making Period an instance of PeriodDtype
    if arr_or_dtype is None:
        return False
    return PeriodDtype.is_dtype(arr_or_dtype)


def is_interval_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the Interval dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is
              of the Interval dtype.

    Examples
    --------
    >>> is_interval_dtype(object)
    False
    >>> is_interval_dtype(IntervalDtype())
    True
    >>> is_interval_dtype([1, 2, 3])
    False
    >>>
    >>> interval = pd.Interval(1, 2, closed="right")
    >>> is_interval_dtype(interval)
    False
    >>> is_interval_dtype(pd.IntervalIndex([interval]))
    True
    """

    # TODO: Consider making Interval an instance of IntervalDtype
    if arr_or_dtype is None:
        return False
    return IntervalDtype.is_dtype(arr_or_dtype)


def is_categorical_dtype(arr_or_dtype):
    """
    Check whether an array-like or dtype is of the Categorical dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype to check.

    Returns
    -------
    boolean : Whether or not the array-like or dtype is
              of the Categorical dtype.

    Examples
    --------
    >>> is_categorical_dtype(object)
    False
    >>> is_categorical_dtype(CategoricalDtype())
    True
    >>> is_categorical_dtype([1, 2, 3])
    False
    >>> is_categorical_dtype(pd.Categorical([1, 2, 3]))
    True
    >>> is_categorical_dtype(pd.CategoricalIndex([1, 2, 3]))
    True
    """

    if arr_or_dtype is None:
        return False
    return CategoricalDtype.is_dtype(arr_or_dtype)


def is_string_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of the string dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the string dtype.

    Examples
    --------
    >>> is_string_dtype(str)
    True
    >>> is_string_dtype(object)
    True
    >>> is_string_dtype(int)
    False
    >>>
    >>> is_string_dtype(np.array(['a', 'b']))
    True
    >>> is_string_dtype(pd.Series([1, 2]))
    False
    """

    # TODO: gh-15585: consider making the checks stricter.

    if arr_or_dtype is None:
        return False
    try:
        dtype = _get_dtype(arr_or_dtype)
        return dtype.kind in ('O', 'S', 'U') and not is_period_dtype(dtype)
    except TypeError:
        return False


def is_period_arraylike(arr):
    """
    Check whether an array-like is a periodical array-like or PeriodIndex.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a periodical
              array-like or PeriodIndex instance.

    Examples
    --------
    >>> is_period_arraylike([1, 2, 3])
    False
    >>> is_period_arraylike(pd.Index([1, 2, 3]))
    False
    >>> is_period_arraylike(pd.PeriodIndex(["2017-01-01"], freq="D"))
    True
    """

    if isinstance(arr, ABCPeriodIndex):
        return True
    elif isinstance(arr, (np.ndarray, ABCSeries)):
        return arr.dtype == object and lib.infer_dtype(arr) == 'period'
    return getattr(arr, 'inferred_type', None) == 'period'


def is_datetime_arraylike(arr):
    """
    Check whether an array-like is a datetime array-like or DatetimeIndex.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a datetime
              array-like or DatetimeIndex.

    Examples
    --------
    >>> is_datetime_arraylike([1, 2, 3])
    False
    >>> is_datetime_arraylike(pd.Index([1, 2, 3]))
    False
    >>> is_datetime_arraylike(pd.DatetimeIndex([1, 2, 3]))
    True
    """

    if isinstance(arr, ABCDatetimeIndex):
        return True
    elif isinstance(arr, (np.ndarray, ABCSeries)):
        return arr.dtype == object and lib.infer_dtype(arr) == 'datetime'
    return getattr(arr, 'inferred_type', None) == 'datetime'


def is_datetimelike(arr):
    """
    Check whether an array-like is a datetime-like array-like.

    Acceptable datetime-like objects are (but not limited to) datetime
    indices, periodic indices, and timedelta indices.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is a datetime-like array-like.

    Examples
    --------
    >>> is_datetimelike([1, 2, 3])
    False
    >>> is_datetimelike(pd.Index([1, 2, 3]))
    False
    >>> is_datetimelike(pd.DatetimeIndex([1, 2, 3]))
    True
    >>> is_datetimelike(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern"))
    True
    >>> is_datetimelike(pd.PeriodIndex([], freq="A"))
    True
    >>> is_datetimelike(np.array([], dtype=np.datetime64))
    True
    >>> is_datetimelike(pd.Series([], dtype="timedelta64[ns]"))
    True
    >>>
    >>> dtype = DatetimeTZDtype("ns", tz="US/Eastern")
    >>> s = pd.Series([], dtype=dtype)
    >>> is_datetimelike(s)
    True
    """

    return (is_datetime64_dtype(arr) or is_datetime64tz_dtype(arr) or
            is_timedelta64_dtype(arr) or
            isinstance(arr, ABCPeriodIndex) or
            is_datetimetz(arr))


def is_dtype_equal(source, target):
    """
    Check if two dtypes are equal.

    Parameters
    ----------
    source : The first dtype to compare
    target : The second dtype to compare

    Returns
    ----------
    boolean : Whether or not the two dtypes are equal.

    Examples
    --------
    >>> is_dtype_equal(int, float)
    False
    >>> is_dtype_equal("int", int)
    True
    >>> is_dtype_equal(object, "category")
    False
    >>> is_dtype_equal(CategoricalDtype(), "category")
    True
    >>> is_dtype_equal(DatetimeTZDtype(), "datetime64")
    False
    """

    try:
        source = _get_dtype(source)
        target = _get_dtype(target)
        return source == target
    except (TypeError, AttributeError):

        # invalid comparison
        # object == category will hit this
        return False


def is_dtype_union_equal(source, target):
    """
    Check whether two arrays have compatible dtypes to do a union.
    numpy types are checked with ``is_dtype_equal``. Extension types are
    checked separately.

    Parameters
    ----------
    source : The first dtype to compare
    target : The second dtype to compare

    Returns
    ----------
    boolean : Whether or not the two dtypes are equal.

    >>> is_dtype_equal("int", int)
    True

    >>> is_dtype_equal(CategoricalDtype(['a', 'b'],
    ...                CategoricalDtype(['b', 'c']))
    True

    >>> is_dtype_equal(CategoricalDtype(['a', 'b'],
    ...                CategoricalDtype(['b', 'c'], ordered=True))
    False
    """
    source = _get_dtype(source)
    target = _get_dtype(target)
    if is_categorical_dtype(source) and is_categorical_dtype(target):
        # ordered False for both
        return source.ordered is target.ordered
    return is_dtype_equal(source, target)


def is_any_int_dtype(arr_or_dtype):
    """
    DEPRECATED: This function will be removed in a future version.

    Check whether the provided array or dtype is of an integer dtype.

    In this function, timedelta64 instances are also considered "any-integer"
    type objects and will return True.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of an integer dtype.

    Examples
    --------
    >>> is_any_int_dtype(str)
    False
    >>> is_any_int_dtype(int)
    True
    >>> is_any_int_dtype(float)
    False
    >>> is_any_int_dtype(np.uint64)
    True
    >>> is_any_int_dtype(np.datetime64)
    False
    >>> is_any_int_dtype(np.timedelta64)
    True
    >>> is_any_int_dtype(np.array(['a', 'b']))
    False
    >>> is_any_int_dtype(pd.Series([1, 2]))
    True
    >>> is_any_int_dtype(np.array([], dtype=np.timedelta64))
    True
    >>> is_any_int_dtype(pd.Index([1, 2.]))  # float
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, np.integer)


def is_integer_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of an integer dtype.

    Unlike in `in_any_int_dtype`, timedelta64 instances will return False.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of an integer dtype
              and not an instance of timedelta64.

    Examples
    --------
    >>> is_integer_dtype(str)
    False
    >>> is_integer_dtype(int)
    True
    >>> is_integer_dtype(float)
    False
    >>> is_integer_dtype(np.uint64)
    True
    >>> is_integer_dtype(np.datetime64)
    False
    >>> is_integer_dtype(np.timedelta64)
    False
    >>> is_integer_dtype(np.array(['a', 'b']))
    False
    >>> is_integer_dtype(pd.Series([1, 2]))
    True
    >>> is_integer_dtype(np.array([], dtype=np.timedelta64))
    False
    >>> is_integer_dtype(pd.Index([1, 2.]))  # float
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return (issubclass(tipo, np.integer) and
            not issubclass(tipo, (np.datetime64, np.timedelta64)))


def is_signed_integer_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a signed integer dtype.

    Unlike in `in_any_int_dtype`, timedelta64 instances will return False.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a signed integer dtype
              and not an instance of timedelta64.

    Examples
    --------
    >>> is_signed_integer_dtype(str)
    False
    >>> is_signed_integer_dtype(int)
    True
    >>> is_signed_integer_dtype(float)
    False
    >>> is_signed_integer_dtype(np.uint64)  # unsigned
    False
    >>> is_signed_integer_dtype(np.datetime64)
    False
    >>> is_signed_integer_dtype(np.timedelta64)
    False
    >>> is_signed_integer_dtype(np.array(['a', 'b']))
    False
    >>> is_signed_integer_dtype(pd.Series([1, 2]))
    True
    >>> is_signed_integer_dtype(np.array([], dtype=np.timedelta64))
    False
    >>> is_signed_integer_dtype(pd.Index([1, 2.]))  # float
    False
    >>> is_signed_integer_dtype(np.array([1, 2], dtype=np.uint32))  # unsigned
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return (issubclass(tipo, np.signedinteger) and
            not issubclass(tipo, (np.datetime64, np.timedelta64)))


def is_unsigned_integer_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of an unsigned integer dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of an
              unsigned integer dtype.

    Examples
    --------
    >>> is_unsigned_integer_dtype(str)
    False
    >>> is_unsigned_integer_dtype(int)  # signed
    False
    >>> is_unsigned_integer_dtype(float)
    False
    >>> is_unsigned_integer_dtype(np.uint64)
    True
    >>> is_unsigned_integer_dtype(np.array(['a', 'b']))
    False
    >>> is_unsigned_integer_dtype(pd.Series([1, 2]))  # signed
    False
    >>> is_unsigned_integer_dtype(pd.Index([1, 2.]))  # float
    False
    >>> is_unsigned_integer_dtype(np.array([1, 2], dtype=np.uint32))
    True
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return (issubclass(tipo, np.unsignedinteger) and
            not issubclass(tipo, (np.datetime64, np.timedelta64)))


def is_int64_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of the int64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the int64 dtype.

    Notes
    -----
    Depending on system architecture, the return value of `is_int64_dtype(
    int)` will be True if the OS uses 64-bit integers and False if the OS
    uses 32-bit integers.

    Examples
    --------
    >>> is_int64_dtype(str)
    False
    >>> is_int64_dtype(np.int32)
    False
    >>> is_int64_dtype(np.int64)
    True
    >>> is_int64_dtype(float)
    False
    >>> is_int64_dtype(np.uint64)  # unsigned
    False
    >>> is_int64_dtype(np.array(['a', 'b']))
    False
    >>> is_int64_dtype(np.array([1, 2], dtype=np.int64))
    True
    >>> is_int64_dtype(pd.Index([1, 2.]))  # float
    False
    >>> is_int64_dtype(np.array([1, 2], dtype=np.uint32))  # unsigned
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, np.int64)


def is_int_or_datetime_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of an
    integer, timedelta64, or datetime64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of an
              integer, timedelta64, or datetime64 dtype.

    Examples
    --------
    >>> is_int_or_datetime_dtype(str)
    False
    >>> is_int_or_datetime_dtype(int)
    True
    >>> is_int_or_datetime_dtype(float)
    False
    >>> is_int_or_datetime_dtype(np.uint64)
    True
    >>> is_int_or_datetime_dtype(np.datetime64)
    True
    >>> is_int_or_datetime_dtype(np.timedelta64)
    True
    >>> is_int_or_datetime_dtype(np.array(['a', 'b']))
    False
    >>> is_int_or_datetime_dtype(pd.Series([1, 2]))
    True
    >>> is_int_or_datetime_dtype(np.array([], dtype=np.timedelta64))
    True
    >>> is_int_or_datetime_dtype(np.array([], dtype=np.datetime64))
    True
    >>> is_int_or_datetime_dtype(pd.Index([1, 2.]))  # float
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return (issubclass(tipo, np.integer) or
            issubclass(tipo, (np.datetime64, np.timedelta64)))


def is_datetime64_any_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of the datetime64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the datetime64 dtype.

    Examples
    --------
    >>> is_datetime64_any_dtype(str)
    False
    >>> is_datetime64_any_dtype(int)
    False
    >>> is_datetime64_any_dtype(np.datetime64)  # can be tz-naive
    True
    >>> is_datetime64_any_dtype(DatetimeTZDtype("ns", "US/Eastern"))
    True
    >>> is_datetime64_any_dtype(np.array(['a', 'b']))
    False
    >>> is_datetime64_any_dtype(np.array([1, 2]))
    False
    >>> is_datetime64_any_dtype(np.array([], dtype=np.datetime64))
    True
    >>> is_datetime64_any_dtype(pd.DatetimeIndex([1, 2, 3],
                                dtype=np.datetime64))
    True
    """

    if arr_or_dtype is None:
        return False
    return (is_datetime64_dtype(arr_or_dtype) or
            is_datetime64tz_dtype(arr_or_dtype))


def is_datetime64_ns_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of the datetime64[ns] dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the datetime64[ns] dtype.

    Examples
    --------
    >>> is_datetime64_ns_dtype(str)
    False
    >>> is_datetime64_ns_dtype(int)
    False
    >>> is_datetime64_ns_dtype(np.datetime64)  # no unit
    False
    >>> is_datetime64_ns_dtype(DatetimeTZDtype("ns", "US/Eastern"))
    True
    >>> is_datetime64_ns_dtype(np.array(['a', 'b']))
    False
    >>> is_datetime64_ns_dtype(np.array([1, 2]))
    False
    >>> is_datetime64_ns_dtype(np.array([], dtype=np.datetime64))  # no unit
    False
    >>> is_datetime64_ns_dtype(np.array([],
                               dtype="datetime64[ps]"))  # wrong unit
    False
    >>> is_datetime64_ns_dtype(pd.DatetimeIndex([1, 2, 3],
                               dtype=np.datetime64))  # has 'ns' unit
    True
    """

    if arr_or_dtype is None:
        return False
    try:
        tipo = _get_dtype(arr_or_dtype)
    except TypeError:
        if is_datetime64tz_dtype(arr_or_dtype):
            tipo = _get_dtype(arr_or_dtype.dtype)
        else:
            return False
    return tipo == _NS_DTYPE or getattr(tipo, 'base', None) == _NS_DTYPE


def is_timedelta64_ns_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of the timedelta64[ns] dtype.

    This is a very specific dtype, so generic ones like `np.timedelta64`
    will return False if passed into this function.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the
              timedelta64[ns] dtype.

    Examples
    --------
    >>> is_timedelta64_ns_dtype(np.dtype('m8[ns]'))
    True
    >>> is_timedelta64_ns_dtype(np.dtype('m8[ps]'))  # Wrong frequency
    False
    >>> is_timedelta64_ns_dtype(np.array([1, 2], dtype='m8[ns]'))
    True
    >>> is_timedelta64_ns_dtype(np.array([1, 2], dtype=np.timedelta64))
    False
    """

    if arr_or_dtype is None:
        return False
    try:
        tipo = _get_dtype(arr_or_dtype)
        return tipo == _TD_DTYPE
    except TypeError:
        return False


def is_datetime_or_timedelta_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of
    a timedelta64 or datetime64 dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a
              timedelta64, or datetime64 dtype.

    Examples
    --------
    >>> is_datetime_or_timedelta_dtype(str)
    False
    >>> is_datetime_or_timedelta_dtype(int)
    False
    >>> is_datetime_or_timedelta_dtype(np.datetime64)
    True
    >>> is_datetime_or_timedelta_dtype(np.timedelta64)
    True
    >>> is_datetime_or_timedelta_dtype(np.array(['a', 'b']))
    False
    >>> is_datetime_or_timedelta_dtype(pd.Series([1, 2]))
    False
    >>> is_datetime_or_timedelta_dtype(np.array([], dtype=np.timedelta64))
    True
    >>> is_datetime_or_timedelta_dtype(np.array([], dtype=np.datetime64))
    True
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, (np.datetime64, np.timedelta64))


def _is_unorderable_exception(e):
    """
    Check if the exception raised is an unorderable exception.

    The error message differs for 3 <= PY <= 3.5 and PY >= 3.6, so
    we need to condition based on Python version.

    Parameters
    ----------
    e : Exception or sub-class
        The exception object to check.

    Returns
    -------
    boolean : Whether or not the exception raised is an unorderable exception.
    """

    if PY36:
        return "'>' not supported between instances of" in str(e)

    elif PY3:
        return 'unorderable' in str(e)
    return False


def is_numeric_v_string_like(a, b):
    """
    Check if we are comparing a string-like object to a numeric ndarray.

    NumPy doesn't like to compare such objects, especially numeric arrays
    and scalar string-likes.

    Parameters
    ----------
    a : array-like, scalar
        The first object to check.
    b : array-like, scalar
        The second object to check.

    Returns
    -------
    boolean : Whether we return a comparing a string-like
              object to a numeric array.

    Examples
    --------
    >>> is_numeric_v_string_like(1, 1)
    False
    >>> is_numeric_v_string_like("foo", "foo")
    False
    >>> is_numeric_v_string_like(1, "foo")  # non-array numeric
    False
    >>> is_numeric_v_string_like(np.array([1]), "foo")
    True
    >>> is_numeric_v_string_like("foo", np.array([1]))  # symmetric check
    True
    >>> is_numeric_v_string_like(np.array([1, 2]), np.array(["foo"]))
    True
    >>> is_numeric_v_string_like(np.array(["foo"]), np.array([1, 2]))
    True
    >>> is_numeric_v_string_like(np.array([1]), np.array([2]))
    False
    >>> is_numeric_v_string_like(np.array(["foo"]), np.array(["foo"]))
    False
    """

    is_a_array = isinstance(a, np.ndarray)
    is_b_array = isinstance(b, np.ndarray)

    is_a_numeric_array = is_a_array and is_numeric_dtype(a)
    is_b_numeric_array = is_b_array and is_numeric_dtype(b)
    is_a_string_array = is_a_array and is_string_like_dtype(a)
    is_b_string_array = is_b_array and is_string_like_dtype(b)

    is_a_scalar_string_like = not is_a_array and is_string_like(a)
    is_b_scalar_string_like = not is_b_array and is_string_like(b)

    return ((is_a_numeric_array and is_b_scalar_string_like) or
            (is_b_numeric_array and is_a_scalar_string_like) or
            (is_a_numeric_array and is_b_string_array) or
            (is_b_numeric_array and is_a_string_array))


def is_datetimelike_v_numeric(a, b):
    """
    Check if we are comparing a datetime-like object to a numeric object.

    By "numeric," we mean an object that is either of an int or float dtype.

    Parameters
    ----------
    a : array-like, scalar
        The first object to check.
    b : array-like, scalar
        The second object to check.

    Returns
    -------
    boolean : Whether we return a comparing a datetime-like
              to a numeric object.

    Examples
    --------
    >>> dt = np.datetime64(pd.datetime(2017, 1, 1))
    >>>
    >>> is_datetimelike_v_numeric(1, 1)
    False
    >>> is_datetimelike_v_numeric(dt, dt)
    False
    >>> is_datetimelike_v_numeric(1, dt)
    True
    >>> is_datetimelike_v_numeric(dt, 1)  # symmetric check
    True
    >>> is_datetimelike_v_numeric(np.array([dt]), 1)
    True
    >>> is_datetimelike_v_numeric(np.array([1]), dt)
    True
    >>> is_datetimelike_v_numeric(np.array([dt]), np.array([1]))
    True
    >>> is_datetimelike_v_numeric(np.array([1]), np.array([2]))
    False
    >>> is_datetimelike_v_numeric(np.array([dt]), np.array([dt]))
    False
    """

    if not hasattr(a, 'dtype'):
        a = np.asarray(a)
    if not hasattr(b, 'dtype'):
        b = np.asarray(b)

    def is_numeric(x):
        """
        Check if an object has a numeric dtype (i.e. integer or float).
        """
        return is_integer_dtype(x) or is_float_dtype(x)

    is_datetimelike = needs_i8_conversion
    return ((is_datetimelike(a) and is_numeric(b)) or
            (is_datetimelike(b) and is_numeric(a)))


def is_datetimelike_v_object(a, b):
    """
    Check if we are comparing a datetime-like object to an object instance.

    Parameters
    ----------
    a : array-like, scalar
        The first object to check.
    b : array-like, scalar
        The second object to check.

    Returns
    -------
    boolean : Whether we return a comparing a datetime-like
              to an object instance.

    Examples
    --------
    >>> obj = object()
    >>> dt = np.datetime64(pd.datetime(2017, 1, 1))
    >>>
    >>> is_datetimelike_v_object(obj, obj)
    False
    >>> is_datetimelike_v_object(dt, dt)
    False
    >>> is_datetimelike_v_object(obj, dt)
    True
    >>> is_datetimelike_v_object(dt, obj)  # symmetric check
    True
    >>> is_datetimelike_v_object(np.array([dt]), obj)
    True
    >>> is_datetimelike_v_object(np.array([obj]), dt)
    True
    >>> is_datetimelike_v_object(np.array([dt]), np.array([obj]))
    True
    >>> is_datetimelike_v_object(np.array([obj]), np.array([obj]))
    False
    >>> is_datetimelike_v_object(np.array([dt]), np.array([1]))
    False
    >>> is_datetimelike_v_object(np.array([dt]), np.array([dt]))
    False
    """

    if not hasattr(a, 'dtype'):
        a = np.asarray(a)
    if not hasattr(b, 'dtype'):
        b = np.asarray(b)

    is_datetimelike = needs_i8_conversion
    return ((is_datetimelike(a) and is_object_dtype(b)) or
            (is_datetimelike(b) and is_object_dtype(a)))


def needs_i8_conversion(arr_or_dtype):
    """
    Check whether the array or dtype should be converted to int64.

    An array-like or dtype "needs" such a conversion if the array-like
    or dtype is of a datetime-like dtype

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype should be converted to int64.

    Examples
    --------
    >>> needs_i8_conversion(str)
    False
    >>> needs_i8_conversion(np.int64)
    False
    >>> needs_i8_conversion(np.datetime64)
    True
    >>> needs_i8_conversion(np.array(['a', 'b']))
    False
    >>> needs_i8_conversion(pd.Series([1, 2]))
    False
    >>> needs_i8_conversion(pd.Series([], dtype="timedelta64[ns]"))
    True
    >>> needs_i8_conversion(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern"))
    True
    """

    if arr_or_dtype is None:
        return False
    return (is_datetime_or_timedelta_dtype(arr_or_dtype) or
            is_datetime64tz_dtype(arr_or_dtype) or
            is_period_dtype(arr_or_dtype))


def is_numeric_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a numeric dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a numeric dtype.

    Examples
    --------
    >>> is_numeric_dtype(str)
    False
    >>> is_numeric_dtype(int)
    True
    >>> is_numeric_dtype(float)
    True
    >>> is_numeric_dtype(np.uint64)
    True
    >>> is_numeric_dtype(np.datetime64)
    False
    >>> is_numeric_dtype(np.timedelta64)
    False
    >>> is_numeric_dtype(np.array(['a', 'b']))
    False
    >>> is_numeric_dtype(pd.Series([1, 2]))
    True
    >>> is_numeric_dtype(pd.Index([1, 2.]))
    True
    >>> is_numeric_dtype(np.array([], dtype=np.timedelta64))
    False
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return (issubclass(tipo, (np.number, np.bool_)) and
            not issubclass(tipo, (np.datetime64, np.timedelta64)))


def is_string_like_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a string-like dtype.

    Unlike `is_string_dtype`, the object dtype is excluded because it
    is a mixed dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of the string dtype.

    Examples
    --------
    >>> is_string_like_dtype(str)
    True
    >>> is_string_like_dtype(object)
    False
    >>> is_string_like_dtype(np.array(['a', 'b']))
    True
    >>> is_string_like_dtype(pd.Series([1, 2]))
    False
    """

    if arr_or_dtype is None:
        return False
    try:
        dtype = _get_dtype(arr_or_dtype)
        return dtype.kind in ('S', 'U')
    except TypeError:
        return False


def is_float_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a float dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a float dtype.

    Examples
    --------
    >>> is_float_dtype(str)
    False
    >>> is_float_dtype(int)
    False
    >>> is_float_dtype(float)
    True
    >>> is_float_dtype(np.array(['a', 'b']))
    False
    >>> is_float_dtype(pd.Series([1, 2]))
    False
    >>> is_float_dtype(pd.Index([1, 2.]))
    True
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, np.floating)


def is_floating_dtype(arr_or_dtype):
    """
    DEPRECATED: This function will be removed in a future version.

    Check whether the provided array or dtype is an instance of
    numpy's float dtype.

    Unlike, `is_float_dtype`, this check is a lot stricter, as it requires
    `isinstance` of `np.floating` and not `issubclass`.
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return isinstance(tipo, np.floating)


def is_bool_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a boolean dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a boolean dtype.

    Examples
    --------
    >>> is_bool_dtype(str)
    False
    >>> is_bool_dtype(int)
    False
    >>> is_bool_dtype(bool)
    True
    >>> is_bool_dtype(np.bool)
    True
    >>> is_bool_dtype(np.array(['a', 'b']))
    False
    >>> is_bool_dtype(pd.Series([1, 2]))
    False
    >>> is_bool_dtype(np.array([True, False]))
    True
    """

    if arr_or_dtype is None:
        return False
    try:
        tipo = _get_dtype_type(arr_or_dtype)
    except ValueError:
        # this isn't even a dtype
        return False

    if isinstance(arr_or_dtype, ABCIndexClass):

        # TODO(jreback)
        # we don't have a boolean Index class
        # so its object, we need to infer to
        # guess this
        return (arr_or_dtype.is_object and
                arr_or_dtype.inferred_type == 'boolean')

    return issubclass(tipo, np.bool_)


def is_extension_type(arr):
    """
    Check whether an array-like is of a pandas extension class instance.

    Extension classes include categoricals, pandas sparse objects (i.e.
    classes represented within the pandas library and not ones external
    to it like scipy sparse matrices), and datetime-like arrays.

    Parameters
    ----------
    arr : array-like
        The array-like to check.

    Returns
    -------
    boolean : Whether or not the array-like is of a pandas
              extension class instance.

    Examples
    --------
    >>> is_extension_type([1, 2, 3])
    False
    >>> is_extension_type(np.array([1, 2, 3]))
    False
    >>>
    >>> cat = pd.Categorical([1, 2, 3])
    >>>
    >>> is_extension_type(cat)
    True
    >>> is_extension_type(pd.Series(cat))
    True
    >>> is_extension_type(pd.SparseArray([1, 2, 3]))
    True
    >>> is_extension_type(pd.SparseSeries([1, 2, 3]))
    True
    >>>
    >>> from scipy.sparse import bsr_matrix
    >>> is_extension_type(bsr_matrix([1, 2, 3]))
    False
    >>> is_extension_type(pd.DatetimeIndex([1, 2, 3]))
    False
    >>> is_extension_type(pd.DatetimeIndex([1, 2, 3], tz="US/Eastern"))
    True
    >>>
    >>> dtype = DatetimeTZDtype("ns", tz="US/Eastern")
    >>> s = pd.Series([], dtype=dtype)
    >>> is_extension_type(s)
    True
    """

    if is_categorical(arr):
        return True
    elif is_sparse(arr):
        return True
    elif is_datetimetz(arr):
        return True
    return False


def is_complex_dtype(arr_or_dtype):
    """
    Check whether the provided array or dtype is of a complex dtype.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array or dtype to check.

    Returns
    -------
    boolean : Whether or not the array or dtype is of a compex dtype.

    Examples
    --------
    >>> is_complex_dtype(str)
    False
    >>> is_complex_dtype(int)
    False
    >>> is_complex_dtype(np.complex)
    True
    >>> is_complex_dtype(np.array(['a', 'b']))
    False
    >>> is_complex_dtype(pd.Series([1, 2]))
    False
    >>> is_complex_dtype(np.array([1 + 1j, 5]))
    True
    """

    if arr_or_dtype is None:
        return False
    tipo = _get_dtype_type(arr_or_dtype)
    return issubclass(tipo, np.complexfloating)


def _coerce_to_dtype(dtype):
    """
    Coerce a string or np.dtype to a pandas or numpy
    dtype if possible.

    If we cannot convert to a pandas dtype initially,
    we convert to a numpy dtype.

    Parameters
    ----------
    dtype : The dtype that we want to coerce.

    Returns
    -------
    pd_or_np_dtype : The coerced dtype.
    """

    if is_categorical_dtype(dtype):
        categories = getattr(dtype, 'categories', None)
        ordered = getattr(dtype, 'ordered', False)
        dtype = CategoricalDtype(categories=categories, ordered=ordered)
    elif is_datetime64tz_dtype(dtype):
        dtype = DatetimeTZDtype(dtype)
    elif is_period_dtype(dtype):
        dtype = PeriodDtype(dtype)
    elif is_interval_dtype(dtype):
        dtype = IntervalDtype(dtype)
    else:
        dtype = np.dtype(dtype)
    return dtype


def _get_dtype(arr_or_dtype):
    """
    Get the dtype instance associated with an array
    or dtype object.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype object whose dtype we want to extract.

    Returns
    -------
    obj_dtype : The extract dtype instance from the
                passed in array or dtype object.

    Raises
    ------
    TypeError : The passed in object is None.
    """

    if arr_or_dtype is None:
        raise TypeError("Cannot deduce dtype from null object")
    if isinstance(arr_or_dtype, np.dtype):
        return arr_or_dtype
    elif isinstance(arr_or_dtype, type):
        return np.dtype(arr_or_dtype)
    elif isinstance(arr_or_dtype, CategoricalDtype):
        return arr_or_dtype
    elif isinstance(arr_or_dtype, DatetimeTZDtype):
        return arr_or_dtype
    elif isinstance(arr_or_dtype, PeriodDtype):
        return arr_or_dtype
    elif isinstance(arr_or_dtype, IntervalDtype):
        return arr_or_dtype
    elif isinstance(arr_or_dtype, string_types):
        if is_categorical_dtype(arr_or_dtype):
            return CategoricalDtype.construct_from_string(arr_or_dtype)
        elif is_datetime64tz_dtype(arr_or_dtype):
            return DatetimeTZDtype.construct_from_string(arr_or_dtype)
        elif is_period_dtype(arr_or_dtype):
            return PeriodDtype.construct_from_string(arr_or_dtype)
        elif is_interval_dtype(arr_or_dtype):
            return IntervalDtype.construct_from_string(arr_or_dtype)
    elif isinstance(arr_or_dtype, (ABCCategorical, ABCCategoricalIndex)):
        return arr_or_dtype.dtype

    if hasattr(arr_or_dtype, 'dtype'):
        arr_or_dtype = arr_or_dtype.dtype
    return np.dtype(arr_or_dtype)


def _get_dtype_type(arr_or_dtype):
    """
    Get the type (NOT dtype) instance associated with
    an array or dtype object.

    Parameters
    ----------
    arr_or_dtype : array-like
        The array-like or dtype object whose type we want to extract.

    Returns
    -------
    obj_type : The extract type instance from the
               passed in array or dtype object.
    """

    if isinstance(arr_or_dtype, np.dtype):
        return arr_or_dtype.type
    elif isinstance(arr_or_dtype, type):
        return np.dtype(arr_or_dtype).type
    elif isinstance(arr_or_dtype, CategoricalDtype):
        return CategoricalDtypeType
    elif isinstance(arr_or_dtype, DatetimeTZDtype):
        return DatetimeTZDtypeType
    elif isinstance(arr_or_dtype, IntervalDtype):
        return IntervalDtypeType
    elif isinstance(arr_or_dtype, PeriodDtype):
        return PeriodDtypeType
    elif isinstance(arr_or_dtype, string_types):
        if is_categorical_dtype(arr_or_dtype):
            return CategoricalDtypeType
        elif is_datetime64tz_dtype(arr_or_dtype):
            return DatetimeTZDtypeType
        elif is_period_dtype(arr_or_dtype):
            return PeriodDtypeType
        elif is_interval_dtype(arr_or_dtype):
            return IntervalDtypeType
        return _get_dtype_type(np.dtype(arr_or_dtype))
    try:
        return arr_or_dtype.dtype.type
    except AttributeError:
        return type(None)


def _get_dtype_from_object(dtype):
    """
    Get a numpy dtype.type-style object for a dtype object.

    This methods also includes handling of the datetime64[ns] and
    datetime64[ns, TZ] objects.

    If no dtype can be found, we return ``object``.

    Parameters
    ----------
    dtype : dtype, type
        The dtype object whose numpy dtype.type-style
        object we want to extract.

    Returns
    -------
    dtype_object : The extracted numpy dtype.type-style object.
    """

    if isinstance(dtype, type) and issubclass(dtype, np.generic):
        # Type object from a dtype
        return dtype
    elif is_categorical(dtype):
        return CategoricalDtype().type
    elif is_datetimetz(dtype):
        return DatetimeTZDtype(dtype).type
    elif isinstance(dtype, np.dtype):  # dtype object
        try:
            _validate_date_like_dtype(dtype)
        except TypeError:
            # Should still pass if we don't have a date-like
            pass
        return dtype.type
    elif isinstance(dtype, string_types):
        if dtype in ['datetimetz', 'datetime64tz']:
            return DatetimeTZDtype.type
        elif dtype in ['period']:
            raise NotImplementedError

        if dtype == 'datetime' or dtype == 'timedelta':
            dtype += '64'

        try:
            return _get_dtype_from_object(getattr(np, dtype))
        except (AttributeError, TypeError):
            # Handles cases like _get_dtype(int) i.e.,
            # Python objects that are valid dtypes
            # (unlike user-defined types, in general)
            #
            # TypeError handles the float16 type code of 'e'
            # further handle internal types
            pass

    return _get_dtype_from_object(np.dtype(dtype))


def _validate_date_like_dtype(dtype):
    """
    Check whether the dtype is a date-like dtype. Raises an error if invalid.

    Parameters
    ----------
    dtype : dtype, type
        The dtype to check.

    Raises
    ------
    TypeError : The dtype could not be casted to a date-like dtype.
    ValueError : The dtype is an illegal date-like dtype (e.g. the
                 the frequency provided is too specific)
    """

    try:
        typ = np.datetime_data(dtype)[0]
    except ValueError as e:
        raise TypeError('{error}'.format(error=e))
    if typ != 'generic' and typ != 'ns':
        msg = '{name!r} is too specific of a frequency, try passing {type!r}'
        raise ValueError(msg.format(name=dtype.name, type=dtype.type.__name__))


_string_dtypes = frozenset(map(_get_dtype_from_object, (binary_type,
                                                        text_type)))


def pandas_dtype(dtype):
    """
    Converts input into a pandas only dtype object or a numpy dtype object.

    Parameters
    ----------
    dtype : object to be converted

    Returns
    -------
    np.dtype or a pandas dtype
    """

    if isinstance(dtype, DatetimeTZDtype):
        return dtype
    elif isinstance(dtype, PeriodDtype):
        return dtype
    elif isinstance(dtype, CategoricalDtype):
        return dtype
    elif isinstance(dtype, IntervalDtype):
        return dtype
    elif isinstance(dtype, string_types):
        try:
            return DatetimeTZDtype.construct_from_string(dtype)
        except TypeError:
            pass

        if dtype.startswith('period[') or dtype.startswith('Period['):
            # do not parse string like U as period[U]
            try:
                return PeriodDtype.construct_from_string(dtype)
            except TypeError:
                pass

        elif dtype.startswith('interval[') or dtype.startswith('Interval['):
            try:
                return IntervalDtype.construct_from_string(dtype)
            except TypeError:
                pass

        try:
            return CategoricalDtype.construct_from_string(dtype)
        except TypeError:
            pass
    elif isinstance(dtype, ExtensionDtype):
        return dtype

    try:
        npdtype = np.dtype(dtype)
    except (TypeError, ValueError):
        raise

    # Any invalid dtype (such as pd.Timestamp) should raise an error.
    # np.dtype(invalid_type).kind = 0 for such objects. However, this will
    # also catch some valid dtypes such as object, np.object_ and 'object'
    # which we safeguard against by catching them earlier and returning
    # np.dtype(valid_dtype) before this condition is evaluated.
    if dtype in [object, np.object_, 'object', 'O']:
        return npdtype
    elif npdtype.kind == 'O':
        raise TypeError('dtype {dtype} not understood'.format(dtype=dtype))

    return npdtype
