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

domdfcoding / domdf_python_tools / 7166137878

11 Dec 2023 10:30AM UTC coverage: 97.456%. Remained the same
7166137878

push

github

domdfcoding
Bump version v3.7.0 -> v3.8.0

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

2145 of 2201 relevant lines covered (97.46%)

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
"""
1✔
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  # 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
                        return months[month.capitalize()[:3]]  # type: ignore
1✔
199
                except KeyError:
1✔
200
                        raise ValueError(error_text)
1✔
201

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

208

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

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

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

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

228

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

233
        .. note::
234

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

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

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

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

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

256

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

261
        .. versionadded:: 1.4.0
262

263
        :param year:
264
        """
265

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

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

277

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

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

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

293
        timezone: Optional[datetime.tzinfo]
294

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

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

302

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

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

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

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

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

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

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

322

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

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

330
        .. note::
331

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

336
        .. versionadded:: 3.5.0
337

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

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

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

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

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

360

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

363
try:
1✔
364

365
        # 3rd party
366
        import pytz
1✔
367

368
        __all__.extend(_pytz_functions)
1✔
369

370
except ImportError as e:
1✔
371

372
        if __name__ == "__main__":
373

374
                # stdlib
375
                import warnings
376

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

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

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

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

391
                class SelfWrapper(ModuleType):
1✔
392

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

401
                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