Coverage for pygeodesy/streprs.py: 94%

272 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-06 12:20 -0500

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Floating point and other formatting utilities. 

5''' 

6 

7from pygeodesy.basics import isint, islistuple, isscalar, isstr, itemsorted, \ 

8 _zip, _0_0 

9# from pygeodesy.constants import _0_0 

10from pygeodesy.errors import _or, _IsnotError, _TypeError, _ValueError, \ 

11 _xkwds_get, _xkwds_item2 

12from pygeodesy.internals import _DUNDER_nameof 

13from pygeodesy.interns import NN, _0_, _0to9_, MISSING, _BAR_, _COMMASPACE_, \ 

14 _DOT_, _E_, _ELLIPSIS_, _EQUAL_, _H_, _LR_PAIRS, \ 

15 _N_, _name_, _not_scalar_, _PERCENT_, _SPACE_, \ 

16 _STAR_, _UNDER_ 

17from pygeodesy.interns import _convergence_, _distant_, _e_, _exceeds_, \ 

18 _EQUALSPACED_, _f_, _F_, _g_, _limit_, \ 

19 _no_, _tolerance_ # PYCHOK used! 

20from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

21 

22from math import fabs, log10 as _log10 

23 

24__all__ = _ALL_LAZY.streprs 

25__version__ = '24.11.26' 

26 

27_at_ = 'at' # PYCHOK used! 

28_EN_PREC = 6 # max MGRS/OSGR precision, 1 micrometer 

29_EN_WIDE = 5 # number of MGRS/OSGR units, log10(_100km) 

30_eps_ = 'eps' # PYCHOK used! 

31_OKd_ = '._-' # acceptable name characters 

32_PAREN_g = '(%g)' # PYCHOK used! 

33_RESIDUAL_ = 'RESIDUAL' # PYCHOK used! 

34_threshold_ = 'threshold' # PYCHOK used! 

35 

36 

37class _Fmt(str): 

38 '''(INTERNAL) Callable formatting. 

39 ''' 

40 name = NN 

41 

42 def __call__(self, *name_value_, **name_value): 

43 '''Format a C{name=value} pair or C{name, value} pair or 

44 just a single C{value}. 

45 ''' 

46 for n, v in name_value.items(): 

47 break 

48 else: 

49 n, v = name_value_[:2] if len(name_value_) > 1 else \ 

50 (NN, (name_value_ or MISSING)) 

51 t = str.__mod__(self, v) 

52 return NN(n, t) if n else t 

53 

54# def __mod__(self, arg, **unused): 

55# '''Regular C{%} operator. 

56# ''' 

57# return str.__mod__(self, arg) 

58 

59 

60class Fstr(str): 

61 '''(INTERNAL) C{float} format. 

62 ''' 

63 name = NN 

64 

65 def __call__(self, flt, prec=None, ints=False): 

66 '''Format the B{C{flt}} like function L{fstr}. 

67 ''' 

68 # see also function C{fstr} if isscalar case below 

69 t = str.__mod__(_pct(self), flt) if prec is None else next( 

70 _streprs(prec, (flt,), self, ints, True, None)) 

71 return t 

72 

73 def __mod__(self, arg, **unused): 

74 '''Regular C{%} operator. 

75 

76 @arg arg: A C{scalar} value to be formatted (either 

77 the C{scalar}, or a 1-tuple C{(scalar,)}, 

78 or 2-tuple C{(prec, scalar)}. 

79 

80 @raise TypeError: Non-scalar B{C{arg}} value. 

81 

82 @raise ValueError: Invalid B{C{arg}}. 

83 ''' 

84 def _error(arg): 

85 n = _DOT_(Fstr.__name__, self.name or self) 

86 return _SPACE_(n, _PERCENT_, repr(arg)) 

87 

88 prec = 6 # default std %f and %F 

89 if islistuple(arg): 

90 n = len(arg) 

91 if n == 1: 

92 arg = arg[0] 

93 elif n == 2: 

94 prec, arg = arg 

95 else: 

96 raise _ValueError(_error(arg)) 

97 

98 if not isscalar(arg): 

99 raise _TypeError(_error(arg)) 

