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