Edgewall Software

root/trunk/babel/dates.py

Revision 436, 38.6 KB (checked in by cmlenz, 12 months ago)

Fixed quarters in date formatting.

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