Repository: python-babel/babel (python)

Issue:
Support more format for timezone 
# Overview Description

I cannot use `XXXXX` or `ZZZZZ`

# Steps to Reproduce

See this `pycon` log

```pycon
>>> from babel.dates import format_datetime, get_timezone
>>> from datetime import datetime
>>> dt = datetime(2016, 1, 13, 7, 8, 35)
>>> tz = get_timezone('Asia/Seoul')
>>> dt = tz.localize(dt)
>>> format_datetime(dt, 'ZZZZZ')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/item4/.virtualenvs/lektor/lib/python2.7/site-packages/babel/dates.py", line 618, in format_datetime
    return parse_pattern(format).apply(datetime, locale)
  File "/Users/item4/.virtualenvs/lektor/lib/python2.7/site-packages/babel/dates.py", line 1192, in parse_pattern
    append_field()
  File "/Users/item4/.virtualenvs/lektor/lib/python2.7/site-packages/babel/dates.py", line 1156, in append_field
    % (fieldchar[0] * fieldnum[0]))
ValueError: Invalid length for field: 'ZZZZZ'
>>> format_datetime(dt, 'ZZZ')
u'+0900'
>>> format_datetime(dt, 'XXXXX')
u'XXXXX'
>>> format_datetime(dt, 'XXX')
u'XXX'
```

# Actual Results
`ValueError: Invalid length for field: 'ZZZZZ'` ... etc.

# Expected Results

`ZZZZZ` → `+09:00`
`XXXXX` → `+09:00`

# Reproducibility

100%, Repeat my code.

# Additional Information:

Python 2.7
Mac OS X

See [Date Field Symbol Table](http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) for spec.

Pull request description:
Fix #325 Support more format for timezone
Added the Support for timezone format 'ZZZZZ' and 'X'

Patch:
diff --git a/babel/dates.py b/babel/dates.py
index b41945b..1af9955 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -370,20 +370,25 @@ def get_time_format(format='medium', locale=LC_TIME):
     return Locale.parse(locale).time_formats[format]
 
 
-def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME):
+def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME, return_z=False):
     """Return the timezone associated with the given `datetime` object formatted
     as string indicating the offset from GMT.
 
     >>> dt = datetime(2007, 4, 1, 15, 30)
     >>> get_timezone_gmt(dt, locale='en')
     u'GMT+00:00'
-
+    >>> get_timezone_gmt(dt, locale='en', return_z=True)
+    'Z'
+    >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
+    u'+00'
     >>> tz = get_timezone('America/Los_Angeles')
     >>> dt = tz.localize(datetime(2007, 4, 1, 15, 30))
     >>> get_timezone_gmt(dt, locale='en')
     u'GMT-07:00'
     >>> get_timezone_gmt(dt, 'short', locale='en')
     u'-0700'
+    >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
+    u'-07'
 
     The long format depends on the locale, for example in France the acronym
     UTC string is used instead of GMT:
@@ -395,8 +400,10 @@ def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME):
 
     :param datetime: the ``datetime`` object; if `None`, the current date and
                      time in UTC is used
-    :param width: either "long" or "short"
+    :param width: either "long" or "short" or "iso8601" or "iso8601_short"
     :param locale: the `Locale` object, or a locale string
+    :param return_z: True or False; Function returns indicator "Z"
+                     when local time offset is 0
     """
     datetime = _ensure_datetime_tzinfo(_get_datetime(datetime))
     locale = Locale.parse(locale)
@@ -404,14 +411,20 @@ def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME):
     offset = datetime.tzinfo.utcoffset(datetime)
     seconds = offset.days * 24 * 60 * 60 + offset.seconds
     hours, seconds = divmod(seconds, 3600)
-    if width == 'short':
+    if return_z and hours == 0 and seconds == 0:
+        return 'Z'
+    elif seconds == 0 and width == 'iso8601_short':
+        return u'%+03d' % hours
+    elif width == 'short' or width == 'iso8601_short':
         pattern = u'%+03d%02d'
