Skip to content

Latitude/Longitude(0, m, s, direction="South"/"West") drops the hemisphere — returns positive coordinate #40

@consigcody94

Description

@consigcody94

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions