Module Gnumed.pycommon.gmDateTime

GNUmed date/time handling.

This modules provides access to date/time handling and offers an fuzzy timestamp implementation

It utilizes

    - Python time
    - Python datetime

Note that if you want locale-aware formatting you need to call

    locale.setlocale(locale.LC_ALL, '')

somewhere before importing this script.

Note Regarding Utc Offsets

Looking from Greenwich: WEST (IOW "behind"): negative values EAST (IOW "ahead"): positive values

This is in compliance with what datetime.tzinfo.utcoffset() does but NOT what time.altzone/time.timezone do !

This module also implements a class which allows the programmer to define the degree of fuzziness, uncertainty or imprecision of the timestamp contained within.

This is useful in fields such as medicine where only partial timestamps may be known for certain events.

Other useful links:

    <http://joda-time.sourceforge.net/key_instant.html>

Global variables

var STR2PYDT_DEFAULT_PATTERNS

Default patterns being passed to strptime().

var STR2PYDT_PARSERS : list[typing.Callable[[str], dict]]

Specialized parsers for string -> datetime conversion.

Functions

def calculate_apparent_age(start=None, end=None) ‑> tuple
Expand source code
def calculate_apparent_age(start=None, end=None) -> tuple:
        """Calculate age in a way humans naively expect it.

        This does *not* take into account time zones which may
        shift the result by up to one day.

        Args:
                start: the beginning of the period-to-be-aged, the 'birth' if you will
                end: the end of the period, default *now*

        Returns:
                A tuple (years, ..., seconds) as simple differences
                between the fields:

                        (years, months, days, hours, minutes, seconds)
        """
        assert not((end is None) and (start is None)), 'one of <start> or <end> must be given'

        now = pyDT.datetime.now(gmCurrentLocalTimezone)
        if end is None:
                if start <= now:
                        end = now
                else:
                        end = start
                        start = now
        if end < start:
                raise ValueError('calculate_apparent_age(): <end> (%s) before <start> (%s)' % (end, start))
        if end == start:
                return (0, 0, 0, 0, 0, 0)

        # steer clear of leap years
        if end.month == 2:
                if end.day == 29:
                        if not is_leap_year(start.year):
                                end = end.replace(day = 28)

        # years
        years = end.year - start.year
        end = end.replace(year = start.year)
        if end < start:
                years = years - 1

        # months
        if end.month == start.month:
                if end < start:
                        months = 11
                else:
                        months = 0
        else:
                months = end.month - start.month
                if months < 0:
                        months = months + 12
                if end.day > gregorian_month_length[start.month]:
                        end = end.replace(month = start.month, day = gregorian_month_length[start.month])
                else:
                        end = end.replace(month = start.month)
                if end < start:
                        months = months - 1

        # days
        if end.day == start.day:
                if end < start:
                        days = gregorian_month_length[start.month] - 1
                else:
                        days = 0
        else:
                days = end.day - start.day
                if days < 0:
                        days = days + gregorian_month_length[start.month]
                end = end.replace(day = start.day)
                if end < start:
                        days = days - 1

        # hours
        if end.hour == start.hour:
                hours = 0
        else:
                hours = end.hour - start.hour
                if hours < 0:
                        hours = hours + 24
                end = end.replace(hour = start.hour)
                if end < start:
                        hours = hours - 1

        # minutes
        if end.minute == start.minute:
                minutes = 0
        else:
                minutes = end.minute - start.minute
                if minutes < 0:
                        minutes = minutes + 60
                end = end.replace(minute = start.minute)
                if end < start:
                        minutes = minutes - 1

        # seconds
        if end.second == start.second:
                seconds = 0
        else:
                seconds = end.second - start.second
                if seconds < 0:
                        seconds = seconds + 60
                end = end.replace(second = start.second)
                if end < start:
                        seconds = seconds - 1

        return (years, months, days, hours, minutes, seconds)

Calculate age in a way humans naively expect it.

This does not take into account time zones which may shift the result by up to one day.

Args

start
the beginning of the period-to-be-aged, the 'birth' if you will
end
the end of the period, default now

Returns

A tuple (years, …, seconds) as simple differences between the fields:

    (years, months, days, hours, minutes, seconds)
def format_apparent_age_medically(age=None)
Expand source code
def format_apparent_age_medically(age=None):
        """<age> must be a tuple as created by calculate_apparent_age()"""

        (years, months, days, hours, minutes, seconds) = age

        # at least 1 year ?
        if years > 0:
                if months == 0:
                        return '%s%s' % (
                                years,
                                _('y::year_abbreviation').replace('::year_abbreviation', '')
                        )
                return '%s%s %s%s' % (
                        years,
                        _('y::year_abbreviation').replace('::year_abbreviation', ''),
                        months,
                        _('m::month_abbreviation').replace('::month_abbreviation', '')
                )

        # at least 1 month ?
        if months > 0:
                if days == 0:
                        return '%s%s' % (
                                months,
                                _('mo::month_only_abbreviation').replace('::month_only_abbreviation', '')
                        )

                result = '%s%s' % (
                        months,
                        _('m::month_abbreviation').replace('::month_abbreviation', '')
                )

                weeks, days = divmod(days, 7)
                if int(weeks) != 0:
                        result += '%s%s' % (
                                int(weeks),
                                _('w::week_abbreviation').replace('::week_abbreviation', '')
                        )
                if int(days) != 0:
                        result += '%s%s' % (
                                int(days),
                                _('d::day_abbreviation').replace('::day_abbreviation', '')
                        )

                return result

        # between 7 days and 1 month
        if days > 7:
                return "%s%s" % (
                        days,
                        _('d::day_abbreviation').replace('::day_abbreviation', '')
                )

        # between 1 and 7 days ?
        if days > 0:
                if hours == 0:
                        return '%s%s' % (
                                days,
                                _('d::day_abbreviation').replace('::day_abbreviation', '')
                        )
                return '%s%s (%s%s)' % (
                        days,
                        _('d::day_abbreviation').replace('::day_abbreviation', ''),
                        hours,
                        _('h::hour_abbreviation').replace('::hour_abbreviation', '')
                )

        # between 5 hours and 1 day
        if hours > 5:
                return '%s%s' % (
                        hours,
                        _('h::hour_abbreviation').replace('::hour_abbreviation', '')
                )

        # between 1 and 5 hours
        if hours > 1:
                if minutes == 0:
                        return '%s%s' % (
                                hours,
                                _('h::hour_abbreviation').replace('::hour_abbreviation', '')
                        )
                return '%s:%02d' % (
                        hours,
                        minutes
                )

        # between 5 and 60 minutes
        if minutes > 5:
                return "0:%02d" % minutes

        # less than 5 minutes
        if minutes == 0:
                return '%s%s' % (
                        seconds,
                        _('s::second_abbreviation').replace('::second_abbreviation', '')
                )
        if seconds == 0:
                return "0:%02d" % minutes
        return "%s.%s%s" % (
                minutes,
                seconds,
                _('s::second_abbreviation').replace('::second_abbreviation', '')
        )

must be a tuple as created by calculate_apparent_age()

def format_dob(dob: datetime.datetime,
format: str = '%Y %b %d',
none_string: str = None,
dob_is_estimated: bool = False) ‑> str
Expand source code
def format_dob(dob:pyDT.datetime, format:str='%Y %b %d', none_string:str=None, dob_is_estimated:bool=False) -> str:
        if dob is None:
                return none_string if none_string else _('** DOB unknown **')

        dob_txt = dob.strftime(format)
        if dob_is_estimated:
                dob_txt = '\u2248' + dob_txt
        return dob_txt
def format_interval(interval=None,
accuracy_wanted: int = None,
none_string: str = None,
verbose: bool = False) ‑> str
Expand source code
def format_interval(interval=None, accuracy_wanted:int=None, none_string:str=None, verbose:bool=False) -> str:
        """Formats an interval.
        """
        if interval is None:
                return none_string

        if accuracy_wanted is None:
                accuracy_wanted = ACC_SECONDS
        parts = __split_up_interval(interval)
        tags = __get_time_part_tags(verbose = verbose, time_parts = parts)
        # special cases
        special_case_formatted = __format_interval__special_cases(parts, tags, accuracy_wanted)
        if special_case_formatted:
                return special_case_formatted

        # normal cases
        formatted_intv = ''
        if parts['years'] > 0:
                formatted_intv += '%s%s' % (int(parts['years']), tags['years'])
        if accuracy_wanted < ACC_MONTHS:
                return formatted_intv.strip()

        if parts['months'] > 0:
                formatted_intv += ' %s%s' % (int(parts['months']), tags['months'])
        if accuracy_wanted < ACC_WEEKS:
                return formatted_intv.strip()

        if parts['weeks'] > 0:
                formatted_intv += ' %s%s' % (int(parts['weeks']), tags['weeks'])
        if accuracy_wanted < ACC_DAYS:
                return formatted_intv.strip()

        if parts['days'] > 0:
                formatted_intv += ' %s%s' % (int(parts['days']), tags['days'])
        if accuracy_wanted < ACC_HOURS:
                return formatted_intv.strip()

        if parts['hours'] > 0:
                formatted_intv += ' %s%s' % (int(parts['hours']), tags['hours'])
        if accuracy_wanted < ACC_MINUTES:
                return formatted_intv.strip()

        if parts['mins'] > 0:
                formatted_intv += ' %s%s' % (int(parts['mins']), tags['minutes'])
        if accuracy_wanted < ACC_SECONDS:
                return formatted_intv.strip()

        if parts['secs'] > 0:
                formatted_intv += ' %s%s' % (int(parts['secs']), tags['seconds'])
        return formatted_intv.strip()

Formats an interval.

