• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

domdfcoding / domdf_python_tools / 20027381159

08 Dec 2025 12:01PM UTC coverage: 97.275% (-0.04%) from 97.313%
20027381159

push

github

domdfcoding
Make UserFloat hashable again

2 of 2 new or added lines in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

2142 of 2202 relevant lines covered (97.28%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.9
/domdf_python_tools/dates.py
1
#  !/usr/bin/env python
2
#
3
#  dates.py
4
"""
5
Utilities for working with dates and times.
6

7
.. extras-require:: dates
8
        :pyproject:
9

10

11
**Data:**
12

13
.. autosummary::
14

15
        ~domdf_python_tools.dates.months
16
        ~domdf_python_tools.dates.month_full_names
17
        ~domdf_python_tools.dates.month_short_names
18

19
"""
20
#
21
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
22
#
23
#  Parts of the docstrings based on the Python 3.8.2 Documentation
24
#  Licensed under the Python Software Foundation License Version 2.
25
#  Copyright © 2001-2020 Python Software Foundation. All rights reserved.
26
#  Copyright © 2000 BeOpen.com. All rights reserved.
27
#  Copyright © 1995-2000 Corporation for National Research Initiatives. All rights reserved.
28
#  Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
29
#
30
#  Permission is hereby granted, free of charge, to any person obtaining a copy
31
#  of this software and associated documentation files (the "Software"), to deal
32
#  in the Software without restriction, including without limitation the rights
33
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
#  copies of the Software, and to permit persons to whom the Software is
35
#  furnished to do so, subject to the following conditions:
36
#
37
#  The above copyright notice and this permission notice shall be included in all
38
#  copies or substantial portions of the Software.
39
#
40
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
41
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
42
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
43
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
44
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
45
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
46
#  OR OTHER DEALINGS IN THE SOFTWARE.
47
#
48
#  calc_easter from https://code.activestate.com/recipes/576517-calculate-easter-western-given-a-year/
49
#  Copyright © 2008 Martin Diers
50
#  Licensed under the MIT License
51
#
52

53
# stdlib
54
import datetime
1✔
55
import sys
1✔
56
import time
1✔
57
import typing
1✔
58
from collections import OrderedDict
1✔
59
from types import ModuleType
1✔
60
from typing import Optional, Union
1✔
61

62
__all__ = [
1✔
63
                "current_tzinfo",
64
                "set_timezone",
65
                "utc_timestamp_to_datetime",
66
                "months",
67
                "parse_month",
68
                "get_month_number",
69
                "check_date",
70
                "calc_easter",
71
                "month_short_names",
72
                "month_full_names",
73
                "is_bst",
74
                ]
75

76

77
def current_tzinfo() -> Optional[datetime.tzinfo]:
1✔
78
        """
79
        Returns a tzinfo object for the current timezone.
80
        """
81

82
        return datetime.datetime.now().astimezone().tzinfo  # pragma: no cover (hard to test)
83

84

85
#
86
# def datetime_to_utc_timestamp(datetime, current_tzinfo=None):
87
#         """
88
#         Convert a :class:`datetime.datetime` object to seconds since UNIX epoch, in UTC time
89
#
90
#         :param datetime:
91
#         :type datetime: :class:`datetime.datetime`
92
#         :param current_tzinfo: A tzinfo object representing the current timezone.
93
#                 If None it will be inferred.
94
#         :type current_tzinfo: :class:`datetime.tzinfo`
95
#
96
#         :return: Timestamp in UTC timezone
97
#         :rtype: float
98
#         """
99
#
100
#         return datetime.astimezone(current_tzinfo).timestamp()
101
#
102

103

104
def set_timezone(obj: datetime.datetime, tzinfo: datetime.tzinfo) -> datetime.datetime:
1✔
105
        """
106
        Sets the timezone / tzinfo of the given :class:`datetime.datetime` object.
107
        This will not convert the time (i.e. the hours will stay the same).
108
        Use :meth:`datetime.datetime.astimezone` to accomplish that.
109

110
        :param obj:
111
        :param tzinfo:
112
        """
113

114
        return obj.replace(tzinfo=tzinfo)
1✔
115

116

117
def utc_timestamp_to_datetime(
1✔
118
                utc_timestamp: Union[float, int],
119
                output_tz: Optional[datetime.tzinfo] = None,
120
                ) -> datetime.datetime:
121
        """
122
        Convert UTC timestamp (seconds from UNIX epoch) to a :class:`datetime.datetime` object.
123

124
        If ``output_tz`` is :py:obj:`None` the timestamp is converted to the platform’s local date and time,
125
        and the local timezone is inferred and set for the object.
126

127
        If ``output_tz`` is not :py:obj:`None`, it must be an instance of a :class:`datetime.tzinfo` subclass,
128
        and the timestamp is converted to ``output_tz``’s time zone.
129

130

131
        :param utc_timestamp: The timestamp to convert to a datetime object
132
        :param output_tz: The timezone to output the datetime object for.
133
                If :py:obj:`None` it will be inferred.
134

135
        :return: The timestamp as a datetime object.
136

137
        :raises OverflowError: if the timestamp is out of the range
138
                of values supported by the platform C localtime() or gmtime() functions,
139
                and OSError on localtime() or gmtime() failure. It’s common for this to
140
                be restricted to years in 1970 through 2038.
141
        """
142

143
        new_datetime = datetime.datetime.fromtimestamp(utc_timestamp, output_tz)
1✔
144
        return new_datetime.astimezone(output_tz)
1✔
145

146

147
if sys.version_info <= (3, 7, 2):  # pragma: no cover (py37+)
148
        MonthsType = OrderedDict
149
else:  # pragma: no cover (<py37)
150
        MonthsType = typing.OrderedDict[str, str]  # type: ignore[misc,assignment]  # noqa: TYP006
1✔
151

152
#: Mapping of 3-character shortcodes to full month names.
153
months: MonthsType = OrderedDict(
1✔
154
                Jan="January",
155
                Feb="February",
156
                Mar="March",
157
                Apr="April",
158
                May="May",
159
                Jun="June",
160
                Jul="July",
161
                Aug="August",
162
                Sep="September",
163
                Oct="October",
164
                Nov="November",
165
                Dec="December",
166
                )
167

168
month_short_names = tuple(months.keys())
1✔
169
"""
170
List of the short names for months in the Gregorian calendar.
171

172
.. versionadded:: 2.0.0
173
"""
174

175
month_full_names = tuple(months.values())
1✔
176
"""
177
List of the full names for months in the Gregorian calendar.
178

179
.. versionadded:: 2.0.0
180
"""
181

182

183
def parse_month(month: Union[str, int]) -> str:
1✔
184
        """
185
        Converts an integer or shorthand month into the full month name.
186

187
        :param month: The month number or shorthand name
188

189
        :return: The full name of the month
190
        """
191

192
        error_text = f"The given month ({month!r}) is not recognised."
1✔
193

194
        try:
1✔
195
                month = int(month)
1✔
196
        except ValueError:
1✔
197
                try:
1✔
198
                        assert not isinstance(month, int)
1✔
199
                        return months[month.capitalize()[:3]]
1✔
200
                except KeyError:
1✔
201
                        raise ValueError(error_text)
1✔
202

203
        # Only get here if first try succeeded
204
        if 0 < month <= 12:
1✔
205
                return list(months.values())[month - 1]
1✔
206
        else:
207
                raise ValueError(error_text)
1✔
208

209

210
def get_month_number(month: Union[str, int]) -> int:
1✔
211
        """
212
        Returns the number of the given month.
213
        If ``month`` is already a number between 1 and 12 it will be returned immediately.
214

215
        :param month: The month to convert to a number
216

217
        :return: The number of the month
218
        """
219

220
        if isinstance(month, int):
1✔
221
                if 0 < month <= 12:
1✔
222
                        return month
1✔
223
                else:
224
                        raise ValueError(f"The given month ({month!r}) is not recognised.")
1✔
225
        else:
226
                month = parse_month(month)
1✔
227
                return list(months.values()).index(month) + 1
1✔
228

229

230
def check_date(month: Union[str, int], day: int, leap_year: bool = True) -> bool:
1✔
231
        """
232
        Returns :py:obj:`True` if the day number is valid for the given month.
233

234
        .. note::
235

236
                This function will return :py:obj:`True` for the 29th Feb.
237
                If you don't want this behaviour set ``leap_year`` to :py:obj:`False`.
238

239
        .. latex:vspace:: -10px
240

241
        :param month: The month to test.
242
        :param day: The day number to test.
243
        :param leap_year: Whether to return :py:obj:`True` for 29th Feb.
244
        """
245

246
        # Ensure day is an integer
247
        day = int(day)
1✔
248
        month = get_month_number(month)
1✔
249
        year = 2020 if leap_year else 2019
1✔
250

251
        try:
1✔
252
                datetime.date(year, month, day)
1✔
253
                return True
1✔
254
        except ValueError:
1✔
255
                return False
1✔
256

257

258
def calc_easter(year: int) -> datetime.date:
1✔
259
        """
260
        Returns the date of Easter in the given year.
261

262
        .. versionadded:: 1.4.0
263

264
        :param year:
265
        """
266

267
        a = year % 19
1✔
268
        b = year // 100
1✔
269
        c = year % 100
1✔
270
        d = (19 * a + b - b // 4 - ((b - (b + 8) // 25 + 1) // 3) + 15) % 30
1✔
271
        e = (32 + 2 * (b % 4) + 2 * (c // 4) - d - (c % 4)) % 7
1✔
272
        f = d + e - 7 * ((a + 11 * d + 22 * e) // 451) + 114
1✔
273
        month = f // 31
1✔
274
        day = f % 31 + 1
1✔
275

276
        return datetime.date(year, month, day)
1✔
277

278

279
def get_utc_offset(
1✔
280
                tz: Union[datetime.tzinfo, str],
281
                date: Optional[datetime.datetime] = None,
282
                ) -> Optional[datetime.timedelta]:
283
        """
284
        Returns the offset between UTC and the requested timezone on the given date.
285
        If ``date`` is :py:obj:`None` then the current date is used.
286

287
        :param tz: ``pytz.timezone`` or a string representing the timezone
288
        :param date: The date to obtain the UTC offset for
289
        """
290

291
        if date is None:
1✔
292
                date = datetime.datetime.now(pytz.utc)
1✔
293

294
        timezone: Optional[datetime.tzinfo]
295

296
        if isinstance(tz, str):
1✔
297
                timezone = get_timezone(tz, date)
1✔
298
        else:
299
                timezone = tz  # pragma: no cover (hard to test)
300

301
        return date.replace(tzinfo=pytz.utc).astimezone(timezone).utcoffset()
1✔
302

303

304
def get_timezone(tz: str, date: Optional[datetime.datetime] = None) -> Optional[datetime.tzinfo]:
1✔
305
        """
306
        Returns a localized ``pytz.timezone`` object for the given date.
307

308
        If ``date`` is :py:obj:`None` then the current date is used.
309

310
        .. latex:vspace:: -10px
311

312
        :param tz: A string representing a pytz timezone
313
        :param date: The date to obtain the timezone for
314
        """
315

316
        if date is None:  # pragma: no cover (hard to test)
317
                date = datetime.datetime.now(pytz.utc)
318

319
        d = date.replace(tzinfo=None)
1✔
320

321
        return pytz.timezone(tz).localize(d).tzinfo
1✔
322

323

324
def is_bst(the_date: Union[time.struct_time, datetime.date]) -> bool:
1✔
325
        """
326
        Calculates whether the given day falls within British Summer Time.
327

328
        This function should also be applicable to other timezones
329
        which change to summer time on the same date (e.g. Central European Summer Time).
330

331
        .. note::
332

333
                This function does not consider the time of day,
334
                and therefore does not handle the fact that the time changes at 1 AM GMT.
335
                It also does not account for historic deviations from the current norm.
336

337
        .. versionadded:: 3.5.0
338

339
        :param the_date: A :class:`time.struct_time`, :class:`datetime.date`
340
                or :class:`datetime.datetime` representing the target date.
341

342
        :returns: :py:obj:`True` if the date falls within British Summer Time, :py:obj:`False` otherwise.
343
        """
344

345
        if isinstance(the_date, datetime.date):
1✔
346
                the_date = the_date.timetuple()
1✔
347

348
        day, month, dow = the_date.tm_mday, the_date.tm_mon, (the_date.tm_wday + 1) % 7
1✔
349

350
        if 3 > month > 10:
1✔
UNCOV
351
                return False
×
352
        elif 3 < month < 10:
1✔
353
                return True
1✔
354
        elif month == 3:
1✔
355
                return day - dow >= 25
1✔
356
        elif month == 10:
1✔
357
                return day - dow < 25
1✔
358
        else:
359
                return False
1✔
360

361

362
_pytz_functions = ["get_utc_offset", "get_timezone"]
1✔
363

364
try:
1✔
365

366
        # 3rd party
367
        import pytz
1✔
368

369
        __all__.extend(_pytz_functions)
1✔
370

371
except ImportError as e:
1✔
372

373
        if __name__ == "__main__":
374

375
                # stdlib
376
                import warnings
377

378
                # this package
379
                from domdf_python_tools.words import word_join
380

381
                warnings.warn(
382
                                f"""\
383
                '{word_join(_pytz_functions)}' require pytz (https://pypi.org/project/pytz/), but it could not be imported.
384

385
                The error was: {e}.
386
                """
387
                                )
388

389
        else:
390
                _actual_module = sys.modules[__name__]
1✔
391

392
                class SelfWrapper(ModuleType):
1✔
393

394
                        def __getattr__(self, name):
1✔
395
                                if name in _pytz_functions:
1✔
396
                                        raise ImportError(
1✔
397
                                                        f"{name!r} requires pytz (https://pypi.org/project/pytz/), but it could not be imported."
398
                                                        )
399
                                else:
400
                                        return getattr(_actual_module, name)
1✔
401

402
                sys.modules[__name__] = SelfWrapper(__name__)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc