Mercurial > babel > old > mirror
annotate 0.9.x/babel/dates.py @ 513:3a00f8293d47 stable
Fix bad check in format_time (closes #257), reported with patch and tests by jomae
author | fschwarz |
---|---|
date | Fri, 04 Mar 2011 22:34:14 +0000 |
parents | d05e5e1674dd |
children | 931424efdcf0 |
rev | line source |
---|---|
263 | 1 # -*- coding: utf-8 -*- |
2 # | |
3 # Copyright (C) 2007 Edgewall Software | |
4 # All rights reserved. | |
5 # | |
6 # This software is licensed as described in the file COPYING, which | |
7 # you should have received as part of this distribution. The terms | |
8 # are also available at http://babel.edgewall.org/wiki/License. | |
9 # | |
10 # This software consists of voluntary contributions made by many | |
11 # individuals. For the exact contribution history, see the revision | |
12 # history and logs, available at http://babel.edgewall.org/log/. | |
13 | |
14 """Locale dependent formatting and parsing of dates and times. | |
15 | |
16 The default locale for the functions in this module is determined by the | |
17 following environment variables, in that order: | |
18 | |
19 * ``LC_TIME``, | |
20 * ``LC_ALL``, and | |
21 * ``LANG`` | |
22 """ | |
23 | |
24 from datetime import date, datetime, time, timedelta, tzinfo | |
25 import re | |
26 | |
27 from babel.core import default_locale, get_global, Locale | |
28 from babel.util import UTC | |
29 | |
30 __all__ = ['format_date', 'format_datetime', 'format_time', | |
31 'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time'] | |
32 __docformat__ = 'restructuredtext en' | |
33 | |
34 LC_TIME = default_locale('LC_TIME') | |
35 | |
36 # Aliases for use in scopes where the modules are shadowed by local variables | |
37 date_ = date | |
38 datetime_ = datetime | |
39 time_ = time | |
40 | |
41 def get_period_names(locale=LC_TIME): | |
42 """Return the names for day periods (AM/PM) used by the locale. | |
43 | |
44 >>> get_period_names(locale='en_US')['am'] | |
45 u'AM' | |
46 | |
47 :param locale: the `Locale` object, or a locale string | |
48 :return: the dictionary of period names | |
49 :rtype: `dict` | |
50 """ | |
51 return Locale.parse(locale).periods | |
52 | |
53 def get_day_names(width='wide', context='format', locale=LC_TIME): | |
54 """Return the day names used by the locale for the specified format. | |
55 | |
56 >>> get_day_names('wide', locale='en_US')[1] | |
57 u'Tuesday' | |
58 >>> get_day_names('abbreviated', locale='es')[1] | |
59 u'mar' | |
60 >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] | |
61 u'D' | |
62 | |
63 :param width: the width to use, one of "wide", "abbreviated", or "narrow" | |
64 :param context: the context, either "format" or "stand-alone" | |
65 :param locale: the `Locale` object, or a locale string | |
66 :return: the dictionary of day names | |
67 :rtype: `dict` | |
68 """ | |
69 return Locale.parse(locale).days[context][width] | |
70 | |
71 def get_month_names(width='wide', context='format', locale=LC_TIME): | |
72 """Return the month names used by the locale for the specified format. | |
73 | |
74 >>> get_month_names('wide', locale='en_US')[1] | |
75 u'January' | |
76 >>> get_month_names('abbreviated', locale='es')[1] | |
77 u'ene' | |
78 >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] | |
79 u'J' | |
80 | |
81 :param width: the width to use, one of "wide", "abbreviated", or "narrow" | |
82 :param context: the context, either "format" or "stand-alone" | |
83 :param locale: the `Locale` object, or a locale string | |
84 :return: the dictionary of month names | |
85 :rtype: `dict` | |
86 """ | |
87 return Locale.parse(locale).months[context][width] | |
88 | |
89 def get_quarter_names(width='wide', context='format', locale=LC_TIME): | |
90 """Return the quarter names used by the locale for the specified format. | |
91 | |
92 >>> get_quarter_names('wide', locale='en_US')[1] | |
93 u'1st quarter' | |
94 >>> get_quarter_names('abbreviated', locale='de_DE')[1] | |
95 u'Q1' | |
96 | |
97 :param width: the width to use, one of "wide", "abbreviated", or "narrow" | |
98 :param context: the context, either "format" or "stand-alone" | |
99 :param locale: the `Locale` object, or a locale string | |
100 :return: the dictionary of quarter names | |
101 :rtype: `dict` | |
102 """ | |
103 return Locale.parse(locale).quarters[context][width] | |
104 | |
105 def get_era_names(width='wide', locale=LC_TIME): | |
106 """Return the era names used by the locale for the specified format. | |
107 | |
108 >>> get_era_names('wide', locale='en_US')[1] | |
109 u'Anno Domini' | |
110 >>> get_era_names('abbreviated', locale='de_DE')[1] | |
111 u'n. Chr.' | |
112 | |
113 :param width: the width to use, either "wide", "abbreviated", or "narrow" | |
114 :param locale: the `Locale` object, or a locale string | |
115 :return: the dictionary of era names | |
116 :rtype: `dict` | |
117 """ | |
118 return Locale.parse(locale).eras[width] | |
119 | |
120 def get_date_format(format='medium', locale=LC_TIME): | |
121 """Return the date formatting patterns used by the locale for the specified | |
122 format. | |
123 | |
124 >>> get_date_format(locale='en_US') | |
125 <DateTimePattern u'MMM d, yyyy'> | |
126 >>> get_date_format('full', locale='de_DE') | |
127 <DateTimePattern u'EEEE, d. MMMM yyyy'> | |
128 | |
129 :param format: the format to use, one of "full", "long", "medium", or | |
130 "short" | |
131 :param locale: the `Locale` object, or a locale string | |
132 :return: the date format pattern | |
133 :rtype: `DateTimePattern` | |
134 """ | |
135 return Locale.parse(locale).date_formats[format] | |
136 | |
137 def get_datetime_format(format='medium', locale=LC_TIME): | |
138 """Return the datetime formatting patterns used by the locale for the | |
139 specified format. | |
140 | |
141 >>> get_datetime_format(locale='en_US') | |
142 u'{1} {0}' | |
143 | |
144 :param format: the format to use, one of "full", "long", "medium", or | |
145 "short" | |
146 :param locale: the `Locale` object, or a locale string | |
147 :return: the datetime format pattern | |
148 :rtype: `unicode` | |
149 """ | |
150 patterns = Locale.parse(locale).datetime_formats | |
151 if format not in patterns: | |
152 format = None | |
153 return patterns[format] | |
154 | |
155 def get_time_format(format='medium', locale=LC_TIME): | |
156 """Return the time formatting patterns used by the locale for the specified | |
157 format. | |
158 | |
159 >>> get_time_format(locale='en_US') | |
160 <DateTimePattern u'h:mm:ss a'> | |
161 >>> get_time_format('full', locale='de_DE') | |
162 <DateTimePattern u'HH:mm:ss v'> | |
163 | |
164 :param format: the format to use, one of "full", "long", "medium", or | |
165 "short" | |
166 :param locale: the `Locale` object, or a locale string | |
167 :return: the time format pattern | |
168 :rtype: `DateTimePattern` | |
169 """ | |
170 return Locale.parse(locale).time_formats[format] | |
171 | |
172 def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME): | |
173 """Return the timezone associated with the given `datetime` object formatted | |
174 as string indicating the offset from GMT. | |
175 | |
176 >>> dt = datetime(2007, 4, 1, 15, 30) | |
177 >>> get_timezone_gmt(dt, locale='en') | |
178 u'GMT+00:00' | |
179 | |
180 >>> from pytz import timezone | |
181 >>> tz = timezone('America/Los_Angeles') | |
182 >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) | |
183 >>> get_timezone_gmt(dt, locale='en') | |
184 u'GMT-08:00' | |
185 >>> get_timezone_gmt(dt, 'short', locale='en') | |
186 u'-0800' | |
187 | |
381 | 188 The long format depends on the locale, for example in France the acronym |
189 UTC string is used instead of GMT: | |
263 | 190 |
191 >>> get_timezone_gmt(dt, 'long', locale='fr_FR') | |
381 | 192 u'UTC-08:00' |
263 | 193 |
194 :param datetime: the ``datetime`` object; if `None`, the current date and | |
351 | 195 time in UTC is used |
263 | 196 :param width: either "long" or "short" |
197 :param locale: the `Locale` object, or a locale string | |
198 :return: the GMT offset representation of the timezone | |
199 :rtype: `unicode` | |
200 :since: version 0.9 | |
201 """ | |
202 if datetime is None: | |
351 | 203 datetime = datetime_.utcnow() |
263 | 204 elif isinstance(datetime, (int, long)): |
351 | 205 datetime = datetime_.utcfromtimestamp(datetime).time() |
263 | 206 if datetime.tzinfo is None: |
207 datetime = datetime.replace(tzinfo=UTC) | |
208 locale = Locale.parse(locale) | |
209 | |
458 | 210 offset = datetime.tzinfo.utcoffset(datetime) |
263 | 211 seconds = offset.days * 24 * 60 * 60 + offset.seconds |
212 hours, seconds = divmod(seconds, 3600) | |
213 if width == 'short': | |
214 pattern = u'%+03d%02d' | |
215 else: | |
216 pattern = locale.zone_formats['gmt'] % '%+03d:%02d' | |
217 return pattern % (hours, seconds // 60) | |
218 | |
219 def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME): | |
220 """Return a representation of the given timezone using "location format". | |
221 | |
222 The result depends on both the local display name of the country and the | |
223 city assocaited with the time zone: | |
224 | |
225 >>> from pytz import timezone | |
226 >>> tz = timezone('America/St_Johns') | |
227 >>> get_timezone_location(tz, locale='de_DE') | |
228 u"Kanada (St. John's)" | |
229 >>> tz = timezone('America/Mexico_City') | |
230 >>> get_timezone_location(tz, locale='de_DE') | |
231 u'Mexiko (Mexiko-Stadt)' | |
232 | |
233 If the timezone is associated with a country that uses only a single | |
234 timezone, just the localized country name is returned: | |
235 | |
236 >>> tz = timezone('Europe/Berlin') | |
237 >>> get_timezone_name(tz, locale='de_DE') | |
238 u'Deutschland' | |
239 | |
240 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines | |
241 the timezone; if `None`, the current date and time in | |
242 UTC is assumed | |
243 :param locale: the `Locale` object, or a locale string | |
244 :return: the localized timezone name using location format | |
245 :rtype: `unicode` | |
246 :since: version 0.9 | |
247 """ | |
248 if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): | |
249 dt = None | |
250 tzinfo = UTC | |
251 elif isinstance(dt_or_tzinfo, (datetime, time)): | |
252 dt = dt_or_tzinfo | |
253 if dt.tzinfo is not None: | |
254 tzinfo = dt.tzinfo | |
255 else: | |
256 tzinfo = UTC | |
257 else: | |
258 dt = None | |
259 tzinfo = dt_or_tzinfo | |
260 locale = Locale.parse(locale) | |
261 | |
262 if hasattr(tzinfo, 'zone'): | |
263 zone = tzinfo.zone | |
264 else: | |
265 zone = tzinfo.tzname(dt or datetime.utcnow()) | |
266 | |
267 # Get the canonical time-zone code | |
268 zone = get_global('zone_aliases').get(zone, zone) | |
269 | |
270 info = locale.time_zones.get(zone, {}) | |
271 | |
272 # Otherwise, if there is only one timezone for the country, return the | |
273 # localized country name | |
274 region_format = locale.zone_formats['region'] | |
275 territory = get_global('zone_territories').get(zone) | |
276 if territory not in locale.territories: | |
277 territory = 'ZZ' # invalid/unknown | |
278 territory_name = locale.territories[territory] | |
279 if territory and len(get_global('territory_zones').get(territory, [])) == 1: | |
280 return region_format % (territory_name) | |
281 | |
282 # Otherwise, include the city in the output | |
283 fallback_format = locale.zone_formats['fallback'] | |
284 if 'city' in info: | |
285 city_name = info['city'] | |
286 else: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
287 metazone = get_global('meta_zones').get(zone) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
288 metazone_info = locale.meta_zones.get(metazone, {}) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
289 if 'city' in metazone_info: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
290 city_name = metainfo['city'] |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
291 elif '/' in zone: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
292 city_name = zone.split('/', 1)[1].replace('_', ' ') |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
293 else: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
294 city_name = zone.replace('_', ' ') |
263 | 295 |
296 return region_format % (fallback_format % { | |
297 '0': city_name, | |
298 '1': territory_name | |
299 }) | |
300 | |
301 def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, | |
302 locale=LC_TIME): | |
303 r"""Return the localized display name for the given timezone. The timezone | |
304 may be specified using a ``datetime`` or `tzinfo` object. | |
305 | |
306 >>> from pytz import timezone | |
307 >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles')) | |
308 >>> get_timezone_name(dt, locale='en_US') | |
309 u'Pacific Standard Time' | |
310 >>> get_timezone_name(dt, width='short', locale='en_US') | |
311 u'PST' | |
312 | |
313 If this function gets passed only a `tzinfo` object and no concrete | |
314 `datetime`, the returned display name is indenpendent of daylight savings | |
315 time. This can be used for example for selecting timezones, or to set the | |
316 time of events that recur across DST changes: | |
317 | |
318 >>> tz = timezone('America/Los_Angeles') | |
319 >>> get_timezone_name(tz, locale='en_US') | |
320 u'Pacific Time' | |
321 >>> get_timezone_name(tz, 'short', locale='en_US') | |
322 u'PT' | |
323 | |
324 If no localized display name for the timezone is available, and the timezone | |
325 is associated with a country that uses only a single timezone, the name of | |
326 that country is returned, formatted according to the locale: | |
327 | |
328 >>> tz = timezone('Europe/Berlin') | |
329 >>> get_timezone_name(tz, locale='de_DE') | |
330 u'Deutschland' | |
331 >>> get_timezone_name(tz, locale='pt_BR') | |
332 u'Hor\xe1rio Alemanha' | |
333 | |
334 On the other hand, if the country uses multiple timezones, the city is also | |
335 included in the representation: | |
336 | |
337 >>> tz = timezone('America/St_Johns') | |
338 >>> get_timezone_name(tz, locale='de_DE') | |
339 u"Kanada (St. John's)" | |
340 | |
341 The `uncommon` parameter can be set to `True` to enable the use of timezone | |
342 representations that are not commonly used by the requested locale. For | |
466 | 343 example, while in French the central European timezone is usually |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
344 abbreviated as "HEC", in Canadian French, this abbreviation is not in |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
345 common use, so a generic name would be chosen by default: |
263 | 346 |
347 >>> tz = timezone('Europe/Paris') | |
348 >>> get_timezone_name(tz, 'short', locale='fr_CA') | |
349 u'France' | |
350 >>> get_timezone_name(tz, 'short', uncommon=True, locale='fr_CA') | |
351 u'HEC' | |
352 | |
353 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines | |
354 the timezone; if a ``tzinfo`` object is used, the | |
355 resulting display name will be generic, i.e. | |
356 independent of daylight savings time; if `None`, the | |
357 current date in UTC is assumed | |
358 :param width: either "long" or "short" | |
359 :param uncommon: whether even uncommon timezone abbreviations should be used | |
360 :param locale: the `Locale` object, or a locale string | |
361 :return: the timezone display name | |
362 :rtype: `unicode` | |
363 :since: version 0.9 | |
364 :see: `LDML Appendix J: Time Zone Display Names | |
365 <http://www.unicode.org/reports/tr35/#Time_Zone_Fallback>`_ | |
366 """ | |
367 if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): | |
368 dt = None | |
369 tzinfo = UTC | |
370 elif isinstance(dt_or_tzinfo, (datetime, time)): | |
371 dt = dt_or_tzinfo | |
372 if dt.tzinfo is not None: | |
373 tzinfo = dt.tzinfo | |
374 else: | |
375 tzinfo = UTC | |
376 else: | |
377 dt = None | |
378 tzinfo = dt_or_tzinfo | |
379 locale = Locale.parse(locale) | |
380 | |
381 if hasattr(tzinfo, 'zone'): | |
382 zone = tzinfo.zone | |
383 else: | |
351 | 384 zone = tzinfo.tzname(dt) |
263 | 385 |
386 # Get the canonical time-zone code | |
387 zone = get_global('zone_aliases').get(zone, zone) | |
388 | |
389 info = locale.time_zones.get(zone, {}) | |
390 # Try explicitly translated zone names first | |
391 if width in info: | |
392 if dt is None: | |
393 field = 'generic' | |
394 else: | |
351 | 395 dst = tzinfo.dst(dt) |
396 if dst is None: | |
397 field = 'generic' | |
398 elif dst == 0: | |
399 field = 'standard' | |
400 else: | |
401 field = 'daylight' | |
263 | 402 if field in info[width]: |
403 return info[width][field] | |
404 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
405 metazone = get_global('meta_zones').get(zone) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
406 if metazone: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
407 metazone_info = locale.meta_zones.get(metazone, {}) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
408 if width in metazone_info and (uncommon or metazone_info.get('common')): |
263 | 409 if dt is None: |
410 field = 'generic' | |
411 else: | |
412 field = tzinfo.dst(dt) and 'daylight' or 'standard' | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
413 if field in metazone_info[width]: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
414 return metazone_info[width][field] |
263 | 415 |
416 # If we have a concrete datetime, we assume that the result can't be | |
417 # independent of daylight savings time, so we return the GMT offset | |
418 if dt is not None: | |
419 return get_timezone_gmt(dt, width=width, locale=locale) | |
420 | |
421 return get_timezone_location(dt_or_tzinfo, locale=locale) | |
422 | |
423 def format_date(date=None, format='medium', locale=LC_TIME): | |
424 """Return a date formatted according to the given pattern. | |
425 | |
426 >>> d = date(2007, 04, 01) | |
427 >>> format_date(d, locale='en_US') | |
428 u'Apr 1, 2007' | |
429 >>> format_date(d, format='full', locale='de_DE') | |
430 u'Sonntag, 1. April 2007' | |
431 | |
432 If you don't want to use the locale default formats, you can specify a | |
433 custom date pattern: | |
434 | |
435 >>> format_date(d, "EEE, MMM d, ''yy", locale='en') | |
436 u"Sun, Apr 1, '07" | |
437 | |
438 :param date: the ``date`` or ``datetime`` object; if `None`, the current | |
439 date is used | |
440 :param format: one of "full", "long", "medium", or "short", or a custom | |
441 date/time pattern | |
442 :param locale: a `Locale` object or a locale identifier | |
443 :rtype: `unicode` | |
444 | |
445 :note: If the pattern contains time fields, an `AttributeError` will be | |
446 raised when trying to apply the formatting. This is also true if | |
447 the value of ``date`` parameter is actually a ``datetime`` object, | |
448 as this function automatically converts that to a ``date``. | |
449 """ | |
450 if date is None: | |
451 date = date_.today() | |
452 elif isinstance(date, datetime): | |
453 date = date.date() | |
454 | |
455 locale = Locale.parse(locale) | |
456 if format in ('full', 'long', 'medium', 'short'): | |
457 format = get_date_format(format, locale=locale) | |
458 pattern = parse_pattern(format) | |
505 | 459 return pattern.apply(date, locale) |
263 | 460 |
461 def format_datetime(datetime=None, format='medium', tzinfo=None, | |
462 locale=LC_TIME): | |
463 """Return a date formatted according to the given pattern. | |
464 | |
465 >>> dt = datetime(2007, 04, 01, 15, 30) | |
466 >>> format_datetime(dt, locale='en_US') | |
467 u'Apr 1, 2007 3:30:00 PM' | |
468 | |
469 For any pattern requiring the display of the time-zone, the third-party | |
470 ``pytz`` package is needed to explicitly specify the time-zone: | |
471 | |
472 >>> from pytz import timezone | |
473 >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'), | |
474 ... locale='fr_FR') | |
475 u'dimanche 1 avril 2007 17:30:00 HEC' | |
476 >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", | |
477 ... tzinfo=timezone('US/Eastern'), locale='en') | |
478 u'2007.04.01 AD at 11:30:00 EDT' | |
479 | |
480 :param datetime: the `datetime` object; if `None`, the current date and | |
481 time is used | |
482 :param format: one of "full", "long", "medium", or "short", or a custom | |
483 date/time pattern | |
484 :param tzinfo: the timezone to apply to the time for display | |
485 :param locale: a `Locale` object or a locale identifier | |
486 :rtype: `unicode` | |
487 """ | |
488 if datetime is None: | |
351 | 489 datetime = datetime_.utcnow() |
263 | 490 elif isinstance(datetime, (int, long)): |
381 | 491 datetime = datetime_.utcfromtimestamp(datetime) |
263 | 492 elif isinstance(datetime, time): |
493 datetime = datetime_.combine(date.today(), datetime) | |
494 if datetime.tzinfo is None: | |
495 datetime = datetime.replace(tzinfo=UTC) | |
496 if tzinfo is not None: | |
497 datetime = datetime.astimezone(tzinfo) | |
498 if hasattr(tzinfo, 'normalize'): # pytz | |
499 datetime = tzinfo.normalize(datetime) | |
500 | |
501 locale = Locale.parse(locale) | |
502 if format in ('full', 'long', 'medium', 'short'): | |
503 return get_datetime_format(format, locale=locale) \ | |
504 .replace('{0}', format_time(datetime, format, tzinfo=None, | |
505 locale=locale)) \ | |
506 .replace('{1}', format_date(datetime, format, locale=locale)) | |
507 else: | |
508 return parse_pattern(format).apply(datetime, locale) | |
509 | |
510 def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME): | |
511 """Return a time formatted according to the given pattern. | |
512 | |
513 >>> t = time(15, 30) | |
514 >>> format_time(t, locale='en_US') | |
515 u'3:30:00 PM' | |
516 >>> format_time(t, format='short', locale='de_DE') | |
517 u'15:30' | |
518 | |
519 If you don't want to use the locale default formats, you can specify a | |
520 custom time pattern: | |
521 | |
522 >>> format_time(t, "hh 'o''clock' a", locale='en') | |
523 u"03 o'clock PM" | |
524 | |
525 For any pattern requiring the display of the time-zone, the third-party | |
526 ``pytz`` package is needed to explicitly specify the time-zone: | |
527 | |
528 >>> from pytz import timezone | |
351 | 529 >>> t = datetime(2007, 4, 1, 15, 30) |
530 >>> tzinfo = timezone('Europe/Paris') | |
531 >>> t = tzinfo.localize(t) | |
532 >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') | |
533 u'15:30:00 HEC' | |
263 | 534 >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'), |
535 ... locale='en') | |
351 | 536 u"09 o'clock AM, Eastern Daylight Time" |
537 | |
538 As that example shows, when this function gets passed a | |
539 ``datetime.datetime`` value, the actual time in the formatted string is | |
540 adjusted to the timezone specified by the `tzinfo` parameter. If the | |
541 ``datetime`` is "naive" (i.e. it has no associated timezone information), | |
542 it is assumed to be in UTC. | |
543 | |
544 These timezone calculations are **not** performed if the value is of type | |
545 ``datetime.time``, as without date information there's no way to determine | |
546 what a given time would translate to in a different timezone without | |
547 information about whether daylight savings time is in effect or not. This | |
548 means that time values are left as-is, and the value of the `tzinfo` | |
549 parameter is only used to display the timezone name if needed: | |
550 | |
551 >>> t = time(15, 30) | |
552 >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'), | |
553 ... locale='fr_FR') | |
554 u'15:30:00 HEC' | |
555 >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'), | |
556 ... locale='en_US') | |
557 u'3:30:00 PM ET' | |
263 | 558 |
559 :param time: the ``time`` or ``datetime`` object; if `None`, the current | |
351 | 560 time in UTC is used |
263 | 561 :param format: one of "full", "long", "medium", or "short", or a custom |
562 date/time pattern | |
563 :param tzinfo: the time-zone to apply to the time for display | |
564 :param locale: a `Locale` object or a locale identifier | |
565 :rtype: `unicode` | |
566 | |
567 :note: If the pattern contains date fields, an `AttributeError` will be | |
568 raised when trying to apply the formatting. This is also true if | |
569 the value of ``time`` parameter is actually a ``datetime`` object, | |
570 as this function automatically converts that to a ``time``. | |
571 """ | |
572 if time is None: | |
351 | 573 time = datetime.utcnow() |
263 | 574 elif isinstance(time, (int, long)): |
351 | 575 time = datetime.utcfromtimestamp(time) |
263 | 576 if time.tzinfo is None: |
577 time = time.replace(tzinfo=UTC) | |
351 | 578 if isinstance(time, datetime): |
579 if tzinfo is not None: | |
580 time = time.astimezone(tzinfo) | |
513
3a00f8293d47
Fix bad check in format_time (closes #257), reported with patch and tests by jomae
fschwarz
parents:
505
diff
changeset
|
581 if hasattr(tzinfo, 'normalize'): # pytz |
351 | 582 time = tzinfo.normalize(time) |
583 time = time.timetz() | |
584 elif tzinfo is not None: | |
585 time = time.replace(tzinfo=tzinfo) | |
263 | 586 |
587 locale = Locale.parse(locale) | |
588 if format in ('full', 'long', 'medium', 'short'): | |
589 format = get_time_format(format, locale=locale) | |
590 return parse_pattern(format).apply(time, locale) | |
591 | |
592 def parse_date(string, locale=LC_TIME): | |
593 """Parse a date from a string. | |
594 | |
595 This function uses the date format for the locale as a hint to determine | |
596 the order in which the date fields appear in the string. | |
597 | |
598 >>> parse_date('4/1/04', locale='en_US') | |
599 datetime.date(2004, 4, 1) | |
600 >>> parse_date('01.04.2004', locale='de_DE') | |
601 datetime.date(2004, 4, 1) | |
602 | |
603 :param string: the string containing the date | |
604 :param locale: a `Locale` object or a locale identifier | |
605 :return: the parsed date | |
606 :rtype: `date` | |
607 """ | |
608 # TODO: try ISO format first? | |
609 format = get_date_format(locale=locale).pattern.lower() | |
610 year_idx = format.index('y') | |
611 month_idx = format.index('m') | |
612 if month_idx < 0: | |
613 month_idx = format.index('l') | |
614 day_idx = format.index('d') | |
615 | |
616 indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')] | |
617 indexes.sort() | |
618 indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) | |
619 | |
620 # FIXME: this currently only supports numbers, but should also support month | |
621 # names, both in the requested locale, and english | |
622 | |
623 numbers = re.findall('(\d+)', string) | |
624 year = numbers[indexes['Y']] | |
625 if len(year) == 2: | |
626 year = 2000 + int(year) | |
627 else: | |
628 year = int(year) | |
629 month = int(numbers[indexes['M']]) | |
630 day = int(numbers[indexes['D']]) | |
631 if month > 12: | |
632 month, day = day, month | |
633 return date(year, month, day) | |
634 | |
635 def parse_datetime(string, locale=LC_TIME): | |
636 """Parse a date and time from a string. | |
637 | |
638 This function uses the date and time formats for the locale as a hint to | |
639 determine the order in which the time fields appear in the string. | |
640 | |
641 :param string: the string containing the date and time | |
642 :param locale: a `Locale` object or a locale identifier | |
643 :return: the parsed date/time | |
644 :rtype: `datetime` | |
645 """ | |
646 raise NotImplementedError | |
647 | |
648 def parse_time(string, locale=LC_TIME): | |
649 """Parse a time from a string. | |
650 | |
651 This function uses the time format for the locale as a hint to determine | |
652 the order in which the time fields appear in the string. | |
653 | |
654 >>> parse_time('15:30:00', locale='en_US') | |
655 datetime.time(15, 30) | |
656 | |
657 :param string: the string containing the time | |
658 :param locale: a `Locale` object or a locale identifier | |
659 :return: the parsed time | |
660 :rtype: `time` | |
661 """ | |
662 # TODO: try ISO format first? | |
663 format = get_time_format(locale=locale).pattern.lower() | |
664 hour_idx = format.index('h') | |
665 if hour_idx < 0: | |
666 hour_idx = format.index('k') | |
667 min_idx = format.index('m') | |
668 sec_idx = format.index('s') | |
669 | |
670 indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')] | |
671 indexes.sort() | |
672 indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) | |
673 | |
674 # FIXME: support 12 hour clock, and 0-based hour specification | |
675 # and seconds should be optional, maybe minutes too | |
676 # oh, and time-zones, of course | |
677 | |
678 numbers = re.findall('(\d+)', string) | |
679 hour = int(numbers[indexes['H']]) | |
680 minute = int(numbers[indexes['M']]) | |
681 second = int(numbers[indexes['S']]) | |
682 return time(hour, minute, second) | |
683 | |
684 | |
685 class DateTimePattern(object): | |
686 | |
687 def __init__(self, pattern, format): | |
688 self.pattern = pattern | |
689 self.format = format | |
690 | |
691 def __repr__(self): | |
692 return '<%s %r>' % (type(self).__name__, self.pattern) | |
693 | |
694 def __unicode__(self): | |
695 return self.pattern | |
696 | |
697 def __mod__(self, other): | |
698 assert type(other) is DateTimeFormat | |
699 return self.format % other | |
700 | |
701 def apply(self, datetime, locale): | |
702 return self % DateTimeFormat(datetime, locale) | |
703 | |
704 | |
705 class DateTimeFormat(object): | |
706 | |
707 def __init__(self, value, locale): | |
708 assert isinstance(value, (date, datetime, time)) | |
709 if isinstance(value, (datetime, time)) and value.tzinfo is None: | |
710 value = value.replace(tzinfo=UTC) | |
711 self.value = value | |
712 self.locale = Locale.parse(locale) | |
713 | |
714 def __getitem__(self, name): | |
715 char = name[0] | |
716 num = len(name) | |
717 if char == 'G': | |
718 return self.format_era(char, num) | |
719 elif char in ('y', 'Y', 'u'): | |
720 return self.format_year(char, num) | |
721 elif char in ('Q', 'q'): | |
722 return self.format_quarter(char, num) | |
723 elif char in ('M', 'L'): | |
724 return self.format_month(char, num) | |
725 elif char in ('w', 'W'): | |
726 return self.format_week(char, num) | |
727 elif char == 'd': | |
728 return self.format(self.value.day, num) | |
729 elif char == 'D': | |
730 return self.format_day_of_year(num) | |
731 elif char == 'F': | |
732 return self.format_day_of_week_in_month() | |
733 elif char in ('E', 'e', 'c'): | |
734 return self.format_weekday(char, num) | |
735 elif char == 'a': | |
736 return self.format_period(char) | |
737 elif char == 'h': | |
278 | 738 if self.value.hour % 12 == 0: |
739 return self.format(12, num) | |
740 else: | |
741 return self.format(self.value.hour % 12, num) | |
263 | 742 elif char == 'H': |
743 return self.format(self.value.hour, num) | |
744 elif char == 'K': | |
278 | 745 return self.format(self.value.hour % 12, num) |
263 | 746 elif char == 'k': |
278 | 747 if self.value.hour == 0: |
748 return self.format(24, num) | |
749 else: | |
750 return self.format(self.value.hour, num) | |
263 | 751 elif char == 'm': |
752 return self.format(self.value.minute, num) | |
753 elif char == 's': | |
754 return self.format(self.value.second, num) | |
755 elif char == 'S': | |
756 return self.format_frac_seconds(num) | |
757 elif char == 'A': | |
758 return self.format_milliseconds_in_day(num) | |
759 elif char in ('z', 'Z', 'v', 'V'): | |
760 return self.format_timezone(char, num) | |
761 else: | |
762 raise KeyError('Unsupported date/time field %r' % char) | |
763 | |
764 def format_era(self, char, num): | |
765 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] | |
766 era = int(self.value.year >= 0) | |
767 return get_era_names(width, self.locale)[era] | |
768 | |
769 def format_year(self, char, num): | |
770 value = self.value.year | |
771 if char.isupper(): | |
772 week = self.get_week_number(self.get_day_of_year()) | |
773 if week == 0: | |
774 value -= 1 | |
775 year = self.format(value, num) | |
776 if num == 2: | |
777 year = year[-2:] | |
778 return year | |
779 | |
399 | 780 def format_quarter(self, char, num): |
781 quarter = (self.value.month - 1) // 3 + 1 | |
782 if num <= 2: | |
783 return ('%%0%dd' % num) % quarter | |
784 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] | |
785 context = {'Q': 'format', 'q': 'stand-alone'}[char] | |
786 return get_quarter_names(width, context, self.locale)[quarter] | |
787 | |
263 | 788 def format_month(self, char, num): |
789 if num <= 2: | |
790 return ('%%0%dd' % num) % self.value.month | |
791 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
278
diff
changeset
|
792 context = {'M': 'format', 'L': 'stand-alone'}[char] |
263 | 793 return get_month_names(width, context, self.locale)[self.value.month] |
794 | |
795 def format_week(self, char, num): | |
796 if char.islower(): # week of year | |
797 day_of_year = self.get_day_of_year() | |
798 week = self.get_week_number(day_of_year) | |
799 if week == 0: | |
800 date = self.value - timedelta(days=day_of_year) | |
801 week = self.get_week_number(self.get_day_of_year(date), | |
802 date.weekday()) | |
803 return self.format(week, num) | |
804 else: # week of month | |
805 week = self.get_week_number(self.value.day) | |
806 if week == 0: | |
807 date = self.value - timedelta(days=self.value.day) | |
808 week = self.get_week_number(date.day, date.weekday()) | |
809 pass | |
810 return '%d' % week | |
811 | |
812 def format_weekday(self, char, num): | |
813 if num < 3: | |
814 if char.islower(): | |
815 value = 7 - self.locale.first_week_day + self.value.weekday() | |
816 return self.format(value % 7 + 1, num) | |
817 num = 3 | |
818 weekday = self.value.weekday() | |
819 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] | |
820 context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] | |
821 return get_day_names(width, context, self.locale)[weekday] | |
822 | |
823 def format_day_of_year(self, num): | |
824 return self.format(self.get_day_of_year(), num) | |
825 | |
826 def format_day_of_week_in_month(self): | |
827 return '%d' % ((self.value.day - 1) / 7 + 1) | |
828 | |
829 def format_period(self, char): | |
278 | 830 period = {0: 'am', 1: 'pm'}[int(self.value.hour >= 12)] |
263 | 831 return get_period_names(locale=self.locale)[period] |
832 | |
833 def format_frac_seconds(self, num): | |
834 value = str(self.value.microsecond) | |
835 return self.format(round(float('.%s' % value), num) * 10**num, num) | |
836 | |
837 def format_milliseconds_in_day(self, num): | |
838 msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ | |
839 self.value.minute * 60000 + self.value.hour * 3600000 | |
840 return self.format(msecs, num) | |
841 | |
842 def format_timezone(self, char, num): | |
843 width = {3: 'short', 4: 'long'}[max(3, num)] | |
844 if char == 'z': | |
845 return get_timezone_name(self.value, width, locale=self.locale) | |
846 elif char == 'Z': | |
847 return get_timezone_gmt(self.value, width, locale=self.locale) | |
848 elif char == 'v': | |
849 return get_timezone_name(self.value.tzinfo, width, | |
850 locale=self.locale) | |
851 elif char == 'V': | |
852 if num == 1: | |
853 return get_timezone_name(self.value.tzinfo, width, | |
854 uncommon=True, locale=self.locale) | |
855 return get_timezone_location(self.value.tzinfo, locale=self.locale) | |
856 | |
857 def format(self, value, length): | |
858 return ('%%0%dd' % length) % value | |
859 | |
860 def get_day_of_year(self, date=None): | |
861 if date is None: | |
862 date = self.value | |
863 return (date - date_(date.year, 1, 1)).days + 1 | |
864 | |
865 def get_week_number(self, day_of_period, day_of_week=None): | |
866 """Return the number of the week of a day within a period. This may be | |
867 the week number in a year or the week number in a month. | |
868 | |
869 Usually this will return a value equal to or greater than 1, but if the | |
870 first week of the period is so short that it actually counts as the last | |
871 week of the previous period, this function will return 0. | |
872 | |
873 >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) | |
874 >>> format.get_week_number(6) | |
875 1 | |
876 | |
877 >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) | |
878 >>> format.get_week_number(6) | |
879 2 | |
880 | |
881 :param day_of_period: the number of the day in the period (usually | |
882 either the day of month or the day of year) | |
883 :param day_of_week: the week day; if ommitted, the week day of the | |
884 current date is assumed | |
885 """ | |
886 if day_of_week is None: | |
887 day_of_week = self.value.weekday() | |
888 first_day = (day_of_week - self.locale.first_week_day - | |
889 day_of_period + 1) % 7 | |
890 if first_day < 0: | |
891 first_day += 7 | |
892 week_number = (day_of_period + first_day - 1) / 7 | |
893 if 7 - first_day >= self.locale.min_week_days: | |
894 week_number += 1 | |
895 return week_number | |
896 | |
897 | |
898 PATTERN_CHARS = { | |
899 'G': [1, 2, 3, 4, 5], # era | |
900 'y': None, 'Y': None, 'u': None, # year | |
901 'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4], # quarter | |
902 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month | |
903 'w': [1, 2], 'W': [1], # week | |
904 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day | |
905 'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5], # week day | |
906 'a': [1], # period | |
907 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour | |
908 'm': [1, 2], # minute | |
909 's': [1, 2], 'S': None, 'A': None, # second | |
910 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 4] # zone | |
911 } | |
912 | |
913 def parse_pattern(pattern): | |
914 """Parse date, time, and datetime format patterns. | |
915 | |
916 >>> parse_pattern("MMMMd").format | |
917 u'%(MMMM)s%(d)s' | |
918 >>> parse_pattern("MMM d, yyyy").format | |
919 u'%(MMM)s %(d)s, %(yyyy)s' | |
920 | |
921 Pattern can contain literal strings in single quotes: | |
922 | |
923 >>> parse_pattern("H:mm' Uhr 'z").format | |
924 u'%(H)s:%(mm)s Uhr %(z)s' | |
925 | |
926 An actual single quote can be used by using two adjacent single quote | |
927 characters: | |
928 | |
929 >>> parse_pattern("hh' o''clock'").format | |
930 u"%(hh)s o'clock" | |
931 | |
932 :param pattern: the formatting pattern to parse | |
933 """ | |
934 if type(pattern) is DateTimePattern: | |
935 return pattern | |
936 | |
937 result = [] | |
938 quotebuf = None | |
939 charbuf = [] | |
940 fieldchar = [''] | |
941 fieldnum = [0] | |
942 | |
943 def append_chars(): | |
944 result.append(''.join(charbuf).replace('%', '%%')) | |
945 del charbuf[:] | |
946 | |
947 def append_field(): | |
948 limit = PATTERN_CHARS[fieldchar[0]] | |
949 if limit and fieldnum[0] not in limit: | |
950 raise ValueError('Invalid length for field: %r' | |
951 % (fieldchar[0] * fieldnum[0])) | |
952 result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0])) | |
953 fieldchar[0] = '' | |
954 fieldnum[0] = 0 | |
955 | |
956 for idx, char in enumerate(pattern.replace("''", '\0')): | |
957 if quotebuf is None: | |
958 if char == "'": # quote started | |
959 if fieldchar[0]: | |
960 append_field() | |
961 elif charbuf: | |
962 append_chars() | |
963 quotebuf = [] | |
964 elif char in PATTERN_CHARS: | |
965 if charbuf: | |
966 append_chars() | |
967 if char == fieldchar[0]: | |
968 fieldnum[0] += 1 | |
969 else: | |
970 if fieldchar[0]: | |
971 append_field() | |
972 fieldchar[0] = char | |
973 fieldnum[0] = 1 | |
974 else: | |
975 if fieldchar[0]: | |
976 append_field() | |
977 charbuf.append(char) | |
978 | |
979 elif quotebuf is not None: | |
980 if char == "'": # end of quote | |
981 charbuf.extend(quotebuf) | |
982 quotebuf = None | |
983 else: # inside quote | |
984 quotebuf.append(char) | |
985 | |
986 if fieldchar[0]: | |
987 append_field() | |
988 elif charbuf: | |
989 append_chars() | |
990 | |
991 return DateTimePattern(pattern, u''.join(result).replace('\0', "'")) |