Skip to content

Commit 6af0d51

Browse files
authored
Merge pull request #157 from philanderson71/fix-for-angles.py
Fix for angles.py dec2hp() and hp2...()
2 parents 01530cf + c9039c6 commit 6af0d51

File tree

2 files changed

+93
-14
lines changed

2 files changed

+93
-14
lines changed

geodepy/angles.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -934,11 +934,27 @@ def dec2hp(dec):
934934
:type dec: float
935935
:return: HP Notation (DDD.MMSSSS)
936936
:rtype: float
937-
"""
937+
"""
938938
minute, second = divmod(abs(dec) * 3600, 60)
939939
degree, minute = divmod(minute, 60)
940-
hp = degree + (minute / 100) + (second / 10000)
941-
hp = round(hp, 16)
940+
941+
# floating point precision is 13 places for the variable 'dec' where values
942+
# are between 256 and 512 degrees. Precision improves for smaller angles.
943+
# In calculating the variable 'second' the precision is degraded by a factor of 3600
944+
# Therefore 'second' should be rounded to 9 DP and tested for carry.
945+
if round(second, 9) == 60:
946+
second = 0
947+
minute += 1
948+
949+
# to avoid precision issues with floating point operations
950+
# a string will be built to represent a sexagesimal number and then converted to float
951+
degree = f'{int(degree)}'
952+
minute = f'{int(minute):02}'
953+
second = f'{second:012.9f}'.rstrip('0').replace('.', '')
954+
955+
hp_string = f'{degree}.{minute}{second}'
956+
hp = float(hp_string)
957+
942958
return hp if dec >= 0 else -hp
943959

944960

@@ -1015,18 +1031,20 @@ def hp2dec(hp):
10151031
"""
10161032
# Check if 1st and 3rd decimal place greater than 5 (invalid HP Notation)
10171033
hp = float(hp)
1018-
hp_dec_str = f'{hp:.17f}'.split('.')[1]
1019-
if int(hp_dec_str[0]) > 5:
1034+
hp_deg_str, hp_mmss_str = f'{hp:.13f}'.split('.')
1035+
if int(hp_mmss_str[0]) > 5:
10201036
raise ValueError(f'Invalid HP Notation: 1st decimal place greater '
10211037
f'than 5: {hp}')
1022-
if len(hp_dec_str) > 2:
1023-
if int(hp_dec_str[2]) > 5:
1038+
if len(hp_mmss_str) > 2:
1039+
if int(hp_mmss_str[2]) > 5:
10241040
raise ValueError(f'Invalid HP Notation: 3rd decimal place greater '
10251041
f'than 5: {hp}')
1026-
degmin, second = divmod(abs(hp) * 1000, 10)
1027-
degree, minute = divmod(degmin, 100)
1028-
dec = degree + (minute / 60) + (second / 360)
1029-
dec = round(dec, 16)
1042+
# parse string to avoid precision problems with floating point ops and base 10 numbers
1043+
deg = abs(int(hp_deg_str))
1044+
min = int(hp_mmss_str[:2])
1045+
sec = float(hp_mmss_str[2:4] + '.' + hp_mmss_str[4:])
1046+
dec = sec / 3600 + min / 60 + deg
1047+
10301048
return dec if hp >= 0 else -dec
10311049

10321050

geodepy/tests/test_angles.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
2-
from math import radians
2+
import os
3+
from math import radians, pi
34

45
from geodepy.angles import (DECAngle, HPAngle, GONAngle, DMSAngle, DDMAngle,
56
dec2hp, dec2hpa, dec2gon, dec2gona,
@@ -85,6 +86,31 @@
8586

8687

8788
class TestConvert(unittest.TestCase):
89+
def setUp(self):
90+
self.testData = []
91+
degreeValues = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256]
92+
dec_places = 13
93+
error = 10**-(dec_places - 4)
94+
for deg in degreeValues:
95+
for min in range(60):
96+
for sec in range(60):
97+
if sec:
98+
hp_minus = float(f'{deg:4d}.{min:02d}{sec-1:02d}' + '9' * (dec_places - 4))
99+
dec_minus = deg + (min / 60.0 + (sec - error) / 3600.0)
100+
gon_minus = 400.0 / 360.0 * dec_minus
101+
rad_minus = pi / 180.0 * dec_minus
102+
self.testData.append([hp_minus, dec_minus, gon_minus, rad_minus])
103+
hp = float(f'{deg:4d}.{min:02d}{sec:02d}')
104+
hp_plus = float(f'{deg:4d}.{min:02d}{sec:02d}' + '0' * (dec_places - 5) + '1')
105+
dec = deg + (min / 60.0 + sec / 3600.0)
106+
gon = 400.0 / 360.0 * dec
107+
rad = pi / 180.0 * dec
108+
self.testData.append([hp, dec, gon, rad])
109+
dec_plus = deg + (min / 60.0 + (sec + error) / 3600.0)
110+
gon_plus = 400.0 / 360.0 * dec_plus
111+
rad_plus = pi / 180.0 * dec_plus
112+
self.testData.append([hp_plus, dec_plus, gon_plus, rad_plus])
113+
88114
def test_DECAngle(self):
89115
# Test DECAngle Methods
90116
for num, ex in enumerate(deca_exs):
@@ -465,6 +491,10 @@ def test_dec2hp(self):
465491
for num, ex in enumerate(hp_exs):
466492
self.assertAlmostEqual(ex, dec2hp(dec_exs[num]), 13)
467493
self.assertAlmostEqual(-ex, dec2hp(-dec_exs[num]), 13)
494+
for check in self.testData:
495+
hp, dec, gon, rad = check
496+
self.assertAlmostEqual(hp, dec2hp(dec), 13)
497+
self.assertAlmostEqual(-hp, dec2hp(-dec), 13)
468498

469499
def test_dec2hpa(self):
470500
for num, ex in enumerate(dec_exs):
@@ -475,6 +505,10 @@ def test_dec2gon(self):
475505
for num, ex in enumerate(dec_exs):
476506
self.assertAlmostEqual(dec2gon(ex), gon_exs[num], 13)
477507
self.assertAlmostEqual(dec2gon(-ex), -gon_exs[num], 13)
508+
for check in self.testData:
509+
hp, dec, gon, rad = check
510+
self.assertAlmostEqual(gon, dec2gon(dec), 13)
511+
self.assertAlmostEqual(-gon, dec2gon(-dec), 13)
478512

479513
def test_dec2gona(self):
480514
for num, ex in enumerate(dec_exs):
@@ -495,6 +529,13 @@ def test_hp2dec(self):
495529
for num, ex in enumerate(dec_exs):
496530
self.assertAlmostEqual(ex, hp2dec(hp_exs[num]), 13)
497531
self.assertAlmostEqual(-ex, hp2dec(-hp_exs[num]), 13)
532+
for check in self.testData:
533+
hp, dec, gon, rad = check
534+
self.assertAlmostEqual(dec, hp2dec(hp), 13)
535+
self.assertAlmostEqual(-dec, hp2dec(-hp), 13)
536+
537+
self.assertAlmostEqual(0, hp2dec(0), 13)
538+
self.assertAlmostEqual(258, hp2dec(258), 13)
498539
self.assertAlmostEqual(hp2dec(hp_exs[0]) + hp2dec(hp_exs[1]),
499540
dec_exs[0] + dec_exs[1], 13)
500541
# Test that invalid minutes and seconds components raise errors
@@ -523,6 +564,10 @@ def test_hp2gon(self):
523564
for num, ex in enumerate(hp_exs):
524565
self.assertAlmostEqual(hp2gon(ex), gon_exs[num], 13)
525566
self.assertAlmostEqual(hp2gon(-ex), -gon_exs[num], 13)
567+
for check in self.testData:
568+
hp, dec, gon, rad = check
569+
self.assertAlmostEqual(gon, hp2gon(hp), 13)
570+
self.assertAlmostEqual(-gon, hp2gon(-hp), 13)
526571

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

532577
def test_hp2rad(self):
533578
for num, ex in enumerate(hp_exs):
534-
self.assertEqual(hp2rad(ex), rad_exs[num])
535-
self.assertEqual(hp2rad(-ex), -rad_exs[num])
579+
self.assertAlmostEqual(hp2rad(ex), rad_exs[num], 15)
580+
self.assertAlmostEqual(hp2rad(-ex), -rad_exs[num], 15)
581+
for check in self.testData:
582+
hp, dec, gon, rad = check
583+
self.assertAlmostEqual(rad, hp2rad(hp), 15)
584+
self.assertAlmostEqual(-rad, hp2rad(-hp), 15)
536585

537586
def test_hp2dms(self):
538587
self.assertEqual(dms_ex.degree, hp2dms(hp_ex).degree)
@@ -552,6 +601,10 @@ def test_gon2dec(self):
552601
for num, ex in enumerate(gon_exs):
553602
self.assertAlmostEqual(gon2dec(ex), dec_exs[num], 14)
554603
self.assertAlmostEqual(gon2dec(-ex), -dec_exs[num], 14)
604+
for check in self.testData:
605+
hp, dec, gon, rad = check
606+
self.assertAlmostEqual(dec, gon2dec(gon), delta = 5.8e-14)
607+
self.assertAlmostEqual(-dec, gon2dec(-gon), delta = 5.8e-14)
555608

556609
def test_gon2deca(self):
557610
for num, ex in enumerate(gon_exs):
@@ -562,6 +615,10 @@ def test_gon2hp(self):
562615
for num, ex in enumerate(gon_exs):
563616
self.assertEqual(gon2hp(ex), hp_exs[num])
564617
self.assertEqual(gon2hp(-ex), -hp_exs[num])
618+
for check in self.testData:
619+
hp, dec, gon, rad = check
620+
self.assertAlmostEqual(hp, gon2hp(gon), 13)
621+
self.assertAlmostEqual(-hp, gon2hp(-gon), 13)
565622

566623
def test_gon2hpa(self):
567624
for num, ex in enumerate(gon_exs):
@@ -572,6 +629,10 @@ def test_gon2rad(self):
572629
for num, ex in enumerate(gon_exs):
573630
self.assertAlmostEqual(gon2rad(ex), rad_exs[num], 15)
574631
self.assertAlmostEqual(gon2rad(-ex), -rad_exs[num], 15)
632+
for check in self.testData:
633+
hp, dec, gon, rad = check
634+
self.assertAlmostEqual(rad, gon2rad(gon), 13)
635+
self.assertAlmostEqual(-rad, gon2rad(-gon), 13)
575636

576637
def test_gon2dms(self):
577638
for num, ex in enumerate(gon_exs):

0 commit comments

Comments
 (0)