1from __future__ import annotations
2
3from datetime import (
4 datetime,
5 time,
6)
7from typing import TYPE_CHECKING
8
9import numpy as np
10
11from pandas._libs.lib import is_list_like
12
13from pandas.core.dtypes.generic import (
14 ABCIndex,
15 ABCSeries,
16)
17from pandas.core.dtypes.missing import notna
18
19if TYPE_CHECKING:
20 from pandas._typing import DateTimeErrorChoices
21
22
23def to_time(
24 arg,
25 format: str | None = None,
26 infer_time_format: bool = False,
27 errors: DateTimeErrorChoices = "raise",
28):
29 """
30 Parse time strings to time objects using fixed strptime formats ("%H:%M",
31 "%H%M", "%I:%M%p", "%I%M%p", "%H:%M:%S", "%H%M%S", "%I:%M:%S%p",
32 "%I%M%S%p")
33
34 Use infer_time_format if all the strings are in the same format to speed
35 up conversion.
36
37 Parameters
38 ----------
39 arg : string in time format, datetime.time, list, tuple, 1-d array, Series
40 format : str, default None
41 Format used to convert arg into a time object. If None, fixed formats
42 are used.
43 infer_time_format: bool, default False
44 Infer the time format based on the first non-NaN element. If all
45 strings are in the same format, this will speed up conversion.
46 errors : {'raise', 'coerce'}, default 'raise'
47 - If 'raise', then invalid parsing will raise an exception
48 - If 'coerce', then invalid parsing will be set as None
49
50 Returns
51 -------
52 datetime.time
53 """
54 if errors not in ("raise", "coerce"):
55 raise ValueError("errors must be one of 'raise', or 'coerce'.")
56
57 def _convert_listlike(arg, format):
58 if isinstance(arg, (list, tuple)):
59 arg = np.array(arg, dtype="O")
60
61 elif getattr(arg, "ndim", 1) > 1:
62 raise TypeError(
63 "arg must be a string, datetime, list, tuple, 1-d array, or Series"
64 )
65
66 arg = np.asarray(arg, dtype="O")
67
68 if infer_time_format and format is None:
69 format = _guess_time_format_for_array(arg)
70
71 times: list[time | None] = []
72 if format is not None:
73 for element in arg:
74 try:
75 times.append(datetime.strptime(element, format).time())
76 except (ValueError, TypeError) as err:
77 if errors == "raise":
78 msg = (
79 f"Cannot convert {element} to a time with given "
80 f"format {format}"
81 )
82 raise ValueError(msg) from err
83 times.append(None)
84 else:
85 formats = _time_formats[:]
86 format_found = False
87 for element in arg:
88 time_object = None
89 try:
90 time_object = time.fromisoformat(element)
91 except (ValueError, TypeError):
92 for time_format in formats:
93 try:
94 time_object = datetime.strptime(element, time_format).time()
95 if not format_found:
96 # Put the found format in front
97 fmt = formats.pop(formats.index(time_format))
98 formats.insert(0, fmt)
99 format_found = True
100 break
101 except (ValueError, TypeError):
102 continue
103
104 if time_object is not None:
105 times.append(time_object)
106 elif errors == "raise":
107 raise ValueError(f"Cannot convert arg {arg} to a time")
108 else:
109 times.append(None)
110
111 return times
112
113 if arg is None:
114 return arg
115 elif isinstance(arg, time):
116 return arg
117 elif isinstance(arg, ABCSeries):
118 values = _convert_listlike(arg._values, format)
119 return arg._constructor(values, index=arg.index, name=arg.name)
120 elif isinstance(arg, ABCIndex):
121 return _convert_listlike(arg, format)
122 elif is_list_like(arg):
123 return _convert_listlike(arg, format)
124
125 return _convert_listlike(np.array([arg]), format)[0]
126
127
128# Fixed time formats for time parsing
129_time_formats = [
130 "%H:%M",
131 "%H%M",
132 "%I:%M%p",
133 "%I%M%p",
134 "%H:%M:%S",
135 "%H%M%S",
136 "%I:%M:%S%p",
137 "%I%M%S%p",
138]
139
140
141def _guess_time_format_for_array(arr):
142 # Try to guess the format based on the first non-NaN element
143 non_nan_elements = notna(arr).nonzero()[0]
144 if len(non_nan_elements):
145 element = arr[non_nan_elements[0]]
146 for time_format in _time_formats:
147 try:
148 datetime.strptime(element, time_format)
149 return time_format
150 except ValueError:
151 pass
152
153 return None