def format_interval_medically(interval: datetime.timedelta = None,
terse: bool = False,
approximation_prefix: str = None,
zero_duration_strings: list[str] = None)
Expand source code
def format_interval_medically(interval:pyDT.timedelta=None, terse:bool=False, approximation_prefix:str=None, zero_duration_strings:list[str]=None):
        """Formats an interval.

                This isn't mathematically correct but close enough for display.

        Args:
                interval: the interval to format
                terse: output terse formatting or not
                approximation_mark: an approxiation mark to apply in the formatting, if any
                zero_duration_strings: a list of two strings, terse and verbose form, to return if a zero duration interval is to be formatted
        """
        assert interval is not None, '<interval> must be given'

        if interval.total_seconds() == 0:
                if not zero_duration_strings:
                        zero_duration_strings = [
                                _('zero_duration_symbol::\u2300').removeprefix('zero_duration_symbol::'),
                                _('zero_duration_text::no duration').removeprefix('zero_duration_text::')
                        ]
                if terse:
                        return zero_duration_strings[0]
                return zero_duration_strings[1]

        spacer = '' if terse else ' '
        prefix = approximation_prefix if approximation_prefix else ''
        # more than 1 year ?
        if interval.days > 364:
                years, days = divmod(interval.days, AVG_DAYS_PER_GREGORIAN_YEAR)
                months, day = divmod(days, 30.33)
                if int(months) == 0:
                        return '%s%s%s%s' % (
                                prefix,
                                spacer,
                                int(years),
                                _('interval_format_tag::years::y')[-1:]
                        )

                return '%s%s%s%s%s%s%s' % (
                        prefix,
                        spacer,
                        int(years),
                        _('interval_format_tag::years::y')[-1:],
                        spacer,
                        int(months),
                        _('interval_format_tag::months::m')[-1:]
                )

        # more than 30 days / 1 month ?
        if interval.days > 30:
                months, days = divmod(interval.days, 30.33)                                     # type: ignore [assignment]
                weeks, days = divmod(days, 7)
                result = '%s%s%s%s' % (
                        prefix,
                        spacer,
                        int(months),
                        _('interval_format_tag::months::m')[-1:]
                )
                if int(weeks) != 0:
                        result += '%s%s%s' % (spacer, int(weeks), _('interval_format_tag::weeks::w')[-1:])
                if int(days) != 0:
                        result += '%s%s%s' % (spacer, int(days), _('interval_format_tag::days::d')[-1:])
                return result

        # between 7 and 30 days ?
        if interval.days > 7:
                return '%s%s%s%s' % (prefix, spacer, interval.days, _('interval_format_tag::days::d')[-1:])

        # between 1 and 7 days ?
        if interval.days > 0:
                hours, seconds = divmod(interval.seconds, 3600)
                if hours == 0:
                        return '%s%s%s%s' % (prefix, spacer, interval.days, _('interval_format_tag::days::d')[-1:])

                return "%s%s%s%s%s%sh" % (
                        prefix,
                        spacer,
                        interval.days,
                        _('interval_format_tag::days::d')[-1:],
                        spacer,
                        int(hours)
                )

        # between 5 hours and 1 day
        if interval.seconds > (5*3600):
                return '%s%s%sh' % (prefix, spacer, int(interval.seconds // 3600))

        # between 1 and 5 hours
        if interval.seconds > 3600:
                hours, seconds = divmod(interval.seconds, 3600)
                minutes = seconds // 60
                if minutes == 0:
                        return '%s%s%sh' % (prefix, spacer, int(hours))
                return '%s:%02d' % (int(hours), int(minutes))

        # minutes only
        if interval.seconds > (5*60):
                return "0:%02d" % (int(interval.seconds // 60))

        # seconds
        minutes, seconds = divmod(interval.seconds, 60)
        if minutes == 0:
                return '%ss' % int(seconds)
        if seconds == 0:
                return '0:%02d' % int(minutes)
        return '%s.%ss' % (int(minutes), int(seconds))

Formats an interval.

    This isn't mathematically correct but close enough for display.

Args

interval
the interval to format
terse
output terse formatting or not
approximation_mark
an approxiation mark to apply in the formatting, if any
zero_duration_strings
a list of two strings, terse and verbose form, to return if a zero duration interval is to be formatted
def format_pregnancy_months(age)
Expand source code
def format_pregnancy_months(age):
        months, remainder = divmod(age.days, 28)
        return '%s%s' % (
                int(months) + 1,
                _('interval_format_tag::months::m')[-1:]
        )
def format_pregnancy_weeks(age)
Expand source code
def format_pregnancy_weeks(age):
        weeks, days = divmod(age.days, 7)
        return '%s%s%s%s' % (
                int(weeks),
                _('interval_format_tag::weeks::w')[-1:],
                int(days),
                _('interval_format_tag::days::d')[-1:]
        )
def get_date_of_weekday_following_date(weekday, base_dt: datetime.datetime = None)
Expand source code
def get_date_of_weekday_following_date(weekday, base_dt:pyDT.datetime=None):
        # weekday:
        # 0 = Sunday            # will be wrapped to 7
        # 1 = Monday ...
        if weekday not in [0,1,2,3,4,5,6,7]:
                raise ValueError('weekday must be in 0 (Sunday) to 7 (Sunday, again)')
        if weekday == 0:
                weekday = 7
        if base_dt is None:
                base_dt = pydt_now_here()
        dt_weekday = base_dt.isoweekday()               # 1 = Mon
        days2add = weekday - dt_weekday
        if days2add == 0:
                days2add = 7
        elif days2add < 0:
                days2add += 7
        return pydt_add(base_dt, days = days2add)
def get_date_of_weekday_in_week_of_date(weekday: int, base_dt: datetime.datetime = None) ‑> datetime.datetime
Expand source code
def get_date_of_weekday_in_week_of_date(weekday:int, base_dt:pyDT.datetime=None) -> pyDT.datetime:
        # weekday:
        # 0 = Sunday
        # 1 = Monday ...
        assert weekday in [0,1,2,3,4,5,6,7], 'weekday must be in 0 (Sunday) to 7 (Sunday, again)'

        if base_dt is None:
                base_dt = pydt_now_here()
        dt_weekday = base_dt.isoweekday()               # 1 = Mon
        day_diff = dt_weekday - weekday
        days2add = (-1 * day_diff)
        return pydt_add(base_dt, days = days2add)
def get_last_month(dt: datetime.datetime)
Expand source code
def get_last_month(dt:pyDT.datetime):
        last_month = dt.month - 1
        return 12 if last_month == 0 else last_month
def get_next_month(dt: datetime.datetime)
Expand source code
def get_next_month(dt:pyDT.datetime):
        next_month = dt.month + 1
        return 1 if next_month == 13 else next_month
def init()
Expand source code
def init():
        """Initialize date/time handling and log date/time environment."""

        _log.debug('datetime.now()   : [%s]' % pyDT.datetime.now())
        _log.debug('time.localtime() : [%s]' % str(time.localtime()))
        _log.debug('time.gmtime()    : [%s]' % str(time.gmtime()))

        try:
                _log.debug('$TZ: [%s]' % os.environ['TZ'])
        except KeyError:
                _log.debug('$TZ not defined')

        _log.debug('time.daylight           : [%s] (whether or not DST is locally used at all)', time.daylight)
        _log.debug('time.timezone           : [%s] seconds (+/-: WEST/EAST of Greenwich)', time.timezone)
        _log.debug('time.altzone            : [%s] seconds (+/-: WEST/EAST of Greenwich)', time.altzone)
        _log.debug('time.tzname             : [%s / %s] (non-DST / DST)' % time.tzname)
        _log.debug('time.localtime.tm_zone  : [%s]', time.localtime().tm_zone)
        _log.debug('time.localtime.tm_gmtoff: [%s]', time.localtime().tm_gmtoff)

        global py_timezone_name
        py_timezone_name = time.tzname[0]

        global py_dst_timezone_name
        py_dst_timezone_name = time.tzname[1]

        global dst_locally_in_use
        dst_locally_in_use = (time.daylight != 0)

        global dst_currently_in_effect
        dst_currently_in_effect = bool(time.localtime()[8])
        _log.debug('DST currently in effect: [%s]' % dst_currently_in_effect)

        if (not dst_locally_in_use) and dst_currently_in_effect:
                _log.error('system inconsistency: DST not in use - but DST currently in effect ?')

        global current_local_utc_offset_in_seconds
        msg = 'DST currently%sin effect: using UTC offset of [%s] seconds instead of [%s] seconds'
        if dst_currently_in_effect:
                current_local_utc_offset_in_seconds = time.altzone * -1
                _log.debug(msg % (' ', time.altzone * -1, time.timezone * -1))
        else:
                current_local_utc_offset_in_seconds = time.timezone * -1
                _log.debug(msg % (' not ', time.timezone * -1, time.altzone * -1))

        if current_local_utc_offset_in_seconds < 0:
                _log.debug('UTC offset is negative, assuming WEST of Greenwich (clock is "behind")')
        elif current_local_utc_offset_in_seconds > 0:
                _log.debug('UTC offset is positive, assuming EAST of Greenwich (clock is "ahead")')
        else:
                _log.debug('UTC offset is ZERO, assuming Greenwich Time')

        global current_local_iso_numeric_timezone_string
        current_local_iso_numeric_timezone_string = '%s' % current_local_utc_offset_in_seconds
        _log.debug('ISO numeric timezone string: [%s]' % current_local_iso_numeric_timezone_string)

        global current_local_timezone_name
        try:
                current_local_timezone_name = os.environ['TZ']
        except KeyError:
                if dst_currently_in_effect:
                        current_local_timezone_name = time.tzname[1]
                else:
                        current_local_timezone_name = time.tzname[0]

        global gmCurrentLocalTimezone
        gmCurrentLocalTimezone = cPlatformLocalTimezone()
        _log.debug('local-timezone class: %s', cPlatformLocalTimezone)
        _log.debug('local-timezone instance: %s', gmCurrentLocalTimezone)

Initialize date/time handling and log date/time environment.

def is_leap_year(year)
Expand source code
def is_leap_year(year):
        if year < 1582:         # no leap years before Gregorian Reform
                _log.debug('%s: before Gregorian Reform', year)
                return False

        # year is multiple of 4 ?
        div, remainder = divmod(year, 4)
        # * NOT divisible by 4
        # -> common year
        if remainder > 0:
                return False

        # year is a multiple of 100 ?
        div, remainder = divmod(year, 100)
        # * divisible by 4
        # * NOT divisible by 100
        # -> leap year
        if remainder > 0:
                return True

        # year is a multiple of 400 ?
        div, remainder = divmod(year, 400)
        # * divisible by 4
        # * divisible by 100, so, perhaps not leaping ?
        # * but ALSO divisible by 400
        # -> leap year
        if remainder == 0:
                return True

        # all others
        # -> common year
        return False
def pydt_add(dt: datetime.datetime,
years: int = 0,
months: int = 0,
weeks: int = 0,
days: int = 0,
hours: int = 0,
minutes: int = 0,
seconds: int = 0,
milliseconds: int = 0,
microseconds: int = 0) ‑> datetime.datetime
Expand source code
def pydt_add (
        dt:pyDT.datetime,
        years:int=0,
        months:int=0,
        weeks:int=0,
        days:int=0,
        hours:int=0,
        minutes:int=0,
        seconds:int=0,
        milliseconds:int=0,
        microseconds:int=0
) -> pyDT.datetime:
        """Add some time to a given datetime."""
        if months > 11 or months < -11:
                raise ValueError('pydt_add(): months must be within [-11..11]')

        dt = dt + pyDT.timedelta (
                weeks = weeks,
                days = days,
                hours = hours,
                minutes = minutes,
                seconds = seconds,
                milliseconds = milliseconds,
                microseconds = microseconds
        )
        if (years == 0) and (months == 0):
                return dt

        target_year = dt.year + years
        target_month = dt.month + months
        if target_month > 12:
                target_year += 1
                target_month -= 12
        elif target_month < 1:
                target_year -= 1
                target_month += 12
        return pydt_replace(dt, year = target_year, month = target_month, strict = False)

Add some time to a given datetime.

def pydt_is_same_day(dt1, dt2)
Expand source code
def pydt_is_same_day(dt1, dt2):
        if dt1.day != dt2.day:
                return False
        if dt1.month != dt2.month:
                return False
        if dt1.year != dt2.year:
                return False

        return True
def pydt_is_today(dt)
Expand source code
def pydt_is_today(dt):
        """Check whether <dt> is today."""
        if not dt:
                return None

        now = pyDT.datetime.now(gmCurrentLocalTimezone)
        return pydt_is_same_day(dt, now)

Check whether

is today.

def pydt_is_yesterday(dt)
Expand source code
def pydt_is_yesterday(dt):
        """Check whether <dt> is yesterday."""
        if not dt:
                return None

        yesterday = pyDT.datetime.now(gmCurrentLocalTimezone) - pyDT.timedelta(days = 1)
        return pydt_is_same_day(dt, yesterday)

Check whether

is yesterday.

def pydt_max_here()
Expand source code
def pydt_max_here():
        return pyDT.datetime.max.replace(tzinfo = gmCurrentLocalTimezone)
def pydt_now_here()
Expand source code
def pydt_now_here():
        """Returns NOW @ HERE (IOW, in the local timezone."""
        return pyDT.datetime.now(gmCurrentLocalTimezone)

Returns NOW @ HERE (IOW, in the local timezone.

def pydt_replace(dt: datetime.datetime,
strict=True,
year=None,
month=None,
day=None,
hour=None,
minute=None,
second=None,
microsecond=None,
tzinfo=None) ‑> datetime.datetime
Expand source code
def pydt_replace (
        dt:pyDT.datetime,
        strict=True,
        year=None,
        month=None,
        day=None,
        hour=None,
        minute=None,
        second=None,
        microsecond=None,
        tzinfo=None
) -> pyDT.datetime:
        """Replace parts of a datetime.

        Python's datetime.replace() fails if the target datetime
        does not exist (say, going from October 31st to November
        '31st' by replacing the month). This code can take heed
        of such things, when <strict> is False. The result will
        not be mathematically correct but likely what's meant in
        real live (last of October -> last of November). This
        can be particularly striking when going from January
        31st to February 28th (!) in non-leap years ...

        Args:
                strict: adjust - or not - for impossible target datetimes
        """
        # normalization required because .replace() does not
        # deal with keyword arguments being None ...
        if year is None:
                year = dt.year
        if month is None:
                month = dt.month
        if day is None:
                day = dt.day
        if hour is None:
                hour = dt.hour
        if minute is None:
                minute = dt.minute
        if second is None:
                second = dt.second
        if microsecond is None:
                microsecond = dt.microsecond
        if tzinfo is None:
                tzinfo = dt.tzinfo              # can fail on naive dt's

        if strict:
                return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo)

        try:
                return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo)

        except ValueError:
                _log.debug('error replacing datetime member(s): %s', locals())
        # (target/existing) day did not exist in target month (which raised the exception)
        if month == 2:
                if day > 28:
                        if is_leap_year(year):
                                day = 29
                        else:
                                day = 28
        else:
                if day == 31:
                        day = 30
        return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo)

Replace parts of a datetime.

Python's datetime.replace() fails if the target datetime does not exist (say, going from October 31st to November '31st' by replacing the month). This code can take heed of such things, when is False. The result will not be mathematically correct but likely what's meant in real live (last of October -> last of November). This can be particularly striking when going from January 31st to February 28th (!) in non-leap years …

Args

strict
adjust - or not - for impossible target datetimes
def pydt_strftime(dt: datetime.datetime = None,
format: str = '%Y %b %d %H:%M.%S',
none_str: str = None)
Expand source code
def pydt_strftime(dt:pyDT.datetime=None, format:str='%Y %b %d  %H:%M.%S', none_str:str=None):
        if dt is None:
                if none_str is not None:        # can be '', though ...
                        return none_str
                raise ValueError('must provide <none_str> if <dt>=None is to be dealt with')

        try:
                return dt.strftime(format)

        except ValueError:
                _log.exception('strftime() error')
                return 'strftime() error'
def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None)
Expand source code
def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None):
        """
        Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.

        You MUST have called locale.setlocale(locale.LC_ALL, '')
        somewhere in your code previously.

        @param default_time: if you want to force the time part of the time
                stamp to a given value and the user doesn't type any time part
                this value will be used
        @type default_time: an mx.DateTime.DateTimeDelta instance

        @param patterns: list of [time.strptime compatible date/time pattern, accuracy]
        @type patterns: list
        """
        matches = []

        matches.extend(__numbers_only(str2parse))
        matches.extend(__single_slash(str2parse))

        matches.extend ([
                {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = ACC_DAYS),
                        'label': m['label']
                } for m in __single_dot2py_dt(str2parse)
        ])
        matches.extend ([
                {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = ACC_DAYS),
                        'label': m['label']
                } for m in __single_char2py_dt(str2parse)
        ])
        matches.extend ([
                {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = ACC_DAYS),
                        'label': m['label']
                } for m in __explicit_offset2py_dt(str2parse)
        ])

        if patterns is None:
                patterns = []
        patterns.extend([
                '%Y-%m-%d',
                '%y-%m-%d',
                '%Y/%m/%d',
                '%y/%m/%d',
                '%d-%m-%Y',
                '%d-%m-%y',
                '%d/%m/%Y',
                '%d/%m/%y',
                '%d.%m.%Y',
                '%m-%d-%Y',
                '%m-%d-%y',
                '%m/%d/%Y',
                '%m/%d/%y'
        ])

        parts = str2parse.split(maxsplit = 1)
        hour = 11
        minute = 11
        second = 11
        acc = ACC_DAYS
        if len(parts) > 1:
                for pattern in ['%H:%M', '%H:%M:%S']:
                        try:
                                date = pyDT.datetime.strptime(parts[1], pattern)
                                hour = date.hour
                                minute = date.minute
                                second = date.second
                                acc = ACC_MINUTES
                                break
                        except ValueError:
                                # C-level overflow
                                continue
        for pattern in patterns:
                try:
                        ts = pyDT.datetime.strptime(parts[0], pattern).replace (
                                hour = hour,
                                minute = minute,
                                second = second,
                                tzinfo = gmCurrentLocalTimezone
                        )
                        fts = cFuzzyTimestamp(timestamp = ts, accuracy = acc)
                        matches.append ({
                                'data': fts,
                                'label': fts.format_accurately()
                        })
                except ValueError:
                        # C-level overflow
                        continue
        return matches

Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.

