diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 6432252a..672c9fbf 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -896,111 +896,111 @@ available_notification_agents = sorted(notifiers.available_notification_agents() diff --git a/lib/arrow/__init__.py b/lib/arrow/__init__.py new file mode 100644 index 00000000..8407d996 --- /dev/null +++ b/lib/arrow/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from .arrow import Arrow +from .factory import ArrowFactory +from .api import get, now, utcnow + +__version__ = '0.7.0' +VERSION = __version__ diff --git a/lib/arrow/api.py b/lib/arrow/api.py new file mode 100644 index 00000000..495eef49 --- /dev/null +++ b/lib/arrow/api.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +''' +Provides the default implementation of :class:`ArrowFactory ` +methods for use as a module API. + +''' + +from __future__ import absolute_import + +from arrow.factory import ArrowFactory + + +# internal default factory. +_factory = ArrowFactory() + + +def get(*args, **kwargs): + ''' Implements the default :class:`ArrowFactory ` + ``get`` method. + + ''' + + return _factory.get(*args, **kwargs) + +def utcnow(): + ''' Implements the default :class:`ArrowFactory ` + ``utcnow`` method. + + ''' + + return _factory.utcnow() + + +def now(tz=None): + ''' Implements the default :class:`ArrowFactory ` + ``now`` method. + + ''' + + return _factory.now(tz) + + +def factory(type): + ''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` + or derived type. + + :param type: the type, :class:`Arrow ` or derived. + + ''' + + return ArrowFactory(type) + + +__all__ = ['get', 'utcnow', 'now', 'factory', 'iso'] + diff --git a/lib/arrow/arrow.py b/lib/arrow/arrow.py new file mode 100644 index 00000000..d8c63857 --- /dev/null +++ b/lib/arrow/arrow.py @@ -0,0 +1,896 @@ +# -*- coding: utf-8 -*- +''' +Provides the :class:`Arrow ` class, an enhanced ``datetime`` +replacement. + +''' + +from __future__ import absolute_import + +from datetime import datetime, timedelta, tzinfo +from dateutil import tz as dateutil_tz +from dateutil.relativedelta import relativedelta +import calendar +import sys + +from arrow import util, locales, parser, formatter + + +class Arrow(object): + '''An :class:`Arrow ` object. + + Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing + additional functionality. + + :param year: the calendar year. + :param month: the calendar month. + :param day: the calendar day. + :param hour: (optional) the hour. Defaults to 0. + :param minute: (optional) the minute, Defaults to 0. + :param second: (optional) the second, Defaults to 0. + :param microsecond: (optional) the microsecond. Defaults 0. + :param tzinfo: (optional) the ``tzinfo`` object. Defaults to ``None``. + + If tzinfo is None, it is assumed to be UTC on creation. + + Usage:: + + >>> import arrow + >>> arrow.Arrow(2013, 5, 5, 12, 30, 45) + + + ''' + + resolution = datetime.resolution + + _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] + _ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS] + + def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, + tzinfo=None): + + if util.isstr(tzinfo): + tzinfo = parser.TzinfoParser.parse(tzinfo) + tzinfo = tzinfo or dateutil_tz.tzutc() + + self._datetime = datetime(year, month, day, hour, minute, second, + microsecond, tzinfo) + + + # factories: single object, both original and from datetime. + + @classmethod + def now(cls, tzinfo=None): + '''Constructs an :class:`Arrow ` object, representing "now". + + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + + ''' + + utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + dt = utc.astimezone(dateutil_tz.tzlocal() if tzinfo is None else tzinfo) + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, dt.tzinfo) + + @classmethod + def utcnow(cls): + ''' Constructs an :class:`Arrow ` object, representing "now" in UTC + time. + + ''' + + dt = datetime.utcnow() + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, dateutil_tz.tzutc()) + + @classmethod + def fromtimestamp(cls, timestamp, tzinfo=None): + ''' Constructs an :class:`Arrow ` object from a timestamp. + + :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + + ''' + + tzinfo = tzinfo or dateutil_tz.tzlocal() + timestamp = cls._get_timestamp_from_input(timestamp) + dt = datetime.fromtimestamp(timestamp, tzinfo) + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, tzinfo) + + @classmethod + def utcfromtimestamp(cls, timestamp): + '''Constructs an :class:`Arrow ` object from a timestamp, in UTC time. + + :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. + + ''' + + timestamp = cls._get_timestamp_from_input(timestamp) + dt = datetime.utcfromtimestamp(timestamp) + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, dateutil_tz.tzutc()) + + @classmethod + def fromdatetime(cls, dt, tzinfo=None): + ''' Constructs an :class:`Arrow ` object from a ``datetime`` and optional + ``tzinfo`` object. + + :param dt: the ``datetime`` + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. + + ''' + + tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, tzinfo) + + @classmethod + def fromdate(cls, date, tzinfo=None): + ''' Constructs an :class:`Arrow ` object from a ``date`` and optional + ``tzinfo`` object. Time values are set to 0. + + :param date: the ``date`` + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. + ''' + + tzinfo = tzinfo or dateutil_tz.tzutc() + + return cls(date.year, date.month, date.day, tzinfo=tzinfo) + + @classmethod + def strptime(cls, date_str, fmt, tzinfo=None): + ''' Constructs an :class:`Arrow ` object from a date string and format, + in the style of ``datetime.strptime``. + + :param date_str: the date string. + :param fmt: the format string. + :param tzinfo: (optional) an optional ``tzinfo`` + ''' + + dt = datetime.strptime(date_str, fmt) + tzinfo = tzinfo or dt.tzinfo + + return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, tzinfo) + + + # factories: ranges and spans + + @classmethod + def range(cls, frame, start, end=None, tz=None, limit=None): + ''' Returns an array of :class:`Arrow ` objects, representing + an iteration of time between two inputs. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param start: A datetime expression, the start of the range. + :param end: (optional) A datetime expression, the end of the range. + :param tz: (optional) A timezone expression. Defaults to UTC. + :param limit: (optional) A maximum number of tuples to return. + + **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to + return the entire range, with **limit** alone to return a maximum # of results from the + start, and with both to cap a range at a maximum # of results. + + Supported frame values: year, quarter, month, week, day, hour, minute, second + + Recognized datetime expressions: + + - An :class:`Arrow ` object. + - A ``datetime`` object. + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + Usage: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.range('hour', start, end): + ... print repr(r) + ... + + + + + + + ''' + + _, frame_relative, relative_steps = cls._get_frames(frame) + + tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) + + start = cls._get_datetime(start).replace(tzinfo=tzinfo) + end, limit = cls._get_iteration_params(end, limit) + end = cls._get_datetime(end).replace(tzinfo=tzinfo) + + current = cls.fromdatetime(start) + results = [] + + while current <= end and len(results) < limit: + results.append(current) + + values = [getattr(current, f) for f in cls._ATTRS] + current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps}) + + return results + + + @classmethod + def span_range(cls, frame, start, end, tz=None, limit=None): + ''' Returns an array of tuples, each :class:`Arrow ` objects, + representing a series of timespans between two inputs. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param start: A datetime expression, the start of the range. + :param end: (optional) A datetime expression, the end of the range. + :param tz: (optional) A timezone expression. Defaults to UTC. + :param limit: (optional) A maximum number of tuples to return. + + **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to + return the entire range, with **limit** alone to return a maximum # of results from the + start, and with both to cap a range at a maximum # of results. + + Supported frame values: year, quarter, month, week, day, hour, minute, second + + Recognized datetime expressions: + + - An :class:`Arrow ` object. + - A ``datetime`` object. + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + Usage: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.span_range('hour', start, end): + ... print r + ... + (, ) + (, ) + (, ) + (, ) + (, ) + + ''' + tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) + start = cls.fromdatetime(start, tzinfo).span(frame)[0] + _range = cls.range(frame, start, end, tz, limit) + return [r.span(frame) for r in _range] + + + # representations + + def __repr__(self): + + dt = self._datetime + attrs = ', '.join([str(i) for i in [dt.year, dt.month, dt.day, dt.hour, dt.minute, + dt.second, dt.microsecond]]) + + return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__()) + + def __str__(self): + return self._datetime.isoformat() + + def __format__(self, formatstr): + + if len(formatstr) > 0: + return self.format(formatstr) + + return str(self) + + def __hash__(self): + return self._datetime.__hash__() + + + # attributes & properties + + def __getattr__(self, name): + + if name == 'week': + return self.isocalendar()[1] + + if not name.startswith('_'): + value = getattr(self._datetime, name, None) + + if value is not None: + return value + + return object.__getattribute__(self, name) + + @property + def tzinfo(self): + ''' Gets the ``tzinfo`` of the :class:`Arrow ` object. ''' + + return self._datetime.tzinfo + + @tzinfo.setter + def tzinfo(self, tzinfo): + ''' Sets the ``tzinfo`` of the :class:`Arrow ` object. ''' + + self._datetime = self._datetime.replace(tzinfo=tzinfo) + + @property + def datetime(self): + ''' Returns a datetime representation of the :class:`Arrow ` object. ''' + + return self._datetime + + @property + def naive(self): + ''' Returns a naive datetime representation of the :class:`Arrow ` object. ''' + + return self._datetime.replace(tzinfo=None) + + @property + def timestamp(self): + ''' Returns a timestamp representation of the :class:`Arrow ` object. ''' + + return calendar.timegm(self._datetime.utctimetuple()) + + @property + def float_timestamp(self): + ''' Returns a floating-point representation of the :class:`Arrow ` object. ''' + + return self.timestamp + float(self.microsecond) / 1000000 + + + # mutation and duplication. + + def clone(self): + ''' Returns a new :class:`Arrow ` object, cloned from the current one. + + Usage: + + >>> arw = arrow.utcnow() + >>> cloned = arw.clone() + + ''' + + return self.fromdatetime(self._datetime) + + def replace(self, **kwargs): + ''' Returns a new :class:`Arrow ` object with attributes updated + according to inputs. + + Use single property names to set their value absolutely: + + >>> import arrow + >>> arw = arrow.utcnow() + >>> arw + + >>> arw.replace(year=2014, month=6) + + + Use plural property names to shift their current value relatively: + + >>> arw.replace(years=1, months=-1) + + + You can also provide a timezone expression can also be replaced: + + >>> arw.replace(tzinfo=tz.tzlocal()) + + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + ''' + + absolute_kwargs = {} + relative_kwargs = {} + + for key, value in kwargs.items(): + + if key in self._ATTRS: + absolute_kwargs[key] = value + elif key in self._ATTRS_PLURAL or key == 'weeks': + relative_kwargs[key] = value + elif key == 'week': + raise AttributeError('setting absolute week is not supported') + elif key !='tzinfo': + raise AttributeError() + + current = self._datetime.replace(**absolute_kwargs) + current += relativedelta(**relative_kwargs) + + tzinfo = kwargs.get('tzinfo') + + if tzinfo is not None: + tzinfo = self._get_tzinfo(tzinfo) + current = current.replace(tzinfo=tzinfo) + + return self.fromdatetime(current) + + def to(self, tz): + ''' Returns a new :class:`Arrow ` object, converted to the target + timezone. + + :param tz: an expression representing a timezone. + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + Usage:: + + >>> utc = arrow.utcnow() + >>> utc + + + >>> utc.to('US/Pacific') + + + >>> utc.to(tz.tzlocal()) + + + >>> utc.to('-07:00') + + + >>> utc.to('local') + + + >>> utc.to('local').to('utc') + + + ''' + + if not isinstance(tz, tzinfo): + tz = parser.TzinfoParser.parse(tz) + + dt = self._datetime.astimezone(tz) + + return self.__class__(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + dt.microsecond, dt.tzinfo) + + def span(self, frame, count=1): + ''' Returns two new :class:`Arrow ` objects, representing the timespan + of the :class:`Arrow ` object in a given timeframe. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param count: (optional) the number of frames to span. + + Supported frame values: year, quarter, month, week, day, hour, minute, second + + Usage:: + + >>> arrow.utcnow() + + + >>> arrow.utcnow().span('hour') + (, ) + + >>> arrow.utcnow().span('day') + (, ) + + >>> arrow.utcnow().span('day', count=2) + (, ) + + ''' + + frame_absolute, frame_relative, relative_steps = self._get_frames(frame) + + if frame_absolute == 'week': + attr = 'day' + elif frame_absolute == 'quarter': + attr = 'month' + else: + attr = frame_absolute + + index = self._ATTRS.index(attr) + frames = self._ATTRS[:index + 1] + + values = [getattr(self, f) for f in frames] + + for i in range(3 - len(values)): + values.append(1) + + floor = self.__class__(*values, tzinfo=self.tzinfo) + + if frame_absolute == 'week': + floor = floor + relativedelta(days=-(self.isoweekday() - 1)) + elif frame_absolute == 'quarter': + floor = floor + relativedelta(months=-((self.month - 1) % 3)) + + ceil = floor + relativedelta( + **{frame_relative: count * relative_steps}) + relativedelta(microseconds=-1) + + return floor, ceil + + def floor(self, frame): + ''' Returns a new :class:`Arrow ` object, representing the "floor" + of the timespan of the :class:`Arrow ` object in a given timeframe. + Equivalent to the first element in the 2-tuple returned by + :func:`span `. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + + Usage:: + + >>> arrow.utcnow().floor('hour') + + ''' + + return self.span(frame)[0] + + def ceil(self, frame): + ''' Returns a new :class:`Arrow ` object, representing the "ceiling" + of the timespan of the :class:`Arrow ` object in a given timeframe. + Equivalent to the second element in the 2-tuple returned by + :func:`span `. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + + Usage:: + + >>> arrow.utcnow().ceil('hour') + + ''' + + return self.span(frame)[1] + + + # string output and formatting. + + def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'): + ''' Returns a string representation of the :class:`Arrow ` object, + formatted according to a format string. + + :param fmt: the format string. + + Usage:: + + >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') + '2013-05-09 03:56:47 -00:00' + + >>> arrow.utcnow().format('X') + '1368071882' + + >>> arrow.utcnow().format('MMMM DD, YYYY') + 'May 09, 2013' + + >>> arrow.utcnow().format() + '2013-05-09 03:56:47 -00:00' + + ''' + + return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) + + + def humanize(self, other=None, locale='en_us', only_distance=False): + ''' Returns a localized, humanized representation of a relative difference in time. + + :param other: (optional) an :class:`Arrow ` or ``datetime`` object. + Defaults to now in the current :class:`Arrow ` object's timezone. + :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. + :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. + Usage:: + + >>> earlier = arrow.utcnow().replace(hours=-2) + >>> earlier.humanize() + '2 hours ago' + + >>> later = later = earlier.replace(hours=4) + >>> later.humanize(earlier) + 'in 4 hours' + + ''' + + locale = locales.get_locale(locale) + + if other is None: + utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + dt = utc.astimezone(self._datetime.tzinfo) + + elif isinstance(other, Arrow): + dt = other._datetime + + elif isinstance(other, datetime): + if other.tzinfo is None: + dt = other.replace(tzinfo=self._datetime.tzinfo) + else: + dt = other.astimezone(self._datetime.tzinfo) + + else: + raise TypeError() + + delta = int(util.total_seconds(self._datetime - dt)) + sign = -1 if delta < 0 else 1 + diff = abs(delta) + delta = diff + + if diff < 10: + return locale.describe('now', only_distance=only_distance) + + if diff < 45: + return locale.describe('seconds', sign, only_distance=only_distance) + + elif diff < 90: + return locale.describe('minute', sign, only_distance=only_distance) + elif diff < 2700: + minutes = sign * int(max(delta / 60, 2)) + return locale.describe('minutes', minutes, only_distance=only_distance) + + elif diff < 5400: + return locale.describe('hour', sign, only_distance=only_distance) + elif diff < 79200: + hours = sign * int(max(delta / 3600, 2)) + return locale.describe('hours', hours, only_distance=only_distance) + + elif diff < 129600: + return locale.describe('day', sign, only_distance=only_distance) + elif diff < 2160000: + days = sign * int(max(delta / 86400, 2)) + return locale.describe('days', days, only_distance=only_distance) + + elif diff < 3888000: + return locale.describe('month', sign, only_distance=only_distance) + elif diff < 29808000: + self_months = self._datetime.year * 12 + self._datetime.month + other_months = dt.year * 12 + dt.month + months = sign * abs(other_months - self_months) + + return locale.describe('months', months, only_distance=only_distance) + + elif diff < 47260800: + return locale.describe('year', sign, only_distance=only_distance) + else: + years = sign * int(max(delta / 31536000, 2)) + return locale.describe('years', years, only_distance=only_distance) + + + # math + + def __add__(self, other): + + if isinstance(other, (timedelta, relativedelta)): + return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) + + raise TypeError() + + def __radd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + + if isinstance(other, timedelta): + return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) + + elif isinstance(other, datetime): + return self._datetime - other + + elif isinstance(other, Arrow): + return self._datetime - other._datetime + + raise TypeError() + + def __rsub__(self, other): + return self.__sub__(other) + + + # comparisons + + def _cmperror(self, other): + raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format( + type(self), type(other))) + + def __eq__(self, other): + + if not isinstance(other, (Arrow, datetime)): + return False + + other = self._get_datetime(other) + + return self._datetime == self._get_datetime(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + + if not isinstance(other, (Arrow, datetime)): + self._cmperror(other) + + return self._datetime > self._get_datetime(other) + + def __ge__(self, other): + + if not isinstance(other, (Arrow, datetime)): + self._cmperror(other) + + return self._datetime >= self._get_datetime(other) + + def __lt__(self, other): + + if not isinstance(other, (Arrow, datetime)): + self._cmperror(other) + + return self._datetime < self._get_datetime(other) + + def __le__(self, other): + + if not isinstance(other, (Arrow, datetime)): + self._cmperror(other) + + return self._datetime <= self._get_datetime(other) + + + # datetime methods + + def date(self): + ''' Returns a ``date`` object with the same year, month and day. ''' + + return self._datetime.date() + + def time(self): + ''' Returns a ``time`` object with the same hour, minute, second, microsecond. ''' + + return self._datetime.time() + + def timetz(self): + ''' Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. ''' + + return self._datetime.timetz() + + def astimezone(self, tz): + ''' Returns a ``datetime`` object, adjusted to the specified tzinfo. + + :param tz: a ``tzinfo`` object. + + ''' + + return self._datetime.astimezone(tz) + + def utcoffset(self): + ''' Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. ''' + + return self._datetime.utcoffset() + + def dst(self): + ''' Returns the daylight savings time adjustment. ''' + return self._datetime.dst() + + def timetuple(self): + ''' Returns a ``time.struct_time``, in the current timezone. ''' + + return self._datetime.timetuple() + + def utctimetuple(self): + ''' Returns a ``time.struct_time``, in UTC time. ''' + + return self._datetime.utctimetuple() + + def toordinal(self): + ''' Returns the proleptic Gregorian ordinal of the date. ''' + + return self._datetime.toordinal() + + def weekday(self): + ''' Returns the day of the week as an integer (0-6). ''' + + return self._datetime.weekday() + + def isoweekday(self): + ''' Returns the ISO day of the week as an integer (1-7). ''' + + return self._datetime.isoweekday() + + def isocalendar(self): + ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). ''' + + return self._datetime.isocalendar() + + def isoformat(self, sep='T'): + '''Returns an ISO 8601 formatted representation of the date and time. ''' + + return self._datetime.isoformat(sep) + + def ctime(self): + ''' Returns a ctime formatted representation of the date and time. ''' + + return self._datetime.ctime() + + def strftime(self, format): + ''' Formats in the style of ``datetime.strptime``. + + :param format: the format string. + + ''' + + return self._datetime.strftime(format) + + def for_json(self): + '''Serializes for the ``for_json`` protocol of simplejson.''' + return self.isoformat() + + # internal tools. + + @staticmethod + def _get_tzinfo(tz_expr): + + if tz_expr is None: + return dateutil_tz.tzutc() + if isinstance(tz_expr, tzinfo): + return tz_expr + else: + try: + return parser.TzinfoParser.parse(tz_expr) + except parser.ParserError: + raise ValueError('\'{0}\' not recognized as a timezone'.format( + tz_expr)) + + @classmethod + def _get_datetime(cls, expr): + + if isinstance(expr, Arrow): + return expr.datetime + + if isinstance(expr, datetime): + return expr + + try: + expr = float(expr) + return cls.utcfromtimestamp(expr).datetime + except: + raise ValueError( + '\'{0}\' not recognized as a timestamp or datetime'.format(expr)) + + @classmethod + def _get_frames(cls, name): + + if name in cls._ATTRS: + return name, '{0}s'.format(name), 1 + + elif name in ['week', 'weeks']: + return 'week', 'weeks', 1 + elif name in ['quarter', 'quarters']: + return 'quarter', 'months', 3 + + raise AttributeError() + + @classmethod + def _get_iteration_params(cls, end, limit): + + if end is None: + + if limit is None: + raise Exception('one of \'end\' or \'limit\' is required') + + return cls.max, limit + + else: + return end, sys.maxsize + + @staticmethod + def _get_timestamp_from_input(timestamp): + + try: + return float(timestamp) + except: + raise ValueError('cannot parse \'{0}\' as a timestamp'.format(timestamp)) + +Arrow.min = Arrow.fromdatetime(datetime.min) +Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/lib/arrow/factory.py b/lib/arrow/factory.py new file mode 100644 index 00000000..a5d690b2 --- /dev/null +++ b/lib/arrow/factory.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +""" +Implements the :class:`ArrowFactory ` class, +providing factory methods for common :class:`Arrow ` +construction scenarios. + +""" + +from __future__ import absolute_import + +from arrow.arrow import Arrow +from arrow import parser +from arrow.util import is_timestamp, isstr + +from datetime import datetime, tzinfo, date +from dateutil import tz as dateutil_tz +from time import struct_time +import calendar + + +class ArrowFactory(object): + ''' A factory for generating :class:`Arrow ` objects. + + :param type: (optional) the :class:`Arrow `-based class to construct from. + Defaults to :class:`Arrow `. + + ''' + + def __init__(self, type=Arrow): + self.type = type + + def get(self, *args, **kwargs): + ''' Returns an :class:`Arrow ` object based on flexible inputs. + + Usage:: + + >>> import arrow + + **No inputs** to get current UTC time:: + + >>> arrow.get() + + + **None** to also get current UTC time:: + + >>> arrow.get(None) + + + **One** :class:`Arrow ` object, to get a copy. + + >>> arw = arrow.utcnow() + >>> arrow.get(arw) + + + **One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get that timestamp in UTC:: + + >>> arrow.get(1367992474.293378) + + + >>> arrow.get(1367992474) + + + >>> arrow.get('1367992474.293378') + + + >>> arrow.get('1367992474') + + + **One** ISO-8601-formatted ``str``, to parse it:: + + >>> arrow.get('2013-09-29T01:26:43.830580') + + + **One** ``tzinfo``, to get the current time in that timezone:: + + >>> arrow.get(tz.tzlocal()) + + + **One** naive ``datetime``, to get that datetime in UTC:: + + >>> arrow.get(datetime(2013, 5, 5)) + + + **One** aware ``datetime``, to get that datetime:: + + >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal())) + + + **One** naive ``date``, to get that date in UTC:: + + >>> arrow.get(date(2013, 5, 5)) + + + **Two** arguments, a naive or aware ``datetime``, and a timezone expression (as above):: + + >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') + + + **Two** arguments, a naive ``date``, and a timezone expression (as above):: + + >>> arrow.get(date(2013, 5, 5), 'US/Pacific') + + + **Two** arguments, both ``str``, to parse the first according to the format of the second:: + + >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') + + + **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: + + >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) + + + **Three or more** arguments, as for the constructor of a ``datetime``:: + + >>> arrow.get(2013, 5, 5, 12, 30, 45) + + + **One** time.struct time:: + >>> arrow.get(gmtime(0)) + + + ''' + + arg_count = len(args) + locale = kwargs.get('locale', 'en_us') + tz = kwargs.get('tzinfo', None) + + # () -> now, @ utc. + if arg_count == 0: + if isinstance(tz, tzinfo): + return self.type.now(tz) + return self.type.utcnow() + + if arg_count == 1: + arg = args[0] + + # (None) -> now, @ utc. + if arg is None: + return self.type.utcnow() + + # try (int, float, str(int), str(float)) -> utc, from timestamp. + if is_timestamp(arg): + return self.type.utcfromtimestamp(arg) + + # (Arrow) -> from the object's datetime. + if isinstance(arg, Arrow): + return self.type.fromdatetime(arg.datetime) + + # (datetime) -> from datetime. + if isinstance(arg, datetime): + return self.type.fromdatetime(arg) + + # (date) -> from date. + if isinstance(arg, date): + return self.type.fromdate(arg) + + # (tzinfo) -> now, @ tzinfo. + elif isinstance(arg, tzinfo): + return self.type.now(arg) + + # (str) -> now, @ tzinfo. + elif isstr(arg): + dt = parser.DateTimeParser(locale).parse_iso(arg) + return self.type.fromdatetime(dt) + + # (struct_time) -> from struct_time + elif isinstance(arg, struct_time): + return self.type.utcfromtimestamp(calendar.timegm(arg)) + + else: + raise TypeError('Can\'t parse single argument type of \'{0}\''.format(type(arg))) + + elif arg_count == 2: + + arg_1, arg_2 = args[0], args[1] + + if isinstance(arg_1, datetime): + + # (datetime, tzinfo) -> fromdatetime @ tzinfo/string. + if isinstance(arg_2, tzinfo) or isstr(arg_2): + return self.type.fromdatetime(arg_1, arg_2) + else: + raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{0}\''.format( + type(arg_2))) + + # (date, tzinfo/str) -> fromdate @ tzinfo/string. + elif isinstance(arg_1, date): + + if isinstance(arg_2, tzinfo) or isstr(arg_2): + return self.type.fromdate(arg_1, tzinfo=arg_2) + else: + raise TypeError('Can\'t parse two arguments of types \'date\', \'{0}\''.format( + type(arg_2))) + + # (str, format) -> parse. + elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): + dt = parser.DateTimeParser(locale).parse(args[0], args[1]) + return self.type.fromdatetime(dt, tzinfo=tz) + + else: + raise TypeError('Can\'t parse two arguments of types \'{0}\', \'{1}\''.format( + type(arg_1), type(arg_2))) + + # 3+ args -> datetime-like via constructor. + else: + return self.type(*args, **kwargs) + + def utcnow(self): + '''Returns an :class:`Arrow ` object, representing "now" in UTC time. + + Usage:: + + >>> import arrow + >>> arrow.utcnow() + + ''' + + return self.type.utcnow() + + def now(self, tz=None): + '''Returns an :class:`Arrow ` object, representing "now". + + :param tz: (optional) An expression representing a timezone. Defaults to local time. + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + Usage:: + + >>> import arrow + >>> arrow.now() + + + >>> arrow.now('US/Pacific') + + + >>> arrow.now('+02:00') + + + >>> arrow.now('local') + + ''' + + if tz is None: + tz = dateutil_tz.tzlocal() + elif not isinstance(tz, tzinfo): + tz = parser.TzinfoParser.parse(tz) + + return self.type.now(tz) diff --git a/lib/arrow/formatter.py b/lib/arrow/formatter.py new file mode 100644 index 00000000..0ae23895 --- /dev/null +++ b/lib/arrow/formatter.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import calendar +import re +from dateutil import tz as dateutil_tz +from arrow import util, locales + + +class DateTimeFormatter(object): + + _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?|a|A|X)') + + def __init__(self, locale='en_us'): + + self.locale = locales.get_locale(locale) + + def format(cls, dt, fmt): + + return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt) + + def _format_token(self, dt, token): + + if token == 'YYYY': + return self.locale.year_full(dt.year) + if token == 'YY': + return self.locale.year_abbreviation(dt.year) + + if token == 'MMMM': + return self.locale.month_name(dt.month) + if token == 'MMM': + return self.locale.month_abbreviation(dt.month) + if token == 'MM': + return '{0:02d}'.format(dt.month) + if token == 'M': + return str(dt.month) + + if token == 'DDDD': + return '{0:03d}'.format(dt.timetuple().tm_yday) + if token == 'DDD': + return str(dt.timetuple().tm_yday) + if token == 'DD': + return '{0:02d}'.format(dt.day) + if token == 'D': + return str(dt.day) + + if token == 'Do': + return self.locale.ordinal_number(dt.day) + + if token == 'dddd': + return self.locale.day_name(dt.isoweekday()) + if token == 'ddd': + return self.locale.day_abbreviation(dt.isoweekday()) + if token == 'd': + return str(dt.isoweekday()) + + if token == 'HH': + return '{0:02d}'.format(dt.hour) + if token == 'H': + return str(dt.hour) + if token == 'hh': + return '{0:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + if token == 'h': + return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + + if token == 'mm': + return '{0:02d}'.format(dt.minute) + if token == 'm': + return str(dt.minute) + + if token == 'ss': + return '{0:02d}'.format(dt.second) + if token == 's': + return str(dt.second) + + if token == 'SSSSSS': + return str('{0:06d}'.format(int(dt.microsecond))) + if token == 'SSSSS': + return str('{0:05d}'.format(int(dt.microsecond / 10))) + if token == 'SSSS': + return str('{0:04d}'.format(int(dt.microsecond / 100))) + if token == 'SSS': + return str('{0:03d}'.format(int(dt.microsecond / 1000))) + if token == 'SS': + return str('{0:02d}'.format(int(dt.microsecond / 10000))) + if token == 'S': + return str(int(dt.microsecond / 100000)) + + if token == 'X': + return str(calendar.timegm(dt.utctimetuple())) + + if token in ['ZZ', 'Z']: + separator = ':' if token == 'ZZ' else '' + tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo + total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) + + sign = '+' if total_minutes > 0 else '-' + total_minutes = abs(total_minutes) + hour, minute = divmod(total_minutes, 60) + + return '{0}{1:02d}{2}{3:02d}'.format(sign, hour, separator, minute) + + if token in ('a', 'A'): + return self.locale.meridian(dt.hour, token) + diff --git a/lib/arrow/locales.py b/lib/arrow/locales.py new file mode 100644 index 00000000..c1141674 --- /dev/null +++ b/lib/arrow/locales.py @@ -0,0 +1,1703 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import unicode_literals + +import inspect +import sys + + +def get_locale(name): + '''Returns an appropriate :class:`Locale ` corresponding + to an inpute locale name. + + :param name: the name of the locale. + + ''' + + locale_cls = _locales.get(name.lower()) + + if locale_cls is None: + raise ValueError('Unsupported locale \'{0}\''.format(name)) + + return locale_cls() + + +# base locale type. + +class Locale(object): + ''' Represents locale-specific data and functionality. ''' + + names = [] + + timeframes = { + 'now': '', + 'seconds': '', + 'minute': '', + 'minutes': '', + 'hour': '', + 'hours': '', + 'day': '', + 'days': '', + 'month': '', + 'months': '', + 'year': '', + 'years': '', + } + + meridians = { + 'am': '', + 'pm': '', + 'AM': '', + 'PM': '', + } + + past = None + future = None + + month_names = [] + month_abbreviations = [] + + day_names = [] + day_abbreviations = [] + + ordinal_day_re = r'(\d+)' + + def __init__(self): + + self._month_name_to_ordinal = None + + def describe(self, timeframe, delta=0, only_distance=False): + ''' Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + ''' + + humanized = self._format_timeframe(timeframe, delta) + if not only_distance: + humanized = self._format_relative(humanized, timeframe, delta) + + return humanized + + def day_name(self, day): + ''' Returns the day name for a specified day of the week. + + :param day: the ``int`` day of the week (1-7). + + ''' + + return self.day_names[day] + + def day_abbreviation(self, day): + ''' Returns the day abbreviation for a specified day of the week. + + :param day: the ``int`` day of the week (1-7). + + ''' + + return self.day_abbreviations[day] + + def month_name(self, month): + ''' Returns the month name for a specified month of the year. + + :param month: the ``int`` month of the year (1-12). + + ''' + + return self.month_names[month] + + def month_abbreviation(self, month): + ''' Returns the month abbreviation for a specified month of the year. + + :param month: the ``int`` month of the year (1-12). + + ''' + + return self.month_abbreviations[month] + + def month_number(self, name): + ''' Returns the month number for a month specified by name or abbreviation. + + :param name: the month name or abbreviation. + + ''' + + if self._month_name_to_ordinal is None: + self._month_name_to_ordinal = self._name_to_ordinal(self.month_names) + self._month_name_to_ordinal.update(self._name_to_ordinal(self.month_abbreviations)) + + return self._month_name_to_ordinal.get(name) + + def year_full(self, year): + ''' Returns the year for specific locale if available + + :param name: the ``int`` year (4-digit) + ''' + return '{0:04d}'.format(year) + + def year_abbreviation(self, year): + ''' Returns the year for specific locale if available + + :param name: the ``int`` year (4-digit) + ''' + return '{0:04d}'.format(year)[2:] + + def meridian(self, hour, token): + ''' Returns the meridian indicator for a specified hour and format token. + + :param hour: the ``int`` hour of the day. + :param token: the format token. + ''' + + if token == 'a': + return self.meridians['am'] if hour < 12 else self.meridians['pm'] + if token == 'A': + return self.meridians['AM'] if hour < 12 else self.meridians['PM'] + + def ordinal_number(self, n): + ''' Returns the ordinal format of a given integer + + :param n: an integer + ''' + return self._ordinal_number(n) + + def _ordinal_number(self, n): + return '{0}'.format(n) + + def _name_to_ordinal(self, lst): + return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) + + def _format_timeframe(self, timeframe, delta): + + return self.timeframes[timeframe].format(abs(delta)) + + def _format_relative(self, humanized, timeframe, delta): + + if timeframe == 'now': + return humanized + + direction = self.past if delta < 0 else self.future + + return direction.format(humanized) + + +# base locale type implementations. + +class EnglishLocale(Locale): + + names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za'] + + past = '{0} ago' + future = 'in {0}' + + timeframes = { + 'now': 'just now', + 'seconds': 'seconds', + 'minute': 'a minute', + 'minutes': '{0} minutes', + 'hour': 'an hour', + 'hours': '{0} hours', + 'day': 'a day', + 'days': '{0} days', + 'month': 'a month', + 'months': '{0} months', + 'year': 'a year', + 'years': '{0} years', + } + + meridians = { + 'am': 'am', + 'pm': 'pm', + 'AM': 'AM', + 'PM': 'PM', + } + + month_names = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', + 'Sep', 'Oct', 'Nov', 'Dec'] + + day_names = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + ordinal_day_re = r'((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))' + + def _ordinal_number(self, n): + if n % 100 not in (11, 12, 13): + remainder = abs(n) % 10 + if remainder == 1: + return '{0}st'.format(n) + elif remainder == 2: + return '{0}nd'.format(n) + elif remainder == 3: + return '{0}rd'.format(n) + return '{0}th'.format(n) + + +class ItalianLocale(Locale): + names = ['it', 'it_it'] + past = '{0} fa' + future = 'tra {0}' + + timeframes = { + 'now': 'adesso', + 'seconds': 'qualche secondo', + 'minute': 'un minuto', + 'minutes': '{0} minuti', + 'hour': 'un\'ora', + 'hours': '{0} ore', + 'day': 'un giorno', + 'days': '{0} giorni', + 'month': 'un mese', + 'months': '{0} mesi', + 'year': 'un anno', + 'years': '{0} anni', + } + + month_names = ['', 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', + 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'] + month_abbreviations = ['', 'gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', + 'set', 'ott', 'nov', 'dic'] + + day_names = ['', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', 'domenica'] + day_abbreviations = ['', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab', 'dom'] + + ordinal_day_re = r'((?P[1-3]?[0-9](?=°))°)' + + def _ordinal_number(self, n): + return '{0}°'.format(n) + + +class SpanishLocale(Locale): + names = ['es', 'es_es'] + past = 'hace {0}' + future = 'en {0}' + + timeframes = { + 'now': 'ahora', + 'seconds': 'segundos', + 'minute': 'un minuto', + 'minutes': '{0} minutos', + 'hour': 'una hora', + 'hours': '{0} horas', + 'day': 'un día', + 'days': '{0} días', + 'month': 'un mes', + 'months': '{0} meses', + 'year': 'un año', + 'years': '{0} años', + } + + month_names = ['', 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', + 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'] + month_abbreviations = ['', 'ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', + 'sep', 'oct', 'nov', 'dic'] + + day_names = ['', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'] + day_abbreviations = ['', 'lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'] + + ordinal_day_re = r'((?P[1-3]?[0-9](?=°))°)' + + def _ordinal_number(self, n): + return '{0}°'.format(n) + + +class FrenchLocale(Locale): + names = ['fr', 'fr_fr'] + past = 'il y a {0}' + future = 'dans {0}' + + timeframes = { + 'now': 'maintenant', + 'seconds': 'quelques secondes', + 'minute': 'une minute', + 'minutes': '{0} minutes', + 'hour': 'une heure', + 'hours': '{0} heures', + 'day': 'un jour', + 'days': '{0} jours', + 'month': 'un mois', + 'months': '{0} mois', + 'year': 'un an', + 'years': '{0} ans', + } + + month_names = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', + 'août', 'septembre', 'octobre', 'novembre', 'décembre'] + month_abbreviations = ['', 'janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', + 'sept', 'oct', 'nov', 'déc'] + + day_names = ['', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'] + day_abbreviations = ['', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim'] + + ordinal_day_re = r'((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)' + + def _ordinal_number(self, n): + if abs(n) == 1: + return '{0}er'.format(n) + return '{0}e'.format(n) + + +class GreekLocale(Locale): + + names = ['el', 'el_gr'] + + past = '{0} πριν' + future = 'σε {0}' + + timeframes = { + 'now': 'τώρα', + 'seconds': 'δευτερόλεπτα', + 'minute': 'ένα λεπτό', + 'minutes': '{0} λεπτά', + 'hour': 'μια ώρα', + 'hours': '{0} ώρες', + 'day': 'μια μέρα', + 'days': '{0} μέρες', + 'month': 'ένα μήνα', + 'months': '{0} μήνες', + 'year': 'ένα χρόνο', + 'years': '{0} χρόνια', + } + + month_names = ['', 'Ιανουαρίου', 'Φεβρουαρίου', 'Μαρτίου', 'Απριλίου', 'Μαΐου', 'Ιουνίου', + 'Ιουλίου', 'Αυγούστου', 'Σεπτεμβρίου', 'Οκτωβρίου', 'Νοεμβρίου', 'Δεκεμβρίου'] + month_abbreviations = ['', 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μαϊ', 'Ιον', 'Ιολ', 'Αυγ', + 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'] + + day_names = ['', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο', 'Κυριακή'] + day_abbreviations = ['', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ', 'Κυρ'] + + +class JapaneseLocale(Locale): + + names = ['ja', 'ja_jp'] + + past = '{0}前' + future = '{0}後' + + timeframes = { + 'now': '現在', + 'seconds': '秒', + 'minute': '1分', + 'minutes': '{0}分', + 'hour': '1時間', + 'hours': '{0}時間', + 'day': '1日', + 'days': '{0}日', + 'month': '1ヶ月', + 'months': '{0}ヶ月', + 'year': '1年', + 'years': '{0}年', + } + + month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', + '9月', '10月', '11月', '12月'] + month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', + ' 9', '10', '11', '12'] + + day_names = ['', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'] + day_abbreviations = ['', '月', '火', '水', '木', '金', '土', '日'] + + +class SwedishLocale(Locale): + + names = ['sv', 'sv_se'] + + past = 'för {0} sen' + future = 'om {0}' + + timeframes = { + 'now': 'just nu', + 'seconds': 'några sekunder', + 'minute': 'en minut', + 'minutes': '{0} minuter', + 'hour': 'en timme', + 'hours': '{0} timmar', + 'day': 'en dag', + 'days': '{0} dagar', + 'month': 'en månad', + 'months': '{0} månader', + 'year': 'ett år', + 'years': '{0} år', + } + + month_names = ['', 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', + 'augusti', 'september', 'oktober', 'november', 'december'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', + 'aug', 'sep', 'okt', 'nov', 'dec'] + + day_names = ['', 'måndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'lördag', 'söndag'] + day_abbreviations = ['', 'mån', 'tis', 'ons', 'tor', 'fre', 'lör', 'sön'] + + +class FinnishLocale(Locale): + + names = ['fi', 'fi_fi'] + + # The finnish grammar is very complex, and its hard to convert + # 1-to-1 to something like English. + + past = '{0} sitten' + future = '{0} kuluttua' + + timeframes = { + 'now': ['juuri nyt', 'juuri nyt'], + 'seconds': ['muutama sekunti', 'muutaman sekunnin'], + 'minute': ['minuutti', 'minuutin'], + 'minutes': ['{0} minuuttia', '{0} minuutin'], + 'hour': ['tunti', 'tunnin'], + 'hours': ['{0} tuntia', '{0} tunnin'], + 'day': ['päivä', 'päivä'], + 'days': ['{0} päivää', '{0} päivän'], + 'month': ['kuukausi', 'kuukauden'], + 'months': ['{0} kuukautta', '{0} kuukauden'], + 'year': ['vuosi', 'vuoden'], + 'years': ['{0} vuotta', '{0} vuoden'], + } + + # Months and days are lowercase in Finnish + month_names = ['', 'tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', + 'toukokuu', 'kesäkuu', 'heinäkuu', 'elokuu', + 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'] + + month_abbreviations = ['', 'tammi', 'helmi', 'maalis', 'huhti', + 'touko', 'kesä', 'heinä', 'elo', + 'syys', 'loka', 'marras', 'joulu'] + + day_names = ['', 'maanantai', 'tiistai', 'keskiviikko', 'torstai', + 'perjantai', 'lauantai', 'sunnuntai'] + + day_abbreviations = ['', 'ma', 'ti', 'ke', 'to', 'pe', 'la', 'su'] + + def _format_timeframe(self, timeframe, delta): + return (self.timeframes[timeframe][0].format(abs(delta)), + self.timeframes[timeframe][1].format(abs(delta))) + + def _format_relative(self, humanized, timeframe, delta): + if timeframe == 'now': + return humanized[0] + + direction = self.past if delta < 0 else self.future + which = 0 if delta < 0 else 1 + + return direction.format(humanized[which]) + + def _ordinal_number(self, n): + return '{0}.'.format(n) + + +class ChineseCNLocale(Locale): + + names = ['zh', 'zh_cn'] + + past = '{0}前' + future = '{0}后' + + timeframes = { + 'now': '刚才', + 'seconds': '几秒', + 'minute': '1分钟', + 'minutes': '{0}分钟', + 'hour': '1小时', + 'hours': '{0}小时', + 'day': '1天', + 'days': '{0}天', + 'month': '1个月', + 'months': '{0}个月', + 'year': '1年', + 'years': '{0}年', + } + + month_names = ['', '一月', '二月', '三月', '四月', '五月', '六月', '七月', + '八月', '九月', '十月', '十一月', '十二月'] + month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', + ' 9', '10', '11', '12'] + + day_names = ['', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] + day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + + +class ChineseTWLocale(Locale): + + names = ['zh_tw'] + + past = '{0}前' + future = '{0}後' + + timeframes = { + 'now': '剛才', + 'seconds': '幾秒', + 'minute': '1分鐘', + 'minutes': '{0}分鐘', + 'hour': '1小時', + 'hours': '{0}小時', + 'day': '1天', + 'days': '{0}天', + 'month': '1個月', + 'months': '{0}個月', + 'year': '1年', + 'years': '{0}年', + } + + month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', + '9月', '10月', '11月', '12月'] + month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', + ' 9', '10', '11', '12'] + + day_names = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'] + day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + + +class KoreanLocale(Locale): + + names = ['ko', 'ko_kr'] + + past = '{0} 전' + future = '{0} 후' + + timeframes = { + 'now': '지금', + 'seconds': '몇초', + 'minute': '일 분', + 'minutes': '{0}분', + 'hour': '1시간', + 'hours': '{0}시간', + 'day': '1일', + 'days': '{0}일', + 'month': '1개월', + 'months': '{0}개월', + 'year': '1년', + 'years': '{0}년', + } + + month_names = ['', '1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', + '9월', '10월', '11월', '12월'] + month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', + ' 9', '10', '11', '12'] + + day_names = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'] + day_abbreviations = ['', '월', '화', '수', '목', '금', '토', '일'] + + +# derived locale types & implementations. +class DutchLocale(Locale): + + names = ['nl', 'nl_nl'] + + past = '{0} geleden' + future = 'over {0}' + + timeframes = { + 'now': 'nu', + 'seconds': 'seconden', + 'minute': 'een minuut', + 'minutes': '{0} minuten', + 'hour': 'een uur', + 'hours': '{0} uur', + 'day': 'een dag', + 'days': '{0} dagen', + 'month': 'een maand', + 'months': '{0} maanden', + 'year': 'een jaar', + 'years': '{0} jaar', + } + + # In Dutch names of months and days are not starting with a capital letter + # like in the English language. + month_names = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', + 'augustus', 'september', 'oktober', 'november', 'december'] + month_abbreviations = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', + 'sep', 'okt', 'nov', 'dec'] + + day_names = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag'] + day_abbreviations = ['', 'ma', 'di', 'wo', 'do', 'vr', 'za', 'zo'] + + +class SlavicBaseLocale(Locale): + + def _format_timeframe(self, timeframe, delta): + + form = self.timeframes[timeframe] + delta = abs(delta) + + if isinstance(form, list): + + if delta % 10 == 1 and delta % 100 != 11: + form = form[0] + elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): + form = form[1] + else: + form = form[2] + + return form.format(delta) + +class BelarusianLocale(SlavicBaseLocale): + + names = ['be', 'be_by'] + + past = '{0} таму' + future = 'праз {0}' + + timeframes = { + 'now': 'зараз', + 'seconds': 'некалькі секунд', + 'minute': 'хвіліну', + 'minutes': ['{0} хвіліну', '{0} хвіліны', '{0} хвілін'], + 'hour': 'гадзіну', + 'hours': ['{0} гадзіну', '{0} гадзіны', '{0} гадзін'], + 'day': 'дзень', + 'days': ['{0} дзень', '{0} дні', '{0} дзён'], + 'month': 'месяц', + 'months': ['{0} месяц', '{0} месяцы', '{0} месяцаў'], + 'year': 'год', + 'years': ['{0} год', '{0} гады', '{0} гадоў'], + } + + month_names = ['', 'студзеня', 'лютага', 'сакавіка', 'красавіка', 'траўня', 'чэрвеня', + 'ліпеня', 'жніўня', 'верасня', 'кастрычніка', 'лістапада', 'снежня'] + month_abbreviations = ['', 'студ', 'лют', 'сак', 'крас', 'трав', 'чэрв', 'ліп', 'жнів', + 'вер', 'каст', 'ліст', 'снеж'] + + day_names = ['', 'панядзелак', 'аўторак', 'серада', 'чацвер', 'пятніца', 'субота', 'нядзеля'] + day_abbreviations = ['', 'пн', 'ат', 'ср', 'чц', 'пт', 'сб', 'нд'] + + +class PolishLocale(SlavicBaseLocale): + + names = ['pl', 'pl_pl'] + + past = '{0} temu' + future = 'za {0}' + + timeframes = { + 'now': 'teraz', + 'seconds': 'kilka sekund', + 'minute': 'minutę', + 'minutes': ['{0} minut', '{0} minuty', '{0} minut'], + 'hour': 'godzina', + 'hours': ['{0} godzin', '{0} godziny', '{0} godzin'], + 'day': 'dzień', + 'days': ['{0} dzień', '{0} dni', '{0} dni'], + 'month': 'miesiąc', + 'months': ['{0} miesiąc', '{0} miesiące', '{0} miesięcy'], + 'year': 'rok', + 'years': ['{0} rok', '{0} lata', '{0} lat'], + } + + month_names = ['', 'styczeń', 'luty', 'marzec', 'kwiecień', 'maj', + 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', + 'listopad', 'grudzień'] + month_abbreviations = ['', 'sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', + 'sie', 'wrz', 'paź', 'lis', 'gru'] + + day_names = ['', 'poniedziałek', 'wtorek', 'środa', 'czwartek', 'piątek', + 'sobota', 'niedziela'] + day_abbreviations = ['', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So', 'Nd'] + + +class RussianLocale(SlavicBaseLocale): + + names = ['ru', 'ru_ru'] + + past = '{0} назад' + future = 'через {0}' + + timeframes = { + 'now': 'сейчас', + 'seconds': 'несколько секунд', + 'minute': 'минуту', + 'minutes': ['{0} минуту', '{0} минуты', '{0} минут'], + 'hour': 'час', + 'hours': ['{0} час', '{0} часа', '{0} часов'], + 'day': 'день', + 'days': ['{0} день', '{0} дня', '{0} дней'], + 'month': 'месяц', + 'months': ['{0} месяц', '{0} месяца', '{0} месяцев'], + 'year': 'год', + 'years': ['{0} год', '{0} года', '{0} лет'], + } + + month_names = ['', 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', + 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'] + month_abbreviations = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', + 'авг', 'сен', 'окт', 'ноя', 'дек'] + + day_names = ['', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', + 'суббота', 'воскресенье'] + day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'] + + +class BulgarianLocale(SlavicBaseLocale): + + names = ['bg', 'bg_BG'] + + past = '{0} назад' + future = 'напред {0}' + + timeframes = { + 'now': 'сега', + 'seconds': 'няколко секунди', + 'minute': 'минута', + 'minutes': ['{0} минута', '{0} минути', '{0} минути'], + 'hour': 'час', + 'hours': ['{0} час', '{0} часа', '{0} часа'], + 'day': 'ден', + 'days': ['{0} ден', '{0} дни', '{0} дни'], + 'month': 'месец', + 'months': ['{0} месец', '{0} месеца', '{0} месеца'], + 'year': 'година', + 'years': ['{0} година', '{0} години', '{0} години'], + } + + month_names = ['', 'януари', 'февруари', 'март', 'април', 'май', 'юни', + 'юли', 'август', 'септември', 'октомври', 'ноември', 'декември'] + month_abbreviations = ['', 'ян', 'февр', 'март', 'апр', 'май', 'юни', 'юли', + 'авг', 'септ', 'окт', 'ноем', 'дек'] + + day_names = ['', 'понеделник', 'вторник', 'сряда', 'четвъртък', 'петък', + 'събота', 'неделя'] + day_abbreviations = ['', 'пон', 'вт', 'ср', 'четв', 'пет', 'съб', 'нед'] + + +class UkrainianLocale(SlavicBaseLocale): + + names = ['ua', 'uk_ua'] + + past = '{0} тому' + future = 'за {0}' + + timeframes = { + 'now': 'зараз', + 'seconds': 'кілька секунд', + 'minute': 'хвилину', + 'minutes': ['{0} хвилину', '{0} хвилини', '{0} хвилин'], + 'hour': 'годину', + 'hours': ['{0} годину', '{0} години', '{0} годин'], + 'day': 'день', + 'days': ['{0} день', '{0} дні', '{0} днів'], + 'month': 'місяць', + 'months': ['{0} місяць', '{0} місяці', '{0} місяців'], + 'year': 'рік', + 'years': ['{0} рік', '{0} роки', '{0} років'], + } + + month_names = ['', 'січня', 'лютого', 'березня', 'квітня', 'травня', 'червня', + 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня'] + month_abbreviations = ['', 'січ', 'лют', 'бер', 'квіт', 'трав', 'черв', 'лип', 'серп', + 'вер', 'жовт', 'лист', 'груд'] + + day_names = ['', 'понеділок', 'вівторок', 'середа', 'четвер', 'п’ятниця', 'субота', 'неділя'] + day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'нд'] + + +class _DeutschLocaleCommonMixin(object): + + past = 'vor {0}' + future = 'in {0}' + + timeframes = { + 'now': 'gerade eben', + 'seconds': 'Sekunden', + 'minute': 'einer Minute', + 'minutes': '{0} Minuten', + 'hour': 'einer Stunde', + 'hours': '{0} Stunden', + 'day': 'einem Tag', + 'days': '{0} Tagen', + 'month': 'einem Monat', + 'months': '{0} Monaten', + 'year': 'einem Jahr', + 'years': '{0} Jahren', + } + + month_names = [ + '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', + 'August', 'September', 'Oktober', 'November', 'Dezember' + ] + + month_abbreviations = [ + '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', + 'Okt', 'Nov', 'Dez' + ] + + day_names = [ + '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', + 'Samstag', 'Sonntag' + ] + + day_abbreviations = [ + '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' + ] + + def _ordinal_number(self, n): + return '{0}.'.format(n) + + +class GermanLocale(_DeutschLocaleCommonMixin, Locale): + + names = ['de', 'de_de'] + + timeframes = _DeutschLocaleCommonMixin.timeframes.copy() + timeframes['days'] = '{0} Tagen' + + +class AustriaLocale(_DeutschLocaleCommonMixin, Locale): + + names = ['de', 'de_at'] + + timeframes = _DeutschLocaleCommonMixin.timeframes.copy() + timeframes['days'] = '{0} Tage' + + +class NorwegianLocale(Locale): + + names = ['nb', 'nb_no'] + + past = 'for {0} siden' + future = 'om {0}' + + timeframes = { + 'now': 'nå nettopp', + 'seconds': 'noen sekunder', + 'minute': 'ett minutt', + 'minutes': '{0} minutter', + 'hour': 'en time', + 'hours': '{0} timer', + 'day': 'en dag', + 'days': '{0} dager', + 'month': 'en måned', + 'months': '{0} måneder', + 'year': 'ett år', + 'years': '{0} år', + } + + month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', + 'juli', 'august', 'september', 'oktober', 'november', + 'desember'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', + 'aug', 'sep', 'okt', 'nov', 'des'] + + day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', + 'lørdag', 'søndag'] + day_abbreviations = ['', 'ma', 'ti', 'on', 'to', 'fr', 'lø', 'sø'] + + +class NewNorwegianLocale(Locale): + + names = ['nn', 'nn_no'] + + past = 'for {0} sidan' + future = 'om {0}' + + timeframes = { + 'now': 'no nettopp', + 'seconds': 'nokre sekund', + 'minute': 'ett minutt', + 'minutes': '{0} minutt', + 'hour': 'ein time', + 'hours': '{0} timar', + 'day': 'ein dag', + 'days': '{0} dagar', + 'month': 'en månad', + 'months': '{0} månader', + 'year': 'eit år', + 'years': '{0} år', + } + + month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', + 'juli', 'august', 'september', 'oktober', 'november', + 'desember'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', + 'aug', 'sep', 'okt', 'nov', 'des'] + + day_names = ['', 'måndag', 'tysdag', 'onsdag', 'torsdag', 'fredag', + 'laurdag', 'sundag'] + day_abbreviations = ['', 'må', 'ty', 'on', 'to', 'fr', 'la', 'su'] + + +class PortugueseLocale(Locale): + names = ['pt', 'pt_pt'] + + past = 'há {0}' + future = 'em {0}' + + timeframes = { + 'now': 'agora', + 'seconds': 'segundos', + 'minute': 'um minuto', + 'minutes': '{0} minutos', + 'hour': 'uma hora', + 'hours': '{0} horas', + 'day': 'um dia', + 'days': '{0} dias', + 'month': 'um mês', + 'months': '{0} meses', + 'year': 'um ano', + 'years': '{0} anos', + } + + month_names = ['', 'janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', + 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'] + month_abbreviations = ['', 'jan', 'fev', 'mar', 'abr', 'maio', 'jun', 'jul', 'ago', + 'set', 'out', 'nov', 'dez'] + + day_names = ['', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', + 'sábado', 'domingo'] + day_abbreviations = ['', 'seg', 'ter', 'qua', 'qui', 'sex', 'sab', 'dom'] + + +class BrazilianPortugueseLocale(PortugueseLocale): + names = ['pt_br'] + + past = 'fazem {0}' + + +class TagalogLocale(Locale): + + names = ['tl'] + + past = 'nakaraang {0}' + future = '{0} mula ngayon' + + timeframes = { + 'now': 'ngayon lang', + 'seconds': 'segundo', + 'minute': 'isang minuto', + 'minutes': '{0} minuto', + 'hour': 'isang oras', + 'hours': '{0} oras', + 'day': 'isang araw', + 'days': '{0} araw', + 'month': 'isang buwan', + 'months': '{0} buwan', + 'year': 'isang taon', + 'years': '{0} taon', + } + + month_names = ['', 'Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', + 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'] + month_abbreviations = ['', 'Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', + 'Set', 'Okt', 'Nob', 'Dis'] + + day_names = ['', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado', 'Linggo'] + day_abbreviations = ['', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab', 'Lin'] + + +class VietnameseLocale(Locale): + + names = ['vi', 'vi_vn'] + + past = '{0} trước' + future = '{0} nữa' + + timeframes = { + 'now': 'hiện tại', + 'seconds': 'giây', + 'minute': 'một phút', + 'minutes': '{0} phút', + 'hour': 'một giờ', + 'hours': '{0} giờ', + 'day': 'một ngày', + 'days': '{0} ngày', + 'month': 'một tháng', + 'months': '{0} tháng', + 'year': 'một năm', + 'years': '{0} năm', + } + + month_names = ['', 'Tháng Một', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu', 'Tháng Bảy', + 'Tháng Tám', 'Tháng Chín', 'Tháng Mười', 'Tháng Mười Một', 'Tháng Mười Hai'] + month_abbreviations = ['', 'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 'Tháng 7', 'Tháng 8', + 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'] + + day_names = ['', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy', 'Chủ Nhật'] + day_abbreviations = ['', 'Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7', 'CN'] + + +class TurkishLocale(Locale): + + names = ['tr', 'tr_tr'] + + past = '{0} önce' + future = '{0} sonra' + + timeframes = { + 'now': 'şimdi', + 'seconds': 'saniye', + 'minute': 'bir dakika', + 'minutes': '{0} dakika', + 'hour': 'bir saat', + 'hours': '{0} saat', + 'day': 'bir gün', + 'days': '{0} gün', + 'month': 'bir ay', + 'months': '{0} ay', + 'year': 'a yıl', + 'years': '{0} yıl', + } + + month_names = ['', 'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', + 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'] + month_abbreviations = ['', 'Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', + 'Eyl', 'Eki', 'Kas', 'Ara'] + + day_names = ['', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'] + day_abbreviations = ['', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'] + + +class ArabicLocale(Locale): + + names = ['ar', 'ar_eg'] + + past = 'منذ {0}' + future = 'خلال {0}' + + timeframes = { + 'now': 'الآن', + 'seconds': 'ثوان', + 'minute': 'دقيقة', + 'minutes': '{0} دقائق', + 'hour': 'ساعة', + 'hours': '{0} ساعات', + 'day': 'يوم', + 'days': '{0} أيام', + 'month': 'شهر', + 'months': '{0} شهور', + 'year': 'سنة', + 'years': '{0} سنوات', + } + + month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', + 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', + 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + + day_names = ['', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] + day_abbreviations = ['', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] + + +class IcelandicLocale(Locale): + + def _format_timeframe(self, timeframe, delta): + + timeframe = self.timeframes[timeframe] + if delta < 0: + timeframe = timeframe[0] + elif delta > 0: + timeframe = timeframe[1] + + return timeframe.format(abs(delta)) + + names = ['is', 'is_is'] + + past = 'fyrir {0} síðan' + future = 'eftir {0}' + + timeframes = { + 'now': 'rétt í þessu', + 'seconds': ('nokkrum sekúndum', 'nokkrar sekúndur'), + 'minute': ('einni mínútu', 'eina mínútu'), + 'minutes': ('{0} mínútum', '{0} mínútur'), + 'hour': ('einum tíma', 'einn tíma'), + 'hours': ('{0} tímum', '{0} tíma'), + 'day': ('einum degi', 'einn dag'), + 'days': ('{0} dögum', '{0} daga'), + 'month': ('einum mánuði', 'einn mánuð'), + 'months': ('{0} mánuðum', '{0} mánuði'), + 'year': ('einu ári', 'eitt ár'), + 'years': ('{0} árum', '{0} ár'), + } + + meridians = { + 'am': 'f.h.', + 'pm': 'e.h.', + 'AM': 'f.h.', + 'PM': 'e.h.', + } + + month_names = ['', 'janúar', 'febrúar', 'mars', 'apríl', 'maí', 'júní', + 'júlí', 'ágúst', 'september', 'október', 'nóvember', 'desember'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maí', 'jún', + 'júl', 'ágú', 'sep', 'okt', 'nóv', 'des'] + + day_names = ['', 'mánudagur', 'þriðjudagur', 'miðvikudagur', 'fimmtudagur', + 'föstudagur', 'laugardagur', 'sunnudagur'] + day_abbreviations = ['', 'mán', 'þri', 'mið', 'fim', 'fös', 'lau', 'sun'] + + +class DanishLocale(Locale): + + names = ['da', 'da_dk'] + + past = 'for {0} siden' + future = 'efter {0}' + + timeframes = { + 'now': 'lige nu', + 'seconds': 'et par sekunder', + 'minute': 'et minut', + 'minutes': '{0} minutter', + 'hour': 'en time', + 'hours': '{0} timer', + 'day': 'en dag', + 'days': '{0} dage', + 'month': 'en måned', + 'months': '{0} måneder', + 'year': 'et år', + 'years': '{0} år', + } + + month_names = ['', 'januar', 'februar', 'marts', 'april', 'maj', 'juni', + 'juli', 'august', 'september', 'oktober', 'november', 'december'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', + 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'] + + day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', + 'lørdag', 'søndag'] + day_abbreviations = ['', 'man', 'tir', 'ons', 'tor', 'fre', 'lør', 'søn'] + + +class MalayalamLocale(Locale): + + names = ['ml'] + + past = '{0} മുമ്പ്' + future = '{0} ശേഷം' + + timeframes = { + 'now': 'ഇപ്പോൾ', + 'seconds': 'സെക്കന്റ്‌', + 'minute': 'ഒരു മിനിറ്റ്', + 'minutes': '{0} മിനിറ്റ്', + 'hour': 'ഒരു മണിക്കൂർ', + 'hours': '{0} മണിക്കൂർ', + 'day': 'ഒരു ദിവസം ', + 'days': '{0} ദിവസം ', + 'month': 'ഒരു മാസം ', + 'months': '{0} മാസം ', + 'year': 'ഒരു വർഷം ', + 'years': '{0} വർഷം ', + } + + meridians = { + 'am': 'രാവിലെ', + 'pm': 'ഉച്ചക്ക് ശേഷം', + 'AM': 'രാവിലെ', + 'PM': 'ഉച്ചക്ക് ശേഷം', + } + + month_names = ['', 'ജനുവരി', 'ഫെബ്രുവരി', 'മാർച്ച്‌', 'ഏപ്രിൽ ', 'മെയ്‌ ', 'ജൂണ്‍', 'ജൂലൈ', + 'ഓഗസ്റ്റ്‌', 'സെപ്റ്റംബർ', 'ഒക്ടോബർ', 'നവംബർ', 'ഡിസംബർ'] + month_abbreviations = ['', 'ജനു', 'ഫെബ് ', 'മാർ', 'ഏപ്രിൽ', 'മേയ്', 'ജൂണ്‍', 'ജൂലൈ', 'ഓഗസ്റ', + 'സെപ്റ്റ', 'ഒക്ടോ', 'നവം', 'ഡിസം'] + + day_names = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] + day_abbreviations = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] + + +class HindiLocale(Locale): + + names = ['hi'] + + past = '{0} पहले' + future = '{0} बाद' + + timeframes = { + 'now': 'अभि', + 'seconds': 'सेकंड्', + 'minute': 'एक मिनट ', + 'minutes': '{0} मिनट ', + 'hour': 'एक घंट', + 'hours': '{0} घंटे', + 'day': 'एक दिन', + 'days': '{0} दिन', + 'month': 'एक माह ', + 'months': '{0} महीने ', + 'year': 'एक वर्ष ', + 'years': '{0} साल ', + } + + meridians = { + 'am': 'सुबह', + 'pm': 'शाम', + 'AM': 'सुबह', + 'PM': 'शाम', + } + + month_names = ['', 'जनवरी', 'फ़रवरी', 'मार्च', 'अप्रैल ', 'मई', 'जून', 'जुलाई', + 'आगस्त', 'सितम्बर', 'अकतूबर', 'नवेम्बर', 'दिसम्बर'] + month_abbreviations = ['', 'जन', 'फ़र', 'मार्च', 'अप्रै', 'मई', 'जून', 'जुलाई', 'आग', + 'सित', 'अकत', 'नवे', 'दिस'] + + day_names = ['', 'सोमवार', 'मंगलवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] + day_abbreviations = ['', 'सोम', 'मंगल', 'बुध', 'गुरुवार', 'शुक्र', 'शनि', 'रवि'] + +class CzechLocale(Locale): + names = ['cs', 'cs_cz'] + + timeframes = { + 'now': 'Teď', + 'seconds': { + 'past': '{0} sekundami', + 'future': ['{0} sekundy', '{0} sekund'] + }, + 'minute': {'past': 'minutou', 'future': 'minutu', 'zero': '{0} minut'}, + 'minutes': { + 'past': '{0} minutami', + 'future': ['{0} minuty', '{0} minut'] + }, + 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodin'}, + 'hours': { + 'past': '{0} hodinami', + 'future': ['{0} hodiny', '{0} hodin'] + }, + 'day': {'past': 'dnem', 'future': 'den', 'zero': '{0} dnů'}, + 'days': { + 'past': '{0} dny', + 'future': ['{0} dny', '{0} dnů'] + }, + 'month': {'past': 'měsícem', 'future': 'měsíc', 'zero': '{0} měsíců'}, + 'months': { + 'past': '{0} měsíci', + 'future': ['{0} měsíce', '{0} měsíců'] + }, + 'year': {'past': 'rokem', 'future': 'rok', 'zero': '{0} let'}, + 'years': { + 'past': '{0} lety', + 'future': ['{0} roky', '{0} let'] + } + } + + past = 'Před {0}' + future = 'Za {0}' + + month_names = ['', 'leden', 'únor', 'březen', 'duben', 'květen', 'červen', + 'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'] + month_abbreviations = ['', 'led', 'úno', 'bře', 'dub', 'kvě', 'čvn', 'čvc', + 'srp', 'zář', 'říj', 'lis', 'pro'] + + day_names = ['', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', + 'sobota', 'neděle'] + day_abbreviations = ['', 'po', 'út', 'st', 'čt', 'pá', 'so', 'ne'] + + + def _format_timeframe(self, timeframe, delta): + '''Czech aware time frame format function, takes into account the differences between past and future forms.''' + form = self.timeframes[timeframe] + if isinstance(form, dict): + if delta == 0: + form = form['zero'] # And *never* use 0 in the singular! + elif delta > 0: + form = form['future'] + else: + form = form['past'] + delta = abs(delta) + + if isinstance(form, list): + if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): + form = form[0] + else: + form = form[1] + + return form.format(delta) + +class FarsiLocale(Locale): + + names = ['fa', 'fa_ir'] + + past = '{0} قبل' + future = 'در {0}' + + timeframes = { + 'now': 'اکنون', + 'seconds': 'ثانیه', + 'minute': 'یک دقیقه', + 'minutes': '{0} دقیقه', + 'hour': 'یک ساعت', + 'hours': '{0} ساعت', + 'day': 'یک روز', + 'days': '{0} روز', + 'month': 'یک ماه', + 'months': '{0} ماه', + 'year': 'یک سال', + 'years': '{0} سال', + } + + meridians = { + 'am': 'قبل از ظهر', + 'pm': 'بعد از ظهر', + 'AM': 'قبل از ظهر', + 'PM': 'بعد از ظهر', + } + + month_names = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', + 'Sep', 'Oct', 'Nov', 'Dec'] + + day_names = ['', 'دو شنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه', 'یکشنبه'] + day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + +class MacedonianLocale(Locale): + names = ['mk', 'mk_mk'] + + past = 'пред {0}' + future = 'за {0}' + + timeframes = { + 'now': 'сега', + 'seconds': 'секунди', + 'minute': 'една минута', + 'minutes': '{0} минути', + 'hour': 'еден саат', + 'hours': '{0} саати', + 'day': 'еден ден', + 'days': '{0} дена', + 'month': 'еден месец', + 'months': '{0} месеци', + 'year': 'една година', + 'years': '{0} години', + } + + meridians = { + 'am': 'дп', + 'pm': 'пп', + 'AM': 'претпладне', + 'PM': 'попладне', + } + + month_names = ['', 'Јануари', 'Февруари', 'Март', 'Април', 'Мај', 'Јуни', 'Јули', 'Август', 'Септември', 'Октомври', + 'Ноември', 'Декември'] + month_abbreviations = ['', 'Јан.', ' Фев.', ' Мар.', ' Апр.', ' Мај', ' Јун.', ' Јул.', ' Авг.', ' Септ.', ' Окт.', + ' Ноем.', ' Декем.'] + + day_names = ['', 'Понеделник', ' Вторник', ' Среда', ' Четврток', ' Петок', ' Сабота', ' Недела'] + day_abbreviations = ['', 'Пон.', ' Вт.', ' Сре.', ' Чет.', ' Пет.', ' Саб.', ' Нед.'] + + +class HebrewLocale(Locale): + + names = ['he', 'he_IL'] + + past = 'לפני {0}' + future = 'בעוד {0}' + + timeframes = { + 'now': 'הרגע', + 'seconds': 'שניות', + 'minute': 'דקה', + 'minutes': '{0} דקות', + 'hour': 'שעה', + 'hours': '{0} שעות', + '2-hours': 'שעתיים', + 'day': 'יום', + 'days': '{0} ימים', + '2-days': 'יומיים', + 'month': 'חודש', + 'months': '{0} חודשים', + '2-months': 'חודשיים', + 'year': 'שנה', + 'years': '{0} שנים', + '2-years': 'שנתיים', + } + + meridians = { + 'am': 'לפנ"צ', + 'pm': 'אחר"צ', + 'AM': 'לפני הצהריים', + 'PM': 'אחרי הצהריים', + } + + month_names = ['', 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', + 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'] + month_abbreviations = ['', 'ינו׳', 'פבר׳', 'מרץ', 'אפר׳', 'מאי', 'יוני', 'יולי', 'אוג׳', + 'ספט׳', 'אוק׳', 'נוב׳', 'דצמ׳'] + + day_names = ['', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת', 'ראשון'] + day_abbreviations = ['', 'ב׳', 'ג׳', 'ד׳', 'ה׳', 'ו׳', 'ש׳', 'א׳'] + + def _format_timeframe(self, timeframe, delta): + '''Hebrew couple of aware''' + couple = '2-{0}'.format(timeframe) + if abs(delta) == 2 and couple in self.timeframes: + return self.timeframes[couple].format(abs(delta)) + else: + return self.timeframes[timeframe].format(abs(delta)) + +class MarathiLocale(Locale): + + names = ['mr'] + + past = '{0} आधी' + future = '{0} नंतर' + + timeframes = { + 'now': 'सद्य', + 'seconds': 'सेकंद', + 'minute': 'एक मिनिट ', + 'minutes': '{0} मिनिट ', + 'hour': 'एक तास', + 'hours': '{0} तास', + 'day': 'एक दिवस', + 'days': '{0} दिवस', + 'month': 'एक महिना ', + 'months': '{0} महिने ', + 'year': 'एक वर्ष ', + 'years': '{0} वर्ष ', + } + + meridians = { + 'am': 'सकाळ', + 'pm': 'संध्याकाळ', + 'AM': 'सकाळ', + 'PM': 'संध्याकाळ', + } + + month_names = ['', 'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलै', + 'अॉगस्ट', 'सप्टेंबर', 'अॉक्टोबर', 'नोव्हेंबर', 'डिसेंबर'] + month_abbreviations = ['', 'जान', 'फेब्रु', 'मार्च', 'एप्रि', 'मे', 'जून', 'जुलै', 'अॉग', + 'सप्टें', 'अॉक्टो', 'नोव्हें', 'डिसें'] + + day_names = ['', 'सोमवार', 'मंगळवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] + day_abbreviations = ['', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शनि', 'रवि'] + +def _map_locales(): + + locales = {} + + for cls_name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + if issubclass(cls, Locale): + for name in cls.names: + locales[name.lower()] = cls + + return locales + +class CatalaLocale(Locale): + names = ['ca', 'ca_ca'] + past = 'Fa {0}' + future = '{0}' # I don't know what's the right phrase in catala for the future. + + timeframes = { + 'now': 'Ara mateix', + 'seconds': 'segons', + 'minute': '1 minut', + 'minutes': '{0} minuts', + 'hour': 'una hora', + 'hours': '{0} hores', + 'day': 'un dia', + 'days': '{0} dies', + 'month': 'un mes', + 'months': '{0} messos', + 'year': 'un any', + 'years': '{0} anys', + } + + month_names = ['', 'Jener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Decembre'] + month_abbreviations = ['', 'Jener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Decembre'] + day_names = ['', 'Dilluns', 'Dimars', 'Dimecres', 'Dijous', 'Divendres', 'Disabte', 'Diumenge'] + day_abbreviations = ['', 'Dilluns', 'Dimars', 'Dimecres', 'Dijous', 'Divendres', 'Disabte', 'Diumenge'] + +class BasqueLocale(Locale): + names = ['eu', 'eu_eu'] + past = 'duela {0}' + future = '{0}' # I don't know what's the right phrase in Basque for the future. + + timeframes = { + 'now': 'Orain', + 'seconds': 'segundu', + 'minute': 'minutu bat', + 'minutes': '{0} minutu', + 'hour': 'ordu bat', + 'hours': '{0} ordu', + 'day': 'egun bat', + 'days': '{0} egun', + 'month': 'hilabete bat', + 'months': '{0} hilabet', + 'year': 'urte bat', + 'years': '{0} urte', + } + + month_names = ['', 'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak', 'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'] + month_abbreviations = ['', 'urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'] + day_names = ['', 'asteleehna', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata', 'igandea'] + day_abbreviations = ['', 'al', 'ar', 'az', 'og', 'ol', 'lr', 'ig'] + + +class HungarianLocale(Locale): + + names = ['hu', 'hu_hu'] + + past = '{0} ezelőtt' + future = '{0} múlva' + + timeframes = { + 'now': 'éppen most', + 'seconds': { + 'past': 'másodpercekkel', + 'future': 'pár másodperc' + }, + 'minute': {'past': 'egy perccel', 'future': 'egy perc'}, + 'minutes': {'past': '{0} perccel', 'future': '{0} perc'}, + 'hour': {'past': 'egy órával', 'future': 'egy óra'}, + 'hours': {'past': '{0} órával', 'future': '{0} óra'}, + 'day': { + 'past': 'egy nappal', + 'future': 'egy nap' + }, + 'days': { + 'past': '{0} nappal', + 'future': '{0} nap' + }, + 'month': {'past': 'egy hónappal', 'future': 'egy hónap'}, + 'months': {'past': '{0} hónappal', 'future': '{0} hónap'}, + 'year': {'past': 'egy évvel', 'future': 'egy év'}, + 'years': {'past': '{0} évvel', 'future': '{0} év'}, + } + + month_names = ['', 'január', 'február', 'március', 'április', 'május', + 'június', 'július', 'augusztus', 'szeptember', + 'október', 'november', 'december'] + month_abbreviations = ['', 'jan', 'febr', 'márc', 'ápr', 'máj', 'jún', + 'júl', 'aug', 'szept', 'okt', 'nov', 'dec'] + + day_names = ['', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', + 'szombat', 'vasárnap'] + day_abbreviations = ['', 'hét', 'kedd', 'szer', 'csüt', 'pént', + 'szom', 'vas'] + + meridians = { + 'am': 'de', + 'pm': 'du', + 'AM': 'DE', + 'PM': 'DU', + } + + def _format_timeframe(self, timeframe, delta): + form = self.timeframes[timeframe] + + if isinstance(form, dict): + if delta > 0: + form = form['future'] + else: + form = form['past'] + + return form.format(abs(delta)) + + +class ThaiLocale(Locale): + + names = ['th', 'th_th'] + + past = '{0}{1}ที่ผ่านมา' + future = 'ในอีก{1}{0}' + + timeframes = { + 'now': 'ขณะนี้', + 'seconds': 'ไม่กี่วินาที', + 'minute': '1 นาที', + 'minutes': '{0} นาที', + 'hour': '1 ชั่วโมง', + 'hours': '{0} ชั่วโมง', + 'day': '1 วัน', + 'days': '{0} วัน', + 'month': '1 เดือน', + 'months': '{0} เดือน', + 'year': '1 ปี', + 'years': '{0} ปี', + } + + month_names = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', + 'พฤษภาคม', 'มิถุนายน', 'กรกฏาคม', 'สิงหาคม', + 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'] + month_abbreviations = ['', 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', + 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', + 'พ.ย.', 'ธ.ค.'] + + day_names = ['', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', + 'เสาร์', 'อาทิตย์'] + day_abbreviations = ['', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส', 'อา'] + + meridians = { + 'am': 'am', + 'pm': 'pm', + 'AM': 'AM', + 'PM': 'PM', + } + + BE_OFFSET = 543 + + def year_full(self, year): + '''Thai always use Buddhist Era (BE) which is CE + 543''' + year += self.BE_OFFSET + return '{0:04d}'.format(year) + + def year_abbreviation(self, year): + '''Thai always use Buddhist Era (BE) which is CE + 543''' + year += self.BE_OFFSET + return '{0:04d}'.format(year)[2:] + + def _format_relative(self, humanized, timeframe, delta): + '''Thai normally doesn't have any space between words''' + if timeframe == 'now': + return humanized + space = '' if timeframe == 'seconds' else ' ' + direction = self.past if delta < 0 else self.future + + return direction.format(humanized, space) + + + +class BengaliLocale(Locale): + + names = ['bn', 'bn_bd', 'bn_in'] + + past = '{0} আগে' + future = '{0} পরে' + + timeframes = { + 'now': 'এখন', + 'seconds': 'সেকেন্ড', + 'minute': 'এক মিনিট', + 'minutes': '{0} মিনিট', + 'hour': 'এক ঘণ্টা', + 'hours': '{0} ঘণ্টা', + 'day': 'এক দিন', + 'days': '{0} দিন', + 'month': 'এক মাস', + 'months': '{0} মাস ', + 'year': 'এক বছর', + 'years': '{0} বছর', + } + + meridians = { + 'am': 'সকাল', + 'pm': 'বিকাল', + 'AM': 'সকাল', + 'PM': 'বিকাল', + } + + month_names = ['', 'জানুয়ারি', 'ফেব্রুয়ারি', 'মার্চ', 'এপ্রিল', 'মে', 'জুন', 'জুলাই', + 'আগস্ট', 'সেপ্টেম্বর', 'অক্টোবর', 'নভেম্বর', 'ডিসেম্বর'] + month_abbreviations = ['', 'জানু', 'ফেব', 'মার্চ', 'এপ্রি', 'মে', 'জুন', 'জুল', + 'অগা','সেপ্ট', 'অক্টো', 'নভে', 'ডিসে'] + + day_names = ['', 'সোমবার', 'মঙ্গলবার', 'বুধবার', 'বৃহস্পতিবার', 'শুক্রবার', 'শনিবার', 'রবিবার'] + day_abbreviations = ['', 'সোম', 'মঙ্গল', 'বুধ', 'বৃহঃ', 'শুক্র', 'শনি', 'রবি'] + + def _ordinal_number(self, n): + if n > 10 or n == 0: + return '{0}তম'.format(n) + if n in [1, 5, 7, 8, 9, 10]: + return '{0}ম'.format(n) + if n in [2, 3]: + return '{0}য়'.format(n) + if n == 4: + return '{0}র্থ'.format(n) + if n == 6: + return '{0}ষ্ঠ'.format(n) + + +_locales = _map_locales() diff --git a/lib/arrow/parser.py b/lib/arrow/parser.py new file mode 100644 index 00000000..2c204aee --- /dev/null +++ b/lib/arrow/parser.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import unicode_literals + +from datetime import datetime +from dateutil import tz +import re + +from arrow import locales + + +class ParserError(RuntimeError): + pass + + +class DateTimeParser(object): + + _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') + + _ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}') + _ONE_THROUGH_FIVE_DIGIT_RE = re.compile('\d{1,5}') + _ONE_THROUGH_FOUR_DIGIT_RE = re.compile('\d{1,4}') + _ONE_TWO_OR_THREE_DIGIT_RE = re.compile('\d{1,3}') + _ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}') + _FOUR_DIGIT_RE = re.compile('\d{4}') + _TWO_DIGIT_RE = re.compile('\d{2}') + _TZ_RE = re.compile('[+\-]?\d{2}:?\d{2}') + _TZ_NAME_RE = re.compile('\w[\w+\-/]+') + + + _BASE_INPUT_RE_MAP = { + 'YYYY': _FOUR_DIGIT_RE, + 'YY': _TWO_DIGIT_RE, + 'MM': _TWO_DIGIT_RE, + 'M': _ONE_OR_TWO_DIGIT_RE, + 'DD': _TWO_DIGIT_RE, + 'D': _ONE_OR_TWO_DIGIT_RE, + 'HH': _TWO_DIGIT_RE, + 'H': _ONE_OR_TWO_DIGIT_RE, + 'hh': _TWO_DIGIT_RE, + 'h': _ONE_OR_TWO_DIGIT_RE, + 'mm': _TWO_DIGIT_RE, + 'm': _ONE_OR_TWO_DIGIT_RE, + 'ss': _TWO_DIGIT_RE, + 's': _ONE_OR_TWO_DIGIT_RE, + 'X': re.compile('\d+'), + 'ZZZ': _TZ_NAME_RE, + 'ZZ': _TZ_RE, + 'Z': _TZ_RE, + 'SSSSSS': _ONE_THROUGH_SIX_DIGIT_RE, + 'SSSSS': _ONE_THROUGH_FIVE_DIGIT_RE, + 'SSSS': _ONE_THROUGH_FOUR_DIGIT_RE, + 'SSS': _ONE_TWO_OR_THREE_DIGIT_RE, + 'SS': _ONE_OR_TWO_DIGIT_RE, + 'S': re.compile('\d'), + } + + MARKERS = ['YYYY', 'MM', 'DD'] + SEPARATORS = ['-', '/', '.'] + + def __init__(self, locale='en_us'): + + self.locale = locales.get_locale(locale) + self._input_re_map = self._BASE_INPUT_RE_MAP.copy() + self._input_re_map.update({ + 'MMMM': self._choice_re(self.locale.month_names[1:], re.IGNORECASE), + 'MMM': self._choice_re(self.locale.month_abbreviations[1:], + re.IGNORECASE), + 'Do': re.compile(self.locale.ordinal_day_re), + 'a': self._choice_re( + (self.locale.meridians['am'], self.locale.meridians['pm']) + ), + # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to + # ensure backwards compatibility of this token + 'A': self._choice_re(self.locale.meridians.values()) + }) + + def parse_iso(self, string): + + has_time = 'T' in string or ' ' in string.strip() + space_divider = ' ' in string.strip() + + if has_time: + if space_divider: + date_string, time_string = string.split(' ', 1) + else: + date_string, time_string = string.split('T', 1) + time_parts = re.split('[+-]', time_string, 1) + has_tz = len(time_parts) > 1 + has_seconds = time_parts[0].count(':') > 1 + has_subseconds = '.' in time_parts[0] + + if has_subseconds: + subseconds_token = 'S' * min(len(re.split('\D+', time_parts[0].split('.')[1], 1)[0]), 6) + formats = ['YYYY-MM-DDTHH:mm:ss.%s' % subseconds_token] + elif has_seconds: + formats = ['YYYY-MM-DDTHH:mm:ss'] + else: + formats = ['YYYY-MM-DDTHH:mm'] + else: + has_tz = False + # generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY + # using various separators: -, /, . + l = len(self.MARKERS) + formats = [separator.join(self.MARKERS[:l-i]) + for i in range(l) + for separator in self.SEPARATORS] + + if has_time and has_tz: + formats = [f + 'Z' for f in formats] + + if space_divider: + formats = [item.replace('T', ' ', 1) for item in formats] + + return self._parse_multiformat(string, formats) + + def parse(self, string, fmt): + + if isinstance(fmt, list): + return self._parse_multiformat(string, fmt) + + # fmt is a string of tokens like 'YYYY-MM-DD' + # we construct a new string by replacing each + # token by its pattern: + # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' + fmt_pattern = fmt + tokens = [] + offset = 0 + for m in self._FORMAT_RE.finditer(fmt): + token = m.group(0) + try: + input_re = self._input_re_map[token] + except KeyError: + raise ParserError('Unrecognized token \'{0}\''.format(token)) + input_pattern = '(?P<{0}>{1})'.format(token, input_re.pattern) + tokens.append(token) + # a pattern doesn't have the same length as the token + # it replaces! We keep the difference in the offset variable. + # This works because the string is scanned left-to-right and matches + # are returned in the order found by finditer. + fmt_pattern = fmt_pattern[:m.start() + offset] + input_pattern + fmt_pattern[m.end() + offset:] + offset += len(input_pattern) - (m.end() - m.start()) + match = re.search(fmt_pattern, string, flags=re.IGNORECASE) + if match is None: + raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(fmt_pattern, string)) + parts = {} + for token in tokens: + if token == 'Do': + value = match.group('value') + else: + value = match.group(token) + self._parse_token(token, value, parts) + return self._build_datetime(parts) + + def _parse_token(self, token, value, parts): + + if token == 'YYYY': + parts['year'] = int(value) + elif token == 'YY': + value = int(value) + parts['year'] = 1900 + value if value > 68 else 2000 + value + + elif token in ['MMMM', 'MMM']: + parts['month'] = self.locale.month_number(value.lower()) + + elif token in ['MM', 'M']: + parts['month'] = int(value) + + elif token in ['DD', 'D']: + parts['day'] = int(value) + + elif token in ['Do']: + parts['day'] = int(value) + + elif token.upper() in ['HH', 'H']: + parts['hour'] = int(value) + + elif token in ['mm', 'm']: + parts['minute'] = int(value) + + elif token in ['ss', 's']: + parts['second'] = int(value) + + elif token == 'SSSSSS': + parts['microsecond'] = int(value) + elif token == 'SSSSS': + parts['microsecond'] = int(value) * 10 + elif token == 'SSSS': + parts['microsecond'] = int(value) * 100 + elif token == 'SSS': + parts['microsecond'] = int(value) * 1000 + elif token == 'SS': + parts['microsecond'] = int(value) * 10000 + elif token == 'S': + parts['microsecond'] = int(value) * 100000 + + elif token == 'X': + parts['timestamp'] = int(value) + + elif token in ['ZZZ', 'ZZ', 'Z']: + parts['tzinfo'] = TzinfoParser.parse(value) + + elif token in ['a', 'A']: + if value in ( + self.locale.meridians['am'], + self.locale.meridians['AM'] + ): + parts['am_pm'] = 'am' + elif value in ( + self.locale.meridians['pm'], + self.locale.meridians['PM'] + ): + parts['am_pm'] = 'pm' + + @staticmethod + def _build_datetime(parts): + + timestamp = parts.get('timestamp') + + if timestamp: + tz_utc = tz.tzutc() + return datetime.fromtimestamp(timestamp, tz=tz_utc) + + am_pm = parts.get('am_pm') + hour = parts.get('hour', 0) + + if am_pm == 'pm' and hour < 12: + hour += 12 + elif am_pm == 'am' and hour == 12: + hour = 0 + + return datetime(year=parts.get('year', 1), month=parts.get('month', 1), + day=parts.get('day', 1), hour=hour, minute=parts.get('minute', 0), + second=parts.get('second', 0), microsecond=parts.get('microsecond', 0), + tzinfo=parts.get('tzinfo')) + + def _parse_multiformat(self, string, formats): + + _datetime = None + + for fmt in formats: + try: + _datetime = self.parse(string, fmt) + break + except: + pass + + if _datetime is None: + raise ParserError('Could not match input to any of {0} on \'{1}\''.format(formats, string)) + + return _datetime + + @staticmethod + def _map_lookup(input_map, key): + + try: + return input_map[key] + except KeyError: + raise ParserError('Could not match "{0}" to {1}'.format(key, input_map)) + + @staticmethod + def _try_timestamp(string): + + try: + return float(string) + except: + return None + + @staticmethod + def _choice_re(choices, flags=0): + return re.compile('({0})'.format('|'.join(choices)), flags=flags) + + +class TzinfoParser(object): + + _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)') + + @classmethod + def parse(cls, string): + + tzinfo = None + + if string == 'local': + tzinfo = tz.tzlocal() + + elif string in ['utc', 'UTC']: + tzinfo = tz.tzutc() + + else: + + iso_match = cls._TZINFO_RE.match(string) + + if iso_match: + sign, hours, minutes = iso_match.groups() + seconds = int(hours) * 3600 + int(minutes) * 60 + + if sign == '-': + seconds *= -1 + + tzinfo = tz.tzoffset(None, seconds) + + else: + tzinfo = tz.gettz(string) + + if tzinfo is None: + raise ParserError('Could not parse timezone expression "{0}"', string) + + return tzinfo diff --git a/lib/arrow/util.py b/lib/arrow/util.py new file mode 100644 index 00000000..546cff2c --- /dev/null +++ b/lib/arrow/util.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import sys + +# python 2.6 / 2.7 definitions for total_seconds function. + +def _total_seconds_27(td): # pragma: no cover + return td.total_seconds() + +def _total_seconds_26(td): + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 + + +# get version info and assign correct total_seconds function. + +version = '{0}.{1}.{2}'.format(*sys.version_info[:3]) + +if version < '2.7': # pragma: no cover + total_seconds = _total_seconds_26 +else: # pragma: no cover + total_seconds = _total_seconds_27 + +def is_timestamp(value): + try: + float(value) + return True + except: + return False + +# python 2.7 / 3.0+ definitions for isstr function. + +try: # pragma: no cover + basestring + + def isstr(s): + return isinstance(s, basestring) + +except NameError: #pragma: no cover + + def isstr(s): + return isinstance(s, str) + + +__all__ = ['total_seconds', 'is_timestamp', 'isstr'] diff --git a/lib/dateutil/__init__.py b/lib/dateutil/__init__.py new file mode 100644 index 00000000..743669c7 --- /dev/null +++ b/lib/dateutil/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +__version__ = "2.4.2" diff --git a/lib/dateutil/easter.py b/lib/dateutil/easter.py new file mode 100644 index 00000000..8d30c4eb --- /dev/null +++ b/lib/dateutil/easter.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic easter computing method for any given year, using +Western, Orthodox or Julian algorithms. +""" + +import datetime + +__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different easter + calculation methods: + + 1 - Original calculation in Julian calendar, valid in + dates after 326 AD + 2 - Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3 - Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + EASTER_JULIAN = 1 + EASTER_ORTHODOX = 2 + EASTER_WESTERN = 3 + + The default method is method 3. + + More about the algorithm may be found at: + + http://users.chariot.net.au/~gmarts/eastalg.htm + + and + + http://www.tondering.dk/claus/calendar.html + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19*g + 15) % 30 + j = (y + y//4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y//100 - 16 - (y//100 - 16)//4 + else: + # New method + c = y//100 + h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 + i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) + j = (y + y//4 + i + 2 - c + c//4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6)//40) % 31 + m = 3 + (p + 26)//30 + return datetime.date(int(y), int(m), int(d)) diff --git a/lib/dateutil/parser.py b/lib/dateutil/parser.py new file mode 100644 index 00000000..8c074b39 --- /dev/null +++ b/lib/dateutil/parser.py @@ -0,0 +1,1205 @@ +# -*- coding:iso-8859-1 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import string +import time +import collections +from io import StringIO + +from six import text_type, binary_type, integer_types + +from . import relativedelta +from . import tz + +__all__ = ["parse", "parserinfo"] + + +class _timelex(object): + + def __init__(self, instream): + if isinstance(instream, text_type): + instream = StringIO(instream) + + self.instream = instream + self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_' + '' + '') + self.numchars = '0123456789' + self.whitespace = ' \t\r\n' + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered one + unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + wordchars = self.wordchars + numchars = self.numchars + whitespace = self.whitespace + + while not self.eof: + # We only realize that we've reached the end of a token when we find + # a character that's not part of the current token - since that + # character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if nextchar in wordchars: + state = 'a' + elif nextchar in numchars: + state = '0' + elif nextchar in whitespace: + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if nextchar in wordchars: + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if nextchar in numchars: + token += nextchar + elif nextchar == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or nextchar in wordchars: + token += nextchar + elif nextchar in numchars and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or nextchar in numchars: + token += nextchar + elif nextchar in wordchars and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] == '.')): + l = token.split('.') + token = l[0] + for tok in l[1:]: + self.tokenstack.append('.') + if tok: + self.tokenstack.append(tok) + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + def split(cls, s): + return list(cls(s)) + split = classmethod(split) + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize the + language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (`True`) or month (`False`). If + `yearfirst` is set to `True`, this distinguishes between YDM and + YMD. Default is `False`. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If `True`, the first number is taken to + be the year, otherwise the last number is taken to be the year. + Default is `False`. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), + ("Wed", "Wednesday"), + ("Thu", "Thursday"), + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z"] + PERTAIN = ["of"] + TZOFFSET = {} + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100*100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + if len(name) >= 3: + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + if len(name) >= 3: + try: + return self._months[name.lower()]+1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year): + if year < 100: + year += self._century + if abs(year-self._year) >= 50: + if year < self._year: + year += 100 + else: + year -= 100 + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year) + + if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class parser(object): + + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, ignoretz=False, tzinfos=None, + **kwargs): + """ + Parse the date/time string into a datetime object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + `None`, elements specified in `timestr` replace elements in the + default object. + + :param ignoretz: + Whether or not to ignore the time zone. + + :param tzinfos: + A time zone, to be applied to the date, if `ignoretz` is `True`. + This can be either a subclass of `tzinfo`, a time zone string or an + integer offset. + + :param **kwargs: + Keyword arguments as passed to `_parse()`. + + :return: + Returns a `datetime.datetime` object or, if the `fuzzy_with_tokens` + option is `True`, returns a tuple, the first element being a + `datetime.datetime` object, the second a tuple containing the + fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + `tzinfo` is not in a valid format, or if an invalid date would + be created. + + :raises OverFlowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + default_specified = default is not None + + if not default_specified: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + if kwargs.get('fuzzy_with_tokens', False): + res, skipped_tokens = self._parse(timestr, **kwargs) + else: + res = self._parse(timestr, **kwargs) + + if res is None: + raise ValueError("Unknown string format") + + repl = {} + for attr in ["year", "month", "day", "hour", + "minute", "second", "microsecond"]: + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + ret = default.replace(**repl) + + if res.weekday is not None and not res.day: + ret = ret+relativedelta.relativedelta(weekday=res.weekday) + + if not ignoretz: + if (isinstance(tzinfos, collections.Callable) or + tzinfos and res.tzname in tzinfos): + + if isinstance(tzinfos, collections.Callable): + tzdata = tzinfos(res.tzname, res.tzoffset) + else: + tzdata = tzinfos.get(res.tzname) + + if isinstance(tzdata, datetime.tzinfo): + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(res.tzname, tzdata) + else: + raise ValueError("Offset must be tzinfo subclass, " + "tz string, or int offset.") + ret = ret.replace(tzinfo=tzinfo) + elif res.tzname and res.tzname in time.tzname: + ret = ret.replace(tzinfo=tz.tzlocal()) + elif res.tzoffset == 0: + ret = ret.replace(tzinfo=tz.tzutc()) + elif res.tzoffset: + ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + `parse()`, which passes on its `kwargs` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (`True`) or month (`False`). If + `yearfirst` is set to `True`, this distinguishes between YDM and + YMD. If set to `None`, this value is retrieved from the current + `parserinfo` object (which itself defaults to `False`). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If `True`, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to `None`, the value is retrieved from the current + `parserinfo` object (which itself defaults to `False`). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If `True`, `fuzzy` is automatically set to True, and the parser will + return a tuple where the first element is the parsed + `datetime.datetime` datetimestamp and the second element is a tuple + containing the portions of the string which were ignored, e.g. + "Today is January 1, 2047 at 8:21:00AM" should return + `(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))` + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + # keep up with the last token skipped so we can recombine + # consecutively skipped tokens (-2 for when i begins at 0). + last_skipped_token_i = -2 + skipped_tokens = list() + + try: + # year/month/day list + ymd = [] + + # Index of the month string in ymd + mstridx = -1 + + len_l = len(l) + i = 0 + while i < len_l: + + # Check if it's a number + try: + value_repr = l[i] + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Token is a number + len_li = len(l[i]) + i += 1 + + if (len(ymd) == 3 and len_li in (2, 4) + and res.hour is None and (i >= len_l or (l[i] != ':' and + info.hms(l[i]) is None))): + # 19990101T23[59] + s = l[i-1] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = l[i-1] + + if not ymd and l[i-1].find('.') == -1: + ymd.append(info.convertyear(int(s[:2]))) + ymd.append(int(s[2:4])) + ymd.append(int(s[4:])) + else: + # 19990101T235959[.59] + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = _parsems(s[4:]) + + elif len_li == 8: + # YYYYMMDD + s = l[i-1] + ymd.append(int(s[:4])) + ymd.append(int(s[4:6])) + ymd.append(int(s[6:])) + + elif len_li in (12, 14): + # YYYYMMDDhhmm[ss] + s = l[i-1] + ymd.append(int(s[:4])) + ymd.append(int(s[4:6])) + ymd.append(int(s[6:8])) + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li == 14: + res.second = int(s[12:]) + + elif ((i < len_l and info.hms(l[i]) is not None) or + (i+1 < len_l and l[i] == ' ' and + info.hms(l[i+1]) is not None)): + + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + if l[i] == ' ': + i += 1 + + idx = info.hms(l[i]) + + while True: + if idx == 0: + res.hour = int(value) + + if value % 1: + res.minute = int(60*(value % 1)) + + elif idx == 1: + res.minute = int(value) + + if value % 1: + res.second = int(60*(value % 1)) + + elif idx == 2: + res.second, res.microsecond = \ + _parsems(value_repr) + + i += 1 + + if i >= len_l or idx == 2: + break + + # 12h00 + try: + value_repr = l[i] + value = float(value_repr) + except ValueError: + break + else: + i += 1 + idx += 1 + + if i < len_l: + newidx = info.hms(l[i]) + + if newidx is not None: + idx = newidx + + elif (i == len_l and l[i-2] == ' ' and + info.hms(l[i-3]) is not None): + # X h MM or X m SS + idx = info.hms(l[i-3]) + 1 + + if idx == 1: + res.minute = int(value) + + if value % 1: + res.second = int(60*(value % 1)) + elif idx == 2: + res.second, res.microsecond = \ + _parsems(value_repr) + i += 1 + + elif i+1 < len_l and l[i] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + i += 1 + value = float(l[i]) + res.minute = int(value) + + if value % 1: + res.second = int(60*(value % 1)) + + i += 1 + + if i < len_l and l[i] == ':': + res.second, res.microsecond = _parsems(l[i+1]) + i += 2 + + elif i < len_l and l[i] in ('-', '/', '.'): + sep = l[i] + ymd.append(int(value)) + i += 1 + + if i < len_l and not info.jump(l[i]): + try: + # 01-01[-01] + ymd.append(int(l[i])) + except ValueError: + # 01-Jan[-01] + value = info.month(l[i]) + + if value is not None: + ymd.append(value) + assert mstridx == -1 + mstridx = len(ymd)-1 + else: + return None + + i += 1 + + if i < len_l and l[i] == sep: + # We have three members + i += 1 + value = info.month(l[i]) + + if value is not None: + ymd.append(value) + mstridx = len(ymd)-1 + assert mstridx == -1 + else: + ymd.append(int(l[i])) + + i += 1 + elif i >= len_l or info.jump(l[i]): + if i+1 < len_l and info.ampm(l[i+1]) is not None: + # 12 am + res.hour = int(value) + + if res.hour < 12 and info.ampm(l[i+1]) == 1: + res.hour += 12 + elif res.hour == 12 and info.ampm(l[i+1]) == 0: + res.hour = 0 + + i += 1 + else: + # Year, month or day + ymd.append(int(value)) + i += 1 + elif info.ampm(l[i]) is not None: + + # 12am + res.hour = int(value) + + if res.hour < 12 and info.ampm(l[i]) == 1: + res.hour += 12 + elif res.hour == 12 and info.ampm(l[i]) == 0: + res.hour = 0 + i += 1 + + elif not fuzzy: + return None + else: + i += 1 + continue + + # Check weekday + value = info.weekday(l[i]) + if value is not None: + res.weekday = value + i += 1 + continue + + # Check month name + value = info.month(l[i]) + if value is not None: + ymd.append(value) + assert mstridx == -1 + mstridx = len(ymd)-1 + + i += 1 + if i < len_l: + if l[i] in ('-', '/'): + # Jan-01[-99] + sep = l[i] + i += 1 + ymd.append(int(l[i])) + i += 1 + + if i < len_l and l[i] == sep: + # Jan-01-99 + i += 1 + ymd.append(int(l[i])) + i += 1 + + elif (i+3 < len_l and l[i] == l[i+2] == ' ' + and info.pertain(l[i+1])): + # Jan of 01 + # In this case, 01 is clearly year + try: + value = int(l[i+3]) + except ValueError: + # Wrong guess + pass + else: + # Convert it here to become unambiguous + ymd.append(info.convertyear(value)) + i += 4 + continue + + # Check am/pm + value = info.ampm(l[i]) + if value is not None: + # For fuzzy parsing, 'a' or 'am' (both valid English words) + # may erroneously trigger the AM/PM flag. Deal with that + # here. + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and res.ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if res.hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with ' + + 'AM or PM flag.') + elif not 0 <= res.hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for ' + + '12-hour clock.') + + if val_is_ampm: + if value == 1 and res.hour < 12: + res.hour += 12 + elif value == 0 and res.hour == 12: + res.hour = 0 + + res.ampm = value + + i += 1 + continue + + # Check for a timezone name + if (res.hour is not None and len(l[i]) <= 5 and + res.tzname is None and res.tzoffset is None and + not [x for x in l[i] if x not in + string.ascii_uppercase]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + i += 1 + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i < len_l and l[i] in ('+', '-'): + l[i] = ('+', '-')[l[i] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + continue + + # Check for a numbered timezone + if res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + i += 1 + len_li = len(l[i]) + + if len_li == 4: + # -0300 + res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 + elif i+1 < len_l and l[i+1] == ':': + # -03:00 + res.tzoffset = int(l[i])*3600+int(l[i+2])*60 + i += 2 + elif len_li <= 2: + # -[0]3 + res.tzoffset = int(l[i][:2])*3600 + else: + return None + i += 1 + + res.tzoffset *= signal + + # Look for a timezone name between parenthesis + if (i+3 < len_l and + info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and + 3 <= len(l[i+2]) <= 5 and + not [x for x in l[i+2] + if x not in string.ascii_uppercase]): + # -0300 (BRST) + res.tzname = l[i+2] + i += 4 + continue + + # Check jumps + if not (info.jump(l[i]) or fuzzy): + return None + + if last_skipped_token_i == i - 1: + # recombine the tokens + skipped_tokens[-1] += l[i] + else: + # just append + skipped_tokens.append(l[i]) + last_skipped_token_i = i + i += 1 + + # Process year/month/day + len_ymd = len(ymd) + if len_ymd > 3: + # More than three members!? + return None + elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): + # One member, or two members with a month string + if mstridx != -1: + res.month = ymd[mstridx] + del ymd[mstridx] + + if len_ymd > 1 or mstridx == -1: + if ymd[0] > 31: + res.year = ymd[0] + else: + res.day = ymd[0] + + elif len_ymd == 2: + # Two members with numbers + if ymd[0] > 31: + # 99-01 + res.year, res.month = ymd + elif ymd[1] > 31: + # 01-99 + res.month, res.year = ymd + elif dayfirst and ymd[1] <= 12: + # 13-01 + res.day, res.month = ymd + else: + # 01-13 + res.month, res.day = ymd + + elif len_ymd == 3: + # Three members + if mstridx == 0: + res.month, res.day, res.year = ymd + elif mstridx == 1: + if ymd[0] > 31 or (yearfirst and ymd[2] <= 31): + # 99-Jan-01 + res.year, res.month, res.day = ymd + else: + # 01-Jan-01 + # Give precendence to day-first, since + # two-digit years is usually hand-written. + res.day, res.month, res.year = ymd + + elif mstridx == 2: + # WTF!? + if ymd[1] > 31: + # 01-99-Jan + res.day, res.year, res.month = ymd + else: + # 99-01-Jan + res.year, res.day, res.month = ymd + + else: + if ymd[0] > 31 or \ + (yearfirst and ymd[1] <= 12 and ymd[2] <= 31): + # 99-01-01 + res.year, res.month, res.day = ymd + elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12): + # 13-01-01 + res.day, res.month, res.year = ymd + else: + # 01-13-01 + res.month, res.day, res.year = ymd + + except (IndexError, ValueError, AssertionError): + return None + + if not info.validate(res): + return None + + if fuzzy_with_tokens: + return res, tuple(skipped_tokens) + else: + return res + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + Parse a string in one of the supported formats, using the `parserinfo` + parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If `None`, the default arguments to the `parserinfo` constructor are + used. + + The `**kwargs` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + `None`, elements specified in `timestr` replace elements in the + default object. + + :param ignoretz: + Whether or not to ignore the time zone (boolean). + + :param tzinfos: + A time zone, to be applied to the date, if `ignoretz` is `True`. + This can be either a subclass of `tzinfo`, a time zone string or an + integer offset. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (`True`) or month (`False`). If + `yearfirst` is set to `True`, this distinguishes between YDM and + YMD. If set to `None`, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to `False`). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If `True`, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to `None`, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to `False`). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If `True`, `fuzzy` is automatically set to True, and the parser will + return a tuple where the first element is the parsed + `datetime.datetime` datetimestamp and the second element is a tuple + containing the portions of the string which were ignored, e.g. + "Today is January 1, 2047 at 8:21:00AM" should return + `(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))` + """ + # Python 2.x support: datetimes return their string presentation as + # bytes in 2.x and unicode in 3.x, so it's reasonable to expect that + # the parser will get both kinds. Internally we use unicode only. + if isinstance(timestr, binary_type): + timestr = timestr.decode() + + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + # Python 2.x compatibility: tzstr should be converted to unicode before + # being passed to _timelex. + if isinstance(tzstr, binary_type): + tzstr = tzstr.decode() + + res = self._result() + l = _timelex.split(tzstr) + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2])*3600 + + int(l[i][2:])*60)*signal) + elif i+1 < len_l and l[i+1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i])*3600+int(l[i+2])*60)*signal) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2])*3600*signal) + else: + return None + i += 1 + if res.dstabbr: + break + else: + break + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + i += 2 + if l[i] == '-': + value = int(l[i+1])*-1 + i += 1 + else: + value = int(l[i]) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i])-1) % 7 + else: + x.day = int(l[i]) + i += 2 + x.time = int(l[i]) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + i += 1 + else: + signal = 1 + res.dstoffset = (res.stdoffset+int(l[i]))*signal + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + i += 1 + x.month = int(l[i]) + i += 1 + assert l[i] in ('-', '.') + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + i += 1 + assert l[i] in ('-', '.') + i += 1 + x.weekday = (int(l[i])-1) % 7 + else: + # year day (zero based) + x.yday = int(l[i])+1 + + i += 1 + + if i < len_l and l[i] == '/': + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) + elif i+1 < len_l and l[i+1] == ':': + # -03:00 + x.time = int(l[i])*3600+int(l[i+2])*60 + i += 2 + if i+1 < len_l and l[i+1] == ':': + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2])*3600) + else: + return None + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + + +def _parsems(value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py new file mode 100644 index 00000000..da055728 --- /dev/null +++ b/lib/dateutil/relativedelta.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +from six import integer_types + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) + + +class relativedelta(object): + """ +The relativedelta type is based on the specification of the excellent +work done by M.-A. Lemburg in his +`mx.DateTime `_ extension. +However, notice that this type does *NOT* implement the same algorithm as +his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + +There are two different ways to build a relativedelta instance. The +first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + +The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an aritmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding aritmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc). These instances may + receive a parameter N, specifying the Nth weekday, which could + be positive or negative (like MO(+1) or MO(-2). Not specifying + it is the same as specifying +1. You can also use an integer, + where 0=MO. + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + +Here is the behavior of operations with relativedelta: + +1. Calculate the absolute year, using the 'year' argument, or the + original datetime year, if the argument is not present. + +2. Add the relative 'years' argument to the absolute year. + +3. Do steps 1 and 2 for month/months. + +4. Calculate the absolute day, using the 'day' argument, or the + original datetime day, if the argument is not present. Then, + subtract from the day until it fits in the year and month + found after their operations. + +5. Add the relative 'days' argument to the absolute day. Notice + that the 'weeks' argument is multiplied by 7 and added to + 'days'. + +6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, + microsecond/microseconds. + +7. If the 'weekday' argument is present, calculate the weekday, + with the given (wday, nth) tuple. wday is the index of the + weekday (0-6, 0=Mon), and nth is the number of weeks to add + forward or backward, depending on its signal. Notice that if + the calculated date is already Monday, for example, using + (0, 1) or (0, -1) won't change the day. + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) + self._set_months(months) + dtm = self.__radd__(dt2) + if dt1 < dt2: + while dt1 > dtm: + months += 1 + self._set_months(months) + dtm = self.__radd__(dt2) + else: + while dt1 < dtm: + months -= 1 + self._set_months(months) + dtm = self.__radd__(dt2) + delta = dt1 - dtm + self.seconds = delta.seconds+delta.days*86400 + self.microseconds = delta.microseconds + else: + self.years = years + self.months = months + self.days = days+weeks*7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = self.microseconds//abs(self.microseconds) + div, mod = divmod(self.microseconds*s, 1000000) + self.microseconds = mod*s + self.seconds += div*s + if abs(self.seconds) > 59: + s = self.seconds//abs(self.seconds) + div, mod = divmod(self.seconds*s, 60) + self.seconds = mod*s + self.minutes += div*s + if abs(self.minutes) > 59: + s = self.minutes//abs(self.minutes) + div, mod = divmod(self.minutes*s, 60) + self.minutes = mod*s + self.hours += div*s + if abs(self.hours) > 23: + s = self.hours//abs(self.hours) + div, mod = divmod(self.hours*s, 24) + self.hours = mod*s + self.days += div*s + if abs(self.months) > 11: + s = self.months//abs(self.months) + div, mod = divmod(self.months*s, 12) + self.months = mod*s + self.years += div*s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = self.months//abs(self.months) + div, mod = divmod(self.months*s, 12) + self.months = mod*s + self.years = div*s + else: + self.years = 0 + + def __add__(self, other): + if isinstance(other, relativedelta): + return relativedelta(years=other.years+self.years, + months=other.months+self.months, + days=other.days+self.days, + hours=other.hours+self.hours, + minutes=other.minutes+self.minutes, + seconds=other.seconds+self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=other.year or self.year, + month=other.month or self.month, + day=other.day or self.day, + weekday=other.weekday or self.weekday, + hour=other.hour or self.hour, + minute=other.minute or self.minute, + second=other.second or self.second, + microsecond=(other.microsecond or + self.microsecond)) + if not isinstance(other, datetime.date): + raise TypeError("unsupported type for add operation") + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth)-1)*7 + if nth > 0: + jumpdays += (7-ret.weekday()+weekday) % 7 + else: + jumpdays += (ret.weekday()-weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + raise TypeError("unsupported type for sub operation") + return relativedelta(years=self.years-other.years, + months=self.months-other.months, + days=self.days-other.days, + hours=self.hours-other.hours, + minutes=self.minutes-other.minutes, + seconds=self.seconds-other.seconds, + microseconds=self.microseconds-other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=self.year or other.year, + month=self.month or other.month, + day=self.day or other.day, + weekday=self.weekday or other.weekday, + hour=self.hour or other.hour, + minute=self.minute or other.minute, + second=self.second or other.second, + microsecond=self.microsecond or other.microsecond) + + def __neg__(self): + return relativedelta(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + f = float(other) + return relativedelta(years=int(self.years*f), + months=int(self.months*f), + days=int(self.days*f), + hours=int(self.hours*f), + minutes=int(self.minutes*f), + seconds=int(self.seconds*f), + microseconds=int(self.microseconds*f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return False + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __ne__(self, other): + return not self.__eq__(other) + + def __div__(self, other): + return self.__mul__(1/float(other)) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("%s=%+d" % (attr, value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/rrule.py b/lib/dateutil/rrule.py new file mode 100644 index 00000000..141780b3 --- /dev/null +++ b/lib/dateutil/rrule.py @@ -0,0 +1,1375 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import itertools +import datetime +import calendar +import sys + +from fractions import gcd + +from six import advance_iterator, integer_types +from six.moves import _thread + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n == 0") + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._cache_gen = self._iter() + self._cache_complete = False + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penality. + def count(self): + """ Returns the number of recurrences in this set. It will have go + trough the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def between(self, after, before, inc=False): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + Additionally, it supports the following keyword arguments: + + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + How many occurrences will be generated. + :param until: + If given, this must be a datetime instance, that will specify the + limit of the recurrence. If a recurrence instance happens to be the + same as the datetime instance given in the until keyword, this will + be the last occurrence. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + bymonthday = dtstart.day + elif freq == MONTHLY: + bymonthday = dtstart.day + elif freq == WEEKLY: + byweekday = dtstart.weekday() + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + else: + self._byeaster = None + + # bymonthay + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + self._bymonthday = tuple(sorted(set([x for x in bymonthday if x > 0]))) + self._bynmonthday = tuple(sorted(set([x for x in bymonthday if x < 0]))) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = set((dtstart.hour,)) + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = set((dtstart.minute,)) + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + total += 1 + yield res + if count: + count -= 1 + if not count: + self._len = total + return + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal+i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + total += 1 + yield res + if count: + count -= 1 + if not count: + self._len = total + return + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399-(hour*3600+minute*60+second)) + // interval)*interval) + + rep_rate = (24*3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365+calendar.isleap(year) + self.nextyearlen = 365+calendar.isleap(year+1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None]*self.yearlen + i = datetime.date(year, month, day).toordinal()-self.yearordinal + dset[i] = i + return dset, i, i+1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + self.genlist.remove(self) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + rlist.sort() + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + exlist.sort() + lastdt = None + total = 0 + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + advance_iterator(exlist[0]) + exlist.sort() + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + rlist.sort() + self._len = total + + +class _rrulestr(object): + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg): + l = [] + for wday in value.split(','): + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + exdatevals.append(value) + elif name == "DTSTART": + for parm in parms: + raise ValueError("unsupported DTSTART parm: "+parm) + if not parser: + from dateutil import parser + dtstart = parser.parse(value, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + for datestr in value.split(','): + rset.exdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz.py b/lib/dateutil/tz.py new file mode 100644 index 00000000..31879e8b --- /dev/null +++ b/lib/dateutil/tz.py @@ -0,0 +1,986 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:`datetime.tzinfo` type. There are classes to handle tzfile format files +(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ +environment string (in all known formats), given ranges (with help from +relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os + +from six import string_types, PY3 + +try: + from dateutil.tzwin import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +relativedelta = None +parser = None +rrule = None + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] + + +def tzname_in_python2(myfunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + def inner_func(*args, **kwargs): + if PY3: + return myfunc(*args, **kwargs) + else: + return myfunc(*args, **kwargs).encode() + return inner_func + +ZERO = datetime.timedelta(0) +EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() + + +class tzutc(datetime.tzinfo): + + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def __eq__(self, other): + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class tzoffset(datetime.tzinfo): + + def __init__(self, name, offset): + self._name = name + self._offset = datetime.timedelta(seconds=offset) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + def __eq__(self, other): + return (isinstance(other, tzoffset) and + self._offset == other._offset) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + self._offset.days*86400+self._offset.seconds) + + __reduce__ = object.__reduce__ + + +class tzlocal(datetime.tzinfo): + + _std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + _dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + _dst_offset = _std_offset + + def utcoffset(self, dt): + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if self._isdst(dt): + return self._dst_offset-self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + return time.localtime(timestamp+time.timezone).tm_isdst + + def __eq__(self, other): + if not isinstance(other, tzlocal): + return False + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return False + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt) + + def __ne__(self, other): + return not self.__eq__(other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class tzfile(datetime.tzinfo): + + # http://www.twinsun.com/tz/tz-link.htm + # ftp://ftp.iana.org/tz/tz*.tar.gz + + def __init__(self, fileobj, filename=None): + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + try: + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + self._trans_list = struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4)) + else: + self._trans_list = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + self._trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + self._trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now + # if leapcnt: + # leap = struct.unpack(">%dl" % (leapcnt*2), + # fileobj.read(leapcnt*8)) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # ** Everything has been read ** + finally: + if file_opened_here: + fileobj.close() + + # Build ttinfo list + self._ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + # Round to full-minutes if that's not the case. Python's + # datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 for some information. + gmtoff = (gmtoff+30)//60*60 + tti = _ttinfo() + tti.offset = gmtoff + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + self._ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + trans_idx = [] + for idx in self._trans_idx: + trans_idx.append(self._ttinfo_list[idx]) + self._trans_idx = tuple(trans_idx) + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + self._ttinfo_std = None + self._ttinfo_dst = None + self._ttinfo_before = None + if self._ttinfo_list: + if not self._trans_list: + self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = self._trans_idx[i] + if not self._ttinfo_std and not tti.isdst: + self._ttinfo_std = tti + elif not self._ttinfo_dst and tti.isdst: + self._ttinfo_dst = tti + if self._ttinfo_std and self._ttinfo_dst: + break + else: + if self._ttinfo_dst and not self._ttinfo_std: + self._ttinfo_std = self._ttinfo_dst + + for tti in self._ttinfo_list: + if not tti.isdst: + self._ttinfo_before = tti + break + else: + self._ttinfo_before = self._ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + laststdoffset = 0 + self._trans_list = list(self._trans_list) + for i in range(len(self._trans_list)): + tti = self._trans_idx[i] + if not tti.isdst: + # This is std time. + self._trans_list[i] += tti.offset + laststdoffset = tti.offset + else: + # This is dst time. Convert to std. + self._trans_list[i] += laststdoffset + self._trans_list = tuple(self._trans_list) + + def _find_ttinfo(self, dt, laststd=0): + timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + idx = 0 + for trans in self._trans_list: + if timestamp < trans: + break + idx += 1 + else: + return self._ttinfo_std + if idx == 0: + return self._ttinfo_before + if laststd: + while idx > 0: + tti = self._trans_idx[idx-1] + if not tti.isdst: + return tti + idx -= 1 + else: + return self._ttinfo_std + else: + return self._trans_idx[idx-1] + + def utcoffset(self, dt): + if not self._ttinfo_std: + return ZERO + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if not self._ttinfo_dst: + return ZERO + tti = self._find_ttinfo(dt) + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.delta-self._find_ttinfo(dt, laststd=1).delta + + # An alternative for that would be: + # + # return self._ttinfo_dst.offset-self._ttinfo_std.offset + # + # However, this class stores historical changes in the + # dst offset, so I belive that this wouldn't be the right + # way to implement this. + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return False + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + if not os.path.isfile(self._filename): + raise ValueError("Unpickable %s class" % self.__class__.__name__) + return (self.__class__, (self._filename,)) + + +class tzrange(datetime.tzinfo): + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + global relativedelta + if not relativedelta: + from dateutil import relativedelta + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + def utcoffset(self, dt): + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if self._isdst(dt): + return self._dst_offset-self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def _isdst(self, dt): + if not self._start_delta: + return False + year = datetime.datetime(dt.year, 1, 1) + start = year+self._start_delta + end = year+self._end_delta + dt = dt.replace(tzinfo=None) + if start < end: + return dt >= start and dt < end + else: + return dt >= start or dt < end + + def __eq__(self, other): + if not isinstance(other, tzrange): + return False + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class tzstr(tzrange): + + def __init__(self, s): + global parser + if not parser: + from dateutil import parser + self._s = s + + res = parser._parsetz(s) + if res is None: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC"): + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + def _delta(self, x, isend=0): + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset-self._std_offset + kwargs["seconds"] -= delta.seconds+delta.days*86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(datetime.tzinfo): + def __init__(self, tzid, comps=[]): + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + dt = dt.replace(tzinfo=None) + try: + return self._cachecomp[self._cachedate.index(dt)] + except ValueError: + pass + lastcomp = None + lastcompdt = None + for comp in self._comps: + if not comp.isdst: + # Handle the extra hour in DST -> STD + compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) + else: + compdt = comp.rrule.before(dt, inc=True) + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + self._cachedate.insert(0, dt) + self._cachecomp.insert(0, lastcomp) + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + return lastcomp + + def utcoffset(self, dt): + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + def __init__(self, fileobj): + global rrule + if not rrule: + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + elif hasattr(fileobj, "name"): + self._s = fileobj.name + else: + self._s = repr(fileobj) + + self._vtz = {} + + self._parse_rfc(fileobj.read()) + + def keys(self): + return list(self._vtz.keys()) + + def get(self, tzid=None): + if tzid is None: + keys = list(self._vtz.keys()) + if len(keys) == 0: + raise ValueError("no timezones defined") + elif len(keys) > 1: + raise ValueError("more than one timezone available") + tzid = keys[0] + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2])*3600+int(s[2:])*60)*signal + elif len(s) == 6: + return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal + else: + raise ValueError("invalid offset: "+s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def gettz(name=None): + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name == ":": + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + if name.startswith(":"): + name = name[:-1] + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except WindowsError: + tz = None + if not tz: + from dateutil.zoneinfo import gettz + tz = gettz(name) + if not tz: + for c in name: + # name must have at least one offset to be a tzstr + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = tzutc() + elif name in time.tzname: + tz = tzlocal() + return tz + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/tzwin.py b/lib/dateutil/tzwin.py new file mode 100644 index 00000000..e8a82d75 --- /dev/null +++ b/lib/dateutil/tzwin.py @@ -0,0 +1,184 @@ +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg + +__all__ = ["tzwin", "tzwinlocal"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + +TZKEYNAME = _settzkeyname() + + +class tzwinbase(datetime.tzinfo): + """tzinfo class based on win32's timezones available in the registry.""" + + def utcoffset(self, dt): + if self._isdst(dt): + return datetime.timedelta(minutes=self._dstoffset) + else: + return datetime.timedelta(minutes=self._stdoffset) + + def dst(self, dt): + if self._isdst(dt): + minutes = self._dstoffset - self._stdoffset + return datetime.timedelta(minutes=minutes) + else: + return datetime.timedelta(0) + + def tzname(self, dt): + if self._isdst(dt): + return self._dstname + else: + return self._stdname + + def list(): + """Return a list of all time zones known to the system.""" + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + tzkey = winreg.OpenKey(handle, TZKEYNAME) + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + tzkey.Close() + handle.Close() + return result + list = staticmethod(list) + + def display(self): + return self._display + + def _isdst(self, dt): + if not self._dstmonth: + # dstmonth == 0 signals the zone has no daylight saving time + return False + dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + if dston < dstoff: + return dston <= dt.replace(tzinfo=None) < dstoff + else: + return not dstoff <= dt.replace(tzinfo=None) < dston + + +class tzwin(tzwinbase): + + def __init__(self, name): + self._name = name + + # multiple contexts only possible in 2.7 and 3.1, we still support 2.6 + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, + "%s\%s" % (TZKEYNAME, name)) as tzkey: + keydict = valuestodict(tzkey) + + self._stdname = keydict["Std"].encode("iso-8859-1") + self._dstname = keydict["Dlt"].encode("iso-8859-1") + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + + def __init__(self): + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._stdname = keydict["StandardName"].encode("iso-8859-1") + self._dstname = keydict["DaylightName"].encode("iso-8859-1") + + try: + with winreg.OpenKey( + handle, "%s\%s" % (TZKEYNAME, self._stdname)) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] + self._dstoffset = self._stdoffset-keydict["DaylightBias"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:6] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:6] + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """dayofweek == 0 means Sunday, whichweek 5 means last instance""" + first = datetime.datetime(year, month, 1, hour, minute) + weekdayone = first.replace(day=((dayofweek-first.isoweekday()) % 7+1)) + for n in range(whichweek): + dt = weekdayone+(whichweek-n)*ONEWEEK + if dt.month == month: + return dt + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dict = {} + size = winreg.QueryInfoKey(key)[1] + for i in range(size): + data = winreg.EnumValue(key, i) + dict[data[0]] = data[1] + return dict diff --git a/plexpy/helpers.py b/plexpy/helpers.py index c0c24b47..cb11e87d 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -446,85 +446,4 @@ def sanitize(string): if string: return unicode(string).replace('<','<').replace('>','>') else: - return '' - -def parse_js_date(date): - """ - Taken from moment library. - - Translate the easy-to-use JavaScript format strings to Python's cumbersome - strftime format. Also, this is some ugly code -- and it's completely - order-dependent. - """ - # AM/PM - if 'A' in date: - date = date.replace('A', '%p') - elif 'a' in date: - date = date.replace('a', '%P') - # 24 hours - if 'HH' in date: - date = date.replace('HH', '%H') - elif 'H' in date: - date = date.replace('H', '%k') - # 12 hours - elif 'hh' in date: - date = date.replace('hh', '%I') - elif 'h' in date: - date = date.replace('h', '%l') - # Minutes - if 'mm' in date: - date = date.replace('mm', '%min') - elif 'm' in date: - date = date.replace('m', '%min') - # Seconds - if 'ss' in date: - date = date.replace('ss', '%S') - elif 's' in date: - date = date.replace('s', '%S') - # Milliseconds - if 'SSS' in date: - date = date.replace('SSS', '%3') - # Years - if 'YYYY' in date: - date = date.replace('YYYY', '%Y') - elif 'YY' in date: - date = date.replace('YY', '%y') - # Months - if 'MMMM' in date: - date = date.replace('MMMM', '%B') - elif 'MMM' in date: - date = date.replace('MMM', '%b') - elif 'MM' in date: - date = date.replace('MM', '%m') - elif 'M' in date: - date = date.replace('M', '%m') - # Days of the week - if 'dddd' in date: - date = date.replace('dddd', '%A') - elif 'ddd' in date: - date = date.replace('ddd', '%a') - elif 'dd' in date: - date = date.replace('dd', '%w') - elif 'd' in date: - date = date.replace('d', '%u') - # Days of the year - if 'DDDD' in date: - date = date.replace('DDDD', '%j') - elif 'DDD' in date: - date = date.replace('DDD', '%j') - # Days of the month - elif 'DD' in date: - date = date.replace('DD', '%d') - # 'Do' not valid python time format - elif 'Do' in date: - date = date.replace('Do', '') - elif 'D' in date: - date = date.replace('D', '%d') - # Timezone - if 'zz' in date: - date = date.replace('zz', '%Z') - # A necessary evil right now... - if '%min' in date: - date = date.replace('%min', '%M') - - return date \ No newline at end of file + return '' \ No newline at end of file diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 5ae1c4db..402910d3 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -16,6 +16,7 @@ import re import time +import arrow from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect import plexpy @@ -524,8 +525,8 @@ def build_notify_text(session=None, timeline=None, state=None): 'server_uptime': server_uptime, 'streams': stream_count, 'action': state, - 'datestamp': time.strftime(helpers.parse_js_date(plexpy.CONFIG.DATE_FORMAT)), - 'timestamp': time.strftime(helpers.parse_js_date(plexpy.CONFIG.TIME_FORMAT)), + 'datestamp': arrow.now().format(plexpy.CONFIG.DATE_FORMAT.replace('Do','').replace('zz','')), + 'timestamp': arrow.now().format(plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('zz','')), 'user': user, 'platform': platform, 'player': player,