1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
|
# This file is part of Moksha.
# Copyright (C) 2008-2010 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time
import datetime
def utc_offset(tz):
""" Return the UTC offset for a given timezone.
>>> utc_offset('US/Eastern')
'-4'
"""
utc_offset = ''
now = datetime.now(utc)
now = now.astimezone(timezone(tz))
offset = now.strftime('%z')
if offset.startswith('-'):
offset = offset[1:]
utc_offset += '-'
hours = int(offset[:2])
utc_offset += str(hours)
# FIXME: account for minutes?
#minutes = int(offset[2:])
#if minutes:
# utc_offset += '.%d' % ...
return utc_offset
class DateTimeDisplay(object):
"""
DateTimeDisplay is an object which takes any number of datetime objects
and process them for display::
>>> from datetime import datetime
>>> now = datetime(2009, 5, 12)
>>> later = datetime(2009, 5, 13)
>>> d = DateTimeDisplay(now)
>>> print d
2009-05-12 00:00:00
>>> d.age(later)
'1 day'
>>> d.age(datetime(2010, 7, 10, 10, 10), granularity='minute')
'1 year, 1 month, 29 days, 10 hours and 10 minutes'
>>> d.age(datetime(2010, 7, 10, 10, 10), tz='Europe/Amsterdam')
'1 year, 1 month, 29 days and 10 hours'
>>> d = DateTimeDisplay(datetime(2009, 5, 12, 12, 0, 0))
>>> d.timestamp
datetime.datetime(2009, 5, 12, 12, 0)
>>> d.astimezone('Europe/Amsterdam')
datetime.datetime(2009, 5, 12, 14, 0, tzinfo=<DstTzInfo 'Europe/Amsterdam' CEST+2:00:00 DST>)
"""
def __init__(self, timestamp, format='%Y-%m-%d %H:%M:%S'):
if isinstance(timestamp, basestring) and '.' in timestamp:
timestamp = timestamp.split('.')[0]
self.timestamp = timestamp
if isinstance(timestamp, datetime.datetime):
self.datetime = timestamp
elif isinstance(timestamp, time.struct_time):
self.datetime = datetime.datetime(*timestamp[:-2])
elif isinstance(timestamp, basestring):
if hasattr(datetime, 'strptime'): # Python 2.5+
self.datetime = datetime.datetime.strptime(timestamp, format)
else: # Python 2.4
self.datetime = datetime.datetime(*time.strptime(timestamp, format)[:-2])
else:
raise Exception("You must provide either a datetime object or a"
"string, not %s" % type(timestamp))
def astimezone(self, tz):
""" Return `self.datetime` as a different timezone """
timestamp = self.datetime.replace(tzinfo=utc)
zone = timezone(tz)
return zone.normalize(timestamp.astimezone(zone))
def age(self, end=None, tz=None, granularity='hour', general=False):
"""
Return the distance of time in words from `self.datetime` to `end`.
>>> start = datetime(1984, 11, 02)
>>> now = datetime(2009, 5, 22, 12, 11, 10)
>>> DateTimeDisplay(start).age(now)
'2 decades, 4 years, 6 months, 20 days and 12 hours'
>>> DateTimeDisplay(start).age(now, general=True)
'2 decades'
"""
start = self.datetime
if not end:
end = datetime.datetime.utcnow()
else:
if isinstance(end, DateTimeDisplay):
end = end.datetime
if tz:
zone = timezone(tz)
end = end.replace(tzinfo=utc)
end = zone.normalize(end.astimezone(zone))
start = self.astimezone(tz)
age = distance_of_time_in_words(start, end, granularity=granularity)
if general:
if not age.startswith('less than'):
age = age.split('and')[0].split(',')[0]
return age
def __str__(self):
return self.datetime.strftime('%Y-%m-%d %H:%M:%S %Z%z')
def __repr__(self):
return "<DateTimeDisplay %r>" % self.datetime
# The following functions were all copied *wholesale* from webhelpers.date
def _process_carryover(deltas, carry_over):
"""A helper function to process negative deltas based on the deltas
and the list of tuples that contain the carry over values"""
for smaller, larger, amount in carry_over:
if deltas[smaller] < 0:
deltas[larger] -= 1
deltas[smaller] += amount
def _pluralize_granularity(granularity):
"""Pluralize the given granularity"""
if 'century' == granularity:
return "centuries"
return granularity + "s"
def _delta_string(delta, granularity):
"""Return the string to use for the given delta and ordinality"""
if 1 == delta:
return "1 " + granularity
elif delta > 1:
return str(delta) + " " + _pluralize_granularity(granularity)
def _is_leap_year(year):
if year % 4 == 0 and year % 400 != 0:
return True
return False
def distance_of_time_in_words(from_time, to_time=0, granularity="second",
round=False):
"""
Return the absolute time-distance string for two datetime objects,
ints or any combination you can dream of.
If times are integers, they are interpreted as seconds from now.
``granularity`` dictates where the string calculation is stopped.
If set to seconds (default) you will receive the full string. If
another accuracy is supplied you will receive an approximation.
Available granularities are:
'century', 'decade', 'year', 'month', 'day', 'hour', 'minute',
'second'
Setting ``round`` to true will increase the result by 1 if the fractional
value is greater than 50% of the granularity unit.
Examples:
>>> distance_of_time_in_words(86399, round=True, granularity='day')
'1 day'
>>> distance_of_time_in_words(86399, granularity='day')
'less than 1 day'
>>> distance_of_time_in_words(86399)
'23 hours, 59 minutes and 59 seconds'
>>> distance_of_time_in_words(datetime(2008,3,21, 16,34),
... datetime(2008,2,6,9,45))
'1 month, 15 days, 6 hours and 49 minutes'
>>> distance_of_time_in_words(datetime(2008,3,21, 16,34),
... datetime(2008,2,6,9,45), granularity='decade')
'less than 1 decade'
>>> distance_of_time_in_words(datetime(2008,3,21, 16,34),
... datetime(2008,2,6,9,45), granularity='second')
'1 month, 15 days, 6 hours and 49 minutes'
"""
granularities = ['century', 'decade', 'year', 'month', 'day', 'hour',
'minute', 'second']
# 15 days in the month is a gross approximation, but this
# value is only used if rounding to the nearest month
granularity_size = {'century': 10, 'decade': 10, 'year': 10, 'month': 12,
'day': 15, 'hour': 24, 'minute': 60, 'second': 60 }
if granularity not in granularities:
raise ValueError("Please provide a valid granularity: %s" %
(granularities))
# Get everything into datetimes
if isinstance(from_time, int):
from_time = datetime.datetime.fromtimestamp(time.time()+from_time)
if isinstance(to_time, int):
to_time = datetime.datetime.fromtimestamp(time.time()+to_time)
# Ensure that the to_time is the larger
if from_time > to_time:
s = from_time
from_time = to_time
to_time = s
# Stop if the tiems are equal
elif from_time == to_time:
return "0 " + _pluralize_granularity(granularity)
# Collect up all the differences
deltas = {'century': 0, 'decade': 0, 'year': 0, 'month': 0, 'day': 0,
'hour': 0, 'minute': 0, 'second' : 0}
# Collect the easy deltas
for field in ['month', 'hour', 'day', 'minute', 'second']:
deltas[field] = getattr(to_time,field) - getattr(from_time,field)
# deal with year, century and decade
delta_year = to_time.year - from_time.year
if delta_year >= 100:
deltas['century'] = delta_year // 100
if delta_year % 100 >= 10:
deltas['decade'] = delta_year // 10 - deltas['century'] * 10
if delta_year % 10:
deltas['year'] = delta_year % 10
# Now we need to deal with the negative deltas, as we move from
# the smallest granularity to the largest when we encounter a negative
# we will 'borrow' from the next highest value. Because to_time is
# the larger of the two,
carry_over = [('second', 'minute', granularity_size['second']),
('minute', 'hour', granularity_size['minute']),
('hour', 'day', granularity_size['hour'])]
_process_carryover(deltas, carry_over)
# Day is its own special animal. We need to deal with negative days
# differently depending on what months we are spanning across. We need to
# look up the from_time.month value in order to bring the number of days
# to the end of the month.
month_carry = [None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if deltas['day'] < 0:
deltas['month'] -= 1
# Deal with leap years
if (from_time.month) == 2 and _is_leap_year(from_time.year):
deltas['day'] += 29
else:
deltas['day'] += month_carry[from_time.month]
carry_over = [('month', 'year', granularity_size['month']),
('year', 'decade', granularity_size['year']),
('decade', 'century', granularity_size['decade'])]
_process_carryover(deltas, carry_over)
# Display the differences we care about, at this point we should only have
# positive deltas
return_strings = []
for g in granularities:
delta = deltas[g]
# This is the finest granularity we will display
if g == granularity:
# We can only use rounding if the granularity is higher than
# seconds
if round and g != 'second':
i = granularities.index(g)
# Get the next finest granularity and it's delta
g_p = granularities[i + 1]
delta_p = deltas[g_p]
# Determine if we should round up
if delta_p > granularity_size[g_p] / 2:
delta += 1
if delta != 0:
return_strings.append(_delta_string(delta, g))
if not return_strings:
return "less than 1 " + granularity
break
else:
if delta != 0:
return_strings.append(_delta_string(delta, g))
# We're not rounding, check to see if we have encountered
# any deltas to display, if not our time difference
# is less than our finest granularity
if not return_strings:
return "less than 1 " + granularity
break
# Read the value and continue
else:
if delta != 0:
return_strings.append(_delta_string(delta, g))
if len(return_strings) == 1:
return return_strings[0]
return ", ".join(return_strings[:-1]) + " and " + return_strings[-1]
def time_ago_in_words(from_time, granularity="second", round=False):
"""
Return approximate-time-distance string for ``from_time`` till now.
Same as ``distance_of_time_in_words`` but the endpoint is now.
"""
return distance_of_time_in_words(from_time, datetime.datetime.now(),
granularity, round)
|