Initial revision
[python_utils.git] / datetime_utils.py
1 #!/usr/bin/env python3
2
3 """Utilities related to dates and times and datetimes."""
4
5 import datetime
6 import logging
7 import re
8 from typing import NewType
9
10 import pytz
11
12 import constants
13
14 logger = logging.getLogger(__name__)
15
16
17 def now_pst() -> datetime.datetime:
18     return datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
19
20
21 def now() -> datetime.datetime:
22     return datetime.datetime.now()
23
24
25 def datetime_to_string(
26     dt: datetime.datetime,
27     *,
28     date_time_separator=" ",
29     include_timezone=True,
30     include_dayname=False,
31     include_seconds=True,
32     include_fractional=False,
33     twelve_hour=True,
34 ) -> str:
35     """A nice way to convert a datetime into a string."""
36     fstring = ""
37     if include_dayname:
38         fstring += "%a/"
39     fstring = f"%Y/%b/%d{date_time_separator}"
40     if twelve_hour:
41         fstring += "%I:%M"
42         if include_seconds:
43             fstring += ":%S"
44         fstring += "%p"
45     else:
46         fstring += "%H:%M"
47         if include_seconds:
48             fstring += ":%S"
49     if include_fractional:
50         fstring += ".%f"
51     if include_timezone:
52         fstring += "%z"
53     return dt.strftime(fstring).strip()
54
55
56 def timestamp() -> str:
57     """Return a timestamp for now in Pacific timezone."""
58     ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
59     return datetime_to_string(ts, include_timezone=True)
60
61
62 def time_to_string(
63     dt: datetime.datetime,
64     *,
65     include_seconds=True,
66     include_fractional=False,
67     include_timezone=False,
68     twelve_hour=True,
69 ) -> str:
70     """A nice way to convert a datetime into a time (only) string."""
71     fstring = ""
72     if twelve_hour:
73         fstring += "%l:%M"
74         if include_seconds:
75             fstring += ":%S"
76         fstring += "%p"
77     else:
78         fstring += "%H:%M"
79         if include_seconds:
80             fstring += ":%S"
81     if include_fractional:
82         fstring += ".%f"
83     if include_timezone:
84         fstring += "%z"
85     return dt.strftime(fstring).strip()
86
87
88 def seconds_to_timedelta(seconds: int) -> datetime.timedelta:
89     """Convert a delta in seconds into a timedelta."""
90     return datetime.timedelta(seconds=seconds)
91
92
93 MinuteOfDay = NewType("MinuteOfDay", int)
94
95
96 def minute_number(hour: int, minute: int) -> MinuteOfDay:
97     """Convert hour:minute into minute number from start of day."""
98     return MinuteOfDay(hour * 60 + minute)
99
100
101 def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
102     """Convert a datetime into a minute number (of the day)"""
103     return minute_number(dt.hour, dt.minute)
104
105
106 def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
107     """Convert minute number from start of day into hour:minute am/pm string."""
108     hour = minute_num // 60
109     minute = minute_num % 60
110     ampm = "a"
111     if hour > 12:
112         hour -= 12
113         ampm = "p"
114     if hour == 12:
115         ampm = "p"
116     if hour == 0:
117         hour = 12
118     return f"{hour:2}:{minute:02}{ampm}"
119
120
121 def parse_duration(duration: str) -> int:
122     """Parse a duration in string form."""
123     seconds = 0
124     m = re.search(r'(\d+) *d[ays]*', duration)
125     if m is not None:
126         seconds += int(m.group(1)) * 60 * 60 * 24
127     m = re.search(r'(\d+) *h[ours]*', duration)
128     if m is not None:
129         seconds += int(m.group(1)) * 60 * 60
130     m = re.search(r'(\d+) *m[inutes]*', duration)
131     if m is not None:
132         seconds += int(m.group(1)) * 60
133     m = re.search(r'(\d+) *s[econds]*', duration)
134     if m is not None:
135         seconds += int(m.group(1))
136     return seconds
137
138
139 def describe_duration(age: int) -> str:
140     """Describe a duration."""
141     days = divmod(age, constants.SECONDS_PER_DAY)
142     hours = divmod(days[1], constants.SECONDS_PER_HOUR)
143     minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
144
145     descr = ""
146     if days[0] > 1:
147         descr = f"{int(days[0])} days, "
148     elif days[0] == 1:
149         descr = "1 day, "
150     if hours[0] > 1:
151         descr = descr + f"{int(hours[0])} hours, "
152     elif hours[0] == 1:
153         descr = descr + "1 hour, "
154     if len(descr) > 0:
155         descr = descr + "and "
156     if minutes[0] == 1:
157         descr = descr + "1 minute"
158     else:
159         descr = descr + f"{int(minutes[0])} minutes"
160     return descr
161
162
163 def describe_duration_briefly(age: int) -> str:
164     """Describe a duration briefly."""
165     days = divmod(age, constants.SECONDS_PER_DAY)
166     hours = divmod(days[1], constants.SECONDS_PER_HOUR)
167     minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
168
169     descr = ""
170     if days[0] > 0:
171         descr = f"{int(days[0])}d "
172     if hours[0] > 0:
173         descr = descr + f"{int(hours[0])}h "
174     descr = descr + f"{int(minutes[0])}m"
175     return descr