# HG changeset patch # User cmlenz # Date 1186506825 0 # Node ID 4c5fc8879d030a224bb250a0870c8fae69514da1 # Parent d75cfd01f21893af63fc9de7aaf08144b61f3a77 Properly implement week-in-year (#46). diff --git a/babel/core.py b/babel/core.py --- a/babel/core.py +++ b/babel/core.py @@ -25,8 +25,7 @@ _global_data = None def get_global(key): - """ - Return the dictionary for the given key in the global data. + """Return the dictionary for the given key in the global data. The global data is stored in the ``babel/global.dat`` file and contains information independent of individual locales. @@ -36,6 +35,9 @@ >>> get_global('zone_territories')['Europe/Berlin'] 'DE' + :param: the data key + :return the dictionary found in the global data under the given key + :rtype: `dict` :since: version 0.9 """ global _global_data @@ -49,6 +51,17 @@ fileobj.close() return _global_data.get(key, {}) +LOCALE_ALIASES = { + 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', + 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', + 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', + 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', + 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', + 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', + 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', + 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA' +} + class UnknownLocaleError(Exception): """Exception thrown when a locale is requested for which no locale data @@ -135,11 +148,12 @@ :return: the value of the variable, or any of the fallbacks (``LANGUAGE``, ``LC_ALL``, ``LC_CTYPE``, and ``LANG``) :rtype: `Locale` + :see: `default_locale` """ return cls(default_locale(category)) default = classmethod(default) - def negotiate(cls, preferred, available, sep='_'): + def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES): """Find the best match between available and requested locale strings. >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) @@ -157,11 +171,14 @@ :param preferred: the list of locale identifers preferred by the user :param available: the list of locale identifiers available + :param aliases: a dictionary of aliases for locale identifiers :return: the `Locale` object for the best match, or `None` if no match was found :rtype: `Locale` + :see: `negotiate_locale` """ - identifier = negotiate_locale(preferred, available, sep=sep) + identifier = negotiate_locale(preferred, available, sep=sep, + aliases=aliases) if identifier: return Locale.parse(identifier, sep=sep) negotiate = classmethod(negotiate) @@ -187,6 +204,7 @@ identifier :raise `UnknownLocaleError`: if no locale data is available for the requested locale + :see: `parse_locale` """ if type(identifier) is cls: return identifier @@ -610,7 +628,7 @@ locale = locale.split(':')[0] return '_'.join(filter(None, parse_locale(locale))) -def negotiate_locale(preferred, available, sep='_'): +def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES): """Find the best match between available and requested locale strings. >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) @@ -624,18 +642,50 @@ >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) 'de_DE' + >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) + 'de_DE' + + By default, some web browsers unfortunately do not include the territory + in the locale identifier for many locales, and some don't even allow the + user to easily add the territory. So while you may prefer using qualified + locale identifiers in your web-application, they would not normally match + the language-only locale sent by such browsers. To workaround that, this + function uses a default mapping of commonly used langauge-only locale + identifiers to identifiers including the territory: + + >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) + 'ja_JP' + + Some browsers even use an incorrect or outdated language code, such as "no" + for Norwegian, where the correct locale identifier would actually be "nb_NO" + (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of + such cases, too: + + >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) + 'nb_NO' + + You can override this default mapping by passing a different `aliases` + dictionary to this function, or you can bypass the behavior althogher by + setting the `aliases` parameter to `None`. + :param preferred: the list of locale strings preferred by the user :param available: the list of locale strings available :param sep: character that separates the different parts of the locale strings + :param aliases: a dictionary of aliases for locale identifiers :return: the locale identifier for the best match, or `None` if no match was found :rtype: `str` """ available = [a.lower() for a in available if a] for locale in preferred: - if locale.lower() in available: + ll = locale.lower() + if ll in available: return locale + if aliases: + alias = aliases.get(ll) + if alias and alias.lower() in available: + return alias parts = locale.split(sep) if len(parts) > 1 and parts[0].lower() in available: return parts[0] diff --git a/babel/dates.py b/babel/dates.py --- a/babel/dates.py +++ b/babel/dates.py @@ -745,11 +745,12 @@ return get_month_names(width, context, self.locale)[self.value.month] def format_week(self, char, num): - # FIXME: this should really be based on the first_week_day and - # min_week_days locale data - if char.islower(): - return self.value.strftime('%W') - else: + if char.islower(): # week of year + return self.format(self.get_week_number(self.get_day_of_year()), + num) + else: # week of month + # FIXME: this should really be based on the first_week_day and + # min_week_days locale data return '%d' % ((self.value.day + 6 - self.value.weekday()) / 7 + 1) def format_weekday(self, char, num): @@ -764,8 +765,7 @@ return get_day_names(width, context, self.locale)[weekday] def format_day_of_year(self, num): - delta = self.value - date(self.value.year, 1, 1) - return self.format(delta.days + 1, num) + return self.format(self.get_day_of_year(), num) def format_period(self, char): period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)] @@ -798,6 +798,37 @@ def format(self, value, length): return ('%%0%dd' % length) % value + def get_day_of_year(self): + return (self.value - date(self.value.year, 1, 1)).days + 1 + + def get_week_number(self, day_of_period): + """Return the number of the week of a day within a period. This may be + the week number in a year or the week number in a month. + + Usually this will return a value equal to or greater than 1, but if the + first week of the period is so short that it actually counts as the last + week of the previous period, this function will return 0. + + >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) + >>> format.get_week_number(6) + 1 + + >>> format = DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) + >>> format.get_week_number(6) + 2 + + :param day_of_period: the number of the day in the period (usually + either the day of month or the day of year) + """ + first_day = (self.value.weekday() - self.locale.first_week_day - + day_of_period + 1) % 7 + if first_day < 0: + first_day += 7 + week_number = (day_of_period + first_day - 1) / 7 + if 7 - first_day >= self.locale.min_week_days: + week_number += 1 + return week_number + PATTERN_CHARS = { 'G': [1, 2, 3, 4, 5], # era diff --git a/babel/tests/dates.py b/babel/tests/dates.py --- a/babel/tests/dates.py +++ b/babel/tests/dates.py @@ -23,9 +23,11 @@ class DateTimeFormatTestCase(unittest.TestCase): def test_week_of_year(self): - d = date(2007, 4, 1) + d = date(2006, 1, 8) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['w']) fmt = dates.DateTimeFormat(d, locale='en_US') - self.assertEqual('13', fmt['w']) + self.assertEqual('02', fmt['ww']) def test_week_of_month(self): d = date(2007, 4, 1)