Latitude(0, 30, direction="South") returns +0.5 and reports "North" — the hemisphere is silently dropped whenever the whole-degrees component is zero. The same happens for Longitude(..., direction="West"). The cause is that South/West is implemented as deg = -deg, and -0 carries no sign bit.
Environment
- NOAA-ORR-ERD/PyNUCOS @
0583706dee5e0df6cc387ab5f759f80265a30414 (nucos 3.4.1, editable install from source)
- Windows 11 Pro (10.0.26200), Python 3.14.5, pytest 9.0.3
- Full shipped test suite passes (494 passed) — this path is not covered
Reproduction
Save as repro.py and run python repro.py:
from nucos.lat_long import Latitude, Longitude
lat = Latitude(0, 30, direction="South") # 0 deg 30 min South => expect -0.5
print("Latitude(0, 30, direction='South') :", lat.value, lat.direction())
lon = Longitude(0, 45, direction="West") # 0 deg 45 min West => expect -0.75
print("Longitude(0, 45, direction='West'):", lon.value, lon.direction())
# the float negative-zero path works, which proves the intent:
print("Latitude(-0.0, 30).value :", Latitude(-0.0, 30).value)
Observed output
Latitude(0, 30, direction='South') : 0.5 North
Longitude(0, 45, direction='West'): 0.75 East
Latitude(-0.0, 30).value : -0.5
Expected
-0.5 / "South" and -0.75 / "West". The third line is the proof of intent: Latitude(-0.0, 30) does return -0.5, because a float negative zero preserves the sign bit that ToDecDeg checks. The constructor is supposed to produce exactly that negative-zero path for direction="South", but only does so when the caller happens to pass a float.
Impact: every coordinate in the zero-degree band — the equator for latitude, the prime-meridian band for longitude (e.g. West African coastal waters) — flips hemisphere silently, with no error. Per the comment at lat_long.py:189, these classes are the lat/long interface used in NOAA web apps (ResponseLink, etc.), so bad input never gets a chance to be noticed downstream.
Root cause
nucos/lat_long.py:249-252 (Latitude.__init__; Longitude at line 315 inherits it):
if direction[0].upper() == pdir:
pass
elif direction[0].upper() == ndir:
deg = -deg
For deg == 0 (an int, or float positive zero) -deg is just 0 with no sign bit. Downstream, LatLongConverter.ToDecDeg (lat_long.py:101-105) detects the sign with signbit(d), sees a positive value, and adds the minutes/seconds with a positive sign:
if signbit(d):
Sign = -1
d = abs(d)
else:
Sign = 1
Proposed fix
Don't encode the hemisphere in the sign of deg; track it explicitly and negate the result:
def __init__(self, deg, min=0.0, sec=0.0, direction=None):
ndir = self.negative_direction[0].upper()
pdir = self.positive_direction[0].upper()
negative = signbit(float(deg)) # preserves an explicit -0.0 / negative deg
if direction:
if deg < 0.0:
raise ValueError("degrees cannot be negative if direction is specified")
if direction[0].upper() == pdir:
pass
elif direction[0].upper() == ndir:
negative = True
else:
raise ValueError("direction must start with %r or %r" % (pdir, ndir))
value = LatLongConverter.ToDecDeg(abs(float(deg)), min, sec, max=self.max)
self.value = -value if negative else value
No existing test expectations encode the bug; new tests for Latitude(0, m, s, direction="South") / Longitude(0, m, s, direction="West") should be added with the fix.
Related
Distinct from #5, which is about parsing lat/long strings; this is the Latitude/Longitude constructor path. The DMS formatting of negative zero-degree values is correct (e.g. format_lat_dms(-0.008333) returns '0° 0′ 30.00″ South') — only the constructor loses the sign.
Happy to submit a PR for this if useful.
Latitude(0, 30, direction="South")returns+0.5and reports"North"— the hemisphere is silently dropped whenever the whole-degrees component is zero. The same happens forLongitude(..., direction="West"). The cause is that South/West is implemented asdeg = -deg, and-0carries no sign bit.Environment
0583706dee5e0df6cc387ab5f759f80265a30414(nucos 3.4.1, editable install from source)Reproduction
Save as
repro.pyand runpython repro.py:Observed output
Expected
-0.5/"South"and-0.75/"West". The third line is the proof of intent:Latitude(-0.0, 30)does return-0.5, because a float negative zero preserves the sign bit thatToDecDegchecks. The constructor is supposed to produce exactly that negative-zero path fordirection="South", but only does so when the caller happens to pass a float.Impact: every coordinate in the zero-degree band — the equator for latitude, the prime-meridian band for longitude (e.g. West African coastal waters) — flips hemisphere silently, with no error. Per the comment at
lat_long.py:189, these classes are the lat/long interface used in NOAA web apps (ResponseLink, etc.), so bad input never gets a chance to be noticed downstream.Root cause
nucos/lat_long.py:249-252(Latitude.__init__;Longitudeat line 315 inherits it):For
deg == 0(an int, or float positive zero)-degis just0with no sign bit. Downstream,LatLongConverter.ToDecDeg(lat_long.py:101-105) detects the sign withsignbit(d), sees a positive value, and adds the minutes/seconds with a positive sign:Proposed fix
Don't encode the hemisphere in the sign of
deg; track it explicitly and negate the result:No existing test expectations encode the bug; new tests for
Latitude(0, m, s, direction="South")/Longitude(0, m, s, direction="West")should be added with the fix.Related
Distinct from #5, which is about parsing lat/long strings; this is the
Latitude/Longitudeconstructor path. The DMS formatting of negative zero-degree values is correct (e.g.format_lat_dms(-0.008333)returns'0° 0′ 30.00″ South') — only the constructor loses the sign.Happy to submit a PR for this if useful.