Repository: GeoscienceAustralia/GeodePy

Issue:
Integer Negative Zero issue in DMSAngle and DDMAngle classes
When entering the degree component of a DMSAngle(degree, minute, second) or DDMAngle(degree, minute) object, if the degree component is between -1 and 0 (i.e. DMSAngle(-0,12,14)) this will incorrectly convert this to a positive value. This is because python interprets the input '-0' as an int with value 0 and does not retain the sign of this variable.

Discussion:

Patch:
diff --git a/geodepy/convert.py b/geodepy/convert.py
index ed91357..988581a 100755
--- a/geodepy/convert.py
+++ b/geodepy/convert.py
@@ -23,13 +23,33 @@ def hp2dec(hp):
 
 
 class DMSAngle(object):
-    def __init__(self, degree=0, minute=0, second=0):
-        self.degree = int(degree)
+    def __init__(self, degree=0, minute=0, second=0.0):
+        # Set sign of object based on sign of any variable
+        if degree == 0:
+            if str(degree)[0] == '-':
+                self.sign = -1
+            elif minute < 0:
+                self.sign = -1
+            elif second < 0:
+                self.sign = -1
+            else:
+                self.sign = 1
+        elif degree > 0:
+            self.sign = 1
+        else:  # degree < 0
+            self.sign = -1
+        self.degree = abs(int(degree))
         self.minute = abs(int(minute))
         self.second = abs(second)
 
     def __repr__(self):
-        return '{DMSAngle: ' + str(self.degree) + 'd ' + str(self.minute) + 'm ' + str(self.second) + 's}'
+        if self.sign == -1:
+            signsymbol = '-'
+        elif self.sign == 1:
+            signsymbol = '+'
+        else:
+            signsymbol = 'error'
+        return '{DMSAngle: ' + signsymbol + str(self.degree) + 'd ' + str(self.minute) + 'm ' + str(self.second) + 's}'
 
     def __add__(self, other):
         return dec2dms(self.dec() + other.dec())
@@ -56,13 +76,19 @@ class DMSAngle(object):
             raise TypeError('Multiply only defined between DMSAngle Object and Int or Float')
 
     def __truediv__(self, other):
-        return dec2dms(self.dec() / other)
+        try:
+            return dec2dms(self.dec() / other)
+        except TypeError:
+            raise TypeError('Division only defined between DMSAngle Object and Int or Float')
 
     def __abs__(self):
-        return DMSAngle(abs(self.degree), self.minute, self.second)
+        return DMSAngle(self.degree, self.minute, self.second)
 
     def __neg__(self):
-        return DMSAngle(-self.degree, self.minute, self.second)
+        if self.sign == 1:
+            return DMSAngle(-self.degree, -self.minute, -self.second)
+        else:  # sign == -1
+            return DMSAngle(self.degree, self.minute, self.second)
 
     def __eq__(self, other):
         return self.dec() == other.dec()
@@ -77,28 +103,43 @@ class DMSAngle(object):
         return self.dec() > other.dec()
 
     def dec(self):
-        if self.degree >= 0:
-            return self.degree + (self.minute / 60) + (self.second / 3600)
-        else:
-            return -(abs(self.degree) + (self.minute / 60) + (self.second / 3600))
+        return self.sign * (self.degree + (self.minute / 60) + (self.second / 3600))
 
     def hp(self):
-        if self.degree >= 0:
-            return self.degree + (self.minute / 100) + (self.second / 10000)
-        else:
-            return -(abs(self.degree) + (self.minute / 100) + (self.second / 10000))
+        return self.sign * (self.degree + (self.minute / 100) + (self.second / 10000))
 
     def ddm(self):
-        return DDMAngle(self.degree, self.minute + (self.second/60))
+        if self.sign == 1:
+            return DDMAngle(self.degree, self.minute + (self.second/60))
+        else:  # sign == -1
+            return -DDMAngle(self.degree, self.minute + (self.second/60))
 
 
 class DDMAngle(object):
-    def __init__(self, degree, minute):
-        self.degree = int(degree)
+    def __init__(self, degree=0, minute=0.0):
+        # Set sign of object based on sign of any variable
+        if degree == 0:
+            if str(degree)[0] == '-':
+                self.sign = -1
+            elif minute < 0:
+                self.sign = -1
+            else:
+                self.sign = 1
+        elif degree > 0:
+            self.sign = 1
+        else:  # degree < 0
+            self.sign = -1
+        self.degree = abs(int(degree))
         self.minute = abs(minute)
 
     def __repr__(self):
