1

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

3

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

5'''

6

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

8 _zip, _0_0

9# from pygeodesy.constants import _0_0

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

11 _ValueError, _xkwds_get, _xkwds_item2, _xkwds_pop2

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

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

14 _N_, _name_, _not_, _not_scalar_, _PERCENT_, \

15 _SPACE_, _STAR_, _UNDER_, _dunder_nameof

16 from pygeodesy.interns import _convergence_, _distant_, _e_, _eps_, _exceeds_, \

17 _EQUALSPACED_, _f_, _F_, _g_, _limit_, _no_, \

18 _tolerance_ # PYCHOK used!

19 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

20

21 from math import fabs, log10 as _log10

22

23__all__ = _ALL_LAZY.streprs

24__version__ = '24.04.28'

25

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

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

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

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

30_RESIDUAL_ = 'RESIDUAL' # PYCHOK used!

31_threshold_ = 'threshold' # PYCHOK used!

32

33

34 class _Fmt(str):

35 '''(INTERNAL) Callable formatting.

36 '''

37 name = NN

38

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

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

41 just a single C{value}.

42 '''

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

44 break

45 else:

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

47 (name_value_[0] if name_value_ else MISSING))

48 t = str.__mod__(self, v)

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

50

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

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

53# '''

54# return str.__mod__(self, arg)

55

56

57 class Fstr(str):

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

59 '''

60 name = NN

61

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

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

64 '''

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

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

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

68 return t

69

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

71 '''Regular C{%} operator.

72

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

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

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

76

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

78

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

80 '''

81 def _error(arg):

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

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

84

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

86 if islistuple(arg):

87 n = len(arg)

88 if n == 1:

89 arg = arg[0]

90 elif n == 2:

91 prec, arg = arg

92 else:

93 raise _ValueError(_error(arg))

94

95 if not isscalar(arg):

96 raise _TypeError(_error(arg))

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

98

99

100 class _Sub(str):

101 '''(INTERNAL) Class list formatter.

102 '''

103 # see .ellipsoidalNvector.LatLon.deltaTo

104 def __call__(self, *Classes):

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

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

107

108

109 class Fmt(object):

110 '''Formatting options.

111 '''

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

113 COLON = _Fmt(':%s')

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

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

116 convergence = _Fmt(_convergence_(_PAREN_g))

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

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

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

120 e = Fstr(_e_)

121 E = Fstr(_E_)

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

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

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

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

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

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

128 f = Fstr(_f_)

129 F = Fstr(_F_)

130 g = Fstr(_g_)

131 G = Fstr('G')

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

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

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

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

136 PAREN_g = _Fmt(_PAREN_g)

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

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

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

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

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

142 sub_class = _Sub('%s (sub-)class')

143 TAG = ANGLE

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

145 tolerance = _Fmt(_tolerance_(_PAREN_g))

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

147

148 def __init__(self):

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

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

151 setattr(a, _name_, n)

152

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

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

155 '''

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

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

158

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

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

161 '''

162 if name_i:

163 name, i = _xkwds_item2(name_i)

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

165

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

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

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

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

170 '''

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

172 if tol:

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

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

175 t = t.replace(_tolerance_, _threshold_)

176 return _no_(t)

177

178 Fmt = Fmt() # PYCHOK singleton

179 Fmt.__name__ = Fmt.__class__.__name__

180

181_DOTSTAR_ = Fmt.DOT(_STAR_)

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

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

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

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

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

187

188

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

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

191

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

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

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

195

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

197

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

199 intermediate whitespace characters are coalesced and

200 substituted.

201 '''

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

203 for c in n:

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

205 s = s.replace(c, _SPACE_)

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

207

208

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

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

211 formatted by function L{fstr}.

212

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

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

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

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

217

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

219 of C{str}s.

220 '''

221 def _items(inst, names, Nones):

222 for n in names:

223 v = getattr(inst, n, None)

224 if Nones or v is not None:

225 yield n, v

226

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

228 return Nones, kwds

229

230 Nones, kwds = _Nones_kwds(**Nones_True__pairs_kwds)

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

232

233

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

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

236

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

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

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

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

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

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

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

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

245

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

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

248

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

250

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

252 '''

253 t = extras

254 try: # like .dms.compassPoint

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

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

257 if w > 0:

258 f = 10**p # truncate

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

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

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

262 else: # prec <= -_EN_WIDE

263 t += (NN, NN)

264 except (TypeError, ValueError) as x:

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

266 return t

267

268 if enstr2.__doc__: # PYCHOK expected

269 enstr2.__doc__ %= (_EN_WIDE,)

270

271

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

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

274 '''

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

276 if _DOT_ in s:

277 m = 1 # meter

278 else:

279 s += _0_ * wide

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

281 return float(s), m

282

283 e, m = _s2m2(estr, 0)

284 n, m = _s2m2(nstr, m)

285 if not m:

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

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

288 return e, n, m

289

290

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

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

293

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

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

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

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

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

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

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

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

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

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

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

305 C{repr}, C{str}) or C{None} to raise a TypeError.

306

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

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

309 '''

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

311 return next(_streprs(prec, (floats,), fmt, ints, True, strepr))

312 else:

313 return sep.join(_streprs(prec, floats, fmt, ints, True, strepr))

314

315

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

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

318 t = inst.easting, inst.northing

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

320 T = _E_, _N_

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

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

323 T += _H_,

324 return t, T

325

326

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

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

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

330 if toRepr:

331 n = inst.name

332 if n:

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

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

335 return t

336

337

338 def fstrzs(efstr, ap1z=False):

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

340

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

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

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

344

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

346 '''

347 s = efstr.find(_DOT_)

348 if s >= 0:

349 e = efstr.rfind(Fmt.e)

350 if e < 0:

351 e = efstr.rfind(Fmt.E)

352 if e < 0:

353 e = len(efstr)

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

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

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

357

358 elif ap1z:

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

360 # point and all trailing zeros, ...

361 if efstr.isdigit():

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

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

364 e = efstr.rfind(Fmt.e)

365 if e < 0:

366 e = efstr.rfind(Fmt.E)

367 if e > 0:

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

369

370 return efstr

371

372

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

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

375

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

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

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

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

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

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

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

383 '''

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

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

386

387

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

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

390

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

392 @arg args: Optional positional arguments.

393 @kwarg kwds: Optional keyword arguments.

394

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

396 '''

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

398

399

400 def lrstrip(txt, lrpairs=_LR_PAIRS):

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

402

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

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

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

406

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

408 '''

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

410 while _n(txt) > 2:

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

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

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

414 break # restart

415 else:

416 return txt

417

418

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

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

421

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

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

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

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

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

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

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

429

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

431 '''

432 try:

433 if isinstance(items, dict):

434 items = itemsorted(items)

435 elif not islistuple(items):

436 items = tuple(items)

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

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

439 except (TypeError, ValueError):

440 raise _IsnotError(dict.__name__, '2-tuples', items=items)

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

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

443

444

445 def _pct(fmt):

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

447 '''

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

449

450

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

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

453

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

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

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

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

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

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

460

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

462 '''

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

464

465

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

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

468 '''

469 try:

470 r = int(_log10(resolution))

471 if _EN_WIDE < r or r < -_EN_PREC:

472 raise ValueError

473 except (ValueError, TypeError):

474 raise Error(resolution=resolution)

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

476

477

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

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

480 '''

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

482 if fmt in _FfEeGg:

483 fGg = fmt in _Gg

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

485

486 elif fmt.startswith(_PERCENT_):

487 fGg = False

488 try: # to make sure fmt is valid

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

490 _ = f % (_0_0,)

491 except (TypeError, ValueError):

492 raise _ValueError(fmt=fmt, txt=_not_(repr(_DOTSTAR_)))

493 fmt = f

494

495 else:

496 raise _ValueError(fmt=fmt, txt=_not_(repr(_Fspec_)))

497

498 for i, o in enumerate(objs):

499 if force or isinstance(o, float):

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

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

502 _0_).endswith(_DOT_):

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

504 elif prec > 1:

505 t = fstrzs(t, ap1z=fGg)

506 elif strepr:

507 t = strepr(o)

508 else:

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

510 raise TypeError(_SPACE_(t, _not_scalar_))

511 yield t

512

513

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

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

516

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

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

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

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

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

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

523

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

525 '''

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

527

528

529 def unstr(where, *args, **kwds):

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

531

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

533 @arg args: Optional positional arguments.

534 @kwarg kwds: Optional keyword arguments, except C{B{_fmt}=Fmt.g}

535 and C{B{_ELLIPSIS}=False}.

536

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

538 '''

539 e, kwds = _xkwds_pop2(kwds, _ELLIPSIS=False)

540 g, kwds = _xkwds_pop2(kwds, _fmt=Fmt.g)

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

542 if e:

543 t += _ELLIPSIS_,

544 if kwds:

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

546 n = where if isstr(where) else _dunder_nameof(where) # _NN_

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

548

549

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

551 '''(INTERNAL) Int formatter'.

552 '''

553 return '%0*d' % w_i

554

555

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

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

558 '''

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

560 if dot:

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

562 return s

563

564

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

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

567 '''

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

569

570

571 def _xattrs(insto, other, *attrs): # see .errors._xattr

572 '''(INTERNAL) Copy attribute values from B{C{other}} to B{C{insto}}.

573

574 @arg insto: Object to copy attribute values to (any C{type}).

575 @arg other: Object to copy attribute values from (any C{type}).

576 @arg attrs: One or more attribute names (C{str}s).

577

578 @return: Object B{C{insto}}, updated.

579

580 @raise AttributeError: An B{C{attrs}} doesn't exist

581 or is not settable.

582 '''

583 def _getattr(o, a):

584 if hasattr(o, a):

585 return getattr(o, a)

586 try:

587 n = o._DOT_(a)

588 except AttributeError:

589 n = Fmt.DOT(a)

590 raise _AttributeError(o, name=n)

591

592 for a in attrs:

593 s = _getattr(other, a)

594 g = _getattr(insto, a)

595 if (g is None and s is not None) or g != s:

596 setattr(insto, a, s) # not settable?

597 return insto

598

599

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

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

602 '''

603 try:

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

605 except Exception as x:

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

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

608

609# **) MIT License

610#

611# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.

612#

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

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

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

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

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

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

619#

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

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

622#

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

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

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

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

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

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

629# OTHER DEALINGS IN THE SOFTWARE.