Repository: astropy/astropy

Issue:
Bug in rounded negative sexagesimal output
There is a bug in sexagesimal_to_string in astropy.coordinates.angle_utilities.
If a coordinate is negative, and is rounded by carrying a 60 up to the next field, the
sign is not handled correctly.   For example:
```python
>>> from astropy import coordinates as coo

>>> coo.angle_utilities.sexagesimal_to_string((-0.,59,59))
u'-0:59:59'
>>> coo.angle_utilities.sexagesimal_to_string((-0.,59,59),precision=0)
u'1:00:00'
>>> coo.angle_utilities.sexagesimal_to_string((-1.,59,59),precision=0)
u'0:00:00'
>>> coo.angle_utilities.sexagesimal_to_string((-2.,59,59),precision=0)
u'-1:00:00'
```
Apparently the value of the 'degrees' field is incremented even if it's negative.

This can be handled by splitting out the sign explicitly and making everything
positive at first (extending what is done presently with the minute and second fields
to the degree field as well), doing the formatting, and then tacking on the minus or 
plus sign at the end as needed.  

It might be worth carrying a sign explicitly in the sexagesimal representation.  This
other problem then could be avoided:
```python
>>> coo.angle_utilities.sexagesimal_to_string((-0,30,30),precision=0)
u'0:30:30'
```
... the integer -0 has the sign stripped away.

EDIT: Code formatting.

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 06782ec985..a177c50ef0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -955,6 +955,9 @@ astropy.coordinates
 - Working around a bug in Numpy 1.14.0 that broke some coordinate
   transformations. [#7105]
 
+- Fixed a bug where negative angles could be rounded wrongly when converting
+  to a string with seconds omitted. [#7148]
+
 astropy.cosmology
 ^^^^^^^^^^^^^^^^^
 
diff --git a/astropy/coordinates/angle_utilities.py b/astropy/coordinates/angle_utilities.py
index e23a177b20..3e970f98f7 100644
--- a/astropy/coordinates/angle_utilities.py
+++ b/astropy/coordinates/angle_utilities.py
@@ -520,14 +520,15 @@ def sexagesimal_to_string(values, precision=None, pad=False, sep=(':',),
     interface to this functionality.
     """
 
-    # If the coordinates are negative, we need to take the absolute value of
-    # the (arc)minutes and (arc)seconds. We need to use np.abs because abs(-0)
-    # is -0.
-    values = (values[0], np.abs(values[1]), np.abs(values[2]))
+    # Check to see if values[0] is negative, using np.copysign to handle -0
+    sign = np.copysign(1.0, values[0])
+    # If the coordinates are negative, we need to take the absolute values.
+    # We use np.abs because abs(-0) is -0
+    # TODO: Is this true? (MHvK, 2018-02-01: not on my system)
+    values = [np.abs(value) for value in values]
 
     if pad:
-        # Check to see if values[0] is negative, using np.copysign to handle -0
-        if np.copysign(1.0, values[0]) == -1:
+        if sign == -1:
             pad = 3
         else:
             pad = 2
@@ -565,17 +566,16 @@ def sexagesimal_to_string(values, precision=None, pad=False, sep=(':',),
     else:
         rounding_thresh = 60.0 - (10.0 ** -precision)
 
-    values = list(values)
     if fields == 3 and values[2] >= rounding_thresh:
         values[2] = 0.0
         values[1] += 1.0
     elif fields < 3 and values[2] >= 30.0:
         values[1] += 1.0
 
-    if fields >= 2 and int(values[1]) >= 60.0:
+    if fields >= 2 and values[1] >= 60.0:
         values[1] = 0.0
         values[0] += 1.0
-    elif fields < 2 and int(values[1]) >= 30.0:
+    elif fields < 2 and values[1] >= 30.0:
         values[0] += 1.0
 
     literal = []
@@ -594,7 +594,8 @@ def sexagesimal_to_string(values, precision=None, pad=False, sep=(':',),
             last_value = '0' + last_value
         literal.append('{last_value}{sep[2]}')
     literal = ''.join(literal)
-    return literal.format(values[0], int(abs(values[1])), abs(values[2]),
+    return literal.format(np.copysign(values[0], sign),
+                          int(values[1]), values[2],
                           sep=sep, pad=pad,
                           last_value=last_value)
 


Test patch:
diff --git a/astropy/coordinates/tests/test_formatting.py b/astropy/coordinates/tests/test_formatting.py
index 0e7f78f50a..67610010e1 100644
--- a/astropy/coordinates/tests/test_formatting.py
+++ b/astropy/coordinates/tests/test_formatting.py
@@ -28,6 +28,11 @@ def test_to_string_precision():
     assert angle2.to_string(precision=1, unit=u.hour) == '-1h14m04.4s'
     assert angle2.to_string(precision=0, unit=u.hour) == '-1h14m04s'
 
+    # Regression test for #7141
+    angle3 = Angle(-0.5, unit=u.degree)
+    assert angle3.to_string(precision=0, fields=3) == '-0d30m00s'
+    assert angle3.to_string(precision=0, fields=2) == '-0d30m'
+    assert angle3.to_string(precision=0, fields=1) == '-1d'
 
 def test_to_string_decimal():