100 return self(arg, prec=prec) # Fstr.__call__(self, arg, prec=prec) 

101 

102 

103class _Sub(str): 

104 '''(INTERNAL) Class list formatter. 

105 ''' 

106 # see .ellipsoidalNvector.LatLon.deltaTo 

107 def __call__(self, *Classes): 

108 t = _or(*(C.__name__ for C in Classes)) 

109 return str.__mod__(self, t or MISSING) 

110 

111 

112class Fmt(object): 

113 '''Formatting options. 

114 ''' 

115 ANGLE = _Fmt('<%s>') 

116 COLON = _Fmt(':%s') 

117# COLONSPACE = _Fmt(': %s') # == _COLONSPACE_(n, v) 

118# COMMASPACE = _Fmt(', %s') # == _COMMASPACE_(n, v) 

119 convergence = _Fmt(_convergence_(_PAREN_g)) 

120 CURLY = _Fmt('{%s}') # BRACES 

121 distant = _Fmt(_distant_('(%.3g)')) 

122 DOT = _Fmt('.%s') # == NN(_DOT_, n) 

123 e = Fstr(_e_) 

124 E = Fstr(_E_) 

125 EQUAL = _Fmt(_EQUAL_(NN, '%s')) 

126 EQUALg = _Fmt(_EQUAL_(NN, '%g')) 

127 EQUALSPACED = _Fmt(_EQUALSPACED_(NN, '%s')) 

128 exceeds_eps = _Fmt(_exceeds_(_eps_, _PAREN_g)) 

129 exceeds_limit = _Fmt(_exceeds_(_limit_, _PAREN_g)) 

130 exceeds_R = _Fmt(_exceeds_(_RESIDUAL_, _PAREN_g)) 

131 f = Fstr(_f_) 

132 F = Fstr(_F_) 

133 g = Fstr(_g_) 

134 G = Fstr('G') 

135 h = Fstr('%+.*f') # height, .streprs.hstr 

136 limit = _Fmt(' %s limit') # .units 

137 LOPEN = _Fmt('(%s]') # left-open range (L, R] 

138 PAREN = _Fmt('(%s)') 

139 PAREN_g = _Fmt(_PAREN_g) 

140 PARENSPACED = _Fmt(' (%s)') 

141 QUOTE2 = _Fmt('"%s"') 

142 ROPEN = _Fmt('[%s)') # right-open range [L, R) 

143# SPACE = _Fmt(' %s') # == _SPACE_(n, v) 

144 SQUARE = _Fmt('[%s]') # BRACKETS 

145 TAG = ANGLE 

146 TAGEND = _Fmt('</%s>') 

147 tolerance = _Fmt(_tolerance_(_PAREN_g)) 

148 zone = _Fmt('%02d') # .epsg, .mgrs, .utmupsBase 

149 

150 def __init__(self): 

151 for n, a in self.__class__.__dict__.items(): 

152 if isinstance(a, (Fstr, _Fmt)): 

153 setattr(a, _name_, n) 

154 

155 def __call__(self, obj, prec=9): 

156 '''Return C{str(B{obj})} or C{repr(B{obj})}. 

157 ''' 

158 return str(obj) if isint(obj) else next( 

159 _streprs(prec, (obj,), Fmt.g, False, False, repr)) 

160 

161 def INDEX(self, name=NN, i=None, **name_i): 

162 '''Return C{"B{name}" if B{i} is None else "B{name}[B{i}]"}. 

163 ''' 

164 if name_i: 

165 name, i = _xkwds_item2(name_i) 

166 return name if i is None else self.SQUARE(name, i) 

167 

168 def no_convergence(self, _d, *tol, **thresh): 

169 '''Return C{"no convergence (B{_d})"}, C{"no convergence 

170 (B{_d}), tolerance (B{tol})"} or C{"no convergence 

171 (B{_d}), threshold (B{tol})"}. 

172 ''' 

173 t = Fmt.convergence(fabs(_d)) 

174 if tol: 

175 t = _COMMASPACE_(t, Fmt.tolerance(tol[0])) 

176 if thresh and _xkwds_get(thresh, thresh=False): 

177 t = t.replace(_tolerance_, _threshold_) 

178 return _no_(t) 

179 

180 def repr_at(self, inst, text=NN): 

181 '''Return a C{repr} string C{"<B{text} at B{hex_id}>"}. 

182 ''' 

183 return self.ANGLE(_SPACE_((text or inst), _at_, hex(id(inst)))) 

184 

185Fmt = Fmt() # PYCHOK singleton 

186Fmt.__name__ = Fmt.__class__.__name__ 

187 

188_DOTSTAR_ = Fmt.DOT(_STAR_) 

189# formats %G and %g drop all trailing zeros and the 

190# decimal point, making the float appear as an int 

191_Gg = (Fmt.G, Fmt.g) 

192_FfEeGg = (Fmt.F, Fmt.f, Fmt.E, Fmt.e) + _Gg # float formats 

193_Fspec_ = NN('[%[<flags>][<width>]', _DOTSTAR_, ']', _BAR_.join(_FfEeGg)) # in testStreprs 

194 

195del _convergence_, _distant_, _e_, _eps_, _exceeds_, _EQUALSPACED_,\ 

196 _f_, _F_, _g_, _limit_, _PAREN_g, _RESIDUAL_ 

197 

198 

199def anstr(name, OKd=_OKd_, sub=_UNDER_): 

200 '''Make a valid name of alphanumeric and OKd characters. 

201 

202 @arg name: The original name (C{str}). 

203 @kwarg OKd: Other acceptable characters (C{str}). 

204 @kwarg sub: Substitute for invalid charactes (C{str}). 

205 

206 @return: The modified name (C{str}). 

207 

208 @note: Leading and trailing whitespace characters are removed, 

209 intermediate whitespace characters are coalesced and 

210 substituted. 

211 ''' 

212 s = n = str(name).strip() 

213 for c in n: 

214 if not (c.isalnum() or c in OKd or c in sub): 

215 s = s.replace(c, _SPACE_) 

216 return sub.join(s.strip().split()) 

217 

218 

219def attrs(inst, *names, **Nones_True__pairs_kwds): # prec=6, fmt=Fmt.F, ints=False, Nones=True, sep=_EQUAL_ 

220 '''Get instance attributes as I{name=value} strings, with C{float}s 

221 formatted by function L{fstr}. 

222 

223 @arg inst: The instance (any C{type}). 

224 @arg names: The attribute names, all other positional (C{str}). 

225 @kwarg Nones_True__pairs_kwds: Keyword argument for function L{pairs}, except 

226 C{B{Nones}=True} to in-/exclude missing or C{None}-valued attributes. 

227 

228 @return: A C{tuple(B{sep}.join(t) for t in zip(B{names}, reprs(values, ...)))} 

229 of C{str}s. 

230 ''' 

231 def _items(inst, names, Nones): 

232 for n in names: 

233 v = getattr(inst, n, None) 

234 if Nones or v is not None: 

235 yield n, v 

236 

237 def _Nones_kwds(Nones=True, **kwds): 

238 return Nones, kwds 

239 

240 Nones, kwds = _Nones_kwds(**Nones_True__pairs_kwds) 

241 return pairs(_items(inst, names, Nones), **kwds) 

242 

243 

244def enstr2(easting, northing, prec, *extras, **wide_dot): 

245 '''Return an MGRS/OSGR easting, northing string representations. 

246 

247 @arg easting: Easting from false easting (C{meter}). 

248 @arg northing: Northing from from false northing (C{meter}). 

249 @arg prec: Precision, the number of I{decimal} digits (C{int}) or if 

250 negative, the number of I{units to drop}, like MGRS U{PRECISION 

251 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}. 

252 @arg extras: Optional leading items (C{str}s). 

253 @kwarg wide_dot: Optional keword argument C{B{wide}=%d} for the number of I{unit digits} 

254 (C{int}) and C{B{dot}=False} (C{bool}) to insert a decimal point. 

255 

256 @return: B{C{extras}} + 2-tuple C{(str(B{easting}), str(B{northing}))} or 

257 + 2-tuple C{("", "")} for C{B{prec} <= -B{wide}}. 

258 

259 @raise ValueError: Invalid B{C{easting}}, B{C{northing}} or B{C{prec}}. 

260 

261 @note: The B{C{easting}} and B{C{northing}} values are I{truncated, not rounded}. 

262 ''' 

263 t = extras 

264 try: # like .dms.compassPoint 

265 p = min(int(prec), _EN_PREC) 

266 w = p + _xkwds_get(wide_dot, wide=_EN_WIDE) 

267 if w > 0: 

268 f = 10**p # truncate 

269 d = (-p) if p > 0 and _xkwds_get(wide_dot, dot=False) else 0 

270 t += (_0wdot(w, int(easting * f), d), 

271 _0wdot(w, int(northing * f), d)) 

272 else: # prec <= -_EN_WIDE 

273 t += (NN, NN) 

274 except (TypeError, ValueError) as x: 

275 raise _ValueError(easting=easting, northing=northing, prec=prec, cause=x) 

276 return t 

277 

278if enstr2.__doc__: # PYCHOK expected 

279 enstr2.__doc__ %= (_EN_WIDE,) 

280 

281 

282def _enstr2m3(estr, nstr, wide=_EN_WIDE): # in .mgrs, .osgr 

283 '''(INTERNAL) Convert east- and northing C{str}s to meter and resolution. 

284 ''' 

285 def _s2m2(s, m): # e or n str to float meter 

286 if _DOT_ in s: 

287 m = 1 # meter 

288 else: 

289 s += _0_ * wide 

290 s = _DOT_(s[:wide], s[wide:wide+_EN_PREC]) 

291 return float(s), m 

292 

293 e, m = _s2m2(estr, 0) 

294 n, m = _s2m2(nstr, m) 

295 if not m: 

296 p = max(len(estr), len(nstr)) # 2 = Km, 5 = m, 7 = cm 

297 m = 10**max(-_EN_PREC, wide - p) # resolution, meter 

298 return e, n, m 

299 

300 

301def fstr(floats, prec=6, fmt=Fmt.F, ints=False, sep=_COMMASPACE_, strepr=None, force=True): 

302 '''Convert one or more floats to string, optionally stripped of trailing zero decimals. 

303 

304 @arg floats: Single or a list, sequence, tuple, etc. (C{scalar}s). 

305 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

306 Trailing zero decimals are stripped if B{C{prec}} is 

307 positive, but kept for negative B{C{prec}} values. In 

308 addition, trailing decimal zeros are stripped for U{alternate, 

309 form '#'<https://docs.Python.org/3/library/stdtypes.html 

310 #printf-style-string-formatting>}. 

311 @kwarg fmt: Optional C{float} format (C{letter}). 

312 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}). 

313 @kwarg sep: Separator joining the B{C{floats}} (C{str}). 

314 @kwarg strepr: Optional callable to format non-C{floats} (typically 

315 C{repr}, C{str}) or C{None} to raise a TypeError and used 

316 only if C{B{force} is not True}. 

317 @kwarg force: If C{True}, format all B{C{floats}} using B{C{fmt}}, 

318 otherwise use B{C{strepr}} for non-C{floats} (C{bool}). 

319 

320 @return: The C{sep.join(strs(floats, ...)} joined (C{str}) or single 

321 C{strs((floats,), ...)} (C{str}) if B{C{floats}} is C{scalar}. 

322 ''' 

323 if isscalar(floats): # see Fstr.__call__ above 

324 return next(_streprs(prec, (floats,), fmt, ints, force, strepr)) 

325 else: 

326 return sep.join(_streprs(prec, floats, fmt, ints, force, strepr)) 

327 

328 

329def _fstrENH2(inst, prec, m, fmt=Fmt.F): # in .css, .lcc, .utmupsBase 

330 # (INTERNAL) For C{Css.} and C{Lcc.} C{toRepr} and C{toStr} and C{UtmUpsBase._toStr}. 

331 t = inst.easting, inst.northing 

332 t = tuple(_streprs(prec, t, fmt, False, True, None)) 

333 T = _E_, _N_ 

334 if m is not None and fabs(inst.height): # fabs(self.height) > EPS 

335 t += hstr(inst.height, prec=-2, m=m), 

