Skip to content

Commit

Permalink
Merge pull request #157 from philanderson71/fix-for-angles.py
Browse files Browse the repository at this point in the history
Fix for angles.py dec2hp() and hp2...()
  • Loading branch information
harry093 authored Nov 25, 2024
2 parents 01530cf + c9039c6 commit 6af0d51
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 14 deletions.
40 changes: 29 additions & 11 deletions geodepy/angles.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,11 +934,27 @@ def dec2hp(dec):
:type dec: float
:return: HP Notation (DDD.MMSSSS)
:rtype: float
"""
"""
minute, second = divmod(abs(dec) * 3600, 60)
degree, minute = divmod(minute, 60)
hp = degree + (minute / 100) + (second / 10000)
hp = round(hp, 16)

# floating point precision is 13 places for the variable 'dec' where values
# are between 256 and 512 degrees. Precision improves for smaller angles.
# In calculating the variable 'second' the precision is degraded by a factor of 3600
# Therefore 'second' should be rounded to 9 DP and tested for carry.
if round(second, 9) == 60:
second = 0
minute += 1

# to avoid precision issues with floating point operations
# a string will be built to represent a sexagesimal number and then converted to float
degree = f'{int(degree)}'
minute = f'{int(minute):02}'
second = f'{second:012.9f}'.rstrip('0').replace('.', '')

hp_string = f'{degree}.{minute}{second}'
hp = float(hp_string)

return hp if dec >= 0 else -hp


