Passed
Push — 2.x ( 130cd0...9a1936 )
by Ramon
06:07
created

senaite.core.api.dtime.to_ansi()   A

Complexity

Conditions 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 14
rs 10
c 0
b 0
f 0
cc 3
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2023 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import os
22
import time
23
from datetime import date
24
from datetime import datetime
25
from string import Template
26
27
import six
28
29
import pytz
30
from bika.lims import logger
31
from bika.lims.api import APIError
32
from bika.lims.api import get_tool
33
from DateTime import DateTime
34
from DateTime.DateTime import DateError
35
from DateTime.DateTime import SyntaxError
36
from DateTime.DateTime import TimeError
37
from zope.i18n import translate
38
39
40
def is_str(obj):
41
    """Check if the given object is a string
42
43
    :param obj: arbitrary object
44
    :returns: True when the object is a string
45
    """
46
    return isinstance(obj, six.string_types)
47
48
49
def is_d(dt):
50
    """Check if the date is a Python `date` object
51
52
    :param dt: date to check
53
    :returns: True when the date is a Python `date`
54
    """
55
    return type(dt) is date
56
57
58
def is_dt(dt):
59
    """Check if the date is a Python `datetime` object
60
61
    :param dt: date to check
62
    :returns: True when the date is a Python `datetime`
63
    """
64
    return type(dt) is datetime
65
66
67
def is_DT(dt):
68
    """Check if the date is a Zope `DateTime` object
69
70
    :param dt: object to check
71
    :returns: True when the object is a Zope `DateTime`
72
    """
73
    return type(dt) is DateTime
74
75
76
def is_date(dt):
77
    """Check if the date is a datetime or DateTime object
78
79
    :param dt: date to check
80
    :returns: True when the object is either a datetime or DateTime
81
    """
82
    if is_str(dt):
83
        DT = to_DT(dt)
84
        return is_date(DT)
85
    if is_d(dt):
86
        return True
87
    if is_dt(dt):
88
        return True
89
    if is_DT(dt):
90
        return True
91
    return False
92
93
94
def is_timezone_naive(dt):
95
    """Check if the date is timezone naive
96
97
    :param dt: date to check
98
    :returns: True when the date has no timezone
99
    """
100
    if is_d(dt):
101
        return True
102
    elif is_DT(dt):
103
        return dt.timezoneNaive()
104
    elif is_dt(dt):
105
        return dt.tzinfo is None
106
    elif is_str(dt):
107
        DT = to_DT(dt)
108
        return is_timezone_naive(DT)
109
    raise APIError("Expected a date type, got '%r'" % type(dt))
110
111
112
def is_timezone_aware(dt):
113
    """Check if the date is timezone aware
114
115
    :param dt: date to check
116
    :returns: True when the date has a timezone
117
    """
118
    return not is_timezone_naive(dt)
119
120
121
def to_DT(dt):
122
    """Convert to DateTime
123
124
    :param dt: DateTime/datetime/date
125
    :returns: DateTime object
126
    """
127
    if is_DT(dt):
128
        return dt
129
    elif is_str(dt):
130
        try:
131
            return DateTime(dt)
132
        except (DateError, TimeError):
133
            try:
134
                dt = ansi_to_dt(dt)
135
                return to_DT(dt)
136
            except ValueError:
137
                return None
138
        except (SyntaxError, IndexError):
139
            return None
140
    elif is_dt(dt):
141
        return DateTime(dt.isoformat())
142
    elif is_d(dt):
143
        dt = datetime(dt.year, dt.month, dt.day)
144
        return DateTime(dt.isoformat())
145
    else:
146
        return None
147
148
149
def to_dt(dt):
150
    """Convert to datetime
151
152
    :param dt: DateTime/datetime/date
153
    :returns: datetime object
154
    """
155
    if is_DT(dt):
156
        # get a valid pytz timezone
157
        tz = get_timezone(dt)
158
        dt = dt.asdatetime()
159
        if is_valid_timezone(tz):
160
            dt = to_zone(dt, tz)
161
        return dt
162
    elif is_str(dt):
163
        DT = to_DT(dt)
164
        return to_dt(DT)
165
    elif is_dt(dt):
166
        return dt
167
    elif is_d(dt):
168
        return datetime(dt.year, dt.month, dt.day)
169
    else:
170
        return None
