Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for angles.py dec2hp() and hp2...() #157

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading