changeset 239:4c5fc8879d03

Properly implement week-in-year (#46).
author cmlenz
date Tue, 07 Aug 2007 17:13:45 +0000
parents d75cfd01f218
children e37046445638
files babel/core.py babel/dates.py babel/tests/dates.py
diffstat 3 files changed, 98 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- 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]
--- 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
--- 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)
Copyright (C) 2012-2017 Edgewall Software