171
172
173
def ansi_to_dt(dt):
174
    """The YYYYMMDD format is defined by ANSI X3.30. Therefore, 2 December 1,
175
    1989 would be represented as 19891201. When times are transmitted, they
176
    shall be represented as HHMMSS, and shall be linked to dates as specified
177
    by ANSI X3.43.3 Date and time together shall be specified as up to a
178
    14-character string: YYYYMMDD[HHMMSS]
179
    :param str:
180
    :return: datetime object
181
    """
182
    if not is_str(dt):
183
        raise TypeError("Type is not supported")
184
    if len(dt) == 8:
185
        date_format = "%Y%m%d"
186
    elif len(dt) == 14:
187
        date_format = "%Y%m%d%H%M%S"
188
    else:
189
        raise ValueError("No ANSI format date")
190
    return datetime.strptime(dt, date_format)
191
192
193
def to_ansi(dt, show_time=True):
194
    """Returns the date in ANSI X3.30/X4.43.3) format
195
    :param dt: DateTime/datetime/date
196
    :param show_time: if true, returns YYYYMMDDHHMMSS. YYYYMMDD otherwise
197
    :returns: str that represents the datetime in ANSI format
198
    """
199
    dt = to_dt(dt)
200
    if dt is None:
201
        return None
202
203
    ansi = "{:04d}{:02d}{:02d}".format(dt.year, dt.month, dt.day)
204
    if not show_time:
205
        return ansi
206
    return "{}{:02d}{:02d}{:02d}".format(ansi, dt.hour, dt.minute, dt.second)
207
208
209
def get_timezone(dt, default="Etc/GMT"):
210
    """Get a valid pytz timezone of the datetime object
211
212
    :param dt: date object
213
    :returns: timezone as string, e.g. Etc/GMT or CET
214
    """
215
    tz = None
216
    if is_dt(dt):
217
        tz = dt.tzname()
218
    elif is_DT(dt):
219
        tz = dt.timezone()
220
    elif is_d(dt):
221
        tz = default
222
223
    if tz:
224
        # convert DateTime `GMT` to `Etc/GMT` timezones
225
        # NOTE: `GMT+1` get `Etc/GMT-1`!
226
        if tz.startswith("GMT+0"):
227
            tz = tz.replace("GMT+0", "Etc/GMT")
228
        elif tz.startswith("GMT+"):
229
            tz = tz.replace("GMT+", "Etc/GMT-")
230
        elif tz.startswith("GMT-"):
231
            tz = tz.replace("GMT-", "Etc/GMT+")
232
        elif tz.startswith("GMT"):
233
            tz = tz.replace("GMT", "Etc/GMT")
234
    else:
235
        tz = default
236
237
    return tz
238
239
240
def is_valid_timezone(timezone):
241
    """Checks if the timezone is a valid pytz/Olson name
242
243
    :param timezone: pytz/Olson timezone name
244
    :returns: True when the timezone is a valid zone
245
    """
246
    try:
247
        pytz.timezone(timezone)
248
        return True
249
    except pytz.UnknownTimeZoneError:
250
        return False
251
252
253
def get_os_timezone(default="Etc/GMT"):
254
    """Return the default timezone of the system
255
256
    :returns: OS timezone or default timezone
257
    """
258
    timezone = None
259
    if "TZ" in os.environ.keys():
260
        # Timezone from OS env var
261
        timezone = os.environ["TZ"]
262
    if not timezone:
263
        # Timezone from python time
264
        zones = time.tzname
265
        if zones and len(zones) > 0:
266
            timezone = zones[0]
267
        else:
268
            logger.warn(
269
                "Operating system\'s timezone cannot be found. "
270
                "Falling back to %s." % default)
271
            timezone = default
272
    if not is_valid_timezone(timezone):
273
        return default
274
    return timezone
275
276
277
def to_zone(dt, timezone):
278
    """Convert date to timezone
279
280
    Adds the timezone for timezone naive datetimes
281
282
    :param dt: date object
283
    :param timezone: timezone
284
    :returns: date converted to timezone
285
    """
286
    if is_dt(dt) or is_d(dt):
287
        dt = to_dt(dt)
288
        zone = pytz.timezone(timezone)
289
        if is_timezone_aware(dt):
290
            return dt.astimezone(zone)
291
        return zone.localize(dt)
292
    elif is_DT(dt):
293
        # NOTE: This shifts the time according to the TZ offset
294
        return dt.toZone(timezone)
295
    raise TypeError("Expected a date, got '%r'" % type(dt))
296
297
298
def to_timestamp(dt):
299
    """Generate a Portable Operating System Interface (POSIX) timestamp
300
301
    :param dt: date object
302
    :returns: timestamp in seconds
303
    """