+    elif width == 'iso8601':
+        pattern = u'%+03d:%02d'
     else:
         pattern = locale.zone_formats['gmt'] % '%+03d:%02d'
     return pattern % (hours, seconds // 60)
 
 
-def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
+def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False):
     u"""Return a representation of the given timezone using "location format".
 
     The result depends on both the local display name of the country and the
@@ -420,6 +433,10 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
     >>> tz = get_timezone('America/St_Johns')
     >>> print(get_timezone_location(tz, locale='de_DE'))
     Kanada (St. John’s) Zeit
+    >>> print(get_timezone_location(tz, locale='en'))
+    Canada (St. John’s) Time
+    >>> print(get_timezone_location(tz, locale='en', return_city=True))
+    St. John’s
     >>> tz = get_timezone('America/Mexico_City')
     >>> get_timezone_location(tz, locale='de_DE')
     u'Mexiko (Mexiko-Stadt) Zeit'
@@ -437,7 +454,10 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
                          the timezone; if `None`, the current date and time in
                          UTC is assumed
     :param locale: the `Locale` object, or a locale string
+    :param return_city: True or False, if True then return exemplar city (location)
+                        for the time zone
     :return: the localized timezone name using location format
+
     """
     dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
     locale = Locale.parse(locale)
@@ -459,7 +479,7 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
     if territory not in locale.territories:
         territory = 'ZZ' # invalid/unknown
     territory_name = locale.territories[territory]
-    if territory and len(get_global('territory_zones').get(territory, [])) == 1:
+    if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1:
         return region_format % (territory_name)
 
     # Otherwise, include the city in the output
@@ -476,6 +496,8 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
         else:
             city_name = zone.replace('_', ' ')
 
+    if return_city:
+        return city_name
     return region_format % (fallback_format % {
         '0': city_name,
         '1': territory_name
@@ -483,13 +505,15 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME):
 
 
 def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
-                      locale=LC_TIME, zone_variant=None):
+                      locale=LC_TIME, zone_variant=None, return_zone=False):
     r"""Return the localized display name for the given timezone. The timezone
     may be specified using a ``datetime`` or `tzinfo` object.
 
     >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles'))
     >>> get_timezone_name(dt, locale='en_US')
     u'Pacific Standard Time'
+    >>> get_timezone_name(dt, locale='en_US', return_zone=True)
+    'America/Los_Angeles'
     >>> get_timezone_name(dt, width='short', locale='en_US')
     u'PST'
 
@@ -548,6 +572,8 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
                            values are valid: ``'generic'``, ``'daylight'`` and
                            ``'standard'``.
     :param locale: the `Locale` object, or a locale string
+    :param return_zone: True or False. If true then function
+                        returns long time zone ID
     """
     dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
     locale = Locale.parse(locale)
@@ -572,7 +598,8 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
 
     # Get the canonical time-zone code
     zone = get_global('zone_aliases').get(zone, zone)
-
+    if return_zone:
+        return zone
     info = locale.time_zones.get(zone, {})
     # Try explicitly translated zone names first
     if width in info:
@@ -1167,7 +1194,7 @@ class DateTimeFormat(object):
             return self.format_frac_seconds(num)
         elif char == 'A':
             return self.format_milliseconds_in_day(num)
-        elif char in ('z', 'Z', 'v', 'V'):
+        elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'):
             return self.format_timezone(char, num)
         else:
             raise KeyError('Unsupported date/time field %r' % char)
@@ -1269,11 +1296,17 @@ class DateTimeFormat(object):
         return self.format(msecs, num)
 
     def format_timezone(self, char, num):
-        width = {3: 'short', 4: 'long'}[max(3, num)]
+        width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)]
         if char == 'z':
             return get_timezone_name(self.value, width, locale=self.locale)
         elif char == 'Z':
+            if num == 5:
+                return get_timezone_gmt(self.value, width, locale=self.locale, return_z=True)
             return get_timezone_gmt(self.value, width, locale=self.locale)
+        elif char == 'O':
+            if num == 4:
+                return get_timezone_gmt(self.value, width, locale=self.locale)
+        # TODO: To add support for O:1
         elif char == 'v':
             return get_timezone_name(self.value.tzinfo, width,
                                      locale=self.locale)
@@ -1281,7 +1314,29 @@ class DateTimeFormat(object):
             if num == 1:
                 return get_timezone_name(self.value.tzinfo, width,
                                          uncommon=True, locale=self.locale)
+            elif num == 2:
+                return get_timezone_name(self.value.tzinfo, locale=self.locale, return_zone=True)
+            elif num == 3:
+                return get_timezone_location(self.value.tzinfo, locale=self.locale, return_city=True)
             return get_timezone_location(self.value.tzinfo, locale=self.locale)
+        # Included additional elif condition to add support for 'Xx' in timezone format
+        elif char == 'X':
+            if num == 1:
+                return get_timezone_gmt(self.value, width='iso8601_short', locale=self.locale,
+                                        return_z=True)
+            elif num in (2, 4):
+                return get_timezone_gmt(self.value, width='short', locale=self.locale,
+                                        return_z=True)
+            elif num in (3, 5):
+                return get_timezone_gmt(self.value, width='iso8601', locale=self.locale,
+                                        return_z=True)
+        elif char == 'x':
+            if num == 1:
+                return get_timezone_gmt(self.value, width='iso8601_short', locale=self.locale)
+            elif num in (2, 4):
+                return get_timezone_gmt(self.value, width='short', locale=self.locale)
+            elif num in (3, 5):
+                return get_timezone_gmt(self.value, width='iso8601', locale=self.locale)
 
     def format(self, value, length):
         return ('%%0%dd' % length) % value
@@ -1325,24 +1380,25 @@ class DateTimeFormat(object):
 
 
 PATTERN_CHARS = {
-    'G': [1, 2, 3, 4, 5],                                           # era
-    'y': None, 'Y': None, 'u': None,                                # year
-    'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4],                           # quarter
-    'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5],                     # month
-    'w': [1, 2], 'W': [1],                                          # week
-    'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None,               # day
-    'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5],  # week day
-    'a': [1],                                                       # period
-    'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2],             # hour
-    'm': [1, 2],                                                    # minute
-    's': [1, 2], 'S': None, 'A': None,                              # second
-    'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 4]  # zone
+    'G': [1, 2, 3, 4, 5],                                               # era
+    'y': None, 'Y': None, 'u': None,                                    # year
+    'Q': [1, 2, 3, 4], 'q': [1, 2, 3, 4],                               # quarter
+    'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5],                         # month
+    'w': [1, 2], 'W': [1],                                              # week
+    'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None,                   # day
+    'E': [1, 2, 3, 4, 5], 'e': [1, 2, 3, 4, 5], 'c': [1, 3, 4, 5],      # week day
+    'a': [1],                                                           # period
+    'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2],                 # hour
+    'm': [1, 2],                                                        # minute
+    's': [1, 2], 'S': None, 'A': None,                                  # second
+    'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4],  # zone
+    'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5]       # zone
 }
 
 #: The pattern characters declared in the Date Field Symbol Table
 #: (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
 #: in order of decreasing magnitude.
-PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZvV"
+PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx"
 
 _pattern_cache = {}
 
diff --git a/babel/messages/extract.py b/babel/messages/extract.py
index c2dcd5b..8183d52 100644
--- a/babel/messages/extract.py
+++ b/babel/messages/extract.py
@@ -38,8 +38,7 @@ DEFAULT_KEYWORDS = {
     'dgettext': (2,),
     'dngettext': (2, 3),
     'N_': None,
-    'pgettext': ((1, 'c'), 2),
-    'npgettext': ((1, 'c'), 2, 3)
+    'pgettext': ((1, 'c'), 2)
 }
 
 DEFAULT_MAPPING = [('**.py', 'python')]
diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py
index 9bb46bb..73abf4f 100644
--- a/babel/messages/frontend.py
+++ b/babel/messages/frontend.py
@@ -254,8 +254,6 @@ class extract_messages(Command):
         ('input-paths=', None,
          'files or directories that should be scanned for messages. Separate multiple '
          'files or directories with commas(,)'),
-        ('input-dirs=', None,  # TODO (3.x): Remove me.
-         'alias for input-paths (does allow files as well as directories).'),
     ]
     boolean_options = [
         'no-default-keywords', 'no-location', 'omit-header', 'no-wrap',
@@ -273,7 +271,6 @@ class extract_messages(Command):
         self.no_location = False
         self.omit_header = False
         self.output_file = None
-        self.input_dirs = None
         self.input_paths = None
         self.width = None
         self.no_wrap = False
@@ -287,14 +284,6 @@ class extract_messages(Command):
         self.strip_comments = False
 
     def finalize_options(self):
-        if self.input_dirs:
-            if not self.input_paths:
-                self.input_paths = self.input_dirs
-            else:
-                raise DistutilsOptionError(
-                    'input-dirs and input-paths are mutually exclusive'
-                )
-
         if self.no_default_keywords and not self.keywords:
             raise DistutilsOptionError('you must specify new keywords if you '
                                        'disable the default ones')
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index 1fb2c1c..226ac1c 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -427,13 +427,13 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
                 prefix, _normalize(message.string or '', prefix)
             ))
 
-    sort_by = None
+    messages = list(catalog)
     if sort_output:
-        sort_by = "message"
+        messages.sort()
     elif sort_by_file:
-        sort_by = "location"
+        messages.sort(lambda x,y: cmp(x.locations, y.locations))
 
-    for message in _sort_messages(catalog, sort_by=sort_by):
+    for message in messages:
         if not message.id: # This is the header "message"
             if omit_header:
                 continue
@@ -453,7 +453,7 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
 
         if not no_location:
             locs = []
-            for filename, lineno in sorted(message.locations):
+            for filename, lineno in message.locations:
                 if lineno:
                     locs.append(u'%s:%d' % (filename.replace(os.sep, '/'), lineno))
                 else:
@@ -474,29 +474,8 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
         _write('\n')
 
     if not ignore_obsolete:
-        for message in _sort_messages(
-            catalog.obsolete.values(),
-            sort_by=sort_by
-        ):
+        for message in catalog.obsolete.values():
             for comment in message.user_comments:
                 _write_comment(comment)
             _write_message(message, prefix='#~ ')
             _write('\n')
-
-
-def _sort_messages(messages, sort_by):
-    """
-    Sort the given message iterable by the given criteria.
-
-    Always returns a list.
-
-    :param messages: An iterable of Messages.
-    :param sort_by: Sort by which criteria? Options are `message` and `location`.
-    :return: list[Message]
-    """
-    messages = list(messages)
-    if sort_by == "message":
-        messages.sort()
-    elif sort_by == "location":
-        messages.sort(key=lambda m: m.locations)
-    return messages


Test patch:
diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py
index 66c8cd9..fa03207 100644
--- a/tests/messages/test_extract.py
+++ b/tests/messages/test_extract.py
@@ -132,28 +132,6 @@ msg2 = ngettext('elvis',
                           (3, 'ngettext', (u'elvis', u'elvises', None), [])],
                          messages)
 