-        return '{DDMAngle: ' + str(self.degree) + 'd ' + str(self.minute) + 'm}'
+        if self.sign == -1:
+            signsymbol = '-'
+        elif self.sign == 1:
+            signsymbol = '+'
+        else:
+            signsymbol = 'error'
+        return '{DDMAngle: ' + signsymbol + str(self.degree) + 'd ' + str(self.minute) + 'm}'
 
     def __add__(self, other):
         return dec2ddm(self.dec() + other.dec())
@@ -119,13 +160,19 @@ class DDMAngle(object):
             raise TypeError('Multiply only defined between DMSAngle Object and Int or Float')
 
     def __truediv__(self, other):
-        return dec2ddm(self.dec() / other)
+        try:
+            return dec2ddm(self.dec() / other)
+        except TypeError:
+            raise TypeError('Division only defined between DMSAngle Object and Int or Float')
 
     def __abs__(self):
-        return DDMAngle(abs(self.degree), self.minute)
+        return DDMAngle(self.degree, self.minute)
 
     def __neg__(self):
-        return DDMAngle(-self.degree, self.minute)
+        if self.sign == 1:
+            return DDMAngle(-self.degree, -self.minute)
+        else:  # sign == -1
+            return DDMAngle(self.degree, self.minute)
 
     def __eq__(self, other):
         return self.dec() == other.dec()
@@ -140,21 +187,15 @@ class DDMAngle(object):
         return self.dec() > other.dec()
 
     def dec(self):
-        if self.degree >= 0:
-            return self.degree + (self.minute / 60)
-        else:
-            return -(abs(self.degree) + (self.minute / 60))
+        return self.sign * (self.degree + (self.minute / 60))
 
     def hp(self):
         minute_int, second = divmod(self.minute, 1)
-        if self.degree >= 0:
-            return self.degree + (minute_int / 100) + (second * 0.006)
-        else:
-            return -(abs(self.degree) + (minute_int / 100) + (second * 0.006))
+        return self.sign * (self.degree + (minute_int / 100) + (second * 0.006))
 
     def dms(self):
         minute_int, second = divmod(self.minute, 1)
-        return DMSAngle(self.degree, minute_int, second * 60)
+        return self.sign * DMSAngle(self.degree, minute_int, second * 60)
 
 
 def dec2dms(dec):


Test patch:
diff --git a/geodepy/tests/test_convert.py b/geodepy/tests/test_convert.py
index ae8d265..b0cade3 100644
--- a/geodepy/tests/test_convert.py
+++ b/geodepy/tests/test_convert.py
@@ -3,17 +3,27 @@ from geodepy.convert import dec2hp, hp2dec, DMSAngle, DDMAngle, dec2dms, dec2ddm
 
 dec_ex = 123.74875
 dec_ex2 = 12.575
+dec_ex3 = -12.575
+dec_ex4 = 0.0525
+dec_ex5 = 0.005
+
 hp_ex = 123.44555
 hp_ex2 = 12.3430
 hp_ex3 = -12.3430
+hp_ex4 = 0.0309
+hp_ex5 = 0.0018
 
 dms_ex = DMSAngle(123, 44, 55.5)
 dms_ex2 = DMSAngle(12, 34, 30)
 dms_ex3 = DMSAngle(-12, -34, -30)
+dms_ex4 = DMSAngle(0, 3, 9)
+dms_ex5 = DMSAngle(0, 0, 18)
 
 ddm_ex = DDMAngle(123, 44.925)
 ddm_ex2 = DDMAngle(12, 34.5)
 ddm_ex3 = DDMAngle(-12, -34.5)
+ddm_ex4 = DDMAngle(0, 3.15)
+ddm_ex5 = DDMAngle(0, 0.3)
 
 
 class TestConvert(unittest.TestCase):
@@ -33,6 +43,27 @@ class TestConvert(unittest.TestCase):
         self.assertEqual(hp_ex3, dms_ex3.hp())
         self.assertEqual(ddm_ex, dms_ex.ddm())
 
