1from natural.language import _, _multi 2import datetime 3import math 4import six 5 6 7# Wed, 02 Oct 2002 08:00:00 EST 8# Wed, 02 Oct 2002 13:00:00 GMT 9# Wed, 02 Oct 2002 15:00:00 +0200 10RFC2822_DATETIME_FORMAT = r'%a, %d %b %Y %T %z' 11# Wed, 02 Oct 02 08:00:00 EST 12# Wed, 02 Oct 02 13:00:00 GMT 13# Wed, 02 Oct 02 15:00:00 +0200 14RFC822_DATETIME_FORMAT = r'%a, %d %b %y %T %z' 15# 2012-06-13T15:24:17 16ISO8601_DATETIME_FORMAT = r'%Y-%m-%dT%H:%M:%S' 17# Wed, 02 Oct 2002 18RFC2822_DATE_FORMAT = r'%a, %d %b %Y' 19# Wed, 02 Oct 02 20RFC822_DATE_FORMAT = r'%a, %d %b %y' 21# 2012-06-13 22ISO8601_DATE_FORMAT = r'%Y-%m-%d' 23# All date formats 24ALL_DATE_FORMATS = ( 25 RFC2822_DATE_FORMAT, 26 RFC822_DATE_FORMAT, 27 ISO8601_DATE_FORMAT, 28) 29ALL_DATETIME_FORMATS = ALL_DATE_FORMATS + ( 30 RFC822_DATETIME_FORMAT, 31 ISO8601_DATETIME_FORMAT, 32) 33 34# Precalculated timestamps 35TIME_MINUTE = 60 36TIME_HOUR = 3600 37TIME_DAY = 86400 38TIME_WEEK = 604800 39 40 41def _total_seconds(t): 42 ''' 43 Takes a `datetime.timedelta` object and returns the delta in seconds. 44 45 >>> _total_seconds(datetime.timedelta(23, 42, 123456)) 46 1987242 47 >>> _total_seconds(datetime.timedelta(23, 42, 654321)) 48 1987243 49 ''' 50 return sum([ 51 int(t.days * 86400 + t.seconds), 52 int(round(t.microseconds / 1000000.0)) 53 ]) 54 55 56def _to_datetime(t): 57 ''' 58 Internal function that tries whatever to convert ``t`` into a 59 :class:`datetime.datetime` object. 60 61 62 >>> _to_datetime('2013-12-11') 63 datetime.datetime(2013, 12, 11, 0, 0) 64 >>> _to_datetime('Wed, 11 Dec 2013') 65 datetime.datetime(2013, 12, 11, 0, 0) 66 >>> _to_datetime('Wed, 11 Dec 13') 67 datetime.datetime(2013, 12, 11, 0, 0) 68 >>> _to_datetime('2012-06-13T15:24:17') 69 datetime.datetime(2012, 6, 13, 15, 24, 17) 70 71 ''' 72 73 if isinstance(t, six.integer_types + (float, )): 74 return datetime.datetime.fromtimestamp(t).replace(microsecond=0) 75 76 elif isinstance(t, six.string_types): 77 for date_format in ALL_DATETIME_FORMATS: 78 try: 79 d = datetime.datetime.strptime(t, date_format) 80 return d.replace(microsecond=0) 81 except ValueError: 82 pass 83 84 raise ValueError(_('Format "%s" not supported') % t) 85 86 elif isinstance(t, datetime.datetime): 87 return t.replace(microsecond=0) 88 89 elif isinstance(t, datetime.date): 90 d = datetime.datetime.combine(t, datetime.time(0, 0)) 91 return d.replace(microsecond=0) 92 93 else: 94 raise TypeError 95 96 97def _to_date(t): 98 ''' 99 Internal function that tries whatever to convert ``t`` into a 100 :class:`datetime.date` object. 101 102 >>> _to_date('2013-12-11') 103 datetime.date(2013, 12, 11) 104 >>> _to_date('Wed, 11 Dec 2013') 105 datetime.date(2013, 12, 11) 106 >>> _to_date('Wed, 11 Dec 13') 107 datetime.date(2013, 12, 11) 108 ''' 109 110 if isinstance(t, six.integer_types + (float, )): 111 return datetime.date.fromtimestamp(t) 112 113 elif isinstance(t, six.string_types): 114 for date_format in ALL_DATE_FORMATS: 115 try: 116 return datetime.datetime.strptime(t, date_format).date() 117 except ValueError: 118 pass 119 120 raise ValueError('Format not supported') 121 122 elif isinstance(t, datetime.datetime): 123 return t.date() 124 125 elif isinstance(t, datetime.date): 126 return t 127 128 else: 129 raise TypeError 130 131 132def delta(t1, t2, words=True, justnow=datetime.timedelta(seconds=10)): 133 ''' 134 Calculates the estimated delta between two time objects in human-readable 135 format. Used internally by the :func:`day` and :func:`duration` functions. 136 137 :param t1: timestamp, :class:`datetime.date` or :class:`datetime.datetime` 138 object 139 :param t2: timestamp, :class:`datetime.date` or :class:`datetime.datetime` 140 object 141 :param words: default ``True``, allow words like "yesterday", "tomorrow" 142 and "just now" 143 :param justnow: default ``datetime.timedelta(seconds=10)``, 144 :class:`datetime.timedelta` object representing tolerance for 145 considering a delta as meaning 'just now' 146 147 >>> delta(_to_datetime('2012-06-13T15:24:17'), \ 148_to_datetime('2013-12-11T12:34:56')) 149 ('77 weeks', -594639) 150 ''' 151 152 t1 = _to_datetime(t1) 153 t2 = _to_datetime(t2) 154 diff = t1 - t2 155 date_diff = t1.date() - t2.date() 156 157 # The datetime module includes milliseconds with float precision. Floats 158 # will give unexpected results here, so we round the value here 159 total = math.ceil(_total_seconds(diff)) 160 total_abs = abs(total) 161 162 if total_abs < TIME_DAY: 163 if abs(diff) < justnow and words: 164 return ( 165 _('just now'), 166 0, 167 ) 168 169 elif total_abs < TIME_MINUTE: 170 seconds = total_abs 171 return ( 172 _multi( 173 _('%d second'), 174 _('%d seconds'), 175 seconds 176 ) % (seconds,), 177 0, 178 ) 179 elif total_abs < TIME_MINUTE * 2 and words: 180 return ( 181 _('a minute'), 182 0, 183 ) 184 185 elif total_abs < TIME_HOUR: 186 minutes, seconds = divmod(total_abs, TIME_MINUTE) 187 if total < 0: 188 seconds *= -1 189 return ( 190 _multi( 191 _('%d minute'), 192 _('%d minutes'), 193 minutes 194 ) % (minutes,), 195 seconds, 196 ) 197 198 elif total_abs < TIME_HOUR * 2 and words: 199 return ( 200 _('an hour'), 201 0, 202 ) 203 204 else: 205 hours, seconds = divmod(total_abs, TIME_HOUR) 206 if total < 0: 207 seconds *= -1 208 209 return ( 210 _multi( 211 _('%d hour'), 212 _('%d hours'), 213 hours 214 ) % (hours,), 215 seconds, 216 ) 217 218 elif date_diff.days == 1 and words: 219 return (_('tomorrow'), 0) 220 221 elif date_diff.days == -1 and words: 222 return (_('yesterday'), 0) 223 224 elif total_abs < TIME_WEEK: 225 days, seconds = divmod(total_abs, TIME_DAY) 226 if total < 0: 227 seconds *= -1 228 return ( 229 _multi( 230 _('%d day'), 231 _('%d days'), 232 days 233 ) % (days,), 234 seconds, 235 ) 236 237 elif abs(diff.days) == TIME_WEEK and words: 238 if total > 0: 239 return (_('next week'), diff.seconds) 240 else: 241 return (_('last week'), diff.seconds) 242 243# FIXME 244# 245# The biggest reliable unit we can supply to the user is a week (for now?), 246# because we can not safely determine the amount of days in the covered 247# month/year span. 248 249 else: 250 weeks, seconds = divmod(total_abs, TIME_WEEK) 251 if total < 0: 252 seconds *= -1 253 return ( 254 _multi( 255 _('%d week'), 256 _('%d weeks'), 257 weeks 258 ) % (weeks,), 259 seconds, 260 ) 261 262 263def day(t, now=None, format='%B %d'): 264 ''' 265 Date delta compared to ``t``. You can override ``now`` to specify what date 266 to compare to. 267 268 You can override the date format by supplying a ``format`` parameter. 269 270 :param t: timestamp, :class:`datetime.date` or :class:`datetime.datetime` 271 object 272 :param now: default ``None``, optionally a :class:`datetime.datetime` 273 object 274 :param format: default ``'%B %d'`` 275 276 >>> import time 277 >>> day(time.time()) 278 'today' 279 >>> day(time.time() - 86400) 280 'yesterday' 281 >>> day(time.time() - 604800) 282 'last week' 283 >>> day(time.time() + 86400) 284 'tomorrow' 285 >>> day(time.time() + 604800) 286 'next week' 287 ''' 288 t1 = _to_date(t) 289 t2 = _to_date(now or datetime.datetime.now()) 290 diff = t1 - t2 291 secs = _total_seconds(diff) 292 days = abs(diff.days) 293 294 if days == 0: 295 return _('today') 296 elif days == 1: 297 if secs < 0: 298 return _('yesterday') 299 else: 300 return _('tomorrow') 301 elif days == 7: 302 if secs < 0: 303 return _('last week') 304 else: 305 return _('next week') 306 else: 307 return t1.strftime(format) 308 309 310def duration(t, now=None, precision=1, pad=', ', words=None, 311 justnow=datetime.timedelta(seconds=10)): 312 ''' 313 Time delta compared to ``t``. You can override ``now`` to specify what time 314 to compare to. 315 316 :param t: timestamp, :class:`datetime.date` or :class:`datetime.datetime` 317 object 318 :param now: default ``None``, optionally a :class:`datetime.datetime` 319 object 320 :param precision: default ``1``, number of fragments to return 321 :param words: default ``None``, allow words like "yesterday", if set to 322 ``None`` this will be enabled if ``precision`` is set to 323 ``1`` 324 :param justnow: default ``datetime.timedelta(seconds=10)``, 325 :class:`datetime.timedelta` object passed to :func:`delta` 326 representing tolerance for considering argument ``t`` as 327 meaning 'just now' 328 329 >>> import time 330 >>> from datetime import datetime 331 >>> duration(time.time() + 1) 332 'just now' 333 >>> duration(time.time() + 11) 334 '11 seconds from now' 335 >>> duration(time.time() - 1) 336 'just now' 337 >>> duration(time.time() - 11) 338 '11 seconds ago' 339 >>> duration(time.time() - 3601) 340 'an hour ago' 341 >>> duration(time.time() - 7201) 342 '2 hours ago' 343 >>> duration(time.time() - 1234567) 344 '2 weeks ago' 345 >>> duration(time.time() + 7200, precision=1) 346 '2 hours from now' 347 >>> duration(time.time() - 1234567, precision=3) 348 '2 weeks, 6 hours, 56 minutes ago' 349 >>> duration(datetime(2014, 9, 8), now=datetime(2014, 9, 9)) 350 'yesterday' 351 >>> duration(datetime(2014, 9, 7, 23), now=datetime(2014, 9, 9)) 352 '1 day ago' 353 >>> duration(datetime(2014, 9, 10), now=datetime(2014, 9, 9)) 354 'tomorrow' 355 >>> duration(datetime(2014, 9, 11, 1), now=datetime(2014, 9, 9, 23)) 356 '1 day from now' 357 ''' 358 359 if words is None: 360 words = precision == 1 361 362 t1 = _to_datetime(t) 363 t2 = _to_datetime(now or datetime.datetime.now()) 364 365 if t1 < t2: 366 format = _('%s ago') 367 else: 368 format = _('%s from now') 369 370 result, remains = delta(t1, t2, words=words, justnow=justnow) 371 if result in ( 372 _('just now'), 373 _('yesterday'), 374 _('tomorrow'), 375 _('last week'), 376 _('next week'), 377 ): 378 return result 379 380 elif precision > 1 and remains: 381 t3 = t2 - datetime.timedelta(seconds=remains) 382 return pad.join([ 383 result, 384 duration(t2, t3, precision - 1, pad, words=False), 385 ]) 386 387 else: 388 return format % (result,) 389 390 391def compress(t, sign=False, pad=''): 392 ''' 393 Convert the input to compressed format, works with a 394 :class:`datetime.timedelta` object or a number that represents the number 395 of seconds you want to compress. If you supply a timestamp or a 396 :class:`datetime.datetime` object, it will give the delta relative to the 397 current time. 398 399 You can enable showing a sign in front of the compressed format with the 400 ``sign`` parameter, the default is not to show signs. 401 402 Optionally, you can chose to pad the output. If you wish your values to be 403 separated by spaces, set ``pad`` to ``' '``. 404 405 :param t: seconds or :class:`datetime.timedelta` object 406 :param sign: default ``False`` 407 :param pad: default ``''`` 408 409 >>> compress(1) 410 '1s' 411 >>> compress(12) 412 '12s' 413 >>> compress(123) 414 '2m3s' 415 >>> compress(1234) 416 '20m34s' 417 >>> compress(12345) 418 '3h25m45s' 419 >>> compress(123456) 420 '1d10h17m36s' 421 422 ''' 423 424 if isinstance(t, datetime.timedelta): 425 seconds = t.seconds + (t.days * 86400) 426 elif isinstance(t, six.integer_types + (float, )): 427 return compress(datetime.timedelta(seconds=t), sign, pad) 428 else: 429 return compress(datetime.datetime.now() - _to_datetime(t), sign, pad) 430 431 parts = [] 432 if sign: 433 parts.append('-' if t.days < 0 else '+') 434 435 weeks, seconds = divmod(seconds, TIME_WEEK) 436 days, seconds = divmod(seconds, TIME_DAY) 437 hours, seconds = divmod(seconds, TIME_HOUR) 438 minutes, seconds = divmod(seconds, TIME_MINUTE) 439 440 if weeks: 441 parts.append(_('%dw') % (weeks,)) 442 if days: 443 parts.append(_('%dd') % (days,)) 444 if hours: 445 parts.append(_('%dh') % (hours,)) 446 if minutes: 447 parts.append(_('%dm') % (minutes,)) 448 if seconds: 449 parts.append(_('%ds') % (seconds,)) 450 451 return pad.join(parts) 452