-    def test_npgettext(self):
-        buf = BytesIO(b"""\
-msg1 = npgettext('Strings','pylon',
-                'pylons', count)
-msg2 = npgettext('Strings','elvis',
-                'elvises',
-                 count)
-""")
-        messages = list(extract.extract_python(buf, ('npgettext',), [], {}))
-        self.assertEqual([(1, 'npgettext', (u'Strings', u'pylon', u'pylons', None), []),
-                          (3, 'npgettext', (u'Strings', u'elvis', u'elvises', None), [])],
-                          messages)
-        buf = BytesIO(b"""\
-msg = npgettext('Strings', 'pylon',  # TRANSLATORS: shouldn't be
-                'pylons', # TRANSLATORS: seeing this
-                count)
-""")
-        messages = list(extract.extract_python(buf, ('npgettext',),
-                                               ['TRANSLATORS:'], {}))
-        self.assertEqual([(1, 'npgettext', (u'Strings', u'pylon', u'pylons', None), [])],
-                         messages)
-
     def test_triple_quoted_strings(self):
         buf = BytesIO(b"""\
 msg1 = _('''pylons''')
diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py
index 42e3e38..fc1efad 100644
--- a/tests/messages/test_frontend.py
+++ b/tests/messages/test_frontend.py
@@ -132,19 +132,6 @@ class ExtractMessagesTestCase(unittest.TestCase):
 
         self.assertEqual([this_dir, self.datadir], self.cmd.input_paths)
 
-    def test_input_dirs_is_alias_for_input_paths(self):
-        self.cmd.input_dirs = this_dir
-        self.cmd.output_file = self._pot_file()
-        self.cmd.finalize_options()
-        # Gets listified in `finalize_options`:
-        assert self.cmd.input_paths == [self.cmd.input_dirs]
-
-    def test_input_dirs_is_mutually_exclusive_with_input_paths(self):
-        self.cmd.input_dirs = this_dir
-        self.cmd.input_paths = this_dir
-        self.cmd.output_file = self._pot_file()
-        self.assertRaises(DistutilsOptionError, self.cmd.finalize_options)
-
     def test_extraction_with_default_mapping(self):
         self.cmd.copyright_holder = 'FooBar, Inc.'
         self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py
index f375a1a..4e991c6 100644
--- a/tests/messages/test_pofile.py
+++ b/tests/messages/test_pofile.py
@@ -513,15 +513,6 @@ msgstr[0] "Voh"
 msgstr[1] "Voeh"''' in value
         assert value.find(b'msgid ""') < value.find(b'msgid "bar"') < value.find(b'msgid "foo"')
 
-    def test_file_sorted_po(self):
-        catalog = Catalog()
-        catalog.add(u'bar', locations=[('utils.py', 3)])
-        catalog.add((u'foo', u'foos'), (u'Voh', u'Voeh'), locations=[('main.py', 1)])
-        buf = BytesIO()
-        pofile.write_po(buf, catalog, sort_by_file=True)
-        value = buf.getvalue().strip()
-        assert value.find(b'main.py') < value.find(b'utils.py')
-
     def test_file_with_no_lineno(self):
         catalog = Catalog()
         catalog.add(u'bar', locations=[('utils.py', None)],
diff --git a/tests/test_dates.py b/tests/test_dates.py
index e93fa40..5155c0c 100644
--- a/tests/test_dates.py
+++ b/tests/test_dates.py
@@ -250,6 +250,129 @@ class FormatDatetimeTestCase(unittest.TestCase):
         formatted_string = dates.format_datetime(epoch, format='long', locale='en_US')
         self.assertEqual(u'April 1, 2012 at 3:30:29 PM +0000', formatted_string)
 
+    def test_timezone_formats(self):
+        dt = datetime(2016, 1, 13, 7, 8, 35)
+        tz = dates.get_timezone('America/Los_Angeles')
+        dt = tz.localize(dt)
+        formatted_string = dates.format_datetime(dt, 'z', locale='en')
+        self.assertEqual(u'PST', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'zz', locale='en')
+        self.assertEqual(u'PST', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'zzz', locale='en')
+        self.assertEqual(u'PST', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'zzzz', locale='en')
+        self.assertEqual(u'Pacific Standard Time', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'Z', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZ', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZ', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZ', locale='en')
+        self.assertEqual(u'GMT-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZZ', locale='en')
+        self.assertEqual(u'-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'OOOO', locale='en')
+        self.assertEqual(u'GMT-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VV', locale='en')
+        self.assertEqual(u'America/Los_Angeles', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VVV', locale='en')
+        self.assertEqual(u'Los Angeles', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'X', locale='en')
+        self.assertEqual(u'-08', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XX', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXX', locale='en')
+        self.assertEqual(u'-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXX', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXXX', locale='en')
+        self.assertEqual(u'-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'x', locale='en')
+        self.assertEqual(u'-08', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xx', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxx', locale='en')
+        self.assertEqual(u'-08:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxx', locale='en')
+        self.assertEqual(u'-0800', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxxx', locale='en')
+        self.assertEqual(u'-08:00', formatted_string)
+        dt = datetime(2016, 1, 13, 7, 8, 35)
+        tz = dates.get_timezone('UTC')
+        dt = tz.localize(dt)
+        formatted_string = dates.format_datetime(dt, 'Z', locale='en')
+        self.assertEqual(u'+0000', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZ', locale='en')
+        self.assertEqual(u'+0000', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZ', locale='en')
+        self.assertEqual(u'+0000', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZ', locale='en')
+        self.assertEqual(u'GMT+00:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZZ', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'OOOO', locale='en')
+        self.assertEqual(u'GMT+00:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VV', locale='en')
+        self.assertEqual(u'Etc/GMT', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VVV', locale='en')
+        self.assertEqual(u'GMT', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'X', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XX', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXX', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXX', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXXX', locale='en')
+        self.assertEqual(u'Z', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'x', locale='en')
+        self.assertEqual(u'+00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xx', locale='en')
+        self.assertEqual(u'+0000', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxx', locale='en')
+        self.assertEqual(u'+00:00', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxx', locale='en')
+        self.assertEqual(u'+0000', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxxx', locale='en')
+        self.assertEqual(u'+00:00', formatted_string)
+        dt = datetime(2016, 1, 13, 7, 8, 35)
+        tz = dates.get_timezone('Asia/Kolkata')
+        dt = tz.localize(dt)
+        formatted_string = dates.format_datetime(dt, 'zzzz', locale='en')
+        self.assertEqual(u'India Standard Time', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZ', locale='en')
+        self.assertEqual(u'GMT+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'ZZZZZ', locale='en')
+        self.assertEqual(u'+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'OOOO', locale='en')
+        self.assertEqual(u'GMT+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VV', locale='en')
+        self.assertEqual(u'Asia/Calcutta', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'VVV', locale='en')
+        self.assertEqual(u'Kolkata', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'X', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XX', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXX', locale='en')
+        self.assertEqual(u'+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXX', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'XXXXX', locale='en')
+        self.assertEqual(u'+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'x', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xx', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxx', locale='en')
+        self.assertEqual(u'+05:30', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxx', locale='en')
+        self.assertEqual(u'+0530', formatted_string)
+        formatted_string = dates.format_datetime(dt, 'xxxxx', locale='en')
+        self.assertEqual(u'+05:30', formatted_string)
+
 
 class FormatTimeTestCase(unittest.TestCase):
 
@@ -393,12 +516,13 @@ def test_get_time_format():
 def test_get_timezone_gmt():
     dt = datetime(2007, 4, 1, 15, 30)
     assert dates.get_timezone_gmt(dt, locale='en') == u'GMT+00:00'
-
+    assert dates.get_timezone_gmt(dt, locale='en', return_z=True) == 'Z'
+    assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == u'+00'
     tz = timezone('America/Los_Angeles')
     dt = tz.localize(datetime(2007, 4, 1, 15, 30))
     assert dates.get_timezone_gmt(dt, locale='en') == u'GMT-07:00'
     assert dates.get_timezone_gmt(dt, 'short', locale='en') == u'-0700'
-
+    assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == u'-07'
     assert dates.get_timezone_gmt(dt, 'long', locale='fr_FR') == u'UTC-07:00'
 
 
@@ -406,6 +530,11 @@ def test_get_timezone_location():
     tz = timezone('America/St_Johns')
     assert (dates.get_timezone_location(tz, locale='de_DE') ==
             u"Kanada (St. John\u2019s) Zeit")
+    assert (dates.get_timezone_location(tz, locale='en') ==
+            u'Canada (St. John’s) Time')
+    assert (dates.get_timezone_location(tz, locale='en', return_city=True) ==
+            u'St. John’s')
+
     tz = timezone('America/Mexico_City')
     assert (dates.get_timezone_location(tz, locale='de_DE') ==
             u'Mexiko (Mexiko-Stadt) Zeit')
@@ -419,6 +548,8 @@ def test_get_timezone_name():
     dt = time(15, 30, tzinfo=timezone('America/Los_Angeles'))
     assert (dates.get_timezone_name(dt, locale='en_US') ==
             u'Pacific Standard Time')
+    assert (dates.get_timezone_name(dt, locale='en_US', return_zone=True) ==
+            u'America/Los_Angeles')
     assert dates.get_timezone_name(dt, width='short', locale='en_US') == u'PST'
 
     tz = timezone('America/Los_Angeles')