#! /usr/bin/env python3 # -*- coding: utf-8 -*- """Tools for getting and displaying the time.""" from __future__ import unicode_literals, absolute_import, print_function, division import datetime try: import pytz except: pytz = False def validate_timezone(zone): """Return an IETF timezone from the given IETF zone or common abbreviation. If the length of the zone is 4 or less, it will be upper-cased before being looked up; otherwise it will be title-cased. This is the expected case-insensitivity behavior in the majority of cases. For example, ``'edt'`` and ``'america/new_york'`` will both return ``'America/New_York'``. If the zone is not valid, ``ValueError`` will be raised. If ``pytz`` is not available, and the given zone is anything other than ``'UTC'``, ``ValueError`` will be raised. """ if zone is None: return None if not pytz: if zone.upper() != 'UTC': raise ValueError('Only UTC available, since pytz is not installed.') else: return zone zone = '/'.join(reversed(zone.split(', '))).replace(' ', '_') if len(zone) <= 4: zone = zone.upper() else: zone = zone.title() if zone in pytz.all_timezones: return zone else: raise ValueError("Invalid time zone.") def validate_format(tformat): """Returns the format, if valid, else None""" try: time = datetime.datetime.utcnow() time.strftime(tformat) except: raise ValueError('Invalid time format') return tformat def get_timezone(db=None, config=None, zone=None, nick=None, channel=None): """Find, and return, the approriate timezone Time zone is pulled in the following priority: 1. `zone`, if it is valid 2. The timezone for the channel or nick `zone` in `db` if one is set and valid. 3. The timezone for the nick `nick` in `db`, if one is set and valid. 4. The timezone for the channel `channel` in `db`, if one is set and valid. 5. The default timezone in `config`, if one is set and valid. If `db` is not given, or given but not set up, steps 2 and 3 will be skipped. If `config` is not given, step 4 will be skipped. If no step yeilds a valid timezone, `None` is returned. Valid timezones are those present in the IANA Time Zone Database. Prior to checking timezones, two translations are made to make the zone names more human-friendly. First, the string is split on `', '`, the pieces reversed, and then joined with `'/'`. Next, remaining spaces are replaced with `'_'`. Finally, strings longer than 4 characters are made title-case, and those 4 characters and shorter are made upper-case. This means "new york, america" becomes "America/New_York", and "utc" becomes "UTC". This function relies on `pytz` being available. If it is not available, `None` will always be returned. """ def _check(zone): try: return validate_timezone(zone) except ValueError: return None if not pytz: return None tz = None if zone: tz = _check(zone) if not tz: tz = _check( db.get_nick_or_channel_value(zone, 'timezone')) if not tz and nick: tz = _check(db.get_nick_value(nick, 'timezone')) if not tz and channel: tz = _check(db.get_channel_value(channel, 'timezone')) if not tz and config and config.core.default_timezone: tz = _check(config.core.default_timezone) return tz def format_time(db=None, config=None, zone=None, nick=None, channel=None, time=None): """Return a formatted string of the given time in the given zone. `time`, if given, should be a naive `datetime.datetime` object and will be treated as being in the UTC timezone. If it is not given, the current time will be used. If `zone` is given and `pytz` is available, `zone` must be present in the IANA Time Zone Database; `get_timezone` can be helpful for this. If `zone` is not given or `pytz` is not available, UTC will be assumed. The format for the string is chosen in the following order: 1. The format for the nick `nick` in `db`, if one is set and valid. 2. The format for the channel `channel` in `db`, if one is set and valid. 3. The default format in `config`, if one iqqs set and valid. 4. ISO-8601 If `db` is not given or is not set up, steps 1 and 2 are skipped. If config is not given, step 3 will be skipped.""" tformat = None if db: if nick: tformat = db.get_nick_value(nick, 'time_format') if not tformat and channel: tformat = db.get_channel_value(channel, 'time_format') if not tformat and config and config.core.default_time_format: tformat = config.core.default_time_format if not tformat: tformat = '%Y-%m-%d - %T%Z' if not time: time = datetime.datetime.utcnow() if not pytz or not zone: return time.strftime(tformat) else: if not time.tzinfo: utc = pytz.timezone('UTC') time = utc.localize(time) zone = pytz.timezone(zone) return time.astimezone(zone).strftime(tformat) def relativeTime(bot, nick, telldate): """ Takes a datetime object and returns a string containing the relative time difference between that datetime-string and now. It takes a string. Not a datetime object. Fix that sometime. """ try: telldatetime = datetime.datetime.strptime(telldate, bot.config.core.default_time_format) except ValueError: return("Unable to parse relative time.") tz = get_timezone(bot.db, bot.config, None, nick) timenow = format_time(bot.db, bot.config, tz, nick) try: nowdatetime = datetime.datetime.strptime(timenow, bot.config.core.default_time_format) except ValueError: return("Unable to parse relative time.") timediff = nowdatetime - telldatetime reltime = [] # TODO: Rewrite all of this. if timediff.days: if timediff.days // 365: reltime.append( str(timediff.days // 365) + " year" ) if timediff.days % 365 // 30: reltime.append( str(timediff.days % 365 // 30.416).partition(".")[0] + " month") if timediff.days % 365 % 30: reltime.append( str(timediff.days % 365 % 30.416).partition(".")[0] + " day") else: if timediff.seconds // 3600: reltime.append( str(timediff.seconds // 3600) + " hour" ) if timediff.seconds % 3600 // 60: reltime.append( str(timediff.seconds % 3600 // 60) + " minute" ) for item in reltime: if item.split(' ')[0] != '1': reltime[reltime.index(item)] += 's' if timediff.days == 0 and timediff.seconds < 60: reltime = ["less than a minute"] if not reltime: return("Unable to parse relative time.") return ', '.join(reltime) + ' ago'