You MUST have called locale.setlocale(locale.LC_ALL, '') somewhere in your code previously.

@param default_time: if you want to force the time part of the time stamp to a given value and the user doesn't type any time part this value will be used @type default_time: an mx.DateTime.DateTimeDelta instance

@param patterns: list of [time.strptime compatible date/time pattern, accuracy] @type patterns: list

def str2interval(str_interval=None)
Expand source code
def str2interval(str_interval=None):

        unit_keys = {
                'year': _('yYaA_keys_year'),
                'month': _('mM_keys_month'),
                'week': _('wW_keys_week'),
                'day': _('dD_keys_day'),
                'hour': _('hH_keys_hour')
        }

        str_interval = str_interval.strip()

        # "(~)35(yY)"   - at age 35 years
        keys = '|'.join(list(unit_keys['year'].replace('_keys_year', '')))
        if regex.match(r'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(days = (int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]) * AVG_DAYS_PER_GREGORIAN_YEAR))

        # "(~)12mM" - at age 12 months
        keys = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
                years, months = divmod (
                        int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]),
                        12
                )
                return pyDT.timedelta(days = ((years * AVG_DAYS_PER_GREGORIAN_YEAR) + (months * AVG_DAYS_PER_GREGORIAN_MONTH)))

        # weeks
        keys = '|'.join(list(unit_keys['week'].replace('_keys_week', '')))
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(weeks = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # days
        keys = '|'.join(list(unit_keys['day'].replace('_keys_day', '')))
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(days = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # hours
        keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', '')))
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(hours = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # x/12 - months
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.UNICODE):
                years, months = divmod (
                        int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]),
                        12
                )
                return pyDT.timedelta(days = ((years * AVG_DAYS_PER_GREGORIAN_YEAR) + (months * AVG_DAYS_PER_GREGORIAN_MONTH)))

        # x/52 - weeks
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(weeks = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # x/7 - days
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(days = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # x/24 - hours
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(hours = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # x/60 - minutes
        if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.UNICODE):
                return pyDT.timedelta(minutes = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))

        # nYnM - years, months
        keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', '')))
        keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
        if regex.match(r'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_year, keys_month), str_interval, flags = regex.UNICODE):
                parts = regex.findall(r'\d+', str_interval, flags = regex.UNICODE)
                years, months = divmod(int(parts[1]), 12)
                years += int(parts[0])
                return pyDT.timedelta(days = ((years * AVG_DAYS_PER_GREGORIAN_YEAR) + (months * AVG_DAYS_PER_GREGORIAN_MONTH)))

        # nMnW - months, weeks
        keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
        keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', '')))
        if regex.match(r'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_month, keys_week), str_interval, flags = regex.UNICODE):
                parts = regex.findall(r'\d+', str_interval, flags = regex.UNICODE)
                months, weeks = divmod(int(parts[1]), 4)
                months += int(parts[0])
                return pyDT.timedelta(days = ((months * AVG_DAYS_PER_GREGORIAN_MONTH) + (weeks * DAYS_PER_WEEK)))

        return None
def str2pydt_matches(str2parse: str = None, patterns: list = None) ‑> list
Expand source code
def str2pydt_matches(str2parse:str=None, patterns:list=None) -> list:
        """Turn a string into candidate datetimes.

        Args:
                str2parse: string to turn into candidate datetimes
                patterns: additional patterns to try with strptime()

        A number of default patterns will be tried. Also, a few
        specialized parsers will be run. See the source for
        details.

        If the input contains a space followed by more characters
        matching either hour:minute or hour:minute:second that
        will be used as the time part of the datetime returned.
        Otherwise 11:11:11 will be used as default.

        Note: You must have previously called

                locale.setlocale(locale.LC_ALL, '')

        somewhere in your code.

        Returns:
                List of Python datetimes the input could be parsed as.
        """
        matches:list[dict] = []
        for parser in STR2PYDT_PARSERS:
                matches.extend(parser(str2parse))
        hour = 11
        minute = 11
        second = 11
        lbl_fmt = '%Y-%m-%d'
        parts = str2parse.split(maxsplit = 1)
        if len(parts) > 1:
                for pattern in ['%H:%M', '%H:%M:%S']:
                        try:
                                date = pyDT.datetime.strptime(parts[1], pattern)
                                hour = date.hour
                                minute = date.minute
                                second = date.second
                                lbl_fmt = '%Y-%m-%d %H:%M'
                                break
                        except ValueError:              # C-level overflow
                                continue
        if patterns is None:
                patterns = []
        patterns.extend(STR2PYDT_DEFAULT_PATTERNS)
        for pattern in patterns:
                try:
                        date = pyDT.datetime.strptime(parts[0], pattern).replace (
                                hour = hour,
                                minute = minute,
                                second = second,
                                tzinfo = gmCurrentLocalTimezone
                        )
                        matches.append ({
                                'data': date,
                                'label': date.strftime(lbl_fmt)
                        })
                except ValueError:                      # C-level overflow
                        continue
        return matches

Turn a string into candidate datetimes.

Args

str2parse
string to turn into candidate datetimes
patterns
additional patterns to try with strptime()

A number of default patterns will be tried. Also, a few specialized parsers will be run. See the source for details.

If the input contains a space followed by more characters matching either hour:minute or hour:minute:second that will be used as the time part of the datetime returned. Otherwise 11:11:11 will be used as default.

Note: You must have previously called

    locale.setlocale(locale.LC_ALL, '')

somewhere in your code.

Returns

List of Python datetimes the input could be parsed as.

def wxDate2py_dt(wxDate=None)
Expand source code
def wxDate2py_dt(wxDate=None):
        if not wxDate.IsValid():
                raise ValueError ('invalid wxDate: %s-%s-%s %s:%s %s.%s',
                        wxDate.GetYear(),
                        wxDate.GetMonth(),
                        wxDate.GetDay(),
                        wxDate.GetHour(),
                        wxDate.GetMinute(),
                        wxDate.GetSecond(),
                        wxDate.GetMillisecond()
                )

        try:
                return pyDT.datetime (
                        year = wxDate.GetYear(),
                        month = wxDate.GetMonth() + 1,
                        day = wxDate.GetDay(),
                        tzinfo = gmCurrentLocalTimezone
                )
        except Exception:
                _log.debug ('error converting wxDateTime to Python: %s-%s-%s %s:%s %s.%s',
                        wxDate.GetYear(),
                        wxDate.GetMonth(),
                        wxDate.GetDay(),
                        wxDate.GetHour(),
                        wxDate.GetMinute(),
                        wxDate.GetSecond(),
                        wxDate.GetMillisecond()
                )
                raise

Classes

class cFuzzyTimestamp (timestamp=None, accuracy=8, modifier='')
Expand source code
class cFuzzyTimestamp:

        # FIXME: add properties for year, month, ...

        """A timestamp implementation with definable inaccuracy.

        This class contains an datetime.datetime instance to
        hold the actual timestamp. It adds an accuracy attribute
        to allow the programmer to set the precision of the
        timestamp.

        The timestamp will have to be initialized with a fully
        precise value (which may, of course, contain partially
        fake data to make up for missing values). One can then
        set the accuracy value to indicate up to which part of
        the timestamp the data is valid. Optionally a modifier
        can be set to indicate further specification of the
        value (such as "summer", "afternoon", etc).

        accuracy values:
                1: year only
                ...
                7: everything including milliseconds value

        Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(
        """
        #-----------------------------------------------------------------------
        def __init__(self, timestamp=None, accuracy=ACC_SUBSECONDS, modifier=''):

                if timestamp is None:
                        timestamp = pydt_now_here()
                        accuracy = ACC_SUBSECONDS
                        modifier = ''

                if (accuracy < 1) or (accuracy > 8):
                        raise ValueError('%s.__init__(): <accuracy> must be between 1 and 8' % self.__class__.__name__)

                if not isinstance(timestamp, pyDT.datetime):
                        raise TypeError('%s.__init__(): <timestamp> must be of datetime.datetime type, but is %s' % self.__class__.__name__, type(timestamp))

                if timestamp.tzinfo is None:
                        raise ValueError('%s.__init__(): <tzinfo> must be defined' % self.__class__.__name__)

                self.timestamp = timestamp
                self.accuracy = accuracy
                self.modifier =  modifier

        #-----------------------------------------------------------------------
        # magic API
        #-----------------------------------------------------------------------
        def __str__(self):
                """Return string representation meaningful to a user, also for %s formatting."""
                return self.format_accurately()

        #-----------------------------------------------------------------------
        def __repr__(self):
                """Return string meaningful to a programmer to aid in debugging."""
                tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % (
                        self.__class__.__name__,
                        repr(self.timestamp),
                        self.accuracy,
                        _accuracy_strings[self.accuracy],
                        self.modifier,
                        id(self)
                )
                return tmp

        #-----------------------------------------------------------------------
        # external API
        #-----------------------------------------------------------------------
        def strftime(self, format_string):
                if self.accuracy == 7:
                        return self.timestamp.strftime(format_string)
                return self.format_accurately()

        #-----------------------------------------------------------------------
        def Format(self, format_string):
                return self.strftime(format_string)

        #-----------------------------------------------------------------------
        def format_accurately(self, accuracy=None):
                if accuracy is None:
                        accuracy = self.accuracy

                if accuracy == ACC_YEARS:
                        return str(self.timestamp.year)

                if accuracy == ACC_MONTHS:
                        return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?

                if accuracy == ACC_WEEKS:
                        return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?

                if accuracy == ACC_DAYS:
                        return self.timestamp.strftime('%Y-%m-%d')

                if accuracy == ACC_HOURS:
                        return self.timestamp.strftime("%Y-%m-%d %I%p")

                if accuracy == ACC_MINUTES:
                        return self.timestamp.strftime("%Y-%m-%d %H:%M")

                if accuracy == ACC_SECONDS:
                        return self.timestamp.strftime("%Y-%m-%d %H:%M:%S")

                if accuracy == ACC_SUBSECONDS:
                        return self.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")

                raise ValueError('%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % (
                        self.__class__.__name__,
                        accuracy
                ))

        #-----------------------------------------------------------------------
        def get_pydt(self):
                return self.timestamp

A timestamp implementation with definable inaccuracy.

This class contains an datetime.datetime instance to hold the actual timestamp. It adds an accuracy attribute to allow the programmer to set the precision of the timestamp.

The timestamp will have to be initialized with a fully precise value (which may, of course, contain partially fake data to make up for missing values). One can then set the accuracy value to indicate up to which part of the timestamp the data is valid. Optionally a modifier can be set to indicate further specification of the value (such as "summer", "afternoon", etc).

accuracy values: 1: year only … 7: everything including milliseconds value

Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(

Methods

def Format(self, format_string)
Expand source code
def Format(self, format_string):
        return self.strftime(format_string)
def format_accurately(self, accuracy=None)
Expand source code
def format_accurately(self, accuracy=None):
        if accuracy is None:
                accuracy = self.accuracy

        if accuracy == ACC_YEARS:
                return str(self.timestamp.year)

        if accuracy == ACC_MONTHS:
                return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?

        if accuracy == ACC_WEEKS:
                return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?

        if accuracy == ACC_DAYS:
                return self.timestamp.strftime('%Y-%m-%d')

        if accuracy == ACC_HOURS:
                return self.timestamp.strftime("%Y-%m-%d %I%p")

        if accuracy == ACC_MINUTES:
                return self.timestamp.strftime("%Y-%m-%d %H:%M")

        if accuracy == ACC_SECONDS:
                return self.timestamp.strftime("%Y-%m-%d %H:%M:%S")

        if accuracy == ACC_SUBSECONDS:
                return self.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")

        raise ValueError('%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % (
                self.__class__.__name__,
                accuracy
        ))
def get_pydt(self)
Expand source code
def get_pydt(self):
        return self.timestamp
def strftime(self, format_string)
Expand source code
def strftime(self, format_string):
        if self.accuracy == 7:
                return self.timestamp.strftime(format_string)
        return self.format_accurately()
class cPlatformLocalTimezone
Expand source code
class cPlatformLocalTimezone(pyDT.tzinfo):
        """Local timezone implementation (lifted from the docs).

        A class capturing the platform's idea of local time.

        May result in wrong values on historical times in
        timezones where UTC offset and/or the DST rules had
        changed in the past."""
        def __init__(self):
                self._SECOND = pyDT.timedelta(seconds = 1)
                self._nonDST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.timezone)
                if time.daylight:
                        self._DST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.altzone)
                else:
                        self._DST_OFFSET_FROM_UTC = self._nonDST_OFFSET_FROM_UTC
                self._DST_SHIFT = self._DST_OFFSET_FROM_UTC - self._nonDST_OFFSET_FROM_UTC
                _log.debug('[%s]: UTC->non-DST offset [%s], UTC->DST offset [%s], DST shift [%s]', self.__class__.__name__, self._nonDST_OFFSET_FROM_UTC, self._DST_OFFSET_FROM_UTC, self._DST_SHIFT)

        #-----------------------------------------------------------------------
        def fromutc(self, dt):
                assert dt.tzinfo is self
                stamp = (dt - pyDT.datetime(1970, 1, 1, tzinfo = self)) // self._SECOND
                args = time.localtime(stamp)[:6]
                dst_diff = self._DST_SHIFT // self._SECOND
                # Detect fold
                fold = (args == time.localtime(stamp - dst_diff))
                return pyDT.datetime(*args, microsecond = dt.microsecond, tzinfo = self, fold = fold)

        #-----------------------------------------------------------------------
        def utcoffset(self, dt):
                if self._isdst(dt):
                        return self._DST_OFFSET_FROM_UTC
                return self._nonDST_OFFSET_FROM_UTC

        #-----------------------------------------------------------------------
        def dst(self, dt):
                if self._isdst(dt):
                        return self._DST_SHIFT
                return pyDT.timedelta(0)

        #-----------------------------------------------------------------------
        def tzname(self, dt):
                return time.tzname[self._isdst(dt)]

        #-----------------------------------------------------------------------
        def _isdst(self, dt):
                tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, 0)
                try:
                        stamp = time.mktime(tt)
                except (OverflowError, ValueError):
                        _log.exception('overflow in time.mktime(%s), assuming non-DST', tt)
                        return False

                tt = time.localtime(stamp)
                return tt.tm_isdst > 0