336 T += _H_, 

337 return t, T 

338 

339 

340def _fstrLL0(inst, prec, toRepr): # in .azimuthal, .css 

341 # (INTERNAL) For C{_AlbersBase.}, C{_AzimuthalBase.} and C{CassiniSoldner.} 

342 t = tuple(_streprs(prec, inst.latlon0, Fmt.F, False, True, None)) 

343 if toRepr: 

344 n = inst.name 

345 if n: 

346 t += Fmt.EQUAL(_name_, repr(n)), 

347 t = Fmt.PAREN(inst.classname, _COMMASPACE_.join(t)) 

348 return t 

349 

350 

351def fstrzs(efstr, ap1z=False): 

352 '''Strip trailing zero decimals from a C{float} string. 

353 

354 @arg efstr: Float with or without exponent (C{str}). 

355 @kwarg ap1z: Append the decimal point and one zero decimal 

356 if the B{C{efstr}} is all digits (C{bool}). 

357 

358 @return: Float (C{str}). 

359 ''' 

360 s = efstr.find(_DOT_) 

361 if s >= 0: 

362 e = efstr.rfind(Fmt.e) 

363 if e < 0: 

364 e = efstr.rfind(Fmt.E) 

365 if e < 0: 

366 e = len(efstr) 

367 s += 2 # keep 1st _DOT_ + _0_ 

368 if s < e and efstr[e-1] == _0_: 

369 efstr = NN(efstr[:s], efstr[s:e].rstrip(_0_), efstr[e:]) 

370 

371 elif ap1z: 

372 # %.G and %.g formats may drop the decimal 

373 # point and all trailing zeros, ... 

374 if efstr.isdigit(): 

375 efstr += _DOT_ + _0_ # ... append or ... 

376 else: # ... insert one dot and zero 

377 e = efstr.rfind(Fmt.e) 

378 if e < 0: 

379 e = efstr.rfind(Fmt.E) 

380 if e > 0: 

381 efstr = NN(efstr[:e], _DOT_, _0_, efstr[e:]) 

382 

383 return efstr 

384 

385 

386def hstr(height, prec=2, fmt=Fmt.h, ints=False, m=NN): 

387 '''Return a string for the height value. 

388 

389 @arg height: Height value (C{float}). 

390 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

391 Trailing zero decimals are stripped if B{C{prec}} is 

392 positive, but kept for negative B{C{prec}} values. 

393 @kwarg fmt: Optional C{float} format (C{letter}). 

394 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}). 

395 @kwarg m: Optional unit of the height (C{str}). 

396 ''' 

397 h = next(_streprs(prec, (height,), fmt, ints, True, None)) 

398 return NN(h, str(m)) if m else h 

399 

400 

401def instr(inst, *args, **kwds): 

402 '''Return the string representation of an instantiation. 

403 

404 @arg inst: The instance (any C{type}). 

405 @arg args: Optional positional arguments. 

406 @kwarg kwds: Optional keyword arguments. 

407 

408 @return: Representation (C{str}). 

409 ''' 

410 return unstr(_MODS.named.classname(inst), *args, **kwds) 

411 

412 

413def lrstrip(txt, lrpairs=_LR_PAIRS): 

414 '''Left- I{and} right-strip parentheses, brackets, etc. from a string. 

415 

416 @arg txt: String to be stripped (C{str}). 

417 @kwarg lrpairs: Parentheses, etc. to remove (C{dict} of one or several 

418 C{(Left, Right)} pairs). 

419 

420 @return: Stripped B{C{txt}} (C{str}). 

421 ''' 

422 _e, _s, _n = str.endswith, str.startswith, len 

423 while _n(txt) > 2: 

424 for L, R in lrpairs.items(): 

425 if _e(txt, R) and _s(txt, L): 

426 txt = txt[_n(L):-_n(R)] 

427 break # restart 

428 else: 

429 return txt 

430 

431 

432def pairs(items, prec=6, fmt=Fmt.F, ints=False, sep=_EQUAL_): 