304
    timestamp = 0
305
    if is_DT(dt):
306
        timestamp = dt.timeTime()
307
    elif is_dt(dt):
308
        timestamp = time.mktime(dt.timetuple())
309
    elif is_str(dt):
310
        DT = to_DT(dt)
311
        return to_timestamp(DT)
312
    return timestamp
313
314
315
def from_timestamp(timestamp):
316
    """Generate a datetime object from a POSIX timestamp
317
318
    :param timestamp: POSIX timestamp
319
    :returns: datetime object
320
    """
321
    return datetime.utcfromtimestamp(timestamp)
322
323
324
def to_iso_format(dt):
325
    """Convert to ISO format
326
    """
327
    if is_dt(dt):
328
        return dt.isoformat()
329
    elif is_DT(dt):
330
        return dt.ISO()
331
    elif is_str(dt):
332
        DT = to_DT(dt)
333
        return to_iso_format(DT)
334
    return None
335
336
337
def date_to_string(dt, fmt="%Y-%m-%d", default=""):
338
    """Format the date to string
339
    """
340
    if not is_date(dt):
341
        return default
342
343
    # NOTE: The function `is_date` evaluates also string dates as `True`.
344
    #       We ensure in such a case to have a `DateTime` object and leave
345
    #       possible `datetime` objects unchanged.
346
    if isinstance(dt, six.string_types):
347
        dt = to_DT(dt)
348
349
    try:
350
        return dt.strftime(fmt)
351
    except ValueError:
352
        #  Fix ValueError: year=1111 is before 1900;
353
        #  the datetime strftime() methods require year >= 1900
354
355
        # convert format string to be something like "${Y}-${m}-${d}"
356
        new_fmt = ""
357
        var = False
358
        for x in fmt:
359
            if x == "%":
360
                var = True
361
                new_fmt += "${"
362
                continue
363
            if var:
364
                new_fmt += x
365
                new_fmt += "}"
366
                var = False
367
            else:
368
                new_fmt += x
369
370
        def pad(val):
371
            """Add a zero if val is a single digit
372
            """
373
            return "{:0>2}".format(val)
374
375
        # Manually extract relevant date and time parts
376
        dt = to_DT(dt)
377
        data = {
378
            "Y": dt.year(),
379
            "y": dt.yy(),
380
            "m": dt.mm(),
381
            "d": dt.dd(),
382
            "H": pad(dt.h_24()),
383
            "I": pad(dt.h_12()),
384
            "M": pad(dt.minute()),
385
            "p": dt.ampm().upper(),
386
            "S": dt.second(),
387
        }
388
389
        return Template(new_fmt).safe_substitute(data)
390
391
392
def to_localized_time(dt, long_format=None, time_only=None,
393
                      context=None, request=None, default=""):
394
    """Convert a date object to a localized string
395
396
    :param dt: The date/time to localize
397
    :type dt: str/datetime/DateTime
398
    :param long_format: Return long date/time if True
399
    :type portal_type: boolean/null
400
    :param time_only: If True, only returns time.
401
    :type title: boolean/null
402
    :param context: The current context
403
    :type context: ATContentType
404
    :param request: The current request
405
    :type request: HTTPRequest object
406
    :returns: The formatted date as string
407
    :rtype: string
408
    """
409
    if not dt:
410
        return default
411
412
    try:
413
        ts = get_tool("translation_service")
414
        time_str = ts.ulocalized_time(
415
            dt, long_format, time_only, context, "senaite.core", request)
416
    except ValueError:
417
        # Handle dates < 1900
418
419
        # code taken from Products.CMFPlone.i18nl110n.ulocalized_time
420
        if time_only:
421
            msgid = "time_format"
422
        elif long_format:
423
            msgid = "date_format_long"
424
        else:
425
            msgid = "date_format_short"
426
427
        formatstring = translate(msgid, "senaite.core", {}, request)
428
        if formatstring == msgid:
429
            if msgid == "date_format_long":
430
                formatstring = "%Y-%m-%d %H:%M"  # 2038-01-19 03:14
431
            elif msgid == "date_format_short":
432
                formatstring = "%Y-%m-%d"  # 2038-01-19
433
            elif msgid == "time_format":
434
                formatstring = "%H:%M"  # 03:14
435
            else:
436
                formatstring = "[INTERNAL ERROR]"
437
        time_str = date_to_string(dt, formatstring, default=default)
438
    return time_str
439