+        # Test DMSAngle Sign Conventions
+        self.assertEqual(-dec_ex, DMSAngle(-dms_ex.degree, dms_ex.minute, dms_ex.second).dec())
+        self.assertEqual(dec_ex, DMSAngle(dms_ex.degree, -dms_ex.minute, -dms_ex.second).dec())
+        self.assertAlmostEqual(-dec_ex4, DMSAngle(0, -dms_ex4.minute, dms_ex4.second).dec(), 9)
+        self.assertAlmostEqual(dec_ex4, DMSAngle(0, dms_ex4.minute, dms_ex4.second).dec(), 9)
+        self.assertEqual(-dec_ex5, DMSAngle(0, 0, -dms_ex5.second).dec())
+        self.assertEqual(dec_ex5, DMSAngle(0, 0, dms_ex5.second).dec())
+        self.assertEqual(dms_ex.sign, 1)
+        self.assertEqual(-dms_ex.sign, -1)
+        self.assertEqual(dms_ex4.sign, 1)
+        self.assertEqual(-dms_ex4.sign, -1)
+        self.assertEqual(dms_ex5.sign, 1)
+        self.assertEqual(-dms_ex5.sign, -1)
+        self.assertEqual(DMSAngle(-1, 2, 3).sign, -1)
+        self.assertEqual(DMSAngle(1, -2, 3).sign, 1)
+        self.assertEqual(DMSAngle(1, 2, -3).sign, 1)
+        self.assertEqual(DMSAngle(0, -1, 2).sign, -1)
+        self.assertEqual(DMSAngle(0, 0, -3).sign, -1)
+        self.assertEqual(DMSAngle(-0, 1, 2).sign, 1)
+        self.assertEqual(DMSAngle(-0.0, 1, 2).sign, -1)
+
         # Test DMSAngle Overloads
         self.assertEqual(dec_ex + dec_ex2, (dms_ex + dms_ex2).dec())
         self.assertEqual(dec_ex - dec_ex2, (dms_ex - dms_ex2).dec())
@@ -55,6 +86,8 @@ class TestConvert(unittest.TestCase):
             dms_ex * 'a'
         with self.assertRaises(TypeError):
             'a' * dms_ex
+        with self.assertRaises(TypeError):
+            dms_ex / 'a'
 
     def test_DDMAngle(self):
         # Test DDMAngle Methods
@@ -63,6 +96,24 @@ class TestConvert(unittest.TestCase):
         self.assertEqual(dms_ex, ddm_ex.dms())
         self.assertEqual(hp_ex3, ddm_ex3.hp())
 
+        # Test DMSAngle Sign Conventions
+        self.assertEqual(-dec_ex, DDMAngle(-dms_ex.degree, ddm_ex.minute).dec())
+        self.assertEqual(dec_ex, DDMAngle(dms_ex.degree, -ddm_ex.minute).dec())
+        self.assertAlmostEqual(-dec_ex4, DDMAngle(0, -ddm_ex4.minute).dec(), 9)
+        self.assertAlmostEqual(dec_ex4, DDMAngle(0, ddm_ex4.minute).dec(), 9)
+        self.assertEqual(ddm_ex.sign, 1)
+        self.assertEqual(-ddm_ex.sign, -1)
+        self.assertEqual(ddm_ex4.sign, 1)
+        self.assertEqual(-ddm_ex4.sign, -1)
+        self.assertEqual(ddm_ex5.sign, 1)
+        self.assertEqual(-ddm_ex5.sign, -1)
+        self.assertEqual(DDMAngle(-1, 2).sign, -1)
+        self.assertEqual(DDMAngle(1, -2).sign, 1)
+        self.assertEqual(DDMAngle(1, 2).sign, 1)
+        self.assertEqual(DDMAngle(0, -1).sign, -1)
+        self.assertEqual(DDMAngle(-0, 1).sign, 1)
+        self.assertEqual(DDMAngle(-0.0, 1).sign, -1)
+
         # Test DDMAngle Overloads
         self.assertEqual(dec_ex + dec_ex2, (ddm_ex + ddm_ex2).dec())
         self.assertEqual(dec_ex - dec_ex2, (ddm_ex - ddm_ex2).dec())
@@ -85,6 +136,8 @@ class TestConvert(unittest.TestCase):
             ddm_ex * 'a'
         with self.assertRaises(TypeError):
             'a' * ddm_ex
+        with self.assertRaises(TypeError):
+            ddm_ex / 'a'
 
     def test_dec2dms(self):
         self.assertEqual(dms_ex, dec2dms(dec_ex))
@@ -99,7 +152,8 @@ class TestConvert(unittest.TestCase):
         self.assertEqual(dms_ex.minute, hp2dms(hp_ex).minute)
         self.assertAlmostEqual(dms_ex.second, hp2dms(hp_ex).second, 10)
 
-        self.assertEqual(-dms_ex.degree, hp2dms(-hp_ex).degree)
+        self.assertEqual(-dms_ex.sign, hp2dms(-hp_ex).sign)
+        self.assertEqual(dms_ex.degree, hp2dms(-hp_ex).degree)
         self.assertEqual(dms_ex.minute, hp2dms(-hp_ex).minute)
         self.assertAlmostEqual(dms_ex.second, hp2dms(-hp_ex).second, 10)