433 '''Convert items to I{name=value} strings, with C{float}s handled like L{fstr}. 

434 

435 @arg items: Name-value pairs (C{dict} or 2-{tuple}s of any C{type}s). 

436 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

437 Trailing zero decimals are stripped if B{C{prec}} is 

438 positive, but kept for negative B{C{prec}} values. 

439 @kwarg fmt: Optional C{float} format (C{letter}). 

440 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}). 

441 @kwarg sep: Separator joining I{names} and I{values} (C{str}). 

442 

443 @return: A C{tuple(B{sep}.join(t) for t in B{items}))} of C{str}s. 

444 ''' 

445 try: 

446 if isinstance(items, dict): 

447 items = itemsorted(items) 

448 elif not islistuple(items): 

449 items = tuple(items) 

450 # can't unzip empty items tuple, list, etc. 

451 n, v = _zip(*items) if items else ((), ()) # strict=True 

452 except (TypeError, ValueError): 

453 raise _IsnotError(dict, '2-tuples', items=items) 

454 v = _streprs(prec, v, fmt, ints, False, repr) 

455 return tuple(sep.join(t) for t in _zip(map(str, n), v)) # strict=True 

456 

457 

458def _pct(fmt): 

459 '''(INTERNAL) Prefix C{%} if needed. 

460 ''' 

461 return fmt if _PERCENT_ in fmt else NN(_PERCENT_, fmt) 

462 

463 

464def reprs(objs, prec=6, fmt=Fmt.F, ints=False): 

465 '''Convert objects to C{repr} strings, with C{float}s handled like L{fstr}. 

466 

467 @arg objs: List, sequence, tuple, etc. (any C{type}s). 

468 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

469 Trailing zero decimals are stripped if B{C{prec}} is 

470 positive, but kept for negative B{C{prec}} values. 

471 @kwarg fmt: Optional C{float} format (C{letter}). 

472 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}). 

473 

474 @return: A C{tuple(map(fstr|repr, objs))} of C{str}s. 

475 ''' 

476 return tuple(_streprs(prec, objs, fmt, ints, False, repr)) if objs else () 

477 

478 

479def _resolution10(resolution, Error=ValueError): # in .mgrs, .osgr 

480 '''(INTERNAL) Validate C{resolution} in C{meter}. 

481 ''' 

482 try: 

483 r = int(_log10(resolution)) 

484 if _EN_WIDE < r or r < -_EN_PREC: 

485 raise ValueError 

486 except (ValueError, TypeError): 

487 raise Error(resolution=resolution) 

488 return _MODS.units.Meter(resolution=10**r) 

489 

490 

491def _streprs(prec, objs, fmt, ints, force, strepr): 

492 '''(INTERNAL) Helper for C{fstr}, C{pairs}, C{reprs} and C{strs} 

493 ''' 

494 # <https://docs.Python.org/3/library/stdtypes.html#printf-style-string-formatting> 

495 if fmt in _FfEeGg: 

496 fGg = fmt in _Gg 

497 fmt = NN(_PERCENT_, _DOT_, abs(prec), fmt) 

498 

499 elif fmt.startswith(_PERCENT_): 

500 fGg = False 

501 try: # to make sure fmt is valid 

502 f = fmt.replace(_DOTSTAR_, Fmt.DOT(abs(prec))) 

503 _ = f % (_0_0,) 

504 except (TypeError, ValueError): 

505 raise _ValueError(fmt=fmt, txt_not_=repr(_DOTSTAR_)) 

506 fmt = f 

507 

508 else: 

509 raise _ValueError(fmt=fmt, txt_not_=repr(_Fspec_)) 

510 

511 for i, o in enumerate(objs): 

512 if force or isinstance(o, float): 

513 t = fmt % (float(o),) 

514 if ints and t.rstrip(_0to9_ if isint(o, both=True) else 

515 _0_).endswith(_DOT_): 

516 t = t.split(_DOT_)[0] 

517 elif prec > 1: 

518 t = fstrzs(t, ap1z=fGg) 

519 elif strepr: 

520 t = strepr(o) 

521 else: 

522 t = Fmt.PARENSPACED(Fmt.SQUARE(objs=i), o) 

523 raise TypeError(_SPACE_(t, _not_scalar_)) 

524 yield t 

