1

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

3

4 u'''(INTERNAL) Classes C{_Rcoeffs}, C{_Rdict} and C{_Rtuple} to store the deferred

5 Python versions of coefficients from I{Karney}'s C++ class U{AuxLatitude

6<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxLatitude.html>}.

7

8 Copyright (C) Charles Karney (2022-2024) Karney@Alum.MIT.edu> and licensed under the

9 MIT/X11 License. For more information, see <https://GeographicLib.SourceForge.io>.

10'''

11# make sure int/int division yields float quotient, see .basics

12 from __future__ import division as _; del _ # PYCHOK semicolon

13

14# from pygeodesy.basics import _splituple # _MODS

15 from pygeodesy.constants import _floats as _constants_floats

16 from pygeodesy.errors import _AssertionError, _MODS

17# from pygeodesy.internals import _sizeof # _MODS

18 from pygeodesy.interns import NN, MISSING, _COMMA_, _duplicate_, _NL_, \

19 _QUOTE3_, _SLASH_, _ELLIPSIS4_ # PYCHOK used!

20# from pygeodesy.lazily import _ALL_MODS as _MODS # from .errors

21 from pygeodesy.named import ADict, Property_RO

22# from pygeodesy.props import Property_RO # from .named

23

24__all__ = ()

25__version__ = '25.04.11'

26

27

28 class _Rcoeffs(ADict):

29 '''(INTERNAL) With string-ified C{keys}.

30 '''

31 def __init__(self, ALorder, coeffs):

32 '''New C{_Rcoeffs} from a C{coeffs} dict.

33 '''

34 try:

35 if not isinstance(coeffs, dict):

36 raise _RsError(coeffs=type(coeffs))

37 n = 0

38 for k, d in coeffs.items():

39 if not isinstance(k, _Rkey):

40 raise _RsError(k=type(k))

41 if not isinstance(d, _Rdict):

42 raise _RsError(k=k, d=type(d))

43 n += d.n

44

45 ADict.__init__(self, coeffs)

46 self.set_(ALorder=ALorder, n=n) # in .validate

47 except Exception as x:

48 raise _RsError(ALorder=ALorder, cause=x)

49

50 def bnuz4(self): # in .auxilats.__main__ # PYCHOK no cover

51 # get C{(strB, number, unique, floatB)} rationals

52 b = n = u = z = 0

53 _zB = _MODS.internals._sizeof

54 for R in self._Rtuples():

55 _, _, rs = R.k_n_rs

56 b += _zB(rs)

57 t = R._tuple

58 z += _zB(t) # Float

59 # assert R.Rdict is None

60 n += len(t)

61 u += sum(1 for f in t if f in _constants_floats)

62 return b, n, (n - u), z

63

64 def items(self): # string-ify keys # PYCHOK no cover

65 for n, v in ADict.items(self):

66 yield str(n), v

67

68 def _Rtuples(self): # PYCHOK no cover

69 for d in self.values():

70 if isinstance(d, _Rdict):

71 # yield from d.values()

72 for R in d.values():

73 yield R

74

75 def _validate(self, aL, lenAux):

76 # in .auxily.Aux._CXcoeffs(al, Aux.len(aL))

77 a, n = self.ALorder, self.n # PYCHOK ADict!

78# for R in self._Rtuples():

79# assert isinstance(R, _Rtuple)

80 if aL != a or lenAux != n:

81 raise _RsError(aL=aL, ALorder=a, lenAux=lenAux, n=n)

82 return self

83

84

85 class _Rdict(dict): # in ._CX_#, .auxLat, .rhumb.aux_

86 '''(INTERNAL) Dict of C{_Rtuple}s.

87 '''

88 n = 0 # sum(R.k_n_k[1] for R in Rtuples)

89

90 def __init__(self, nt, *Rtuples):

91 '''New C{_Rdict}.

92 '''

93 if not Rtuples:

94 raise _RsError(Rtuples=MISSING)

95

96 for R in Rtuples:

97 if not isinstance(R, _Rtuple):

98 raise _RsError(R, R=type(R))

99 k, n, _ = R.k_n_rs

100 if k in self:

101 raise _RsError(_duplicate_, k=k)

102 R.Rdict = self

103 self[k] = R # overwritten in self._floatuple

104 self.n += n

105 if self.n != nt:

106 raise _RsError(n=n, nt=nt)

107

108 def _floats(self, rs):

109 # yield floats from a string of comma-separated rationals

110 def _p_q(p=NN, q=1, *x):

111 return (NN if x else p), q

112

