sopel/tools/time.py

191 lines
6.6 KiB
Python
Raw Normal View History

2017-11-22 19:26:40 -05:00
#! /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 is 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 = []
if timediff.days:
if timediff.days // 365:
reltime.append( str(timediff.days // 365) + " year" )
if timediff.days % 365 // 31:
reltime.append( str(timediff.days % 365 // 31) + " month")
if timediff.days % 365 % 31:
reltime.append( str(timediff.days % 365 % 31) + " 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'