123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- # -*- coding: UTF-8 -*-
- "Objects and routines pertaining to date and time (tempora)"
- from __future__ import division, unicode_literals
- import datetime
- import time
- import re
- import numbers
- import functools
- import six
- class Parser(object):
- """
- Datetime parser: parses a date-time string using multiple possible
- formats.
- >>> p = Parser(('%H%M', '%H:%M'))
- >>> tuple(p.parse('1319'))
- (1900, 1, 1, 13, 19, 0, 0, 1, -1)
- >>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y'))
- >>> tuple(dateParser.parse('2003-12-20'))
- (2003, 12, 20, 0, 0, 0, 5, 354, -1)
- >>> tuple(dateParser.parse('16-Dec-1994'))
- (1994, 12, 16, 0, 0, 0, 4, 350, -1)
- >>> tuple(dateParser.parse('5/19/2003'))
- (2003, 5, 19, 0, 0, 0, 0, 139, -1)
- >>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y'))
- >>> tuple(dtParser.parse('2003-12-20 19:13:26'))
- (2003, 12, 20, 19, 13, 26, 5, 354, -1)
- >>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004'))
- (2004, 1, 20, 16, 19, 33, 1, 20, -1)
- Be forewarned, a ValueError will be raised if more than one format
- matches:
- >>> Parser(('%H%M', '%H%M%S')).parse('732')
- Traceback (most recent call last):
- ...
- ValueError: More than one format string matched target 732.
- """
- formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y')
- "some common default formats"
- def __init__(self, formats = None):
- if formats:
- self.formats = formats
- def parse(self, target):
- self.target = target
- results = tuple(filter(None, map(self._parse, self.formats)))
- del self.target
- if not results:
- raise ValueError("No format strings matched the target %s."
- % target)
- if not len(results) == 1:
- raise ValueError("More than one format string matched target %s."
- % target)
- return results[0]
- def _parse(self, format):
- try:
- result = time.strptime(self.target, format)
- except ValueError:
- result = False
- return result
- # some useful constants
- osc_per_year = 290091329207984000
- """
- mean vernal equinox year expressed in oscillations of atomic cesium at the
- year 2000 (see http://webexhibits.org/calendars/timeline.html for more info).
- """
- osc_per_second = 9192631770
- seconds_per_second = 1
- seconds_per_year = 31556940
- seconds_per_minute = 60
- minutes_per_hour = 60
- hours_per_day = 24
- seconds_per_hour = seconds_per_minute * minutes_per_hour
- seconds_per_day = seconds_per_hour * hours_per_day
- days_per_year = seconds_per_year / seconds_per_day
- thirty_days = datetime.timedelta(days=30)
- # these values provide useful averages
- six_months = datetime.timedelta(days=days_per_year/2)
- seconds_per_month = seconds_per_year/12
- hours_per_month=hours_per_day*days_per_year/12
- def strftime(fmt, t):
- """A class to replace the strftime in datetime package or time module.
- Identical to strftime behavior in those modules except supports any
- year.
- Also supports datetime.datetime times.
- Also supports milliseconds using %s
- Also supports microseconds using %u"""
- if isinstance(t, (time.struct_time, tuple)):
- t = datetime.datetime(*t[:6])
- assert isinstance(t, (datetime.datetime, datetime.time, datetime.date))
- try:
- year = t.year
- if year < 1900: t = t.replace(year = 1900)
- except AttributeError:
- year = 1900
- subs = (
- ('%Y', '%04d' % year),
- ('%y', '%02d' % (year % 100)),
- ('%s', '%03d' % (t.microsecond // 1000)),
- ('%u', '%03d' % (t.microsecond % 1000))
- )
- doSub = lambda s, sub: s.replace(*sub)
- doSubs = lambda s: functools.reduce(doSub, subs, s)
- fmt = '%%'.join(map(doSubs, fmt.split('%%')))
- return t.strftime(fmt)
- def strptime(s, fmt, tzinfo = None):
- """
- A function to replace strptime in the time module. Should behave
- identically to the strptime function except it returns a datetime.datetime
- object instead of a time.struct_time object.
- Also takes an optional tzinfo parameter which is a time zone info object.
- """
- res = time.strptime(s, fmt)
- return datetime.datetime(tzinfo = tzinfo, *res[:6])
- class DatetimeConstructor(object):
- """
- >>> cd = DatetimeConstructor.construct_datetime
- >>> cd(datetime.datetime(2011,1,1))
- datetime.datetime(2011, 1, 1, 0, 0)
- """
- @classmethod
- def construct_datetime(cls, *args, **kwargs):
- """Construct a datetime.datetime from a number of different time
- types found in python and pythonwin"""
- if len(args) == 1:
- arg = args[0]
- method = cls.__get_dt_constructor(type(arg).__module__,
- type(arg).__name__)
- result = method(arg)
- try:
- result = result.replace(tzinfo = kwargs.pop('tzinfo'))
- except KeyError:
- pass
- if kwargs:
- first_key = kwargs.keys()[0]
- raise TypeError("{first_key} is an invalid keyword "
- "argument for this function.".format(**locals()))
- else:
- result = datetime.datetime(*args, **kwargs)
- return result
- @classmethod
- def __get_dt_constructor(cls, moduleName, name):
- try:
- method_name = '__dt_from_{moduleName}_{name}__'.format(**locals())
- return getattr(cls, method_name)
- except AttributeError:
- raise TypeError("No way to construct datetime.datetime from "
- "{moduleName}.{name}".format(**locals()))
- @staticmethod
- def __dt_from_datetime_datetime__(source):
- dtattrs = ('year', 'month', 'day', 'hour', 'minute', 'second',
- 'microsecond', 'tzinfo')
- attrs = map(lambda a: getattr(source, a), dtattrs)
- return datetime.datetime(*attrs)
- @staticmethod
- def __dt_from___builtin___time__(pyt):
- "Construct a datetime.datetime from a pythonwin time"
- fmtString = '%Y-%m-%d %H:%M:%S'
- result = strptime(pyt.Format(fmtString), fmtString)
- # get milliseconds and microseconds. The only way to do this is
- # to use the __float__ attribute of the time, which is in days.
- microseconds_per_day = seconds_per_day * 1000000
- microseconds = float(pyt) * microseconds_per_day
- microsecond = int(microseconds % 1000000)
- result = result.replace(microsecond = microsecond)
- return result
- @staticmethod
- def __dt_from_timestamp__(timestamp):
- return datetime.datetime.utcfromtimestamp(timestamp)
- __dt_from___builtin___float__ = __dt_from_timestamp__
- __dt_from___builtin___long__ = __dt_from_timestamp__
- __dt_from___builtin___int__ = __dt_from_timestamp__
- @staticmethod
- def __dt_from_time_struct_time__(s):
- return datetime.datetime(*s[:6])
- def datetime_mod(dt, period, start = None):
- """
- Find the time which is the specified date/time truncated to the time delta
- relative to the start date/time.
- By default, the start time is midnight of the same day as the specified
- date/time.
- >>> datetime_mod(datetime.datetime(2004, 1, 2, 3),
- ... datetime.timedelta(days = 1.5),
- ... start = datetime.datetime(2004, 1, 1))
- datetime.datetime(2004, 1, 1, 0, 0)
- >>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
- ... datetime.timedelta(days = 1.5),
- ... start = datetime.datetime(2004, 1, 1))
- datetime.datetime(2004, 1, 2, 12, 0)
- >>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
- ... datetime.timedelta(days = 7),
- ... start = datetime.datetime(2004, 1, 1))
- datetime.datetime(2004, 1, 1, 0, 0)
- >>> datetime_mod(datetime.datetime(2004, 1, 10, 13),
- ... datetime.timedelta(days = 7),
- ... start = datetime.datetime(2004, 1, 1))
- datetime.datetime(2004, 1, 8, 0, 0)
- """
- if start is None:
- # use midnight of the same day
- start = datetime.datetime.combine(dt.date(), datetime.time())
- # calculate the difference between the specified time and the start date.
- delta = dt - start
- # now aggregate the delta and the period into microseconds
- # Use microseconds because that's the highest precision of these time
- # pieces. Also, using microseconds ensures perfect precision (no floating
- # point errors).
- get_time_delta_microseconds = lambda td: (
- (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds
- )
- delta, period = map(get_time_delta_microseconds, (delta, period))
- offset = datetime.timedelta(microseconds = delta % period)
- # the result is the original specified time minus the offset
- result = dt - offset
- return result
- def datetime_round(dt, period, start = None):
- """
- Find the nearest even period for the specified date/time.
- >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13),
- ... datetime.timedelta(hours = 1))
- datetime.datetime(2004, 11, 13, 8, 0)
- >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13),
- ... datetime.timedelta(hours = 1))
- datetime.datetime(2004, 11, 13, 9, 0)
- >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30),
- ... datetime.timedelta(hours = 1))
- datetime.datetime(2004, 11, 13, 9, 0)
- """
- result = datetime_mod(dt, period, start)
- if abs(dt - result) >= period // 2:
- result += period
- return result
- def get_nearest_year_for_day(day):
- """
- Returns the nearest year to now inferred from a Julian date.
- """
- now = time.gmtime()
- result = now.tm_year
- # if the day is far greater than today, it must be from last year
- if day - now.tm_yday > 365//2:
- result -= 1
- # if the day is far less than today, it must be for next year.
- if now.tm_yday - day > 365//2:
- result += 1
- return result
- def gregorian_date(year, julian_day):
- """
- Gregorian Date is defined as a year and a julian day (1-based
- index into the days of the year).
- >>> gregorian_date(2007, 15)
- datetime.date(2007, 1, 15)
- """
- result = datetime.date(year, 1, 1)
- result += datetime.timedelta(days = julian_day - 1)
- return result
- def get_period_seconds(period):
- """
- return the number of seconds in the specified period
- >>> get_period_seconds('day')
- 86400
- >>> get_period_seconds(86400)
- 86400
- >>> get_period_seconds(datetime.timedelta(hours=24))
- 86400
- >>> get_period_seconds('day + os.system("rm -Rf *")')
- Traceback (most recent call last):
- ...
- ValueError: period not in (second, minute, hour, day, month, year)
- """
- if isinstance(period, six.string_types):
- try:
- name = 'seconds_per_' + period.lower()
- result = globals()[name]
- except KeyError:
- raise ValueError("period not in (second, minute, hour, day, "
- "month, year)")
- elif isinstance(period, numbers.Number):
- result = period
- elif isinstance(period, datetime.timedelta):
- result = period.days * get_period_seconds('day') + period.seconds
- else:
- raise TypeError('period must be a string or integer')
- return result
- def get_date_format_string(period):
- """
- For a given period (e.g. 'month', 'day', or some numeric interval
- such as 3600 (in secs)), return the format string that can be
- used with strftime to format that time to specify the times
- across that interval, but no more detailed.
- For example,
- >>> get_date_format_string('month')
- '%Y-%m'
- >>> get_date_format_string(3600)
- '%Y-%m-%d %H'
- >>> get_date_format_string('hour')
- '%Y-%m-%d %H'
- >>> get_date_format_string(None)
- Traceback (most recent call last):
- ...
- TypeError: period must be a string or integer
- >>> get_date_format_string('garbage')
- Traceback (most recent call last):
- ...
- ValueError: period not in (second, minute, hour, day, month, year)
- """
- # handle the special case of 'month' which doesn't have
- # a static interval in seconds
- if isinstance(period, six.string_types) and period.lower() == 'month':
- return '%Y-%m'
- file_period_secs = get_period_seconds(period)
- format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S')
- intervals = (
- seconds_per_year,
- seconds_per_day,
- seconds_per_hour,
- seconds_per_minute,
- 1, # seconds_per_second
- )
- mods = list(map(lambda interval: file_period_secs % interval, intervals))
- format_pieces = format_pieces[: mods.index(0) + 1]
- return ''.join(format_pieces)
- def divide_timedelta_float(td, divisor):
- """
- Divide a timedelta by a float value
- >>> one_day = datetime.timedelta(days=1)
- >>> half_day = datetime.timedelta(days=.5)
- >>> divide_timedelta_float(one_day, 2.0) == half_day
- True
- >>> divide_timedelta_float(one_day, 2) == half_day
- True
- """
- # td is comprised of days, seconds, microseconds
- dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')]
- dsm = map(lambda elem: elem/divisor, dsm)
- return datetime.timedelta(*dsm)
- def calculate_prorated_values():
- """
- A utility function to prompt for a rate (a string in units per
- unit time), and return that same rate for various time periods.
- """
- rate = six.moves.input("Enter the rate (3/hour, 50/month)> ")
- res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict()
- value = float(res['value'])
- value_per_second = value / get_period_seconds(res['period'])
- for period in ('minute', 'hour', 'day', 'month', 'year'):
- period_value = value_per_second * get_period_seconds(period)
- print("per {period}: {period_value}".format(**locals()))
- def parse_timedelta(str):
- """
- Take a string representing a span of time and parse it to a time delta.
- Accepts any string of comma-separated numbers each with a unit indicator.
- >>> parse_timedelta('1 day')
- datetime.timedelta(1)
- >>> parse_timedelta('1 day, 30 seconds')
- datetime.timedelta(1, 30)
- >>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds')
- datetime.timedelta(47, 28848, 15400)
- """
- deltas = (_parse_timedelta_part(part.strip()) for part in str.split(','))
- return sum(deltas, datetime.timedelta())
- def _parse_timedelta_part(part):
- match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part)
- if not match:
- msg = "Unable to parse {part!r} as a time delta".format(**locals())
- raise ValueError(msg)
- unit = match.group('unit')
- if not unit.endswith('s'):
- unit += 's'
- return datetime.timedelta(**{unit: float(match.group('value'))})
- def divide_timedelta(td1, td2):
- """
- Get the ratio of two timedeltas
- >>> one_day = datetime.timedelta(days=1)
- >>> one_hour = datetime.timedelta(hours=1)
- >>> divide_timedelta(one_hour, one_day) == 1 / 24
- True
- """
- try:
- return td1 / td2
- except TypeError:
- # Python 3.2 gets division
- # http://bugs.python.org/issue2706
- return td1.total_seconds() / td2.total_seconds()
- def date_range(start=None, stop=None, step=None):
- """
- Much like the built-in function range, but works with dates
- >>> range_items = date_range(
- ... datetime.datetime(2005,12,21),
- ... datetime.datetime(2005,12,25),
- ... )
- >>> my_range = tuple(range_items)
- >>> datetime.datetime(2005,12,21) in my_range
- True
- >>> datetime.datetime(2005,12,22) in my_range
- True
- >>> datetime.datetime(2005,12,25) in my_range
- False
- """
- if step is None: step = datetime.timedelta(days=1)
- if start is None: start = datetime.datetime.now()
- while start < stop:
- yield start
- start += step
|