Expand Down Expand Up @@ -1015,18 +1031,20 @@ def hp2dec(hp):
"""
# Check if 1st and 3rd decimal place greater than 5 (invalid HP Notation)
hp = float(hp)
hp_dec_str = f'{hp:.17f}'.split('.')[1]
if int(hp_dec_str[0]) > 5:
hp_deg_str, hp_mmss_str = f'{hp:.13f}'.split('.')
if int(hp_mmss_str[0]) > 5:
raise ValueError(f'Invalid HP Notation: 1st decimal place greater '
f'than 5: {hp}')
if len(hp_dec_str) > 2:
if int(hp_dec_str[2]) > 5:
if len(hp_mmss_str) > 2:
if int(hp_mmss_str[2]) > 5:
raise ValueError(f'Invalid HP Notation: 3rd decimal place greater '
f'than 5: {hp}')
degmin, second = divmod(abs(hp) * 1000, 10)
degree, minute = divmod(degmin, 100)
dec = degree + (minute / 60) + (second / 360)
dec = round(dec, 16)
# parse string to avoid precision problems with floating point ops and base 10 numbers
deg = abs(int(hp_deg_str))
min = int(hp_mmss_str[:2])
sec = float(hp_mmss_str[2:4] + '.' + hp_mmss_str[4:])
dec = sec / 3600 + min / 60 + deg

return dec if hp >= 0 else -dec


Expand Down
67 changes: 64 additions & 3 deletions geodepy/tests/test_angles.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
from math import radians
import os
from math import radians, pi

from geodepy.angles import (DECAngle, HPAngle, GONAngle, DMSAngle, DDMAngle,
dec2hp, dec2hpa, dec2gon, dec2gona,
Expand Down Expand Up @@ -85,6 +86,31 @@


class TestConvert(unittest.TestCase):
def setUp(self):
self.testData = []
degreeValues = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256]
dec_places = 13
error = 10**-(dec_places - 4)
for deg in degreeValues:
for min in range(60):
for sec in range(60):
if sec:
hp_minus = float(f'{deg:4d}.{min:02d}{sec-1:02d}' + '9' * (dec_places - 4))
dec_minus = deg + (min / 60.0 + (sec - error) / 3600.0)
gon_minus = 400.0 / 360.0 * dec_minus
rad_minus = pi / 180.0 * dec_minus
self.testData.append([hp_minus, dec_minus, gon_minus, rad_minus])
hp = float(f'{deg:4d}.{min:02d}{sec:02d}')
hp_plus = float(f'{deg:4d}.{min:02d}{sec:02d}' + '0' * (dec_places - 5) + '1')
dec = deg + (min / 60.0 + sec / 3600.0)
gon = 400.0 / 360.0 * dec
rad = pi / 180.0 * dec
self.testData.append([hp, dec, gon, rad])
dec_plus = deg + (min / 60.0 + (sec + error) / 3600.0)
gon_plus = 400.0 / 360.0 * dec_plus
rad_plus = pi / 180.0 * dec_plus
self.testData.append([hp_plus, dec_plus, gon_plus, rad_plus])

def test_DECAngle(self):
# Test DECAngle Methods
for num, ex in enumerate(deca_exs):
Expand Down Expand Up @@ -465,6 +491,10 @@ def test_dec2hp(self):
for num, ex in enumerate(hp_exs):
self.assertAlmostEqual(ex, dec2hp(dec_exs[num]), 13)
self.assertAlmostEqual(-ex, dec2hp(-dec_exs[num]), 13)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(hp, dec2hp(dec), 13)
self.assertAlmostEqual(-hp, dec2hp(-dec), 13)

def test_dec2hpa(self):
for num, ex in enumerate(dec_exs):
Expand All @@ -475,6 +505,10 @@ def test_dec2gon(self):
for num, ex in enumerate(dec_exs):
self.assertAlmostEqual(dec2gon(ex), gon_exs[num], 13)
self.assertAlmostEqual(dec2gon(-ex), -gon_exs[num], 13)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(gon, dec2gon(dec), 13)
self.assertAlmostEqual(-gon, dec2gon(-dec), 13)

def test_dec2gona(self):
for num, ex in enumerate(dec_exs):
Expand All @@ -495,6 +529,13 @@ def test_hp2dec(self):
for num, ex in enumerate(dec_exs):
self.assertAlmostEqual(ex, hp2dec(hp_exs[num]), 13)
self.assertAlmostEqual(-ex, hp2dec(-hp_exs[num]), 13)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(dec, hp2dec(hp), 13)
self.assertAlmostEqual(-dec, hp2dec(-hp), 13)

self.assertAlmostEqual(0, hp2dec(0), 13)
self.assertAlmostEqual(258, hp2dec(258), 13)
self.assertAlmostEqual(hp2dec(hp_exs[0]) + hp2dec(hp_exs[1]),
dec_exs[0] + dec_exs[1], 13)
# Test that invalid minutes and seconds components raise errors
Expand Down Expand Up @@ -523,6 +564,10 @@ def test_hp2gon(self):
for num, ex in enumerate(hp_exs):
self.assertAlmostEqual(hp2gon(ex), gon_exs[num], 13)
self.assertAlmostEqual(hp2gon(-ex), -gon_exs[num], 13)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(gon, hp2gon(hp), 13)
self.assertAlmostEqual(-gon, hp2gon(-hp), 13)

def test_hp2gona(self):
for num, ex in enumerate(hp_exs):
Expand All @@ -531,8 +576,12 @@ def test_hp2gona(self):

def test_hp2rad(self):
for num, ex in enumerate(hp_exs):
self.assertEqual(hp2rad(ex), rad_exs[num])
self.assertEqual(hp2rad(-ex), -rad_exs[num])
self.assertAlmostEqual(hp2rad(ex), rad_exs[num], 15)
self.assertAlmostEqual(hp2rad(-ex), -rad_exs[num], 15)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(rad, hp2rad(hp), 15)
self.assertAlmostEqual(-rad, hp2rad(-hp), 15)

def test_hp2dms(self):
self.assertEqual(dms_ex.degree, hp2dms(hp_ex).degree)
Expand All @@ -552,6 +601,10 @@ def test_gon2dec(self):
for num, ex in enumerate(gon_exs):
self.assertAlmostEqual(gon2dec(ex), dec_exs[num], 14)
self.assertAlmostEqual(gon2dec(-ex), -dec_exs[num], 14)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(dec, gon2dec(gon), delta = 5.8e-14)
self.assertAlmostEqual(-dec, gon2dec(-gon), delta = 5.8e-14)

def test_gon2deca(self):
for num, ex in enumerate(gon_exs):
Expand All @@ -562,6 +615,10 @@ def test_gon2hp(self):
for num, ex in enumerate(gon_exs):
self.assertEqual(gon2hp(ex), hp_exs[num])
self.assertEqual(gon2hp(-ex), -hp_exs[num])
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(hp, gon2hp(gon), 13)
self.assertAlmostEqual(-hp, gon2hp(-gon), 13)

def test_gon2hpa(self):
for num, ex in enumerate(gon_exs):
Expand All @@ -572,6 +629,10 @@ def test_gon2rad(self):
for num, ex in enumerate(gon_exs):
self.assertAlmostEqual(gon2rad(ex), rad_exs[num], 15)
self.assertAlmostEqual(gon2rad(-ex), -rad_exs[num], 15)
for check in self.testData:
hp, dec, gon, rad = check
self.assertAlmostEqual(rad, gon2rad(gon), 13)
self.assertAlmostEqual(-rad, gon2rad(-gon), 13)

def test_gon2dms(self):
for num, ex in enumerate(gon_exs):
Expand Down

0 comments on commit 6af0d51

Please sign in to comment.