113 _get = _constants_floats.get

114 for r in _MODS.basics._splituple(rs):

115 p, q = _p_q(*r.split(_SLASH_))

116 if p:

117 f = int(p) / int(q) # fractions.Fraction?

118 if not isinstance(f, float):

119 raise _RsError(rs, f=f, p=p, q=q, r=r)

120 yield _get(f, f) # from .constants

121 else:

122 raise _RsError(rs, r=r)

123

124 def _floatuple(self, Rtuple):

125 # return a tuple of floats from an C{_Rtuple}

126 k, n, rs = Rtuple.k_n_rs

127 t = tuple(f for m in map(self._floats, rs)

128 for f in m) # == yield f

129 # @see: <https://StackOverflow.com/questions/10632839/>

130 # and <https://realPython.com/python-flatten-list/>

131 if len(t) != n:

132 raise _RsError(*rs, len=len(t), n=n)

133 self[k] = t # replace _Rtuple with tuple instance

134 return t

135

136

137 class _Rkey(int):

138 '''(INTERNAL) For C{_Rcoeffs}, C{_Rdict} and C{_Rtuple} keys.

139 '''

140 def __new__(cls, k):

141 if not isinstance(k, int):

142 raise _RsError(k=type(k))

143 if not 0 <= k <= 8: # 0.._MODS.auxilats.auxily.Aux.N + 2

144 raise _RsError(k=k)

145 return int.__new__(cls, k)

146

147

148 class _RsError(_AssertionError):

149 '''(INTERNAL) For C{_Rcoeffs}, C{_Rdict or} C{_Rtuple} issues.

150 '''

151 def __init__(self, *rs, **kwds_cause): # PYCHOK no cover

152 if rs:

153 if len(rs) > 1:

154 t = _NL_(NN, *rs)

155 t = NN(_QUOTE3_, t, _QUOTE3_)

156 else: # single rs

157 t = repr(rs[0])

158 kwds_cause.update(txt=t)

159 _AssertionError.__init__(self, **kwds_cause)

160

161

162 class _Rtuple(list): # MUST be list, NOT tuple!

163 '''(INTERNAL) I{Pseudo-tuple} of float rationals used in C{_Rdict}s.

164 '''

165 Rdict = None # set by _Rdict.__init__

166 k_n_rs = None, 0, ()

167

168 def __init__(self, k, n, *rs):

169 '''New C{_Rtuple} with key C{k}, number of floats C{n} and with

170 each C{rs} a C{str} of comma-separated rationals C{"p/q, ..."}

171 where C{p} and C{q} are C{int} digits only.

172 '''

173 try:

174 if not isinstance(k, _Rkey):

175 raise _RsError(k=type(k))

176 if not (isinstance(n, int) and n > 0):

177 raise _RsError(n=type(n))

178 if not rs:

179 raise _RsError(rs=MISSING)

180 for t in rs:

181 if not isinstance(t, str):

182 raise _RsError(rs=type(t))

183 self.k_n_rs = k, n, rs

184 except Exception as x:

185 raise _RsError(*rs, k=k, n=n, cause=x)

186

187 def __getitem__(self, i):

188 return self._tuple[i]

189

190 if _MODS.sys_version_info2 < (3, 0):

191 def __getslice__(self, *i_j): # PYCHOK 3 args

192 return self._tuple[slice(*i_j)]

193

194 def __iter__(self):

195 return iter(self._tuple)

196

197 def __len__(self):

198 return len(self._tuple)

199

200 @Property_RO

201 def _tuple(self):

202 # build the C{tuple} I{once} and replace

203 # C{_Rdict[key]} item with the C{tuple}

204 try:

205 k, n, rs = self.k_n_rs # for except ...

206 t = self.Rdict._floatuple(self)

207# self[:] = t # MUST copy into self?

208 except Exception as x:

209 if len(rs) > 1 and _QUOTE3_ in str(x):

210 rs = rs[0], _ELLIPSIS4_

211 raise _RsError(k=k, n=n, rs=rs, cause=x)

212 del self.Rdict, self.k_n_rs # trash refs

213 return t

214

215 def append(self, arg):

216 raise _RsError(append=arg)

217

218 def extend(self, arg):

219 raise _RsError(extend=arg)

220

221# **) MIT License

222#

223# Copyright (C) 2024-2025 -- mrJean1 at Gmail -- All Rights Reserved.

224#

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

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

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

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

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

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

231#

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

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

234#

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

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

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

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

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

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

241# OTHER DEALINGS IN THE SOFTWARE.