Coverage for pygeodesy / dms.py: 97%
298 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-07 13:30 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-07 13:30 -0400
2# -*- coding: utf-8 -*-
4u'''Parsers and formatters of angles in degrees, minutes and seconds or radians.
6Functions to parse and format bearing, compass, lat- and longitudes in various forms of
7degrees, minutes and seconds with or without degrees, minute and second symbols plus a
8compass point suffix, including parsing of C{decimal} and C{sexagecimal} degrees.
10Set env variable C{PYGEODESY_FMT_FORM} to any C{F_...} form to override default C{F_DMS}
11formatting of lat- and longitudes or to an empty string to restore the default.
13After I{(C) Chris Veness 2011-2024} published under the same MIT Licence**, see
14U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} and
15U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
17@var F_D: Format degrees as unsigned "deg°" with symbol, plus compass point suffix C{N, S, E} or C{W} (C{str}).
18@var F_DM: Format degrees as unsigned "deg°min′" with symbols, plus suffix (C{str}).
19@var F_DMS: Format degrees as unsigned "deg°min′sec″" with symbols, plus suffix (C{str}).
20@var F_DEG: Format degrees as unsigned "[D]DD" I{without} symbol, plus suffix (C{str}).
21@var F_MIN: Format degrees as unsigned "[D]DDMM" I{without} symbols, plus suffix (C{str}).
22@var F_SEC: Format degrees as unsigned "[D]DDMMSS" I{without} symbols, plus suffix (C{str}).
23@var F_D60: Format degrees as unsigned "[D]DD.MMSS" C{sexagecimal} I{without} symbols, plus suffix (C{str}).
24@var F__E: Format degrees as unsigned "%E" I{without} symbols, plus suffix (C{str}).
25@var F__F: Format degrees as unsigned "%F" I{without} symbols, plus suffix (C{str}).
26@var F__G: Format degrees as unsigned "%G" I{without} symbols, plus suffix (C{str}).
27@var F_RAD: Convert degrees to radians and format as unsigned "RR" with symbol, plus suffix (C{str}).
29@var F_D_: Format degrees as signed "-/deg°" with symbol, I{without} suffix (C{str}).
30@var F_DM_: Format degrees as signed "-/deg°min′" with symbols, I{without} suffix (C{str}).
31@var F_DMS_: Format degrees as signed "-/deg°min′sec″" with symbols, I{without} suffix (C{str}).
32@var F_DEG_: Format degrees as signed "-/[D]DD" I{without} symbol, I{without} suffix (C{str}).
33@var F_MIN_: Format degrees as signed "-/[D]DDMM" I{without} symbols, I{without} suffix (C{str}).
34@var F_SEC_: Format degrees as signed "-/[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}).
35@var F_D60_: Format degrees as signed "-/[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}).
36@var F__E_: Format degrees as signed "-/%E" I{without} symbols, I{without} suffix (C{str}).
37@var F__F_: Format degrees as signed "-/%F" I{without} symbols, I{without} suffix (C{str}).
38@var F__G_: Format degrees as signed "-/%G" I{without} symbols, I{without} suffix (C{str}).
39@var F_RAD_: Convert degrees to radians and format as signed "-/RR" I{without} symbol, I{without} suffix (C{str}).
41@var F_D__: Format degrees as signed "-/+deg°" with symbol, I{without} suffix (C{str}).
42@var F_DM__: Format degrees as signed "-/+deg°min′" with symbols, I{without} suffix (C{str}).
43@var F_DMS__: Format degrees as signed "-/+deg°min′sec″" with symbols, I{without} suffix (C{str}).
44@var F_DEG__: Format degrees as signed "-/+[D]DD" I{without} symbol, I{without} suffix (C{str}).
45@var F_MIN__: Format degrees as signed "-/+[D]DDMM" I{without} symbols, without suffix (C{str}).
46@var F_SEC__: Format degrees as signed "-/+[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}).
47@var F_D60__: Format degrees as signed "-/+[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}).
48@var F__E__: Format degrees as signed "-/+%E" I{without} symbols, I{without} suffix (C{str}).
49@var F__F__: Format degrees as signed "-/+%F" I{without} symbols, I{without} suffix (C{str}).
50@var F__G__: Format degrees as signed "-/+%G" I{without} symbols, I{without} suffix (C{str}).
51@var F_RAD__: Convert degrees to radians and format as signed "-/+RR" I{without} symbol, I{without} suffix (C{str}).
53@var S_DEG: Degrees symbol, default C{"°"}
54@var S_MIN: Minutes symbol, default C{"′"} aka I{PRIME}
55@var S_SEC: Seconds symbol, default C{"″"} aka I{DOUBLE_PRIME}
56@var S_RAD: Radians symbol, default C{""} aka L{pygeodesy.NN}
57@var S_DMS: If C{True} include, otherwise cancel all DMS symbols, default C{True}.
58@var S_SEP: Separator between C{deg°|min′|sec″|suffix}, default C{""} aka L{pygeodesy.NN}
60@note: In Python 2-, L{S_DEG}, L{S_MIN}, L{S_SEC}, L{S_RAD} and L{S_SEP} may be multi-byte,
61 non-ascii characters and if so, I{not} C{unicode}.
62'''
64from pygeodesy.basics import copysign0, isLatLon, isodd, issequence, isstr, \
65 neg as _neg, typename # in .ups
66from pygeodesy.constants import _0_0, _0_5, _60_0, _360_0, _3600_0, \
67 _isNAN, _umod_360
68from pygeodesy.errors import ParseError, RangeError, _TypeError, _ValueError, \
69 _parseX, rangerrors, _xError, _xkwds, _envPYGEODESY
70# from pygeodesy.internals import _envPYGEODESY, typename # from .errors
71from pygeodesy.interns import NN, _COMMA_, _d_, _DASH_, _deg_, _degrees_, _DOT_, \
72 _0_, _e_, _E_, _EW_, _f_, _F_, _g_, _MINUS_, _N_, \
73 _NE_, _NS_, _NSEW_, _NW_, _PERCENTDOTSTAR_, _PLUS_, \
74 _PLUSMINUS_, _QUOTE1_, _QUOTE2_, _radians_, _S_, \
75 _SE_, _SPACE_, _SW_, _W_
76from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
77# from pygeodesy.namedTuples import LatLon2Tuple # _MODS
78# from pygeodesy.props import _throwarning # _MODS
79from pygeodesy.streprs import Fmt, fstr, fstrzs, _0wpF
80# from pygeodesy.units import Precision_ # _MODS
81# from pygeodesy.utily import _Wrap # _MODS
83from math import fabs, modf, radians
84try:
85 from string import letters as _LETTERS
86except ImportError: # Python 3+
87 from string import ascii_letters as _LETTERS
89__all__ = _ALL_LAZY.dms
90__version__ = '26.07.04'
92_beyond_ = 'beyond'
93_deg_min_ = 'deg+min'
94_sexagecimal_ = 'sexagecimal'
95_SEXAGECIMUL = 1.e4 # sexagecimal C{D.MMSSss} into decimal C{DMMSS.ss}
97F_D, F_DM, F_DMS, F_DEG, F_MIN, F_SEC, F_D60, F__E, F__F, F__G, F_RAD = _F_s = (
98 _d_, 'dm', 'dms', _deg_, 'min', 'sec', 'd60', _e_, _f_, _g_, 'rad')
99F_D_, F_DM_, F_DMS_, F_DEG_, F_MIN_, F_SEC_, F_D60_, F__E_, F__F_, F__G_, F_RAD_ = (NN(
100 _MINUS_, _) for _ in _F_s)
101F_D__, F_DM__, F_DMS__, F_DEG__, F_MIN__, F_SEC__, F_D60__, F__E__, F__F__, F__G__, F_RAD__ = (NN(
102 _PLUS_, _) for _ in _F_s)
103del _F_s
104_F_DMS = _envPYGEODESY('FMT_FORM') or F_DMS
106_F_case = {F_D: F_D, F_DEG: F_D, _degrees_: F_D, # unsigned _F_s
107 F_DM: F_DM, F_MIN: F_DM, _deg_min_: F_DM,
108 F_D60: F_D60, F_RAD: F_RAD, _radians_: F_RAD,
109 F__E: F__E, F__F: F__F, F__G: F__G} # default F_DMS
110_F_prec = {F_D: 6, F_DM: 4, F_DMS: 2, # default precs
111 F_DEG: 6, F_MIN: 4, F_SEC: 2, F_D60: 0,
112 F__E: 8, F__F: 8, F__G: 8, F_RAD: 5}
113_F_symb = set((F_D, F_DM, F_DMS, _deg_min_)) # == {} pychok -Tb
115S_DEG = _DEGREES_ = '°' # ord() = 176
116S_MIN = _MINUTES_ = '′' # PRIME
117S_SEC = _SECONDS_ = '″' # DOUBLE_PRIME
118S_RAD = _RADIANS_ = NN # PYCHOK radians symbol ""
119S_DMS = True # include DMS symbols
120S_SEP = NN # separator between deg|min|sec|suffix ""
121S_NUL = NN # empty string, kept INTERNAL
123# note: ord(_DEGREES_) == ord('°') == 176, ord('˚') == 730
124_S_norm = {S_DEG: _DEGREES_, '˚': _DEGREES_, '^': _DEGREES_, # _d_: _DEGREES_,
125 S_MIN: _MINUTES_, '’': _MINUTES_, _QUOTE1_: _MINUTES_, # _r_: _RADIANS_
126 S_SEC: _SECONDS_, '”': _SECONDS_, _QUOTE2_: _SECONDS_}
128_WINDS = (_N_, 'NbE', 'NNE', 'NEbN', _NE_, 'NEbE', 'ENE', 'EbN',
129 _E_, 'EbS', 'ESE', 'SEbE', _SE_, 'SEbS', 'SSE', 'SbE',
130 _S_, 'SbW', 'SSW', 'SWbS', _SW_, 'SWbW', 'WSW', 'WbS',
131 _W_, 'WbN', 'WNW', 'NWbW', _NW_, 'NWbN', 'NNW', 'NbW')
134def _D603(sep, s_D=_DOT_, s_M=None, s_S=S_NUL, s_DMS=S_DMS, **unused):
135 '''(INTERNAL) Get the overridden or default pseudo-C{DMS} symbols.
136 '''
137 if s_DMS:
138 M = sep if s_M is None else s_M
139 return s_D, (M or S_NUL), s_S
140 else: # no overriden symbols
141 return _DOT_, sep, S_NUL
144def _DMS3(form, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_DMS=S_DMS, **unused):
145 '''(INTERNAL) Get the overridden or default C{DMS} symbols.
146 '''
147 return (s_D, s_M, s_S) if s_DMS and form in _F_symb else (S_NUL, S_NUL, S_NUL)
150def _dms3(d, ddd, p, w):
151 '''(INTERNAL) Format C{d} as (deg, min, sec) C{str}s with leading zeros.
152 '''
153 d = round(d * _3600_0, p)
154 d, s = divmod(d, _3600_0)
155 m, s = divmod(s, _60_0)
156 return (_0wpF(ddd, 0, d),
157 _0wpF( 2, 0, m),
158 _0wpF(w+2, p, s))
161def _DR2(s_D=S_NUL, s_R=S_RAD, **unused):
162 '''(INTERNAL) Get the overridden or default C{D} and C{RAD} symbols.
163 '''
164 return s_D, s_R
167def _fstrzs(t, **unused):
168 '''(INTERNAL) Pass-thru version of C{.streprs.fstrzs}.
169 '''
170 return t
173def _split3(strDMS, suffix=_NSEW_):
174 '''(INTERNAL) Return sign, stripped B{C{strDMS}} and compass point.
175 '''
176 t = strDMS.strip()
177 s = t[:1] # sign or digit
178 P = t[-1:] # compass point or digit or dot
179 t = t.lstrip(_PLUSMINUS_).rstrip(suffix)
180 return s, t.strip(), P
183def _toDMS(deg, form, prec, sep, ddd, P, s_D_M_S): # MCCABE 15 in .units
184 '''(INTERNAL) Convert C{deg} to C{str}, with/-out sign, DMS symbols and/or suffix.
185 '''
186 try:
187 F = None
188 deg = float(deg)
190 f, s = form, form[:1]
191 if s in _PLUSMINUS_: # signed
192 n = _MINUS_ if deg < 0 else (
193 _PLUS_ if deg > 0 and s == _PLUS_ else NN)
194 P = NN # no suffix
195 f = f.lstrip(_PLUSMINUS_)
196 else: # suffixed
197 n = NN
198 if sep and P: # no sep if no suffix
199 P = NN(sep, P)
200 try:
201 F = _F_case[f] # .strip()
202 except KeyError:
203 f = f.lower() # .strip()
204 F = _F_case.get(f, F_DMS)
206 if prec is None:
207 z = p = _F_prec.get(F, 6)
208 else:
209 z = int(prec)
210 p = abs(z)
211 w = p + (1 if p else 0)
212 z = fstrzs if z > 1 else _fstrzs
213 d = fabs(deg)
215 if F is F_DMS: # 'deg+min+sec', default
216 D, M, S = _DMS3(f, **s_D_M_S)
217 d, m, s = _dms3(d, ddd, p, w)
218 t = NN(n, d, D, sep,
219 m, M, sep,
220 z(s), S, P)
222 elif F is F_DM: # 'deg+min'
223 D, M, _ = _DMS3(f, **s_D_M_S)
224 d, m = divmod(round(d * _60_0, p), _60_0)
225 t = NN(n, _0wpF(ddd, 0, d), D, sep,
226 z(_0wpF(w+2, p, m)), M, P)
228 elif F is F_D: # 'deg'
229 D, _, _ = _DMS3(f, **s_D_M_S)
230 t = NN(n, z(_0wpF(w+ddd, p, d)), D, P)
232 elif F is F_D60: # 'deg.MM|SSss|'
233 D, M, S = _D603(sep, **s_D_M_S)
234 d, m, s = _dms3(d, ddd, p, w)
235 t = z(s).split(_DOT_) + [S, P]
236 t = NN(n, d, D, m, M, *t)
238 elif F is F_RAD:
239 _, R = _DR2(**s_D_M_S)
240 r = NN(_PERCENTDOTSTAR_, _F_) % (p, radians(d))
241 t = NN(n, z(r), R, P)
243 else: # F in (F__E, F__F, F__G)
244 D, _ = _DR2(**s_D_M_S)
245 d = NN(_PERCENTDOTSTAR_, F) % (p, d) # XXX f?
246 t = NN(n, z(d, ap1z=F is F__G), D, P)
248 except Exception as x:
249 d = {} if F is None else dict(F_=F, P=P)
250 raise _xError(x, deg=deg, form=form, prec=prec, **d)
252 return t # NOT unicode in Python 2-
255def bearingDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S):
256 '''Convert bearing to a string (without compass point suffix).
258 @arg bearing: Bearing from North (compass C{degrees360}).
259 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}).
260 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
261 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
262 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all
263 DMS symbol suffixes, see function L{pygeodesy.toDMS}.
265 @return: Compass degrees per the specified B{C{form}} (C{str}).
267 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details.
268 '''
269 return _toDMS(_umod_360(bearing), form, prec, sep, 1, NN, s_D_M_S)
272def _clip(angle, limit, units):
273 '''(INTERNAL) Helper for C{clipDegrees} and C{clipRadians}.
274 '''
275 c = min(limit, max(-limit, angle))
276 if c != angle and rangerrors():
277 t = _SPACE_(fstr(angle, prec=6, ints=True), _beyond_,
278 copysign0(limit, angle), units)
279 raise RangeError(t, txt=None)
280 return c
283def clipDegrees(deg, limit):
284 '''Clip a lat- or longitude to the given range.
286 @arg deg: Unclipped lat- or longitude (C{scalar degrees}).
287 @arg limit: Valid C{-/+B{limit}} range (C{degrees}).
289 @return: Clipped value (C{degrees}).
291 @raise RangeError: If B{C{deg}} outside the valid C{-/+B{limit}} range
292 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
293 '''
294 return _clip(deg, limit, _degrees_) if limit and limit > 0 and \
295 not _isNAN(deg) else deg
298def clipRadians(rad, limit):
299 '''Clip a lat- or longitude to the given range.
301 @arg rad: Unclipped lat- or longitude (C{radians}).
302 @arg limit: Valid C{-/+B{limit}} range (C{radians}).
304 @return: Clipped value (C{radians}).
306 @raise RangeError: If B{C{rad}} outside the valid C{-/+B{limit}} range
307 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
308 '''
309 return _clip(rad, limit, _radians_) if limit and limit > 0 and \
310 not _isNAN(rad) else rad
313def compassDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S):
314 '''Convert bearing to a string suffixed with compass point.
316 @arg bearing: Bearing from North (compass C{degrees360}).
317 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}).
318 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
319 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
320 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all
321 DMS symbol suffixes, see function L{pygeodesy.toDMS}.
323 @return: Compass degrees and point in the specified form (C{str}).
325 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details.
326 '''
327 b = _umod_360(bearing)
328 return _toDMS(b, form, prec, sep, 1, compassPoint(b), s_D_M_S)
331def compassPoint(bearing, prec=3):
332 '''Convert a C{bearing} from North to a compass point.
334 @arg bearing: Bearing (compass C{degrees360}).
335 @kwarg prec: Precision, number of compass point characters:
336 1 for cardinal or basic winds,
337 2 for intercardinal or ordinal or principal winds,
338 3 for secondary-intercardinal or half-winds or
339 4 for quarter-winds).
341 @return: Compass point (1-, 2-, 3- or 4-letter C{str}).
343 @raise ValueError: Invalid B{C{bearing}} or B{C{prec}}.
345 @see: U{Dms.compassPoint
346 <https://GitHub.com/ChrisVeness/geodesy/blob/master/dms.js>}
347 and U{Compass rose<https://WikiPedia.org/wiki/Compass_rose>}.
348 '''
349 try: # like .streprs.enstr2
350 b = _umod_360(bearing)
351 p = _MODS.units.Precision_(prec, low=1, high=4) \
352 if prec != 3 else int(prec)
353 m = 2 << p
354 w = 32 // m # if _isin(m, 4, 8, 16, 32)
355 # not round(b), half-even rounding in Python 3+, but
356 # round-away-from-zero as int(b + copysign0(_0_5, b))
357 w *= int(b * m / _360_0 + _0_5) % m
358 return _WINDS[w]
359 except Exception as x:
360 raise _xError(x, bearing=bearing, prec=prec)
363def degDMS(deg, prec=6, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, neg=_MINUS_, pos=NN):
364 '''Convert degrees to a string in degrees, minutes I{or} seconds.
366 @arg deg: Value in degrees (C{scalar degrees}).
367 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
368 Trailing zero decimals are stripped for C{B{prec}=1}
369 and above, but kept for negative B{C{prec}}.
370 @kwarg s_D: D symbol for degrees (C{str}).
371 @kwarg s_M: M symbol for minutes (C{str}) or C{""} aka L{pygeodesy.NN}.
372 @kwarg s_S: S symbol for seconds (C{str}) or C{""} aka L{pygeodesy.NN}.
373 @kwarg neg: Optional sign for negative (C{'-'}).
374 @kwarg pos: Optional sign for positive (C{""}) aka L{pygeodesy.NN}.
376 @return: I{Either} degrees, minutes I{or} seconds (C{str}).
378 @see: Function L{pygeodesy.toDMS}.
379 '''
380 try:
381 deg = float(deg)
382 except Exception as x:
383 raise _xError(x, deg=deg, prec=prec)
385 d, s = fabs(deg), s_D
386 if d < 1:
387 if s_M:
388 d *= _60_0
389 if d < 1 and s_S:
390 d *= _60_0
391 s = s_S
392 else:
393 s = s_M
394 elif s_S:
395 d *= _3600_0
396 s = s_S
398 z = int(prec)
399 t = Fmt.F(d, prec=abs(z))
400 if z > 1:
401 t = fstrzs(t)
402 n = neg if deg < 0 else pos
403 return NN(n, t, s) # NOT unicode in Python 2-
406def latDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S):
407 '''Convert latitude to a string, optionally suffixed with N or S.
409 @arg deg: Latitude to be formatted (C{scalar degrees}).
410 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}).
411 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
412 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
413 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all
414 DMS symbol suffixes, see function L{pygeodesy.toDMS}.
416 @return: Degrees in the specified form (C{str}).
418 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details.
419 '''
420 P = _S_ if deg < 0 else _N_
421 return _toDMS(deg, form, prec, sep, 2, P, s_D_M_S)
424def latlonDMS(lls, **m_form_prec_sep_s_D_M_S):
425 '''Convert one or more C{LatLon} instances to strings.
427 @arg lls: Single (C{LatLon}) or an iterable (C{LatLon}s).
428 @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments C{B{m}eter},
429 C{B{form}at}, C{B{prec}ision}, B{C{s_D}}, B{C{s_M}}, B{C{s_S}},
430 B{C{s_DMS}} and I{DEPRECATED} C{B{sep}=None}, see function
431 L{pygeodesy.toDMS} and method C{LatLon.toStr} for more details.
433 @return: A C{tuple} of C{str}s if B{C{lls}} is a list, sequence, tuple, etc.
434 of C{LatLon}s or a single C{str} if B{C{lls}} is a single C{LatLon}.
436 @see: Functions L{pygeodesy.latlonDMS_}, L{pygeodesy.latDMS}, L{pygeodesy.lonDMS}
437 and L{pygeodesy.toDMS} and method C{LatLon.toStr}.
439 @note: Keyword argument C{B{sep}=None} to join the resturned C{tuple} has
440 been I{DEPRECATED}, use C{B{sep}.join(B{latlonDMS_}(...))} instead.
441 '''
442 sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S)
443 if isLatLon(lls):
444 t = lls.toStr(**kwds)
445 elif issequence(lls):
446 t = tuple(ll.toStr(**kwds) for ll in lls)
447 if sep: # XXX DEPRECATED, to be removed
448 t = sep.join(t)
449 else:
450 raise _TypeError(lls=lls, **m_form_prec_sep_s_D_M_S)
451 return t
454def latlonDMS_(*lls, **m_form_prec_sep_s_D_M_S):
455 '''Convert one or more C{LatLon} instances to strings.
457 @arg lls: The instances (C{LatLon}s), all positional arguments.
458 @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments C{B{m}eter},
459 C{B{form}at}, C{B{prec}ision}, B{C{s_D}}, B{C{s_M}}, B{C{s_S}},
460 B{C{s_DMS}} and I{DEPRECATED} C{B{sep}=None}, see function
461 L{pygeodesy.toDMS} and method C{LatLon.toStr} for more details.
463 @return: A C{tuple} of C{str}s if 2 or more C{LatLon} instances
464 or a single C{str} if only a single C{LatLon} instance
465 is given in B{C{lls}}.
467 @see: Functions L{pygeodesy.latlonDMS}, L{pygeodesy.latDMS} and
468 L{pygeodesy.lonDMS} and L{pygeodesy.toDMS} and method
469 C{LatLon.toStr}.
471 @note: Keyword argument C{B{sep}=None} to join the resturned C{tuple} has
472 been I{DEPRECATED}, use C{B{sep}.join(B{latlonDMS_}(...))} instead.
473 '''
474 sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S)
475 if not lls:
476 raise _ValueError(lls=lls, **m_form_prec_sep_s_D_M_S)
477 elif len(lls) < 2:
478 lls, sep = lls[0], None
479 t = latlonDMS(lls, **kwds)
480 return sep.join(t) if sep else t
483def _latlonDMS_sep2(where, sep=None, **kwds):
484 '''DEPRECATED, use %r.join(%s(...)) instead.'''
485 if sep: # PYCHOK no cover
486 i = _MODS.inters
487 k = Fmt.EQUAL(sep=repr(sep))
488 k = _SPACE_(i._keyword_, i._arg_, k, i._of_)
489 n = typename(where)
490 t = _latlonDMS_sep2.__doc__ % (sep, n)
491 _MODS.props._throwarning(k, n, t)
492 return sep, kwds
495def lonDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S):
496 '''Convert longitude to a string, optionally suffixed with E or W.
498 @arg deg: Longitude to be formatted (C{scalar degrees}).
499 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}).
500 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
501 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
502 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all
503 DMS symbol suffixes.
505 @return: Degrees in the specified form (C{str}).
507 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details.
508 '''
509 P = _W_ if deg < 0 else _E_
510 return _toDMS(deg, form, prec, sep, 3, P, s_D_M_S)
513def normDMS(strDMS, norm=None, **s_D_M_S):
514 '''Normalize all degrees, minutes and seconds (DMS) I{symbol} suffixes in
515 a string to the default symbols L{S_DEG}, L{S_MIN}, L{S_SEC}.
517 @arg strDMS: Original DMS string (C{str}).
518 @kwarg norm: Optional replacement symbol (C{str}) or C{None} for the default
519 DMS symbol). Use C{B{norm}=""} to remove all DMS symbols.
520 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes C{B{s_D}=}L{S_DEG},
521 C{B{s_M}=}L{S_MIN}, C{B{s_S}=}L{S_SEC} and C{B{s_R}=}L{S_RAD}
522 for radians, each to be replaced by B{C{norm}}.
524 @return: Normalized DMS (C{str}).
525 '''
526 def _s2S2(s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_R=S_RAD):
527 d = {s_D: S_DEG, s_M: S_MIN, s_S: S_SEC, s_R: S_RAD}
528 for s, S in _xkwds(d, **_S_norm).items():
529 if s:
530 yield s, S
532 # XXX strDMS isn't unicode in Python 2- and looping
533 # thru strDMS will yield each byte, hence the loop
534 # thru _s2S2 and replacing the DMS symbols in strDMS
536 if norm is None: # back to default DMS
537 for s, S in _s2S2(**s_D_M_S):
538 if s != S:
539 strDMS = strDMS.replace(s, S)
541 else: # replace or remove all DMS
542 n = norm or NN
543 for s, _ in _s2S2(**s_D_M_S):
544 if s != n:
545 strDMS = strDMS.replace(s, n)
546 if n:
547 strDMS = strDMS.rstrip(n) # XXX not .strip?
549 return strDMS # NOT unicode in Python 2-
552def parseDDDMMSS(strDDDMMSS, suffix=_NSEW_, sep=S_SEP, clip=0, sexagecimal=False):
553 '''Parse a lat- or longitude represention forms as [D]DDMMSS in degrees.
555 @arg strDDDMMSS: Degrees in any of several forms (C{str}) and types (C{float},
556 C{int}, other).
557 @kwarg suffix: Optional compass points (C{str}), valid in B{C{strDDDMMSS}}.
558 @kwarg sep: Optional separator between "[D]DD", "MM", "SS", B{C{suffix}}
559 (L{S_SEP}) in B{C{strDDDMMSS}}.
560 @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{degrees}).
561 @kwarg sexagecimal: If C{True}, convert C{"D.MMSS"} or C{float(D.MMSS)} to
562 C{base-60} "MM" and "SS" digits. See C{form}s L{F_D60},
563 L{F_D60_} and L{F_D60__}.
565 @return: Degrees (C{float}).
567 @raise ParseError: Invalid B{C{strDDDMMSS}} or B{C{clip}} or the form of
568 B{C{strDDDMMSS}} is incompatible with or invalid for the
569 given B{C{suffix}} compass point(s).
571 @raise RangeError: Value of B{C{strDDDMMSS}} outside the valid C{-/+B{clip}}
572 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
574 @note: Type C{str} values "[D]DD", "[D]DDMM", "[D]DDMMSS" and "[D]DD.MMSS"
575 for B{C{strDDDMMSS}} are parsed properly only if I{either} unsigned
576 and suffixed with a valid, compatible, C{cardinal} L{compassPoint}
577 I{or} signed I{or} unsigned, unsuffixed and with keyword argument
578 B{C{suffix}="NS"}, B{C{suffix}="EW"} or a compatible L{compassPoint}.
580 @note: Unlike function L{parseDMS}, type C{float}, C{int} and other non-C{str}
581 B{C{strDDDMMSS}} values are interpreted as C{form} [D]DDMMSS or
582 [D]DD.MMSS. For example, C{int(1230)} is returned as 12.5 and I{not
583 1230.0} degrees. However, C{int(345)} is considered C{form} "DDD"
584 345 I{and not "DDMM" 0345}, unless B{C{suffix}} specifies the compass
585 point. Also, C{float(15.0523)} is returned as 15.0523 decimal
586 degrees and I{not 15°5′23″ sexagecimal}. To consider the latter, use
587 C{float(15.0523)} or C{"15.0523"} and specify the keyword argument
588 C{B{sexagecimal}=True}.
590 @see: Functions L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and
591 L{pygeodesy.parse3llh}.
592 '''
593 def _DDDMMSS(strDDDMMSS, suffix, sep, clip, sexagecimal):
594 S = suffix.upper() or _N_ # bool('' in _NE_) is True
595 if isstr(strDDDMMSS):
596 t = strDDDMMSS.replace(sep, NN) if sep else strDDDMMSS
597 n, s, t, f, P, X = _DDDMMSS6(t, S)
598 if X:
599 pass
600 elif not sexagecimal: # try other forms
601 return _DMS2deg(strDDDMMSS, S, sep, clip, {})
603 if sexagecimal: # move decimal dot from ...
604 n += 4 # ... [D]DD.MMSSs to [D]DDMMSS.s
605 if n < 6:
606 t = _SPACE_(_sexagecimal_, 'digits')
607 raise ParseError(t, n)
608 z = n - len(f) # zeros to append
609 t = (f + (_0_ * z)) if z > 0 else _DOT_(f[:n], f[n:])
610 f = _0_0 # fraction
611 else: # float or int to [D]DDMMSS[.fff]
612 f = float(strDDDMMSS)
613 n, s, t, f, P = _DDDMMSS5(f, S, sexagecimal)
615 if n < 4: # [D]DD[.ddd]
616 if t:
617 f += float(t)
618 t = f,
619 else:
620 n -= 2
621 f += float(t[n:])
622 if n < 4: # [D]DDMM[.mmm]
623 t = float(t[:n]), f
624 else: # [D]DDMMSS[.sss]
625 d = n - 2
626 t = float(t[:d]), float(t[d:n]), f
627 d = _dms2deg(s, P, *t)
628 return clipDegrees(d, float(clip)) if clip else d
630 return _parseX(_DDDMMSS, strDDDMMSS, suffix, sep, clip, sexagecimal,
631 strDDDMMSS=strDDDMMSS, suffix=suffix, sexagecimal=sexagecimal)
634def _DDDMMSS5(f, S, sexagecimal):
635 '''(INTERNAL) Partial C{parseDDDMMSS} of C{float}.
636 '''
637 if sexagecimal:
638 f *= _SEXAGECIMUL
639 m = 6
640 else:
641 m = 0
642 s = P = _PLUS_ # anything except NN, _S_, _SW_, _W_
643 if f < 0:
644 f = -f
645 s = _MINUS_
646 f, i = modf(f) # returns ...
647 t = str(int(i)) # ... float(i)
648 n = len(t) # number of digits to ...
649 if n < m: # ... required min or ...
650 t = (_0_ * (m - n)) + t
651 # ... match the given compass point
652 elif S in (_NS_ if isodd(n) else _EW_):
653 t = _0_ + t
654 # P = S
655 # elif n > 1:
656 # P = (_EW_ if isodd(n) else _NS_)[0]
657 return len(t), s, t, f, P # float(f) fraction
660def _DDDMMSS6(t, S):
661 '''(INTERNAL) Partial C{parseDDDMMSS} of C{str}.
662 '''
663 s, t, P = _split3(t, S)
664 f = t.split(_DOT_)
665 if len(f) > 2:
666 raise ParseError('dots', len(f) - 1)
667 n = len(f[0])
668 f = NN.join(f)
669 if 1 < n < 8 and f.isdigit() and ( # dddN/S/E/W or ddd or +/-ddd
670 (P in S and s.isdigit()) or
671 (P.isdigit() and S in _WINDS # PYCHOK indent
672 and (s in _PLUSMINUS_ or s.isdigit()))):
673 # check [D]DDMMSS form and compass point
674 X = _EW_ if isodd(n) else _NS_
675 if not (P in X or (S in X and (P.isdigit() or P == _DOT_))):
676 t = typename(parseDDDMMSS)[(5 if isodd(n) else 6):]
677 t = _SPACE_('form', t, 'applies', _DASH_.join(X))
678 raise ParseError(t)
679 else:
680 X = NN
681 return n, s, t, f, P, X # str(f) fraction
684def _DMS2deg(strDMS, suffix, sep, clip, s_D_M_S):
685 '''(INTERNAL) Helper for C{parseDDDMMSS} and C{parseDMS}.
686 '''
687 try:
688 d = float(strDMS)
689 except (TypeError, ValueError):
690 s, t, P = _split3(strDMS, suffix.upper())
691 if sep: # remove all DMS symbols
692 t = t.replace(sep, _SPACE_)
693 t = normDMS(t, norm=NN, **s_D_M_S)
694 else: # replace all DMS symbols
695 t = normDMS(t, norm=_SPACE_, **s_D_M_S)
696 t = t.strip().split()
697 d = _dms2deg(s, P, *map(float, t))
698 return clipDegrees(d, float(clip)) if clip else d
701def _dms2deg(n, P, d, m=_0_0, s=_0_0):
702 '''(INTERNAL) Helper for C{parseDDDMMSS}, C{parseRad} and C{_DMS2deg}.
703 '''
704 m += s / _60_0
705 d += m / _60_0
706 if n == _MINUS_ or (P and P in _SW_):
707 d = _neg(d)
708 return d
711def parseDMS(strDMS, suffix=_NSEW_, sep=S_SEP, clip=0, **s_D_M_S): # MCCABE 14
712 '''Parse a lat- or longitude representation in C{degrees}.
714 This is very flexible on formats, allowing signed decimal degrees, degrees
715 and minutes or degrees minutes and seconds optionally suffixed by a cardinal
716 compass point.
718 A variety of symbols, separators and suffixes are accepted, for example
719 "3°37′09″W". Minutes and seconds may be omitted.
721 @arg strDMS: Degrees in any of several forms (C{str}) and types (C{float},
722 C{int}, other).
723 @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
724 @kwarg sep: Optional separator between C{deg°}, C{min′}, C{sec″},
725 B{C{suffix}} (C{''}).
726 @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{degrees}).
727 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees
728 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and
729 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}.
731 @return: Degrees (C{float}).
733 @raise ParseError: Invalid B{C{strDMS}} or B{C{clip}}.
735 @raise RangeError: Value of B{C{strDMS}} outside the valid C{-/+B{clip}} range
736 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
738 @note: Unlike function L{parseDDDMMSS}, type C{float}, C{int} and other non-C{str}
739 B{C{strDMS}} values are considered decimal (and not sexagecimal) degrees.
740 For example, C{int(1230)} is returned as 1230.0 I{and not as 12.5} degrees
741 and C{float(345)} as 345.0 I{and not as 3.75} degrees!
743 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS2},
744 L{pygeodesy.parse3llh} and L{pygeodesy.toDMS}.
745 '''
746 return _parseX(_DMS2deg, strDMS, suffix, sep, clip, s_D_M_S, strDMS=strDMS, suffix=suffix)
749def parseDMS2(strLat, strLon, sep=S_SEP, clipLat=90, clipLon=180, wrap=False, **s_D_M_S):
750 '''Parse a lat- and a longitude representions C{"lat, lon"} in C{degrees}.
752 @arg strLat: Latitude in any of several forms (C{str} or C{degrees}).
753 @arg strLon: Longitude in any of several forms (C{str} or C{degrees}).
754 @kwarg sep: Optional separator between deg°, min′, sec″, suffix (C{''}).
755 @kwarg clipLat: Limit latitude to range C{-/+B{clipLat}} (C{degrees}).
756 @kwarg clipLon: Limit longitude to range C{-/+B{clipLon}} (C{degrees}).
757 @kwarg wrap: If C{True}, wrap or I{normalize} the lat- and longitude,
758 overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}).
759 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees
760 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and
761 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}.
763 @return: A L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
765 @raise ParseError: Invalid B{C{strLat}} or B{C{strLon}}.
767 @raise RangeError: Value of B{C{strLat}} or B{C{strLon}} outside the
768 valid C{-/+B{clipLat}} or C{-/+B{clipLon}} range
769 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
771 @note: See the B{Notes} at function L{parseDMS}.
773 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS},
774 L{pygeodesy.parse3llh} and L{pygeodesy.toDMS}.
775 '''
776 return _2Tuple(strLat, strLon, clipLat, clipLon, wrap, sep=sep, **s_D_M_S)
779def _2Tuple(strLat, strLon, clipLat, clipLon, wrap, **kwds):
780 '''(INTERNAL) Helper for C{parseDMS2} and C{parse3llh}.
781 '''
782 if wrap:
783 _W = _MODS.utily._Wrap
784 ll = _W.latlon(parseDMS(strLat, suffix=_NS_, **kwds),
785 parseDMS(strLon, suffix=_EW_, **kwds))
786 else:
787 # if wrap is None:
788 # clipLat = clipLon = 0
789 ll = (parseDMS(strLat, suffix=_NS_, clip=clipLat, **kwds),
790 parseDMS(strLon, suffix=_EW_, clip=clipLon, **kwds))
791 return _MODS.namedTuples.LatLon2Tuple(*ll)
794def parse3llh(strllh, height=0, sep=_COMMA_, clipLat=90, clipLon=180, wrap=False, **s_D_M_S):
795 '''Parse a string C{"lat, lon [, h]"} representing lat-, longitude in
796 C{degrees} and optional height in C{meter}.
798 The lat- and longitude value must be separated by a separator
799 character. If height is present it must follow, separated by
800 another separator.
802 The lat- and longitude values may be swapped, provided at least
803 one ends with the proper compass point.
805 @arg strllh: Latitude, longitude[, height] (C{str}, ...).
806 @kwarg height: Optional, default height (C{meter}) or C{None}.
807 @kwarg sep: Optional separator between C{"lat lon [h] suffix"} (C{str}).
808 @kwarg clipLat: Limit latitude to range C{-/+B{clipLat}} (C{degrees}).
809 @kwarg clipLon: Limit longitude to range C{-/+B{clipLon}} (C{degrees}).
810 @kwarg wrap: If C{True}, wrap or I{normalize} the lat- and longitude,
811 overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}).
812 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees
813 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and
814 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}.
816 @return: A L{LatLon3Tuple}C{(lat, lon, height)} in C{degrees},
817 C{degrees} and C{float}.
819 @raise RangeError: Lat- or longitude value of B{C{strllh}} outside the
820 valid C{-/+B{clipLat}} or C{-/+B{clipLon}} range
821 and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
823 @raise ValueError: Invalid B{C{strllh}} or B{C{height}}.
825 @note: See the B{Notes} at function L{parseDMS}.
827 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS},
828 L{pygeodesy.parseDMS2} and L{pygeodesy.toDMS}.
829 '''
830 def _3llh(strllh, height, sep, wrap):
831 ll = strllh.strip().split(sep)
832 if len(ll) > 2: # XXX interpret height unit
833 h = float(ll.pop(2).rstrip(_LETTERS + _SPACE_))
834 else:
835 h = height # None from wgrs.Georef.__new__
836 if len(ll) != 2:
837 raise ValueError
839 a, b = [_.strip() for _ in ll] # PYCHOK false
840 if a[-1:] in _EW_ or b[-1:] in _NS_:
841 a, b = b, a
842 t = _2Tuple(a, b, clipLat, clipLon, wrap, **s_D_M_S)
843 return t.to3Tuple(h)
845 return _parseX(_3llh, strllh, height, sep, wrap, strllh=strllh)
848def parseRad(strRad, suffix=_NSEW_, clip=0):
849 '''Parse a string representing angle in C{radians}.
851 @arg strRad: Degrees in any of several forms (C{str} or C{radians}).
852 @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
853 @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{radians}).
855 @return: Radians (C{float}).
857 @raise ParseError: Invalid B{C{strRad}} or B{C{clip}}.
859 @raise RangeError: Value of B{C{strRad}} outside the valid C{-/+B{clip}}
860 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
861 '''
862 def _Rad(strRad, suffix, clip):
863 try:
864 r = float(strRad)
865 except (TypeError, ValueError):
866 s, t, P = _split3(strRad, suffix.upper())
867 r = _dms2deg(s, P, float(t))
868 return clipRadians(r, float(clip)) if clip else r
870 return _parseX(_Rad, strRad, suffix, clip, strRad=strRad, suffix=suffix)
873def precision(form, prec=None):
874 '''Set the default precison for a given F_ form.
876 @arg form: L{F_D}, L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
877 L{F_D60}, L{F__E}, L{F__F}, L{F__G} or L{F_RAD} (C{str}).
878 @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
879 Trailing zero decimals are stripped for C{B{prec}=1}
880 and above, but kept for negative B{C{prec}}.
882 @return: Previous precision for the B{C{form}} (C{int}).
884 @raise ValueError: Invalid B{C{form}} or B{C{prec}} or B{C{prec}}
885 outside range C{-9..+9}.
886 '''
887 try:
888 p = _F_prec[form]
889 except KeyError:
890 raise _ValueError(form=form)
891 if prec is not None: # set as default
892 P_ = _MODS.units.Precision_
893 _F_prec[form] = P_(prec, low=-9, high=9)
894 return p
897def toDMS(deg, form=_F_DMS, prec=2, sep=S_SEP, ddd=2, neg=_MINUS_, pos=_PLUS_, **s_D_M_S):
898 '''Convert I{signed} C{degrees} to string, without suffix.
900 @arg deg: Degrees to be formatted (C{scalar degrees}).
901 @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D}, L{F_DM}, L{F_DMS},
902 L{F_DEG}, L{F_MIN}, L{F_SEC}, L{F_D60}, L{F__E}, L{F__F}, L{F__G},
903 L{F_RAD}, L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_}, L{F_SEC_},
904 L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_}, L{F_D__}, L{F_DM__},
905 L{F_DMS__}, L{F_DEG__}, L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
906 L{F__F__}, L{F__G__} or L{F_RAD__}).
907 @kwarg prec: Number of decimal digits (0..9 or C{None} for default). Trailing zero
908 decimals are stripped for C{B{prec}=1} and above, but kept for negative
909 B{C{prec}}.
910 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
911 @kwarg ddd: Number of (integer) digits for B{C{deg}°} (2 or 3).
912 @kwarg neg: Prefix for negative B{C{deg}} (C{'-'}).
913 @kwarg pos: Prefix for positive B{C{deg}} and signed B{C{form}} (C{'+'}).
914 @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=}L{S_DEG}, C{B{s_M}=}L{S_MIN}
915 C{B{s_S}=}L{S_SEC}, C{B{s_R}=}L{S_RAD} and C{B{s_DMS}=True} to
916 override any or cancel all DMS symbol suffixes. See the B{Notes}
917 below.
919 @return: Degrees in the specified form (C{str}).
921 @note: The degrees, minutes and seconds (DMS) symbol can be overridden in this and
922 other C{*DMS} functions by using optional keyword argments C{B{s_D}="d"},
923 C{B{s_M}="'"} respectively C{B{s_S}='"'}. Using B{C{s_DMS}=None} cancels
924 all C{DMS} symbols to C{""} aka L{pygeodesy.NN}.
926 @note: Sexagecimal format B{C{F_D60}} supports overridable pseudo-DMS symbols
927 positioned at C{"[D]DD<B{s_D}>MM<B{s_M}>SS<B{s_S}>"} with defaults
928 C{B{s_D}="."}, C{B{s_M}=B{sep}} and C{B{s_S}=}L{pygeodesy.NN}.
930 @note: Formats B{C{F__E}}, B{C{F__F}} and B{C{F__G}} is extended with a C{D}-only
931 symbol suffix if defined with keyword argument C{B{s_D}=}L{pygeodesy.NN}.
932 Likewise for B{C{F_RAD}} forms with keyword argument C{B{s_R}=}L{S_RAD}.
934 @see: Function L{pygeodesy.degDMS}
935 '''
936 s = form[:1]
937 f = form[1:] if s in _PLUSMINUS_ else form
938 t = _toDMS(deg, f, prec, sep, ddd, NN, s_D_M_S) # unsigned and -suffixed
939 if deg < 0 and neg:
940 t = neg + t
941 elif deg > 0 and s == _PLUS_ and pos:
942 t = pos + t
943 return t
945# **) MIT License
946#
947# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
948#
949# Permission is hereby granted, free of charge, to any person obtaining a
950# copy of this software and associated documentation files (the "Software"),
951# to deal in the Software without restriction, including without limitation
952# the rights to use, copy, modify, merge, publish, distribute, sublicense,
953# and/or sell copies of the Software, and to permit persons to whom the
954# Software is furnished to do so, subject to the following conditions:
955#
956# The above copyright notice and this permission notice shall be included
957# in all copies or substantial portions of the Software.
958#
959# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
960# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
961# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
962# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
963# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
964# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
965# OTHER DEALINGS IN THE SOFTWARE.