525 

526 

527def strs(objs, prec=6, fmt=Fmt.F, ints=False): 

528 '''Convert objects to C{str} strings, with C{float}s handled like L{fstr}. 

529 

530 @arg objs: List, sequence, tuple, etc. (any C{type}s). 

531 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

532 Trailing zero decimals are stripped if B{C{prec}} is 

533 positive, but kept for negative B{C{prec}} values. 

534 @kwarg fmt: Optional C{float} format (C{letter}). 

535 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}). 

536 

537 @return: A C{tuple(map(fstr|str, objs))} of C{str}s. 

538 ''' 

539 return tuple(_streprs(prec, objs, fmt, ints, False, str)) if objs else () 

540 

541 

542def unstr(where, *args, **kwds_): 

543 '''Return the string representation of an invokation. 

544 

545 @arg where: Class, function, method (C{type}) or name (C{str}). 

546 @arg args: Optional positional arguments. 

547 @kwarg kwds_: Optional keyword arguments, except C{B{_Cdot}=None}, 

548 C{B{_ELLIPSIS}=False} and C{B{_fmt}=Fmt.g}. 

549 

550 @return: Representation (C{str}). 

551 ''' 

552 def _C_e_g_kwds3(_Cdot=None, _ELLIPSIS=0, _fmt=Fmt.g, **kwds): 

553 return _Cdot, _ELLIPSIS, _fmt, kwds 

554 

555 C, e, g, kwds = _C_e_g_kwds3(**kwds_) 

556 if e and len(args) > (e + 1): 

557 t = reprs(args[:e], fmt=g) 

558 t += _ELLIPSIS_, 

559 t += reprs(args[-1:], fmt=g) 

560 else: 

561 t = reprs(args, fmt=g) if args else () 

562 if kwds: 

563 t += pairs(itemsorted(kwds), fmt=g) 

564 n = where if isstr(where) else _DUNDER_nameof(where) # _NN_ 

565 if C and hasattr(C, n): 

566 try: # bound method of class C? 

567 where = where.__func__ 

568 except AttributeError: 

569 pass # method of C? 

570 if getattr(C, n, None) is where: 

571 n = _DOT_(_DUNDER_nameof(C), n) 

572 return Fmt.PAREN(n, _COMMASPACE_.join(t)) 

573 

574 

575def _0wd(*w_i): # in .osgr, .wgrs 

576 '''(INTERNAL) Int formatter'. 

577 ''' 

578 return '%0*d' % w_i 

579 

580 

581def _0wdot(w, f, dot=0): 

582 '''(INTERNAL) Int and Float formatter'. 

583 ''' 

584 s = _0wd(w, int(f)) 

585 if dot: 

586 s = _DOT_(s[:dot], s[dot:]) 

587 return s 

588 

589 

590def _0wpF(*w_p_f): # in .dms, .osgr 

591 '''(INTERNAL) Float deg, min, sec formatter'. 

592 ''' 

593 return '%0*.*f' % w_p_f # XXX was F 

594 

595 

596def _xzipairs(names, values, sep=_COMMASPACE_, fmt=NN, pair_fmt=Fmt.COLON): 

597 '''(INTERNAL) Zip C{names} and C{values} into a C{str}, joined and bracketed. 

598 ''' 

599 try: 

600 t = sep.join(pair_fmt(*t) for t in _zip(names, values)) # strict=True 

601 except Exception as x: 

602 raise _ValueError(names=names, values=values, cause=x) 

603 return (fmt % (t,)) if fmt else t # enc 

604 

605 

606# **) MIT License 

607# 

608# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

609# 

610# Permission is hereby granted, free of charge, to any person obtaining a 

611# copy of this software and associated documentation files (the "Software"), 

612# to deal in the Software without restriction, including without limitation 

613# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

614# and/or sell copies of the Software, and to permit persons to whom the 

615# Software is furnished to do so, subject to the following conditions: 

616# 

617# The above copyright notice and this permission notice shall be included 

618# in all copies or substantial portions of the Software. 

619# 

620# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

621# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

622# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

623# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

624# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

625# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

626# OTHER DEALINGS IN THE SOFTWARE.