Local timezone implementation (lifted from the docs).

A class capturing the platform's idea of local time.

May result in wrong values on historical times in timezones where UTC offset and/or the DST rules had changed in the past.

Ancestors

  • datetime.tzinfo

Methods

def dst(self, dt)
Expand source code
def dst(self, dt):
        if self._isdst(dt):
                return self._DST_SHIFT
        return pyDT.timedelta(0)

datetime -> DST offset as timedelta positive east of UTC.

def fromutc(self, dt)
Expand source code
def fromutc(self, dt):
        assert dt.tzinfo is self
        stamp = (dt - pyDT.datetime(1970, 1, 1, tzinfo = self)) // self._SECOND
        args = time.localtime(stamp)[:6]
        dst_diff = self._DST_SHIFT // self._SECOND
        # Detect fold
        fold = (args == time.localtime(stamp - dst_diff))
        return pyDT.datetime(*args, microsecond = dt.microsecond, tzinfo = self, fold = fold)

datetime in UTC -> datetime in local time.

def tzname(self, dt)
Expand source code
def tzname(self, dt):
        return time.tzname[self._isdst(dt)]

datetime -> string name of time zone.

def utcoffset(self, dt)
Expand source code
def utcoffset(self, dt):
        if self._isdst(dt):
                return self._DST_OFFSET_FROM_UTC
        return self._nonDST_OFFSET_FROM_UTC

datetime -> timedelta showing offset from UTC, negative values indicating West of UTC