Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
FloatConversion.inl
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include <charconv>
5#include <sstream>
6
10#include "Stroika/Foundation/Containers/Common.h"
11#include "Stroika/Foundation/Math/Common.h"
12#include "Stroika/Foundation/Memory/Common.h"
15
17
18 /*
19 ********************************************************************************
20 ******************** FloatConversion::SignificantFigures ***********************
21 ********************************************************************************
22 */
23 constexpr SignificantFigures::SignificantFigures (unsigned int p)
24 : fSignificantFigures_{p}
25 {
26 }
27 constexpr SignificantFigures::SignificantFigures (FullFlag)
28 : fSignificantFigures_{} // kFullPrecision
29 {
30 }
31 template <floating_point T>
32 constexpr auto SignificantFigures::GetEffectiveSignificantFigures () const -> RepType
33 {
34 return fSignificantFigures_.value_or (numeric_limits<T>::max_digits10);
35 }
36 template <IStdBasicStringCompatibleCharacter CHAR>
37 constexpr auto SignificantFigures::Calculate (span<const CHAR> number) -> RepType
38 {
39 // two passes cuz eatLeadingZero depends on scientific, which we find at the end of the text
40 bool dotPresent = false;
41 bool scientific = false;
42 for (const CHAR c : number) {
43 switch (c) {
44 // Some locales use . (most), but europe uses , for same purpose (and used in ISO-8601)
45 case '.':
46 case ',':
47 dotPresent = true;
48 break;
49 case 'e':
50 case 'E':
51 scientific = true;
52 break;
53 }
54 }
55 RepType n{0};
56 RepType trailingZeros{0};
57 bool eatLeadingZeros = not scientific;
58 bool gotoDone = false;
59 for (const CHAR c : number) {
60 switch (c) {
61 case '-':
62 case '+':
63 break;
64 case '0':
65 if (eatLeadingZeros) {
66 // munch
67 }
68 else {
69 ++n;
70 }
71 ++trailingZeros;
72 break;
73 case '1':
74 case '2':
75 case '3':
76 case '4':
77 case '5':
78 case '6':
79 case '7':
80 case '8':
81 case '9':
82 eatLeadingZeros = false;
83 trailingZeros = 0;
84 ++n;
85 break;
86 case '.':
87 case ',':
88 trailingZeros = 0;
89 break;
90 case 'e':
91 case 'E':
92 gotoDone = true; // goto Done; // FIX to use this when c++23
93 break;
94 }
95 if (gotoDone) {
96 break;
97 }
98 }
99 // Done:
100 if (dotPresent) {
101 if (eatLeadingZeros) {
102 // then the trailing zeros we counted were also leading 0, but wern't added to n so do so now
103 n += trailingZeros;
104 }
105 }
106 else {
107 n -= trailingZeros;
108 }
109 return n;
110 }
111
112 /**
113 * From http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf,
114 * init (basic_streambuf...) initializes precision to 6
115 * Stroika need not maintain that default here, but it seems a sensible one...
116 *
117 * \note defined here instead of header cuz needs to be after SignificantFigures::CTOR.
118 */
119 constexpr inline SignificantFigures SignificantFigures::kDefault{6};
120
121 /**
122 * \brief Full precision here means enough digits so that when written out (serialized) - and read back in (deserialized)
123 * you get the exact same answer.
124 *
125 * \see // https://stackoverflow.com/questions/22458355/what-is-the-purpose-of-max-digits10-and-how-is-it-different-from-digits10
126 * numeric_limits<T>::max_digits10
127 *
128 * \note defined here instead of header cuz needs to be after SignificantFigures::CTOR.
129 */
130 constexpr inline SignificantFigures SignificantFigures::kFullPrecision{SignificantFigures::FullFlag::eFull};
131
132 /*
133 ********************************************************************************
134 ********************* FloatConversion::ToStringOptions *************************
135 ********************************************************************************
136 */
137 inline ToStringOptions::ToStringOptions (const locale& l)
138 : fUseLocale_{l}
139 {
140 }
142 : fUseCurrentLocale_{p == PredefinedLocale::eUseCurrentLocale}
143 {
144 }
145 constexpr ToStringOptions::ToStringOptions (ios_base::fmtflags fmtFlags)
146 : fFmtFlags_{fmtFlags}
147 {
148 }
149 constexpr ToStringOptions::ToStringOptions (SignificantFigures precision)
150 : fSignificantFigures_{precision}
151 {
152 }
153 constexpr ToStringOptions::ToStringOptions (FloatFormatType scientificNotation)
154 : fFloatFormat_{scientificNotation}
155 {
156 }
157 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
158 DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
160 constexpr ToStringOptions::ToStringOptions (TrimTrailingZerosType trimTrailingZeros)
161 : fTrimTrailingZeros_{trimTrailingZeros == TrimTrailingZerosType::eTrimZeros}
162 {
163 }
164 DISABLE_COMPILER_MSC_WARNING_END (4996)
165 DISABLE_COMPILER_CLANG_WARNING_END ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
166 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
167
168 constexpr ToStringOptions::ToStringOptions (const ToStringOptions& b1, const ToStringOptions& b2)
169 : ToStringOptions{b1}
170 {
171 Memory::CopyToIf (&fSignificantFigures_, b2.fSignificantFigures_);
172 Memory::CopyToIf (&fFmtFlags_, b2.fFmtFlags_);
173 fUseCurrentLocale_ = b2.fUseCurrentLocale_;
174 Memory::CopyToIf (&fUseLocale_, b2.fUseLocale_);
175 Memory::CopyToIf (&fTrimTrailingZeros_, b2.fTrimTrailingZeros_);
176 Memory::CopyToIf (&fFloatFormat_, b2.fFloatFormat_);
177 }
178 template <typename... ARGS>
179 constexpr ToStringOptions::ToStringOptions (const ToStringOptions& b1, const ToStringOptions& b2, ARGS&&... args)
180 : ToStringOptions{ToStringOptions{b1, b2}, forward<ARGS> (args)...}
181 {
182 }
183 constexpr optional<SignificantFigures> ToStringOptions::GetSignificantFigures () const
184 {
185 return fSignificantFigures_;
186 }
187 constexpr optional<bool> ToStringOptions::GetTrimTrailingZeros () const
188 {
189 return fTrimTrailingZeros_;
190 }
191 inline locale ToStringOptions::GetUseLocale () const
192 {
193 if (fUseCurrentLocale_) {
194 return locale{};
195 }
196 if (fUseLocale_) {
197 return *fUseLocale_;
198 }
199 static const locale kCLocale_ = locale::classic ();
200 return kCLocale_;
201 }
203 {
204 if (not fUseLocale_ and not fUseCurrentLocale_) {
205 return true; // very common case
206 }
207 return GetUseLocale () == locale::classic ();
208 }
209 constexpr optional<FloatFormatType> ToStringOptions::GetFloatFormat () const
210 {
211 return fFloatFormat_;
212 }
213 constexpr optional<ios_base::fmtflags> ToStringOptions::GetIOSFmtFlags () const
214 {
215 return fFmtFlags_;
216 }
217
218 namespace Private_ {
219 inline void TrimTrailingZeros_NotSci_ (String* strResult)
220 {
221 // @todo THIS could be more efficient: use StringBuilder and Peek()/Character::AsASCIIQuietly
222 RequireNotNull (strResult);
223 // strip trailing zeros (after decimal point - before they may indicate magnatude)
224 // And don't do if ends with exponential notation e+40 shouldnt get shortned to e+4!
225 Require (not strResult->Find ('e', eCaseInsensitive).has_value ());
226 size_t pastDot = strResult->find ('.');
227 if (pastDot != String::npos) {
228 ++pastDot;
229 size_t len = strResult->length ();
230 size_t pPastLastZero = len;
231 for (; (pPastLastZero - 1) > pastDot; --pPastLastZero) {
232 if ((*strResult)[pPastLastZero - 1] != '0') {
233 break;
234 }
235 }
236 if (pPastLastZero == pastDot + 1 and (*strResult)[pPastLastZero - 1] == '0') {
237 pPastLastZero -= 2; // dont map 3.000 to 3.0, map it to 3
238 }
239 if (len != pPastLastZero) [[unlikely]] { // check common case of no change, but this substring and assign already pretty optimized (not sure helps performance)
240 *strResult = strResult->SubString (0, pPastLastZero);
241 }
242 }
243 }
244 inline void TrimTrailingZeros_MaybeSci_ (String* strResult)
245 {
246 // @todo THIS could be more efficient: use StringBuilder and Peek()/Character::AsASCIIQuietly
247 RequireNotNull (strResult);
248 // strip trailing zeros (after decimal point - before they may indicate magnatude)
249 // And don't do if ends with exponential notation e+40 shouldnt get shortned to e+4!
250 if (optional<size_t> whereIsE = strResult->Find ('e', eCaseInsensitive)) {
251 size_t pastDot = strResult->find ('.');
252 if (pastDot != String::npos) {
253 ++pastDot;
254 size_t len = *whereIsE - 1;
255 size_t pPastLastZero = len;
256 for (; (pPastLastZero - 1) > pastDot; --pPastLastZero) {
257 if ((*strResult)[pPastLastZero - 1] != '0') {
258 break;
259 }
260 }
261 if (len != pPastLastZero) [[unlikely]] { // check common case of no change, but this substring and assign already pretty optimized (not sure helps performance)
262 *strResult = strResult->SubString (0, pPastLastZero) + strResult->SubString (*whereIsE);
263 }
264 }
265 }
266 else {
267 size_t pastDot = strResult->find ('.');
268 if (pastDot != String::npos) {
269 ++pastDot;
270 size_t len = strResult->length ();
271 size_t pPastLastZero = len;
272 for (; (pPastLastZero - 1) > pastDot; --pPastLastZero) {
273 if ((*strResult)[pPastLastZero - 1] != '0') {
274 break;
275 }
276 }
277 if (pPastLastZero == pastDot + 1 and (*strResult)[pPastLastZero - 1] == '0') {
278 pPastLastZero -= 2; // dont map 3.000 to 3.0, map it to 3
279 }
280 if (len != pPastLastZero) [[unlikely]] { // check common case of no change, but this substring and assign already pretty optimized (not sure helps performance)
281 *strResult = strResult->SubString (0, pPastLastZero);
282 }
283 }
284 }
285 }
286 }
287
288 namespace Private_ {
289 // TEMPLATE version of wcstof(etc) - to make easier to call from generic algorithm
290 template <typename T>
291 T CStr2FloatType_ (const wchar_t* s, wchar_t** e);
292 template <typename T>
293 T CStr2FloatType_ (const char* s, char** e);
294 template <>
295 inline float CStr2FloatType_ (const wchar_t* s, wchar_t** e)
296 {
297 return ::wcstof (s, e);
298 }
299 template <>
300 inline double CStr2FloatType_ (const wchar_t* s, wchar_t** e)
301 {
302 return wcstod (s, e);
303 }
304 template <>
305 inline long double CStr2FloatType_ (const wchar_t* s, wchar_t** e)
306 {
307 return wcstold (s, e);
308 }
309 template <>
310 inline float CStr2FloatType_ (const char* s, char** e)
311 {
312 return ::strtof (s, e);
313 }
314 template <>
315 inline double CStr2FloatType_ (const char* s, char** e)
316 {
317 return strtod (s, e);
318 }
319 template <>
320 inline long double CStr2FloatType_ (const char* s, char** e)
321 {
322 return strtold (s, e);
323 }
324 }
325
326 namespace Private_ {
327 template <typename RETURN_TYPE>
328 RETURN_TYPE ToFloat_ViaStrToD_ (const char* start, const char* end, const char** remainder)
329 {
330 Require (start <= end);
331 if (start == end) {
332 if (remainder != nullptr) {
333 *remainder = start;
334 }
335 return Math::nan<RETURN_TYPE> ();
336 }
337 else {
338 char* e = nullptr;
339 const char* cst = start;
340
341 if (remainder == nullptr) {
342 // REJECT strings with leading space in this case - must match exactly
343 if (::isspace (*cst)) {
344 return Math::nan<RETURN_TYPE> ();
345 }
346 }
347
348 Memory::StackBuffer<char> tmp;
349 if (*end != '\0') {
350 // remap addresses - copying to a temporary buffer, so we can nul-terminate string passed to strtod (etc)
351 size_t len = end - start;
352 tmp.GrowToSize (len + 1);
353 (void)::memcpy (tmp.begin (), start, len);
354 cst = tmp.begin ();
355 tmp[len] = '\0';
356 }
357 RETURN_TYPE d = CStr2FloatType_<RETURN_TYPE> (cst, &e);
358 // If asked to return remainder do so.
359 // If NOT asked to return remainder, treat not using the entire string as forcing result to be a Nan (invalid parse of number of not the whole thing is a number)
360 if (remainder == nullptr) {
361 if (e != end) {
362 d = Math::nan<RETURN_TYPE> ();
363 }
364 }
365 else {
366 *remainder = e - cst + start; // adjust in case we remapped data
367 }
368 return d;
369 }
370 }
371 template <typename RETURN_TYPE>
372 RETURN_TYPE ToFloat_ViaStrToD_ (const wchar_t* start, const wchar_t* end, const wchar_t** remainder)
373 {
374 Require (start <= end);
375 if (start == end) {
376 if (remainder != nullptr) {
377 *remainder = start;
378 }
379 return Math::nan<RETURN_TYPE> ();
380 }
381 else {
382 wchar_t* e = nullptr;
383 const wchar_t* cst = start;
384
385 if (remainder == nullptr) {
386 // REJECT strings with leading space in this case - must match exactly
387 if (::iswspace (*cst)) {
388 return Math::nan<RETURN_TYPE> ();
389 }
390 }
391
392 Memory::StackBuffer<wchar_t> tmp;
393 if (*end != '\0') {
394 // remap addresses - copying to a temporary buffer, so we can nul-terminate string passed to strtod (etc)
395 size_t len = end - start;
396 tmp.GrowToSize (len + 1);
397 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wrestrict\""); // /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29:33: warning: 'void* __builtin_memcpy(void*, const void*, long unsigned int)' accessing 18446744073709551612 bytes at offsets 8 and 0 overlaps 9223372036854775801 bytes at offset -9223372036854775805 [-Wrestrict]
398 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wstringop-overflow=\"");
399 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wstringop-overflow\""); // desperation try to silience
400 (void)::memcpy (tmp.begin (), start, len * sizeof (wchar_t));
401 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wstringop-overflow\"");
402 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wstringop-overflow=\"");
403 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wrestrict\"");
404 cst = tmp.begin ();
405 tmp[len] = '\0';
406 }
407 RETURN_TYPE d = CStr2FloatType_<RETURN_TYPE> (cst, &e);
408 // If asked to return remainder do so.
409 // If NOT asked to return remainder, treat not using the entire string as forcing result to be a Nan (invalid parse of number of not the whole thing is a number)
410 if (remainder == nullptr) {
411 if (e != end) {
412 d = Math::nan<RETURN_TYPE> ();
413 }
414 }
415 else {
416 *remainder = e - cst + start; // adjust in case we remapped data
417 }
418 return d;
419 }
420 }
421 }
422
423 namespace Private_ {
424#if qStroika_Foundation_Debug_AssertionsChecked || !(__cpp_lib_to_chars >= 201611)
425 inline size_t CalcSignificantFigures_ (const String& numStr, bool countZerosAtEndAfterDecPoint = false)
426 {
427 bool leading = true;
428 bool ignoreRest = false;
429 bool seenDot = false;
430 size_t n{};
431 size_t nTrailingZeros{};
432 numStr.Apply ([&] (Character c) {
433 if (ignoreRest) {
434 return;
435 }
436 if (c == '.') {
437 seenDot = true;
438 }
439 if (c == '+' or c == '-' or c == '.') {
440 return;
441 }
442 if (leading and c == '0') {
443 return;
444 }
445 leading = false;
446 if (c == 'e') {
447 ignoreRest = true;
448 return;
449 }
450 if (countZerosAtEndAfterDecPoint and seenDot) {
451 if (c == '0') {
452 ++nTrailingZeros;
453 }
454 else {
455 nTrailingZeros = 0;
456 }
457 }
458 ++n;
459 });
460 return countZerosAtEndAfterDecPoint ? n : n - nTrailingZeros;
461 }
462#endif
463 template <floating_point FLOAT_TYPE>
464 String ToString_OptimizedForCLocaleAndNoStreamFlags_ (FLOAT_TYPE f, SignificantFigures precision)
465 {
466 using namespace Memory;
467 size_t sz = numeric_limits<FLOAT_TYPE>::max_digits10 + numeric_limits<FLOAT_TYPE>::max_exponent10 + 5; // source? "-1.##e+##\0"
468 StackBuffer<char> buf{Memory::eUninitialized, sz};
469 ptrdiff_t resultStrLen;
470 unsigned int effectivePrecision = precision.GetEffectiveSignificantFigures<FLOAT_TYPE> ();
471
472 // XCode 15 still doesn't define __cpp_lib_to_chars, as well as _LIBCPP_VERSION < 190000, I believe --LGP 2024-07-13
473#if __cpp_lib_to_chars >= 201611
474 // empirically, on MSVC, to_chars() is much faster than snprintf (appears 3x apx faster) -- LGP 2021-11-04
475 if (precision == SignificantFigures::kFullPrecision) {
476 resultStrLen = to_chars (buf.begin (), buf.end (), f, chars_format::general).ptr - buf.begin ();
477 }
478 else {
479 resultStrLen = to_chars (buf.begin (), buf.end (), f, chars_format::general, effectivePrecision).ptr - buf.begin ();
480 }
481#else
482 auto mkFmtWithPrecisionArg_ = [] (char* formatBufferStart, [[maybe_unused]] char* formatBufferEnd, char _Spec, bool forceScientific) -> char* {
483 char* fmtPtr = formatBufferStart;
484 *fmtPtr++ = '%';
485 // include precision arg
486 *fmtPtr++ = '.';
487 *fmtPtr++ = '*';
488 if (_Spec != '\0') {
489 *fmtPtr++ = _Spec; // e.g. 'L' qualifier
490 }
491 *fmtPtr++ = forceScientific ? 'e' : 'g'; // format specifier
492 *fmtPtr = '\0';
493 Require (fmtPtr < formatBufferEnd);
494 return formatBufferStart;
495 };
496
497 FLOAT_TYPE useRoundedFloat = Math::Round<FLOAT_TYPE> (f, effectivePrecision);
498 if (precision != SignificantFigures::kFullPrecision) {
499 f = useRoundedFloat;
500 }
501
502 bool forceScientific = fabs (f) >= std::pow (10, effectivePrecision);
503 qStroika_ATTRIBUTE_INDETERMINATE char format[100]; // filled in with mkFmtWithPrecisionArg_
504 resultStrLen = ::snprintf (buf.data (), buf.size (),
505 mkFmtWithPrecisionArg_ (std::begin (format), std::end (format),
506 same_as<FLOAT_TYPE, long double> ? 'L' : '\0', forceScientific),
507 (int)effectivePrecision + 1, f);
508 auto actualPrecIncZeros = CalcSignificantFigures_ (String{span{buf.data (), static_cast<size_t> (resultStrLen)}}, true);
509 if (actualPrecIncZeros > effectivePrecision) {
510 ptrdiff_t nBytes = static_cast<ptrdiff_t> (actualPrecIncZeros) - static_cast<ptrdiff_t> (effectivePrecision);
511 auto numberEnd = buf.data () + resultStrLen;
512 auto ePtr = std::find (buf.data (), numberEnd, 'e');
513 if (ePtr != numberEnd) {
514 Assert (buf.data () <= ePtr - nBytes);
515 memmove (ePtr - nBytes, ePtr, (buf.data () + resultStrLen - ePtr)); // slide 'e+22' back over the lost precision bytes of number
516 }
517 resultStrLen -= nBytes;
518 }
519#endif
520
521 // [[maybe_unused]] auto oo1 = String{span{buf.data (), static_cast<size_t> (resultStrLen)}}.As<wstring> ();
522 // [[maybe_unused]] auto ooo = CalcSignificantFigures_ (String{span{buf.data (), static_cast<size_t> (resultStrLen)}});
523 Verify (resultStrLen > 0 and resultStrLen < static_cast<int> (sz));
524#if qStroika_Foundation_Debug_AssertionsChecked
525 Assert (precision == SignificantFigures::kFullPrecision or
526 CalcSignificantFigures_ (String{span{buf.data (), static_cast<size_t> (resultStrLen)}}) <= effectivePrecision);
527#endif
528 return String{span{buf.data (), static_cast<size_t> (resultStrLen)}};
529 }
530 }
531
532 namespace Private_ {
533 template <floating_point T>
534 inline String formatNonScientific_ (T val, unsigned int nSignificantFigures)
535 {
536 auto compute = [&] () {
537 if (val == 0.0) {
538 // special case for zero cuz cannot compute log10(0)
539 return String{Common::StdCompat::format ("{:.{}f}", 0.0, nSignificantFigures)};
540 }
541 else {
542 // Calculate digits before the decimal point
543 int digits_before = static_cast<int> (floor (log10 (abs (val)))) + 1;
544
545 // Precision for 'f' (fixed) is the number of digits AFTER the decimal
546 unsigned int precision = static_cast<unsigned int> (max (0, static_cast<int> (nSignificantFigures) - digits_before));
547
548 // Use dynamic precision syntax: {:.{}f}
549 // The first {} refers to the value, the second .{} refers to precision
550 return String{Common::StdCompat::format ("{:.{}f}", val, precision)};
551 }
552 };
553 String r = compute ();
554 // @todo RECONSIDER
555 // for example, there is no way for 31200000 to be represented with 6 significant digits (in standard form, you can with scientific)
556 // 212312345.0 with 6 significant digits is 212312000, but we need the extra digits for magnatitude, so not
557 // sure worth losing the action digits we have? CONSIDER!!!!
558 // but for now no ensures --Ensure (SignificantFigures::Calculate (span<const wchar_t>{r.As<wstring> ()}) <= nSignificantFigures);
559 return r;
560 }
561 template <floating_point T>
562 inline String formatNonScientific_ (const locale& l, T val, unsigned int nSignificantFigures)
563 {
564 auto compute = [&] () {
565 if (val == 0.0) {
566 // special case for zero cuz cannot compute log10(0)
567 return Common::StdCompat::format (l, L"{:.{}f}", 0.0, nSignificantFigures);
568 }
569 else {
570 // Calculate digits before the decimal point
571 int digits_before = static_cast<int> (floor (log10 (abs (val)))) + 1;
572
573 // Precision for 'f' (fixed) is the number of digits AFTER the decimal
574 unsigned int precision = static_cast<unsigned int> (max (0, static_cast<int> (nSignificantFigures) - digits_before));
575
576 // Use dynamic precision syntax: {:.{}f}
577 // The first {} refers to the value, the second .{} refers to precision
578 return Common::StdCompat::format (l, L"{:.{}f}", val, precision);
579 }
580 };
581 String r = compute ();
582 Ensure (SignificantFigures::Calculate (span<const wchar_t>{r.As<wstring> ()}) == nSignificantFigures);
583 return r;
584 }
585 }
586
587 namespace Private_ {
588 template <floating_point FLOAT_TYPE>
589 String ToString_GeneralCase_ (FLOAT_TYPE f, const ToStringOptions& options)
590 {
591 unsigned int usePrecision =
592 options.GetSignificantFigures ().value_or (SignificantFigures{}).GetEffectiveSignificantFigures<FLOAT_TYPE> ();
593 FloatFormatType usingFormat = options.GetFloatFormat ().value_or (FloatFormatType::eDEFAULT);
594
595 // if no locale, can use this...
596 // (see if I can use formatNonScientific_) logic for other cases...
597 if (usingFormat == FloatFormatType::eStandard) {
598 Assert (options.GetIOSFmtFlags () == nullopt); // doable in principle, looking/mapping each feature, but not trivial, and probably not needed anytime soon
599 // --LGP 2026-02-10
600 if (options.GetUsingLocaleClassic ()) {
601 return formatNonScientific_ (f, usePrecision);
602 }
603 else {
604 return formatNonScientific_ (options.GetUseLocale (), f, usePrecision);
605 }
606 }
607
608 // expensive to construct, and slightly cheaper to just use thread_local version of
609 // the same stringstream each time (only one per thread can be in use)
610 static thread_local stringstream s;
611 static const ios_base::fmtflags kDefaultIOSFmtFlags_ = s.flags (); // Just copy out of the first constructed stringstream
612
613 s.str (string{});
614 s.clear ();
615
616 s.imbue (options.GetUseLocale ());
617
618 // must set explicitly (even if defaulted) because of the thread_local thing
619 s.flags (options.GetIOSFmtFlags ().value_or (kDefaultIOSFmtFlags_));
620
621 {
622 switch (usingFormat) {
625 s.setf (ios_base::scientific, ios_base::floatfield);
626 Assert (usePrecision >= 2);
627 s.precision (usePrecision - 1); // scientific format is n.mmm with m digits of 'precision' - so 1 + m is # of significat digits
628 break;
630 s.unsetf (ios_base::floatfield); // see std::defaultfloat - not same as ios_base::fixed
631 /*
632 * Precision Field: The stream's precision setting (managed by std::setprecision or std::ios_base::precision())
633 * specifies the maximum number of meaningful digits to display in total (both before and after the decimal point),
634 * not a fixed number of digits after the decimal point (so same as SignificantFigures)
635 */
636 s.precision (usePrecision);
637 break;
640 s.setf (ios_base::fixed, ios_base::floatfield);
641 s.precision (usePrecision); // I think wrong but test -
642 break;
644 AssertNotReached (); // handled above
645 break;
646 DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
647 DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
649 case FloatFormatType::eAutomaticScientific: {
650 s.precision (usePrecision);
651 bool useScientificNotation = abs (f) >= pow (10, usePrecision / 2) or
652 (f != 0 and abs (f) < pow (10, -static_cast<int> (usePrecision) / 2)); // scientific preserves more precision - but non-scientific looks better
653 if (useScientificNotation) {
654 s.setf (ios_base::scientific, ios_base::floatfield);
655 }
656 else {
657 s.unsetf (ios_base::floatfield); // see std::defaultfloat - not same as ios_base::fixed
658 s.precision (usePrecision);
659 }
660 } break;
661 DISABLE_COMPILER_MSC_WARNING_END (4996)
662 DISABLE_COMPILER_CLANG_WARNING_END ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
663 DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
664 default:
666 break;
667 }
668 }
669
670 s << f;
671 string ss = s.str ();
672
673 return options.GetUsingLocaleClassic () ? String{ss} : String::FromNarrowString (ss, options.GetUseLocale ());
674 }
675 }
676
677 namespace Private_ {
678 template <floating_point FLOAT_TYPE>
679 String ToString_String_Implementation_ (FLOAT_TYPE f, const ToStringOptions& options)
680 {
681 auto floatFormat = options.GetFloatFormat ().value_or (FloatFormatType::eDEFAULT);
682 auto result =
683 (options.GetUsingLocaleClassic () and not options.GetIOSFmtFlags () and floatFormat == FloatFormatType::eDefaultFloat)
684 ? Private_::ToString_OptimizedForCLocaleAndNoStreamFlags_ (f, options.GetSignificantFigures ().value_or (SignificantFigures{}))
685 : Private_::ToString_GeneralCase_ (f, options);
686 if (floatFormat == FloatFormatType::eFixedPointWithWhitespaceTrimmed or floatFormat == FloatFormatType::eStandard) {
687 TrimTrailingZeros_NotSci_ (&result);
688 }
689 if (floatFormat == FloatFormatType::eScientificWithWhitespaceTrimmed) {
690 TrimTrailingZeros_MaybeSci_ (&result);
691 }
692#if qCompilerAndStdLib_float2string_defaultfmt_scientificNotStripped_Buggy
693 if (floatFormat == FloatFormatType::eDefaultFloat) {
694 TrimTrailingZeros_MaybeSci_ (&result);
695 }
696#endif
697 return result;
698 }
699 }
700
701 namespace Private_ {
702 //
703 // This RESPECTS THE CURRENT LOCALE
704 // this private function allows nullptr remainder, and behaves somewhat differently if null or not (unlike PUBLIC API)
705 // Roughly (ignoring precision and character set)
706 // Calls strtod
707 // Strtod Skips leading whitespace, but this routine does NOT allow that
708 // if remainder==null, rejects if not all characters eaten (else returns how many eaten in remainder)
709 //
710 // Note - inlined though long, because most of the logic gets compiled out depending on (template or actual)
711 // parameters, and want to assure those 'seen' by optimizer so most of the code eliminated.
712 //
713 template <typename T, IUNICODECanUnambiguouslyConvertFrom CHAR_T>
714 inline T ToFloat_RespectingLocale_ (const span<const CHAR_T> srcSpan, typename span<const CHAR_T>::iterator* remainder)
715 {
716 if (srcSpan.empty ()) {
717 if (remainder != nullptr) {
718 *remainder = srcSpan.begin ();
719 }
720 return Math::nan<T> ();
721 }
722
723 // because strtod, etc, require a NUL-terminated string, and span doesn't generally provide one,
724 // we must copy to a temporary buffer (not super costly, especially since this isn't the main
725 // approach tried typically)
726 Memory::StackBuffer<CHAR_T> srcBufWithNul{Memory::eUninitialized, srcSpan.size () + 1};
727 Memory::CopyBytes (srcSpan, span{srcBufWithNul});
728 srcBufWithNul[srcSpan.size ()] = '\0';
729 const CHAR_T* si = srcBufWithNul.begin ();
730 const CHAR_T* ei = srcBufWithNul.end () - 1; // buf nul-terminated, but dont treat the NUL as part of our length
731 const CHAR_T* ri = ei;
732
733 // since strtod skips leading whitespace, prevent that
734 if (remainder == nullptr) {
735 bool isSpace;
736 if constexpr (sizeof (CHAR_T) == 1) {
737 isSpace = std::isspace (*si);
738 }
739 else if constexpr (sizeof (CHAR_T) == sizeof (wchar_t)) {
740 isSpace = std::iswspace (*si);
741 }
742 else {
743 isSpace = std::iswspace (static_cast<wint_t> (*si)); // not sure how to check without complex conversion logic
744 }
745 if (isSpace) {
746 if (remainder != nullptr) {
747 *remainder = srcSpan.begin ();
748 }
749 return Math::nan<T> ();
750 }
751 }
752 qStroika_ATTRIBUTE_INDETERMINATE T d; // - set below
753 static_assert (same_as<T, float> or same_as<T, double> or same_as<T, long double>);
754 if constexpr (sizeof (CHAR_T) == 1) {
755 if constexpr (same_as<T, float>) {
756 d = ::strtof (reinterpret_cast<const char*> (si), const_cast<char**> (reinterpret_cast<const char**> (&ri)));
757 }
758 else if constexpr (same_as<T, double>) {
759 d = ::strtod (reinterpret_cast<const char*> (si), const_cast<char**> (reinterpret_cast<const char**> (&ri)));
760 }
761 else if constexpr (same_as<T, long double>) {
762 d = ::strtold (reinterpret_cast<const char*> (si), const_cast<char**> (reinterpret_cast<const char**> (&ri)));
763 }
764 else {
766 }
767 }
768 else if constexpr (sizeof (CHAR_T) == sizeof (wchar_t)) {
769 if constexpr (same_as<T, float>) {
770 d = ::wcstof (reinterpret_cast<const wchar_t*> (si), const_cast<wchar_t**> (reinterpret_cast<const wchar_t**> (&ri)));
771 }
772 else if constexpr (same_as<T, double>) {
773 d = ::wcstod (reinterpret_cast<const wchar_t*> (si), const_cast<wchar_t**> (reinterpret_cast<const wchar_t**> (&ri)));
774 }
775 else if constexpr (same_as<T, long double>) {
776 d = ::wcstold (reinterpret_cast<const wchar_t*> (si), const_cast<wchar_t**> (reinterpret_cast<const wchar_t**> (&ri)));
777 }
778 else {
780 }
781 }
782 else {
783 // must utf convert to wchar_t which we support
784 Memory::StackBuffer<wchar_t> wideBuf{Memory::eUninitialized, UTFConvert::ComputeTargetBufferSize<wchar_t> (srcSpan)};
785 span<const wchar_t> wideSpan = UTFConvert::kThe.ConvertSpan (srcSpan, span{wideBuf});
786 if (remainder == nullptr) {
787 d = ToFloat_RespectingLocale_<T, wchar_t> (wideSpan, nullptr);
788 }
789 else {
790 // do the conversion using wchar_t, and then map back the resulting remainder offset
791 span<const wchar_t>::iterator wideRemainder;
792 d = ToFloat_RespectingLocale_<T> (wideSpan, &wideRemainder);
793 ri = UTFConvert::kThe.ConvertOffset<CHAR_T> (wideSpan, wideRemainder - wideSpan.begin ()) + si;
794 }
795 }
796 if (remainder == nullptr) {
797 // if no remainder argument, we require all the text is 'eaten'
798 if (ri != ei) {
799 d = Math::nan<T> ();
800 }
801 }
802 else {
803 *remainder = srcSpan.begin () + (ri - si);
804 }
805 return d;
806 }
807
808 }
809
810 /*
811 ********************************************************************************
812 ************************** FloatConversion::ToString ***************************
813 ********************************************************************************
814 */
815 template <Common::IAnyOf<String, string, wstring> STRING_TYPE, floating_point FLOAT_TYPE>
816 inline STRING_TYPE ToString (FLOAT_TYPE f, const ToStringOptions& options)
817 {
818 if constexpr (same_as<STRING_TYPE, String>) {
819 return Private_::ToString_String_Implementation_ (f, options);
820 }
821 else if constexpr (same_as<STRING_TYPE, string>) {
822 // @todo improve performance for this case
823 Require (options.GetUsingLocaleClassic ());
824 return Private_::ToString_String_Implementation_ (f, options).AsASCII ();
825 }
826 else if constexpr (same_as<STRING_TYPE, wstring>) {
827 // @todo improve performance for this case
828 Require (options.GetUsingLocaleClassic ());
829 return Private_::ToString_String_Implementation_ (f, options).template As<wstring> ();
830 }
831 }
832
833 /*
834 ********************************************************************************
835 *************************** FloatConversion::ToFloat ***************************
836 ********************************************************************************
837 */
838 template <floating_point T, IUNICODECanUnambiguouslyConvertFrom CHAR_T>
839 T ToFloat (span<const CHAR_T> s)
840 {
841 if constexpr (same_as<remove_cv_t<CHAR_T>, char>) {
842 Require (Character::IsASCII (s));
843 }
844 /*
845 * Most of the time we can do this very efficiently, because there are just ascii characters.
846 * Else, fallback on algorithm that understands full unicode character set, and locales
847 */
849 if (Character::AsASCIIQuietly (s, &asciiS)) {
851#if __cpp_lib_to_chars >= 201611 and not qCompilerAndStdLib_from_chars_and_tochars_FP_Precision_Buggy
852#if qCompilerAndStdLib_to_chars_assmes_str_nul_terminated_Buggy
853 asciiS.push_back (0);
854#endif
855 auto b = asciiS.begin ();
856 auto e = asciiS.end ();
857#if qCompilerAndStdLib_to_chars_assmes_str_nul_terminated_Buggy
858 e--;
859#endif
860 if (b != e and *b == '+') [[unlikely]] {
861 ++b; // "the plus sign is not recognized outside of the exponent (only the minus sign is permitted at the beginning)" from https://en.cppreference.com/w/cpp/utility/from_chars
862 }
863 auto [ptr, ec] = from_chars (b, e, result);
864 if (ec == errc::result_out_of_range) [[unlikely]] {
865 result = *b == '-' ? -numeric_limits<T>::infinity () : numeric_limits<T>::infinity ();
866 }
867 else if (ec != std::errc{} or ptr != e) [[unlikely]] {
868 //result = Math::nan<T> (); "if # is 100,1 - in funny locale - may need to use legacy algorithm
869 // @todo ADD OPTION arg to ToFloat so we know if C-Locale so can just return NAN here!...
870 result = Private_::ToFloat_RespectingLocale_<T> (s, nullptr);
871 }
872#else
873 result = Private_::ToFloat_RespectingLocale_<T> (span<const char>{asciiS}, nullptr);
874#endif
875 return result;
876 }
877 return Private_::ToFloat_RespectingLocale_<T> (s, nullptr); // fallback for non-ascii strings to old code
878 }
879 template <floating_point T, IUNICODECanUnambiguouslyConvertFrom CHAR_T>
880 T ToFloat (span<const CHAR_T> s, typename span<const CHAR_T>::iterator* remainder)
881 {
882 RequireNotNull (remainder); // use other overload if 'null'
883 if constexpr (same_as<remove_cv_t<CHAR_T>, char>) {
884 Require (Character::IsASCII (s));
885 }
886 /*
887 * Most of the time we can do this very efficiently, because there are just ascii characters.
888 * Else, fallback on algorithm that understands full unicode character set.
889 */
890#if __cpp_lib_to_chars >= 201611 and not qCompilerAndStdLib_from_chars_and_tochars_FP_Precision_Buggy
892 if (Character::AsASCIIQuietly (s, &asciiS)) {
894 char* b = asciiS.begin ();
895 char* e = asciiS.end ();
896 if (b != e and *b == '+') [[unlikely]] {
897 ++b; // "the plus sign is not recognized outside of the exponent (only the minus sign is permitted at the beginning)" from https://en.cppreference.com/w/cpp/utility/from_chars
898 }
899 auto [ptr, ec] = from_chars (b, e, result);
900 if (ec == errc::result_out_of_range) [[unlikely]] {
901 result = *b == '-' ? -numeric_limits<T>::infinity () : numeric_limits<T>::infinity ();
902 }
903 else if (ec != std::errc{} or ptr != e) [[unlikely]] {
904 //result = Math::nan<T> (); "if # is 100,1 - in funny locale - may need to use legacy algorithm
905 // @todo ADD OPTION arg to ToFloat so we know if C-Locale so can just return NAN here!...
906 typename span<const CHAR_T>::iterator tmpI;
907 result = Private_::ToFloat_RespectingLocale_<T> (s, &tmpI);
908 ptr = tmpI - s.begin () + b;
909 }
910 *remainder = s.begin () + UTFConvert::kThe.ConvertOffset<CHAR_T> (
911 span{reinterpret_cast<const char8_t*> (b), reinterpret_cast<const char8_t*> (e)}, ptr - b);
912 Assert (*remainder <= s.end ());
913 return result;
914 }
915#endif
916 return Private_::ToFloat_RespectingLocale_<T> (s, remainder); // fallback for non-ascii strings to strtod etc code
917 }
918 template <floating_point T, typename STRINGISH_ARG>
919 inline T ToFloat (STRINGISH_ARG&& s)
920 requires (IConvertibleToString<STRINGISH_ARG> or is_convertible_v<STRINGISH_ARG, std::string>)
921 {
922 using DecayedStringishArg = remove_cvref_t<STRINGISH_ARG>;
923 if constexpr (same_as<DecayedStringishArg, const char*> or same_as<DecayedStringishArg, const char8_t*> or
924 same_as<DecayedStringishArg, const char16_t*> or same_as<DecayedStringishArg, const char32_t*> or
925 same_as<DecayedStringishArg, const wchar_t*>) {
926 return ToFloat<T> (span{s, CString::Length (s)});
927 }
928 else if constexpr (same_as<DecayedStringishArg, String>) {
929 Memory::StackBuffer<wchar_t> ignored; // todo optimize for keep ascii case
930 auto sp = s.template GetData<wchar_t> (&ignored);
931 return ToFloat<T> (sp);
932 }
933 else if constexpr (is_convertible_v<DecayedStringishArg, std::string>) {
934 string ss = s;
935 return ToFloat<T> (span{ss.data (), ss.size ()});
936 }
937 else {
938 return ToFloat<T> (String{forward<STRINGISH_ARG> (s)});
939 }
940 }
941 template <floating_point T>
942 inline T ToFloat (const String& s, String* remainder)
943 {
944 RequireNotNull (remainder);
945 Memory::StackBuffer<wchar_t> ignored;
946 span<const wchar_t> srcSpan = s.GetData (&ignored);
947 span<const wchar_t>::iterator tmpRemainder;
948 auto result = ToFloat<T> (srcSpan, &tmpRemainder);
949 *remainder = String{srcSpan.subspan (tmpRemainder - srcSpan.begin ())};
950 return result;
951 }
952
953 //////////////// DEPRECATED STUFF BELOW
954
955 template <typename T>
956 [[deprecated ("Since Stroika v3.0d1 - use span overload")]] inline T ToFloat (const wchar_t* start, const wchar_t* end, const wchar_t** remainder)
957 {
958 Require (start <= end);
959 RequireNotNull (remainder);
961#if __cpp_lib_to_chars >= 201611 and not qCompilerAndStdLib_from_chars_and_tochars_FP_Precision_Buggy
962 /*
963 * Most of the time we can do this very efficiently, because there are just ascii characters.
964 * Else, fallback on older algorithm that understands full unicode character set.
965 */
966 Memory::StackBuffer<char> asciiS;
967 if (Character::AsASCIIQuietly (span{start, end}, &asciiS)) {
968 auto b = asciiS.begin ();
969 auto originalB = b; // needed to properly compute remainder
970 auto e = asciiS.end ();
971 if (remainder != nullptr) [[unlikely]] {
972 // in remainder mode we skip leading whitespace
973 while (b != e and iswspace (*b)) [[unlikely]] {
974 ++b;
975 }
976 }
977 if (b != e and *b == '+') [[unlikely]] {
978 ++b; // "the plus sign is not recognized outside of the exponent (only the minus sign is permitted at the beginning)" from https://en.cppreference.com/w/cpp/utility/from_chars
979 }
980
981 auto [ptr, ec] = from_chars (b, e, result);
982 if (ec == errc::result_out_of_range) [[unlikely]] {
983 return *b == '-' ? -numeric_limits<T>::infinity () : numeric_limits<T>::infinity ();
984 }
985 // if error - return nan
986 if (ec != std::errc{}) [[unlikely]] {
987 result = Math::nan<T> ();
988 }
989 // If asked to return remainder do so.
990 // If NOT asked to return remainder, treat not using the entire string as forcing result to be a Nan (invalid parse of number of not the whole thing is a number)
991 if (remainder == nullptr) [[likely]] {
992 if (ptr != e) [[unlikely]] {
993 result = Math::nan<T> ();
994 }
995 }
996 else {
997 *remainder = ptr - originalB + start; // adjust in case we remapped data
998 }
999 }
1000 else {
1001 result = Private_::ToFloat_ViaStrToD_<T> (start, end, remainder);
1002 }
1003#else
1004 result = Private_::ToFloat_ViaStrToD_<T> (start, end, remainder);
1005#endif
1006 return result;
1007 }
1008 template <typename T = double>
1009 [[deprecated ("Since Stroika v3.0d1 - use span overload")]] inline T ToFloat (const char* start, const char* end)
1010 {
1011 return ToFloat<T> (span{start, end});
1012 }
1013 template <typename T = double>
1014 [[deprecated ("Since Stroika v3.0d1 - use span overload")]] inline T ToFloat (const wchar_t* start, const wchar_t* end)
1015 {
1016 return ToFloat<T> (span{start, end});
1017 }
1018
1019}
#define RequireNotReached()
Definition Assertions.h:386
#define RequireNotNull(p)
Definition Assertions.h:348
#define AssertNotReached()
Definition Assertions.h:356
#define Verify(c)
Definition Assertions.h:420
#define qStroika_ATTRIBUTE_INDETERMINATE
qStroika_ATTRIBUTE_INDETERMINATE is used where you would use a C++ attribute for a variable that is i...
Definition StdCompat.h:395
constexpr bool IsASCII() const noexcept
Return true iff the given character (or all in span) is (are) in the ascii range [0....
static bool AsASCIIQuietly(span< const CHAR_T > fromS, RESULT_T *into)
static String FromNarrowString(const char *from, const locale &l)
Definition String.inl:340
static constexpr size_t npos
Definition String.h:1409
static const UTFConvert kThe
Nearly always use this default UTFConvert.
Definition UTFConvert.h:369
nonvirtual size_t ConvertOffset(span< const SRC_T > source, size_t srcIndex) const
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
nonvirtual void push_back(Common::ArgByValueType< T > e)
nonvirtual Iterator< T > begin() const
Support for ranged for, and STL syntax in general.
@ eStandard
Somewhat like defaultfloat, but never uses scientific notation.
STRING_TYPE ToString(FLOAT_TYPE f, const ToStringOptions &options={})
static const SignificantFigures kFullPrecision
Sentinal value that is interpretted differently depending on the type passed to GetEffectiveSignifica...
static constexpr auto Calculate(span< const CHAR > number) -> RepType
nonvirtual locale GetUseLocale() const
return the selected locale object
nonvirtual bool GetUsingLocaleClassic() const
return true if locale used is locale::classic() - the 'C' locale; mostly used as optimization/special...