cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # cmlenz@1: # Copyright (C) 2007 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@1: # are also available at http://babel.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@1: # history and logs, available at http://babel.edgewall.org/log/. cmlenz@1: cmlenz@1: """Locale dependent formatting and parsing of dates and times. cmlenz@1: cmlenz@1: The default locale for the functions in this module is determined by the cmlenz@1: following environment variables, in that order: cmlenz@1: cmlenz@1: * ``LC_TIME``, cmlenz@1: * ``LC_ALL``, and cmlenz@1: * ``LANG`` cmlenz@1: """ cmlenz@1: cmlenz@394: from __future__ import division jruigrok@515: from datetime import date, datetime, time, timedelta cmlenz@38: import re cmlenz@1: cmlenz@233: from babel.core import default_locale, get_global, Locale cmlenz@39: from babel.util import UTC cmlenz@1: cmlenz@394: __all__ = ['format_date', 'format_datetime', 'format_time', 'format_timedelta', cmlenz@233: 'get_timezone_name', 'parse_date', 'parse_datetime', 'parse_time'] cmlenz@1: __docformat__ = 'restructuredtext en' cmlenz@1: cmlenz@72: LC_TIME = default_locale('LC_TIME') cmlenz@1: cmlenz@33: # Aliases for use in scopes where the modules are shadowed by local variables cmlenz@33: date_ = date cmlenz@33: datetime_ = datetime cmlenz@33: time_ = time cmlenz@33: cmlenz@1: def get_period_names(locale=LC_TIME): cmlenz@1: """Return the names for day periods (AM/PM) used by the locale. cmlenz@1: cmlenz@1: >>> get_period_names(locale='en_US')['am'] cmlenz@1: u'AM' cmlenz@1: cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the dictionary of period names cmlenz@1: :rtype: `dict` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).periods cmlenz@1: cmlenz@1: def get_day_names(width='wide', context='format', locale=LC_TIME): cmlenz@1: """Return the day names used by the locale for the specified format. cmlenz@1: cmlenz@1: >>> get_day_names('wide', locale='en_US')[1] cmlenz@15: u'Tuesday' cmlenz@1: >>> get_day_names('abbreviated', locale='es')[1] cmlenz@15: u'mar' cmlenz@1: >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] cmlenz@15: u'D' cmlenz@1: cmlenz@1: :param width: the width to use, one of "wide", "abbreviated", or "narrow" cmlenz@1: :param context: the context, either "format" or "stand-alone" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the dictionary of day names cmlenz@1: :rtype: `dict` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).days[context][width] cmlenz@1: cmlenz@1: def get_month_names(width='wide', context='format', locale=LC_TIME): cmlenz@1: """Return the month names used by the locale for the specified format. cmlenz@1: cmlenz@1: >>> get_month_names('wide', locale='en_US')[1] cmlenz@1: u'January' cmlenz@1: >>> get_month_names('abbreviated', locale='es')[1] cmlenz@1: u'ene' cmlenz@1: >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] cmlenz@1: u'J' cmlenz@1: cmlenz@1: :param width: the width to use, one of "wide", "abbreviated", or "narrow" cmlenz@1: :param context: the context, either "format" or "stand-alone" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the dictionary of month names cmlenz@1: :rtype: `dict` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).months[context][width] cmlenz@1: cmlenz@1: def get_quarter_names(width='wide', context='format', locale=LC_TIME): cmlenz@1: """Return the quarter names used by the locale for the specified format. cmlenz@1: cmlenz@1: >>> get_quarter_names('wide', locale='en_US')[1] cmlenz@1: u'1st quarter' cmlenz@1: >>> get_quarter_names('abbreviated', locale='de_DE')[1] cmlenz@1: u'Q1' cmlenz@1: cmlenz@1: :param width: the width to use, one of "wide", "abbreviated", or "narrow" cmlenz@1: :param context: the context, either "format" or "stand-alone" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the dictionary of quarter names cmlenz@1: :rtype: `dict` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).quarters[context][width] cmlenz@1: cmlenz@1: def get_era_names(width='wide', locale=LC_TIME): cmlenz@1: """Return the era names used by the locale for the specified format. cmlenz@1: cmlenz@1: >>> get_era_names('wide', locale='en_US')[1] cmlenz@1: u'Anno Domini' cmlenz@1: >>> get_era_names('abbreviated', locale='de_DE')[1] cmlenz@1: u'n. Chr.' cmlenz@1: cmlenz@233: :param width: the width to use, either "wide", "abbreviated", or "narrow" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the dictionary of era names cmlenz@1: :rtype: `dict` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).eras[width] cmlenz@1: cmlenz@1: def get_date_format(format='medium', locale=LC_TIME): cmlenz@1: """Return the date formatting patterns used by the locale for the specified cmlenz@1: format. cmlenz@1: cmlenz@1: >>> get_date_format(locale='en_US') jruigrok@430: cmlenz@1: >>> get_date_format('full', locale='de_DE') jruigrok@430: cmlenz@1: cmlenz@1: :param format: the format to use, one of "full", "long", "medium", or cmlenz@1: "short" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the date format pattern cmlenz@33: :rtype: `DateTimePattern` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).date_formats[format] cmlenz@1: cmlenz@33: def get_datetime_format(format='medium', locale=LC_TIME): cmlenz@33: """Return the datetime formatting patterns used by the locale for the cmlenz@33: specified format. cmlenz@33: cmlenz@33: >>> get_datetime_format(locale='en_US') cmlenz@33: u'{1} {0}' cmlenz@33: cmlenz@33: :param format: the format to use, one of "full", "long", "medium", or cmlenz@33: "short" cmlenz@33: :param locale: the `Locale` object, or a locale string cmlenz@33: :return: the datetime format pattern cmlenz@33: :rtype: `unicode` cmlenz@33: """ cmlenz@33: patterns = Locale.parse(locale).datetime_formats cmlenz@33: if format not in patterns: cmlenz@33: format = None cmlenz@33: return patterns[format] cmlenz@33: cmlenz@1: def get_time_format(format='medium', locale=LC_TIME): cmlenz@1: """Return the time formatting patterns used by the locale for the specified cmlenz@1: format. cmlenz@1: cmlenz@1: >>> get_time_format(locale='en_US') cmlenz@12: cmlenz@1: >>> get_time_format('full', locale='de_DE') jruigrok@430: cmlenz@1: cmlenz@1: :param format: the format to use, one of "full", "long", "medium", or cmlenz@1: "short" cmlenz@1: :param locale: the `Locale` object, or a locale string cmlenz@1: :return: the time format pattern cmlenz@33: :rtype: `DateTimePattern` cmlenz@1: """ cmlenz@1: return Locale.parse(locale).time_formats[format] cmlenz@1: cmlenz@233: def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME): cmlenz@233: """Return the timezone associated with the given `datetime` object formatted cmlenz@233: as string indicating the offset from GMT. cmlenz@233: cmlenz@233: >>> dt = datetime(2007, 4, 1, 15, 30) cmlenz@247: >>> get_timezone_gmt(dt, locale='en') cmlenz@233: u'GMT+00:00' cmlenz@233: cmlenz@233: >>> from pytz import timezone cmlenz@233: >>> tz = timezone('America/Los_Angeles') cmlenz@233: >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz) cmlenz@247: >>> get_timezone_gmt(dt, locale='en') cmlenz@233: u'GMT-08:00' cmlenz@247: >>> get_timezone_gmt(dt, 'short', locale='en') cmlenz@233: u'-0800' cmlenz@233: cmlenz@375: The long format depends on the locale, for example in France the acronym cmlenz@375: UTC string is used instead of GMT: cmlenz@233: cmlenz@233: >>> get_timezone_gmt(dt, 'long', locale='fr_FR') cmlenz@375: u'UTC-08:00' cmlenz@233: cmlenz@247: :param datetime: the ``datetime`` object; if `None`, the current date and cmlenz@348: time in UTC is used cmlenz@233: :param width: either "long" or "short" cmlenz@233: :param locale: the `Locale` object, or a locale string cmlenz@233: :return: the GMT offset representation of the timezone cmlenz@233: :rtype: `unicode` cmlenz@233: :since: version 0.9 cmlenz@233: """ cmlenz@233: if datetime is None: cmlenz@348: datetime = datetime_.utcnow() cmlenz@233: elif isinstance(datetime, (int, long)): cmlenz@348: datetime = datetime_.utcfromtimestamp(datetime).time() cmlenz@233: if datetime.tzinfo is None: cmlenz@233: datetime = datetime.replace(tzinfo=UTC) cmlenz@233: locale = Locale.parse(locale) cmlenz@233: cmlenz@455: offset = datetime.tzinfo.utcoffset(datetime) cmlenz@233: seconds = offset.days * 24 * 60 * 60 + offset.seconds cmlenz@233: hours, seconds = divmod(seconds, 3600) cmlenz@233: if width == 'short': cmlenz@233: pattern = u'%+03d%02d' cmlenz@233: else: cmlenz@233: pattern = locale.zone_formats['gmt'] % '%+03d:%02d' cmlenz@233: return pattern % (hours, seconds // 60) cmlenz@233: cmlenz@233: def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME): cmlenz@233: """Return a representation of the given timezone using "location format". cmlenz@233: cmlenz@233: The result depends on both the local display name of the country and the fschwarz@516: city associated with the time zone: cmlenz@233: cmlenz@233: >>> from pytz import timezone cmlenz@233: >>> tz = timezone('America/St_Johns') cmlenz@233: >>> get_timezone_location(tz, locale='de_DE') cmlenz@233: u"Kanada (St. John's)" cmlenz@233: >>> tz = timezone('America/Mexico_City') cmlenz@233: >>> get_timezone_location(tz, locale='de_DE') cmlenz@233: u'Mexiko (Mexiko-Stadt)' cmlenz@233: cmlenz@233: If the timezone is associated with a country that uses only a single cmlenz@233: timezone, just the localized country name is returned: cmlenz@233: cmlenz@233: >>> tz = timezone('Europe/Berlin') cmlenz@233: >>> get_timezone_name(tz, locale='de_DE') cmlenz@233: u'Deutschland' cmlenz@233: cmlenz@233: :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines cmlenz@233: the timezone; if `None`, the current date and time in cmlenz@233: UTC is assumed cmlenz@233: :param locale: the `Locale` object, or a locale string cmlenz@233: :return: the localized timezone name using location format cmlenz@233: :rtype: `unicode` cmlenz@233: :since: version 0.9 cmlenz@233: """ cmlenz@233: if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): cmlenz@233: dt = None cmlenz@233: tzinfo = UTC cmlenz@233: elif isinstance(dt_or_tzinfo, (datetime, time)): cmlenz@233: dt = dt_or_tzinfo cmlenz@233: if dt.tzinfo is not None: cmlenz@233: tzinfo = dt.tzinfo cmlenz@233: else: cmlenz@233: tzinfo = UTC cmlenz@233: else: cmlenz@233: dt = None cmlenz@233: tzinfo = dt_or_tzinfo cmlenz@233: locale = Locale.parse(locale) cmlenz@233: cmlenz@233: if hasattr(tzinfo, 'zone'): cmlenz@233: zone = tzinfo.zone cmlenz@233: else: cmlenz@233: zone = tzinfo.tzname(dt or datetime.utcnow()) cmlenz@233: cmlenz@233: # Get the canonical time-zone code cmlenz@233: zone = get_global('zone_aliases').get(zone, zone) cmlenz@233: cmlenz@233: info = locale.time_zones.get(zone, {}) cmlenz@233: cmlenz@233: # Otherwise, if there is only one timezone for the country, return the cmlenz@233: # localized country name cmlenz@233: region_format = locale.zone_formats['region'] cmlenz@233: territory = get_global('zone_territories').get(zone) cmlenz@254: if territory not in locale.territories: cmlenz@254: territory = 'ZZ' # invalid/unknown cmlenz@233: territory_name = locale.territories[territory] cmlenz@254: if territory and len(get_global('territory_zones').get(territory, [])) == 1: cmlenz@233: return region_format % (territory_name) cmlenz@233: cmlenz@233: # Otherwise, include the city in the output cmlenz@233: fallback_format = locale.zone_formats['fallback'] cmlenz@233: if 'city' in info: cmlenz@233: city_name = info['city'] cmlenz@233: else: cmlenz@345: metazone = get_global('meta_zones').get(zone) cmlenz@345: metazone_info = locale.meta_zones.get(metazone, {}) cmlenz@345: if 'city' in metazone_info: cmlenz@345: city_name = metainfo['city'] cmlenz@345: elif '/' in zone: cmlenz@345: city_name = zone.split('/', 1)[1].replace('_', ' ') cmlenz@345: else: cmlenz@345: city_name = zone.replace('_', ' ') cmlenz@233: cmlenz@233: return region_format % (fallback_format % { cmlenz@233: '0': city_name, cmlenz@233: '1': territory_name cmlenz@233: }) cmlenz@233: cmlenz@233: def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, cmlenz@233: locale=LC_TIME): cmlenz@233: r"""Return the localized display name for the given timezone. The timezone cmlenz@233: may be specified using a ``datetime`` or `tzinfo` object. cmlenz@233: cmlenz@233: >>> from pytz import timezone cmlenz@233: >>> dt = time(15, 30, tzinfo=timezone('America/Los_Angeles')) cmlenz@233: >>> get_timezone_name(dt, locale='en_US') cmlenz@233: u'Pacific Standard Time' cmlenz@233: >>> get_timezone_name(dt, width='short', locale='en_US') cmlenz@233: u'PST' cmlenz@233: cmlenz@233: If this function gets passed only a `tzinfo` object and no concrete cmlenz@233: `datetime`, the returned display name is indenpendent of daylight savings cmlenz@233: time. This can be used for example for selecting timezones, or to set the cmlenz@233: time of events that recur across DST changes: cmlenz@233: cmlenz@233: >>> tz = timezone('America/Los_Angeles') cmlenz@233: >>> get_timezone_name(tz, locale='en_US') cmlenz@233: u'Pacific Time' cmlenz@233: >>> get_timezone_name(tz, 'short', locale='en_US') cmlenz@233: u'PT' cmlenz@233: cmlenz@233: If no localized display name for the timezone is available, and the timezone cmlenz@233: is associated with a country that uses only a single timezone, the name of cmlenz@233: that country is returned, formatted according to the locale: cmlenz@233: cmlenz@233: >>> tz = timezone('Europe/Berlin') cmlenz@233: >>> get_timezone_name(tz, locale='de_DE') cmlenz@233: u'Deutschland' cmlenz@233: >>> get_timezone_name(tz, locale='pt_BR') cmlenz@233: u'Hor\xe1rio Alemanha' cmlenz@233: cmlenz@233: On the other hand, if the country uses multiple timezones, the city is also cmlenz@233: included in the representation: cmlenz@233: cmlenz@233: >>> tz = timezone('America/St_Johns') cmlenz@233: >>> get_timezone_name(tz, locale='de_DE') cmlenz@233: u"Kanada (St. John's)" cmlenz@233: cmlenz@233: The `uncommon` parameter can be set to `True` to enable the use of timezone cmlenz@233: representations that are not commonly used by the requested locale. For jruigrok@451: example, while in French the central European timezone is usually jruigrok@323: abbreviated as "HEC", in Canadian French, this abbreviation is not in jruigrok@323: common use, so a generic name would be chosen by default: cmlenz@233: cmlenz@233: >>> tz = timezone('Europe/Paris') cmlenz@233: >>> get_timezone_name(tz, 'short', locale='fr_CA') cmlenz@233: u'France' cmlenz@233: >>> get_timezone_name(tz, 'short', uncommon=True, locale='fr_CA') cmlenz@233: u'HEC' cmlenz@233: cmlenz@233: :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines cmlenz@233: the timezone; if a ``tzinfo`` object is used, the cmlenz@233: resulting display name will be generic, i.e. cmlenz@233: independent of daylight savings time; if `None`, the cmlenz@233: current date in UTC is assumed cmlenz@233: :param width: either "long" or "short" cmlenz@233: :param uncommon: whether even uncommon timezone abbreviations should be used cmlenz@233: :param locale: the `Locale` object, or a locale string cmlenz@233: :return: the timezone display name cmlenz@233: :rtype: `unicode` cmlenz@233: :since: version 0.9 cmlenz@233: :see: `LDML Appendix J: Time Zone Display Names cmlenz@233: `_ cmlenz@233: """ cmlenz@233: if dt_or_tzinfo is None or isinstance(dt_or_tzinfo, (int, long)): cmlenz@233: dt = None cmlenz@233: tzinfo = UTC cmlenz@256: elif isinstance(dt_or_tzinfo, (datetime, time)): cmlenz@233: dt = dt_or_tzinfo cmlenz@233: if dt.tzinfo is not None: cmlenz@233: tzinfo = dt.tzinfo cmlenz@233: else: cmlenz@233: tzinfo = UTC cmlenz@233: else: cmlenz@233: dt = None cmlenz@233: tzinfo = dt_or_tzinfo cmlenz@233: locale = Locale.parse(locale) cmlenz@233: cmlenz@233: if hasattr(tzinfo, 'zone'): cmlenz@233: zone = tzinfo.zone cmlenz@233: else: cmlenz@348: zone = tzinfo.tzname(dt) cmlenz@233: cmlenz@233: # Get the canonical time-zone code cmlenz@233: zone = get_global('zone_aliases').get(zone, zone) cmlenz@233: cmlenz@233: info = locale.time_zones.get(zone, {}) cmlenz@233: # Try explicitly translated zone names first cmlenz@233: if width in info: cmlenz@233: if dt is None: cmlenz@233: field = 'generic' cmlenz@233: else: cmlenz@348: dst = tzinfo.dst(dt) cmlenz@348: if dst is None: cmlenz@348: field = 'generic' cmlenz@348: elif dst == 0: cmlenz@348: field = 'standard' cmlenz@348: else: cmlenz@348: field = 'daylight' cmlenz@233: if field in info[width]: cmlenz@233: return info[width][field] cmlenz@233: cmlenz@345: metazone = get_global('meta_zones').get(zone) cmlenz@345: if metazone: cmlenz@345: metazone_info = locale.meta_zones.get(metazone, {}) cmlenz@345: if width in metazone_info and (uncommon or metazone_info.get('common')): cmlenz@233: if dt is None: cmlenz@233: field = 'generic' cmlenz@233: else: cmlenz@233: field = tzinfo.dst(dt) and 'daylight' or 'standard' cmlenz@345: if field in metazone_info[width]: cmlenz@345: return metazone_info[width][field] cmlenz@233: cmlenz@233: # If we have a concrete datetime, we assume that the result can't be cmlenz@233: # independent of daylight savings time, so we return the GMT offset cmlenz@233: if dt is not None: cmlenz@253: return get_timezone_gmt(dt, width=width, locale=locale) cmlenz@233: cmlenz@233: return get_timezone_location(dt_or_tzinfo, locale=locale) cmlenz@233: cmlenz@33: def format_date(date=None, format='medium', locale=LC_TIME): cmlenz@101: """Return a date formatted according to the given pattern. cmlenz@1: cmlenz@1: >>> d = date(2007, 04, 01) cmlenz@1: >>> format_date(d, locale='en_US') cmlenz@1: u'Apr 1, 2007' cmlenz@1: >>> format_date(d, format='full', locale='de_DE') cmlenz@1: u'Sonntag, 1. April 2007' cmlenz@1: cmlenz@16: If you don't want to use the locale default formats, you can specify a cmlenz@16: custom date pattern: cmlenz@16: cmlenz@29: >>> format_date(d, "EEE, MMM d, ''yy", locale='en') cmlenz@16: u"Sun, Apr 1, '07" cmlenz@16: cmlenz@33: :param date: the ``date`` or ``datetime`` object; if `None`, the current cmlenz@33: date is used cmlenz@16: :param format: one of "full", "long", "medium", or "short", or a custom cmlenz@16: date/time pattern cmlenz@19: :param locale: a `Locale` object or a locale identifier cmlenz@1: :rtype: `unicode` cmlenz@19: cmlenz@19: :note: If the pattern contains time fields, an `AttributeError` will be cmlenz@19: raised when trying to apply the formatting. This is also true if cmlenz@19: the value of ``date`` parameter is actually a ``datetime`` object, cmlenz@19: as this function automatically converts that to a ``date``. cmlenz@1: """ cmlenz@33: if date is None: cmlenz@33: date = date_.today() cmlenz@33: elif isinstance(date, datetime): cmlenz@18: date = date.date() cmlenz@34: cmlenz@1: locale = Locale.parse(locale) cmlenz@1: if format in ('full', 'long', 'medium', 'short'): cmlenz@1: format = get_date_format(format, locale=locale) cmlenz@1: pattern = parse_pattern(format) jruigrok@502: return pattern.apply(date, locale) cmlenz@1: cmlenz@34: def format_datetime(datetime=None, format='medium', tzinfo=None, cmlenz@34: locale=LC_TIME): jruigrok@433: r"""Return a date formatted according to the given pattern. cmlenz@1: cmlenz@33: >>> dt = datetime(2007, 04, 01, 15, 30) cmlenz@33: >>> format_datetime(dt, locale='en_US') cmlenz@33: u'Apr 1, 2007 3:30:00 PM' cmlenz@33: cmlenz@34: For any pattern requiring the display of the time-zone, the third-party cmlenz@34: ``pytz`` package is needed to explicitly specify the time-zone: cmlenz@34: cmlenz@34: >>> from pytz import timezone cmlenz@233: >>> format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'), cmlenz@233: ... locale='fr_FR') jruigrok@433: u'dimanche 1 avril 2007 17:30:00 Heure avanc\xe9e de l\u2019Europe centrale' cmlenz@34: >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", cmlenz@34: ... tzinfo=timezone('US/Eastern'), locale='en') cmlenz@34: u'2007.04.01 AD at 11:30:00 EDT' cmlenz@34: cmlenz@33: :param datetime: the `datetime` object; if `None`, the current date and cmlenz@33: time is used cmlenz@16: :param format: one of "full", "long", "medium", or "short", or a custom cmlenz@16: date/time pattern cmlenz@29: :param tzinfo: the timezone to apply to the time for display cmlenz@19: :param locale: a `Locale` object or a locale identifier cmlenz@1: :rtype: `unicode` cmlenz@1: """ cmlenz@33: if datetime is None: cmlenz@348: datetime = datetime_.utcnow() cmlenz@34: elif isinstance(datetime, (int, long)): cmlenz@376: datetime = datetime_.utcfromtimestamp(datetime) cmlenz@34: elif isinstance(datetime, time): cmlenz@34: datetime = datetime_.combine(date.today(), datetime) cmlenz@34: if datetime.tzinfo is None: cmlenz@34: datetime = datetime.replace(tzinfo=UTC) cmlenz@34: if tzinfo is not None: cmlenz@34: datetime = datetime.astimezone(tzinfo) cmlenz@101: if hasattr(tzinfo, 'normalize'): # pytz cmlenz@34: datetime = tzinfo.normalize(datetime) cmlenz@34: cmlenz@18: locale = Locale.parse(locale) cmlenz@18: if format in ('full', 'long', 'medium', 'short'): cmlenz@33: return get_datetime_format(format, locale=locale) \ cmlenz@34: .replace('{0}', format_time(datetime, format, tzinfo=None, cmlenz@33: locale=locale)) \ cmlenz@33: .replace('{1}', format_date(datetime, format, locale=locale)) cmlenz@33: else: cmlenz@33: return parse_pattern(format).apply(datetime, locale) cmlenz@1: cmlenz@34: def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME): jruigrok@433: r"""Return a time formatted according to the given pattern. cmlenz@1: cmlenz@1: >>> t = time(15, 30) cmlenz@1: >>> format_time(t, locale='en_US') cmlenz@1: u'3:30:00 PM' cmlenz@1: >>> format_time(t, format='short', locale='de_DE') cmlenz@1: u'15:30' cmlenz@1: cmlenz@16: If you don't want to use the locale default formats, you can specify a cmlenz@16: custom time pattern: cmlenz@16: cmlenz@16: >>> format_time(t, "hh 'o''clock' a", locale='en') cmlenz@16: u"03 o'clock PM" cmlenz@16: cmlenz@29: For any pattern requiring the display of the time-zone, the third-party cmlenz@29: ``pytz`` package is needed to explicitly specify the time-zone: cmlenz@29: cmlenz@101: >>> from pytz import timezone cmlenz@348: >>> t = datetime(2007, 4, 1, 15, 30) cmlenz@348: >>> tzinfo = timezone('Europe/Paris') cmlenz@348: >>> t = tzinfo.localize(t) cmlenz@348: >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') jruigrok@433: u'15:30:00 Heure avanc\xe9e de l\u2019Europe centrale' cmlenz@34: >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=timezone('US/Eastern'), cmlenz@34: ... locale='en') cmlenz@348: u"09 o'clock AM, Eastern Daylight Time" cmlenz@348: cmlenz@348: As that example shows, when this function gets passed a cmlenz@348: ``datetime.datetime`` value, the actual time in the formatted string is cmlenz@348: adjusted to the timezone specified by the `tzinfo` parameter. If the cmlenz@348: ``datetime`` is "naive" (i.e. it has no associated timezone information), cmlenz@348: it is assumed to be in UTC. cmlenz@348: cmlenz@348: These timezone calculations are **not** performed if the value is of type cmlenz@348: ``datetime.time``, as without date information there's no way to determine cmlenz@348: what a given time would translate to in a different timezone without cmlenz@348: information about whether daylight savings time is in effect or not. This cmlenz@348: means that time values are left as-is, and the value of the `tzinfo` cmlenz@348: parameter is only used to display the timezone name if needed: cmlenz@348: cmlenz@348: >>> t = time(15, 30) cmlenz@348: >>> format_time(t, format='full', tzinfo=timezone('Europe/Paris'), cmlenz@348: ... locale='fr_FR') jruigrok@430: u'15:30:00 Heure normale de l\u2019Europe centrale' cmlenz@348: >>> format_time(t, format='full', tzinfo=timezone('US/Eastern'), cmlenz@348: ... locale='en_US') jruigrok@430: u'3:30:00 PM Eastern Standard Time' cmlenz@29: cmlenz@33: :param time: the ``time`` or ``datetime`` object; if `None`, the current cmlenz@348: time in UTC is used cmlenz@16: :param format: one of "full", "long", "medium", or "short", or a custom cmlenz@16: date/time pattern cmlenz@29: :param tzinfo: the time-zone to apply to the time for display cmlenz@19: :param locale: a `Locale` object or a locale identifier cmlenz@1: :rtype: `unicode` cmlenz@19: cmlenz@19: :note: If the pattern contains date fields, an `AttributeError` will be cmlenz@19: raised when trying to apply the formatting. This is also true if cmlenz@19: the value of ``time`` parameter is actually a ``datetime`` object, cmlenz@19: as this function automatically converts that to a ``time``. cmlenz@1: """ cmlenz@33: if time is None: cmlenz@348: time = datetime.utcnow() cmlenz@33: elif isinstance(time, (int, long)): cmlenz@348: time = datetime.utcfromtimestamp(time) cmlenz@29: if time.tzinfo is None: cmlenz@34: time = time.replace(tzinfo=UTC) cmlenz@348: if isinstance(time, datetime): cmlenz@348: if tzinfo is not None: cmlenz@348: time = time.astimezone(tzinfo) cmlenz@390: if hasattr(tzinfo, 'normalize'): # pytz cmlenz@348: time = tzinfo.normalize(time) cmlenz@348: time = time.timetz() cmlenz@348: elif tzinfo is not None: cmlenz@348: time = time.replace(tzinfo=tzinfo) cmlenz@34: cmlenz@1: locale = Locale.parse(locale) cmlenz@1: if format in ('full', 'long', 'medium', 'short'): cmlenz@1: format = get_time_format(format, locale=locale) cmlenz@1: return parse_pattern(format).apply(time, locale) cmlenz@1: cmlenz@390: TIMEDELTA_UNITS = ( cmlenz@390: ('year', 3600 * 24 * 365), cmlenz@390: ('month', 3600 * 24 * 30), cmlenz@390: ('week', 3600 * 24 * 7), cmlenz@390: ('day', 3600 * 24), cmlenz@390: ('hour', 3600), cmlenz@390: ('minute', 60), cmlenz@390: ('second', 1) cmlenz@390: ) cmlenz@390: cmlenz@394: def format_timedelta(delta, granularity='second', threshold=.85, locale=LC_TIME): cmlenz@390: """Return a time delta according to the rules of the given locale. cmlenz@394: cmlenz@390: >>> format_timedelta(timedelta(weeks=12), locale='en_US') jruigrok@430: u'3 mths' cmlenz@390: >>> format_timedelta(timedelta(seconds=1), locale='es') jruigrok@430: u'1 s' cmlenz@394: cmlenz@394: The granularity parameter can be provided to alter the lowest unit cmlenz@394: presented, which defaults to a second. cmlenz@390: cmlenz@394: >>> format_timedelta(timedelta(hours=3), granularity='day', cmlenz@394: ... locale='en_US') cmlenz@395: u'1 day' cmlenz@394: cmlenz@394: The threshold parameter can be used to determine at which value the cmlenz@394: presentation switches to the next higher unit. A higher threshold factor cmlenz@394: means the presentation will switch later. For example: cmlenz@394: cmlenz@394: >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US') cmlenz@394: u'1 day' cmlenz@394: >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US') jruigrok@430: u'23 hrs' cmlenz@394: cmlenz@390: :param delta: a ``timedelta`` object representing the time difference to cmlenz@394: format, or the delta in seconds as an `int` value cmlenz@394: :param granularity: determines the smallest unit that should be displayed, cmlenz@394: the value can be one of "year", "month", "week", "day", cmlenz@394: "hour", "minute" or "second" cmlenz@394: :param threshold: factor that determines at which point the presentation cmlenz@394: switches to the next higher unit cmlenz@394: :param locale: a `Locale` object or a locale identifier cmlenz@394: :rtype: `unicode` cmlenz@390: """ cmlenz@394: if isinstance(delta, timedelta): cmlenz@394: seconds = int((delta.days * 86400) + delta.seconds) cmlenz@394: else: cmlenz@394: seconds = delta cmlenz@390: locale = Locale.parse(locale) cmlenz@390: cmlenz@394: for unit, secs_per_unit in TIMEDELTA_UNITS: cmlenz@394: value = abs(seconds) / secs_per_unit cmlenz@394: if value >= threshold or unit == granularity: cmlenz@395: if unit == granularity and value > 0: cmlenz@395: value = max(1, value) cmlenz@394: value = int(round(value)) cmlenz@394: plural_form = locale.plural_form(value) cmlenz@390: pattern = locale._data['unit_patterns'][unit][plural_form] cmlenz@394: return pattern.replace('{0}', str(value)) cmlenz@394: cmlenz@394: return u'' cmlenz@390: cmlenz@1: def parse_date(string, locale=LC_TIME): cmlenz@38: """Parse a date from a string. cmlenz@38: cmlenz@38: This function uses the date format for the locale as a hint to determine cmlenz@38: the order in which the date fields appear in the string. cmlenz@38: cmlenz@38: >>> parse_date('4/1/04', locale='en_US') cmlenz@38: datetime.date(2004, 4, 1) cmlenz@38: >>> parse_date('01.04.2004', locale='de_DE') cmlenz@38: datetime.date(2004, 4, 1) cmlenz@38: cmlenz@38: :param string: the string containing the date cmlenz@38: :param locale: a `Locale` object or a locale identifier cmlenz@38: :return: the parsed date cmlenz@38: :rtype: `date` cmlenz@38: """ cmlenz@38: # TODO: try ISO format first? cmlenz@38: format = get_date_format(locale=locale).pattern.lower() cmlenz@38: year_idx = format.index('y') cmlenz@38: month_idx = format.index('m') cmlenz@38: if month_idx < 0: cmlenz@38: month_idx = format.index('l') cmlenz@38: day_idx = format.index('d') cmlenz@38: cmlenz@38: indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')] cmlenz@38: indexes.sort() cmlenz@38: indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) cmlenz@38: cmlenz@38: # FIXME: this currently only supports numbers, but should also support month cmlenz@38: # names, both in the requested locale, and english cmlenz@38: cmlenz@38: numbers = re.findall('(\d+)', string) cmlenz@38: year = numbers[indexes['Y']] cmlenz@38: if len(year) == 2: cmlenz@38: year = 2000 + int(year) cmlenz@38: else: cmlenz@38: year = int(year) cmlenz@38: month = int(numbers[indexes['M']]) cmlenz@38: day = int(numbers[indexes['D']]) cmlenz@38: if month > 12: cmlenz@38: month, day = day, month cmlenz@38: return date(year, month, day) cmlenz@1: cmlenz@1: def parse_datetime(string, locale=LC_TIME): cmlenz@46: """Parse a date and time from a string. cmlenz@46: cmlenz@46: This function uses the date and time formats for the locale as a hint to cmlenz@46: determine the order in which the time fields appear in the string. cmlenz@46: cmlenz@46: :param string: the string containing the date and time cmlenz@46: :param locale: a `Locale` object or a locale identifier cmlenz@46: :return: the parsed date/time cmlenz@46: :rtype: `datetime` cmlenz@46: """ cmlenz@1: raise NotImplementedError cmlenz@1: cmlenz@1: def parse_time(string, locale=LC_TIME): cmlenz@46: """Parse a time from a string. cmlenz@38: cmlenz@38: This function uses the time format for the locale as a hint to determine cmlenz@38: the order in which the time fields appear in the string. cmlenz@38: cmlenz@38: >>> parse_time('15:30:00', locale='en_US') cmlenz@38: datetime.time(15, 30) cmlenz@38: cmlenz@38: :param string: the string containing the time cmlenz@38: :param locale: a `Locale` object or a locale identifier cmlenz@38: :return: the parsed time cmlenz@38: :rtype: `time` cmlenz@38: """ cmlenz@38: # TODO: try ISO format first? cmlenz@38: format = get_time_format(locale=locale).pattern.lower() cmlenz@38: hour_idx = format.index('h') cmlenz@38: if hour_idx < 0: cmlenz@38: hour_idx = format.index('k') cmlenz@38: min_idx = format.index('m') cmlenz@38: sec_idx = format.index('s') cmlenz@38: cmlenz@38: indexes = [(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')] cmlenz@38: indexes.sort() cmlenz@38: indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)]) cmlenz@38: cmlenz@38: # FIXME: support 12 hour clock, and 0-based hour specification cmlenz@46: # and seconds should be optional, maybe minutes too cmlenz@46: # oh, and time-zones, of course cmlenz@38: cmlenz@38: numbers = re.findall('(\d+)', string) cmlenz@38: hour = int(numbers[indexes['H']]) cmlenz@38: minute = int(numbers[indexes['M']]) cmlenz@38: second = int(numbers[indexes['S']]) cmlenz@38: return time(hour, minute, second) cmlenz@1: cmlenz@1: cmlenz@12: class DateTimePattern(object): cmlenz@1: cmlenz@1: def __init__(self, pattern, format): cmlenz@1: self.pattern = pattern cmlenz@1: self.format = format cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: return '<%s %r>' % (type(self).__name__, self.pattern) cmlenz@1: cmlenz@1: def __unicode__(self): cmlenz@1: return self.pattern cmlenz@1: cmlenz@1: def __mod__(self, other): aronacher@391: if type(other) is not DateTimeFormat: aronacher@391: return NotImplemented cmlenz@1: return self.format % other cmlenz@1: cmlenz@1: def apply(self, datetime, locale): cmlenz@1: return self % DateTimeFormat(datetime, locale) cmlenz@1: cmlenz@1: cmlenz@1: class DateTimeFormat(object): cmlenz@1: cmlenz@1: def __init__(self, value, locale): cmlenz@1: assert isinstance(value, (date, datetime, time)) cmlenz@29: if isinstance(value, (datetime, time)) and value.tzinfo is None: cmlenz@29: value = value.replace(tzinfo=UTC) cmlenz@1: self.value = value cmlenz@1: self.locale = Locale.parse(locale) cmlenz@1: cmlenz@1: def __getitem__(self, name): cmlenz@15: char = name[0] cmlenz@15: num = len(name) cmlenz@15: if char == 'G': cmlenz@15: return self.format_era(char, num) cmlenz@215: elif char in ('y', 'Y', 'u'): cmlenz@15: return self.format_year(char, num) cmlenz@15: elif char in ('Q', 'q'): cmlenz@15: return self.format_quarter(char, num) cmlenz@15: elif char in ('M', 'L'): cmlenz@15: return self.format_month(char, num) cmlenz@215: elif char in ('w', 'W'): cmlenz@215: return self.format_week(char, num) cmlenz@15: elif char == 'd': cmlenz@15: return self.format(self.value.day, num) cmlenz@221: elif char == 'D': cmlenz@221: return self.format_day_of_year(num) cmlenz@241: elif char == 'F': cmlenz@241: return self.format_day_of_week_in_month() cmlenz@15: elif char in ('E', 'e', 'c'): cmlenz@15: return self.format_weekday(char, num) cmlenz@15: elif char == 'a': cmlenz@15: return self.format_period(char) cmlenz@15: elif char == 'h': jonas@273: if self.value.hour % 12 == 0: jonas@273: return self.format(12, num) jonas@273: else: jonas@273: return self.format(self.value.hour % 12, num) cmlenz@15: elif char == 'H': cmlenz@15: return self.format(self.value.hour, num) cmlenz@18: elif char == 'K': jonas@273: return self.format(self.value.hour % 12, num) cmlenz@18: elif char == 'k': jonas@273: if self.value.hour == 0: jonas@273: return self.format(24, num) jonas@273: else: jonas@274: return self.format(self.value.hour, num) cmlenz@15: elif char == 'm': cmlenz@15: return self.format(self.value.minute, num) cmlenz@15: elif char == 's': cmlenz@15: return self.format(self.value.second, num) cmlenz@216: elif char == 'S': cmlenz@217: return self.format_frac_seconds(num) cmlenz@217: elif char == 'A': cmlenz@217: return self.format_milliseconds_in_day(num) cmlenz@233: elif char in ('z', 'Z', 'v', 'V'): cmlenz@29: return self.format_timezone(char, num) cmlenz@1: else: cmlenz@15: raise KeyError('Unsupported date/time field %r' % char) cmlenz@1: cmlenz@15: def format_era(self, char, num): cmlenz@1: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] cmlenz@1: era = int(self.value.year >= 0) cmlenz@1: return get_era_names(width, self.locale)[era] cmlenz@1: cmlenz@15: def format_year(self, char, num): cmlenz@242: value = self.value.year cmlenz@242: if char.isupper(): cmlenz@242: week = self.get_week_number(self.get_day_of_year()) cmlenz@242: if week == 0: cmlenz@242: value -= 1 cmlenz@1: year = self.format(value, num) cmlenz@1: if num == 2: cmlenz@1: year = year[-2:] cmlenz@1: return year cmlenz@1: cmlenz@396: def format_quarter(self, char, num): cmlenz@396: quarter = (self.value.month - 1) // 3 + 1 cmlenz@396: if num <= 2: cmlenz@396: return ('%%0%dd' % num) % quarter cmlenz@396: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] cmlenz@396: context = {'Q': 'format', 'q': 'stand-alone'}[char] cmlenz@396: return get_quarter_names(width, context, self.locale)[quarter] cmlenz@396: cmlenz@15: def format_month(self, char, num): cmlenz@1: if num <= 2: cmlenz@1: return ('%%0%dd' % num) % self.value.month cmlenz@1: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] cmlenz@344: context = {'M': 'format', 'L': 'stand-alone'}[char] cmlenz@1: return get_month_names(width, context, self.locale)[self.value.month] cmlenz@1: cmlenz@215: def format_week(self, char, num): cmlenz@239: if char.islower(): # week of year cmlenz@242: day_of_year = self.get_day_of_year() cmlenz@242: week = self.get_week_number(day_of_year) cmlenz@240: if week == 0: cmlenz@242: date = self.value - timedelta(days=day_of_year) cmlenz@242: week = self.get_week_number(self.get_day_of_year(date), cmlenz@242: date.weekday()) cmlenz@240: return self.format(week, num) cmlenz@239: else: # week of month cmlenz@240: week = self.get_week_number(self.value.day) cmlenz@240: if week == 0: cmlenz@242: date = self.value - timedelta(days=self.value.day) cmlenz@242: week = self.get_week_number(date.day, date.weekday()) cmlenz@240: pass cmlenz@240: return '%d' % week cmlenz@215: cmlenz@15: def format_weekday(self, char, num): cmlenz@15: if num < 3: cmlenz@15: if char.islower(): cmlenz@15: value = 7 - self.locale.first_week_day + self.value.weekday() cmlenz@15: return self.format(value % 7 + 1, num) cmlenz@15: num = 3 cmlenz@15: weekday = self.value.weekday() cmlenz@15: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] cmlenz@15: context = {3: 'format', 4: 'format', 5: 'stand-alone'}[num] cmlenz@1: return get_day_names(width, context, self.locale)[weekday] cmlenz@1: cmlenz@221: def format_day_of_year(self, num): cmlenz@239: return self.format(self.get_day_of_year(), num) cmlenz@221: cmlenz@241: def format_day_of_week_in_month(self): cmlenz@394: return '%d' % ((self.value.day - 1) // 7 + 1) cmlenz@241: cmlenz@15: def format_period(self, char): jonas@273: period = {0: 'am', 1: 'pm'}[int(self.value.hour >= 12)] cmlenz@1: return get_period_names(locale=self.locale)[period] cmlenz@1: cmlenz@217: def format_frac_seconds(self, num): cmlenz@216: value = str(self.value.microsecond) cmlenz@216: return self.format(round(float('.%s' % value), num) * 10**num, num) cmlenz@216: cmlenz@217: def format_milliseconds_in_day(self, num): cmlenz@217: msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ cmlenz@217: self.value.minute * 60000 + self.value.hour * 3600000 cmlenz@217: return self.format(msecs, num) cmlenz@217: cmlenz@29: def format_timezone(self, char, num): cmlenz@233: width = {3: 'short', 4: 'long'}[max(3, num)] cmlenz@233: if char == 'z': cmlenz@233: return get_timezone_name(self.value, width, locale=self.locale) cmlenz@29: elif char == 'Z': cmlenz@247: return get_timezone_gmt(self.value, width, locale=self.locale) cmlenz@233: elif char == 'v': cmlenz@233: return get_timezone_name(self.value.tzinfo, width, cmlenz@233: locale=self.locale) cmlenz@233: elif char == 'V': cmlenz@233: if num == 1: cmlenz@233: return get_timezone_name(self.value.tzinfo, width, cmlenz@233: uncommon=True, locale=self.locale) cmlenz@233: return get_timezone_location(self.value.tzinfo, locale=self.locale) cmlenz@29: cmlenz@1: def format(self, value, length): cmlenz@1: return ('%%0%dd' % length) % value cmlenz@1: cmlenz@242: def get_day_of_year(self, date=None): cmlenz@242: if date is None: cmlenz@242: date = self.value cmlenz@242: return (date - date_(date.year, 1, 1)).days + 1 cmlenz@239: cmlenz@242: def get_week_number(self, day_of_period, day_of_week=None): cmlenz@239: """Return the number of the week of a day within a period. This may be cmlenz@239: the week number in a year or the week number in a month. cmlenz@239: cmlenz@239: Usually this will return a value equal to or greater than 1, but if the cmlenz@239: first week of the period is so short that it actually counts as the last cmlenz@239: week of the previous period, this function will return 0. cmlenz@239: cmlenz@239: >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) cmlenz@239: >>> format.get_week_number(6) cmlenz@239: 1 cmlenz@239: cmlenz@239: >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) cmlenz@239: >>> format.get_week_number(6) cmlenz@239: 2 cmlenz@239: cmlenz@239: :param day_of_period: the number of the day in the period (usually cmlenz@239: either the day of month or the day of year) cmlenz@242: :param day_of_week: the week day; if ommitted, the week day of the cmlenz@242: current date is assumed cmlenz@239: """ cmlenz@242: if day_of_week is None: cmlenz@242: day_of_week = self.value.weekday() cmlenz@242: first_day = (day_of_week - self.locale.first_week_day - cmlenz@239: day_of_period + 1) % 7 cmlenz@239: if first_day < 0: cmlenz@239: first_day += 7 cmlenz@394: week_number = (day_of_period + first_day - 1) // 7 cmlenz@239: if 7 - first_day >= self.locale.min_week_days: cmlenz@239: week_number += 1 cmlenz@239: return week_number cmlenz@239: cmlenz@1: cmlenz@1: PATTERN_CHARS = { cmlenz@15: 'G': [1, 2, 3, 4, 5], # era cmlenz@15: 'y': None, 'Y': None, 'u': None, # year cmlenz@15: 'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4], # quarter cmlenz@15: 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month cmlenz@15: 'w': [1, 2], 'W': [1], # week cmlenz@15: 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day cmlenz@15: 'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5], # week day cmlenz@15: 'a': [1], # period cmlenz@15: 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour cmlenz@15: 'm': [1, 2], # minute cmlenz@15: 's': [1, 2], 'S': None, 'A': None, # second cmlenz@233: 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 4] # zone cmlenz@1: } cmlenz@1: cmlenz@1: def parse_pattern(pattern): cmlenz@1: """Parse date, time, and datetime format patterns. cmlenz@1: cmlenz@1: >>> parse_pattern("MMMMd").format cmlenz@1: u'%(MMMM)s%(d)s' cmlenz@1: >>> parse_pattern("MMM d, yyyy").format cmlenz@1: u'%(MMM)s %(d)s, %(yyyy)s' cmlenz@16: cmlenz@16: Pattern can contain literal strings in single quotes: cmlenz@16: cmlenz@1: >>> parse_pattern("H:mm' Uhr 'z").format cmlenz@1: u'%(H)s:%(mm)s Uhr %(z)s' cmlenz@1: cmlenz@16: An actual single quote can be used by using two adjacent single quote cmlenz@16: characters: cmlenz@16: cmlenz@16: >>> parse_pattern("hh' o''clock'").format cmlenz@16: u"%(hh)s o'clock" cmlenz@16: cmlenz@1: :param pattern: the formatting pattern to parse cmlenz@1: """ cmlenz@12: if type(pattern) is DateTimePattern: cmlenz@1: return pattern cmlenz@1: cmlenz@1: result = [] cmlenz@1: quotebuf = None cmlenz@1: charbuf = [] cmlenz@1: fieldchar = [''] cmlenz@1: fieldnum = [0] cmlenz@1: cmlenz@1: def append_chars(): cmlenz@1: result.append(''.join(charbuf).replace('%', '%%')) cmlenz@1: del charbuf[:] cmlenz@1: cmlenz@1: def append_field(): cmlenz@1: limit = PATTERN_CHARS[fieldchar[0]] cmlenz@15: if limit and fieldnum[0] not in limit: cmlenz@1: raise ValueError('Invalid length for field: %r' cmlenz@1: % (fieldchar[0] * fieldnum[0])) cmlenz@1: result.append('%%(%s)s' % (fieldchar[0] * fieldnum[0])) cmlenz@1: fieldchar[0] = '' cmlenz@1: fieldnum[0] = 0 cmlenz@1: cmlenz@16: for idx, char in enumerate(pattern.replace("''", '\0')): cmlenz@1: if quotebuf is None: cmlenz@1: if char == "'": # quote started cmlenz@1: if fieldchar[0]: cmlenz@1: append_field() cmlenz@1: elif charbuf: cmlenz@1: append_chars() cmlenz@1: quotebuf = [] cmlenz@1: elif char in PATTERN_CHARS: cmlenz@1: if charbuf: cmlenz@1: append_chars() cmlenz@1: if char == fieldchar[0]: cmlenz@1: fieldnum[0] += 1 cmlenz@1: else: cmlenz@1: if fieldchar[0]: cmlenz@1: append_field() cmlenz@1: fieldchar[0] = char cmlenz@1: fieldnum[0] = 1 cmlenz@1: else: cmlenz@1: if fieldchar[0]: cmlenz@1: append_field() cmlenz@1: charbuf.append(char) cmlenz@1: cmlenz@1: elif quotebuf is not None: cmlenz@16: if char == "'": # end of quote cmlenz@1: charbuf.extend(quotebuf) cmlenz@1: quotebuf = None cmlenz@1: else: # inside quote cmlenz@1: quotebuf.append(char) cmlenz@1: cmlenz@1: if fieldchar[0]: cmlenz@1: append_field() cmlenz@1: elif charbuf: cmlenz@1: append_chars() cmlenz@1: cmlenz@16: return DateTimePattern(pattern, u''.join(result).replace('\0', "'"))