FastLED 3.10.8
Loading...
Searching...
No Matches
stdio.h
Go to the documentation of this file.
1#pragma once
2
3#include "fl/stl/int.h"
4#include "fl/stl/cstddef.h"
5#include "fl/stl/cstdio.h" // For fl::print and fl::println
6#include "fl/stl/string.h" // For fl::string, to_hex
7#include "fl/stl/strstream.h"
9#include "fl/stl/noexcept.h"
10
11namespace fl {
12
13// Non-template fast path for format strings with no arguments. Keeping this
14// out of line avoids instantiating sstream formatting at every literal call.
15void printf(const char* format) FL_NO_EXCEPT;
16int snprintf(char* buffer, fl::size size, const char* format) FL_NO_EXCEPT;
17
58template<typename... Args>
59void printf(const char* format, const Args&... args) FL_NO_EXCEPT;
60
102template<typename... Args>
103int snprintf(char* buffer, fl::size size, const char* format, const Args&... args) FL_NO_EXCEPT;
104
105
107
108namespace printf_detail {
109
110// Helper to parse format specifiers and extract precision
112 char type = '\0'; // Format character (d, f, s, etc.)
113 int precision = -1; // Precision for floating point
114 int width = 0; // Minimum field width
115 bool uppercase = false; // For hex formatting
116 bool left_align = false; // '-' flag: left-align
117 bool zero_pad = false; // '0' flag: zero-padding
118 bool show_sign = false; // '+' flag: always show sign
119 bool space_sign = false; // ' ' flag: space for positive
120 bool alt_form = false; // '#' flag: alternate form (0x, 0)
121
122 FormatSpec() = default;
123 explicit FormatSpec(char t) FL_NO_EXCEPT : type(t) {}
124 FormatSpec(char t, int prec) FL_NO_EXCEPT : type(t), precision(prec) {}
125};
126
127// Parse a format specifier from the format string
128// Returns the format spec and advances the pointer past the specifier
129// Format: %[flags][width][.precision][length]type
131 FormatSpec spec;
132
133 if (*format != '%') {
134 return spec;
135 }
136
137 ++format; // Skip the '%'
138
139 // Handle literal '%'
140 if (*format == '%') {
141 spec.type = '%';
142 ++format;
143 return spec;
144 }
145
146 // Parse flags: -, +, space, #, 0 (can be in any order)
147 bool parsing_flags = true;
148 while (parsing_flags) {
149 switch (*format) {
150 case '-':
151 spec.left_align = true;
152 ++format;
153 break;
154 case '+':
155 spec.show_sign = true;
156 ++format;
157 break;
158 case ' ':
159 spec.space_sign = true;
160 ++format;
161 break;
162 case '#':
163 spec.alt_form = true;
164 ++format;
165 break;
166 case '0':
167 spec.zero_pad = true;
168 ++format;
169 break;
170 default:
171 parsing_flags = false;
172 break;
173 }
174 }
175
176 // Parse width (decimal number)
177 if (*format >= '0' && *format <= '9') {
178 spec.width = 0;
179 while (*format >= '0' && *format <= '9') {
180 spec.width = spec.width * 10 + (*format - '0');
181 ++format;
182 }
183 }
184
185 // Parse precision for floating point
186 if (*format == '.') {
187 ++format; // Skip the '.'
188 spec.precision = 0;
189 while (*format >= '0' && *format <= '9') {
190 spec.precision = spec.precision * 10 + (*format - '0');
191 ++format;
192 }
193 }
194
195 // Skip length modifiers (l, ll, h, hh, L, z, t, j)
196 // We don't use them in our simple printf, but we need to skip them
197 // to get to the actual type character
198 if (*format == 'h' || *format == 'l') {
199 char first = *format;
200 ++format;
201 // Check for double character modifiers (hh, ll)
202 if (*format == first) {
203 ++format;
204 }
205 } else if (*format == 'L' || *format == 'z' || *format == 't' || *format == 'j') {
206 ++format;
207 }
208
209 // Get the format type
210 spec.type = *format;
211 if (spec.type == 'X') {
212 spec.uppercase = true;
213 spec.type = 'x'; // Normalize to lowercase for processing
214 }
215
216 // Only advance past the type character if there actually is one. A format
217 // string ending in a bare '%' leaves spec.type == '\0'; advancing here
218 // would step past the NUL terminator and make the caller's loop read out
219 // of bounds.
220 if (spec.type != '\0') {
221 ++format;
222 }
223 return spec;
224}
225
226// Convert unsigned integer to octal string
227template<typename T>
229 if (value == 0) {
230 return "0";
231 }
232
233 char buffer[32]; // Enough for 64-bit octal
234 int pos = 31;
235 buffer[pos] = '\0';
236
237 unsigned long long val = static_cast<unsigned long long>(value);
238 while (val > 0) {
239 buffer[--pos] = '0' + (val & 7);
240 val >>= 3;
241 }
242
243 return fl::string(&buffer[pos]);
244}
245
246// Apply width and padding to a string based on format spec
247inline fl::string apply_width(const fl::string& str, const FormatSpec& spec, bool is_numeric = false) FL_NO_EXCEPT {
248 int len = static_cast<int>(str.length());
249
250 // No width specified or content already wider
251 if (spec.width <= len) {
252 return str;
253 }
254
255 int padding = spec.width - len;
256 char pad_char = ' ';
257
258 // Zero-padding only for numeric types and right-align
259 if (spec.zero_pad && is_numeric && !spec.left_align) {
260 pad_char = '0';
261
262 // Handle sign for zero-padding: move sign to front
263 if (!str.empty() && (str[0] == '-' || str[0] == '+' || str[0] == ' ')) {
265 result += str[0]; // Sign first
266 for (int i = 0; i < padding; ++i) {
267 result += pad_char;
268 }
269 for (size_t i = 1; i < str.length(); ++i) {
270 result += str[i];
271 }
272 return result;
273 }
274
275 // Handle 0x prefix for zero-padding
276 if (str.length() >= 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
278 result += str[0]; // '0'
279 result += str[1]; // 'x' or 'X'
280 for (int i = 0; i < padding; ++i) {
281 result += pad_char;
282 }
283 for (size_t i = 2; i < str.length(); ++i) {
284 result += str[i];
285 }
286 return result;
287 }
288 }
289
291 if (spec.left_align) {
292 // Left-align: content then padding
293 result = str;
294 for (int i = 0; i < padding; ++i) {
295 result += pad_char;
296 }
297 } else {
298 // Right-align: padding then content
299 for (int i = 0; i < padding; ++i) {
300 result += pad_char;
301 }
302 result += str;
303 }
304
305 return result;
306}
307
308// Largest magnitude whose scaled form still fits an i64 with room for the
309// rounding step. Anything past this cannot be rendered digit-by-digit from an
310// integer, so it goes to the scientific path below.
311constexpr float kFloatDecimalLimit = 4.0e18f;
312
313// True for a value that is neither NaN nor an infinity.
314//
315// Written without <cmath> because this header is reachable from every
316// platform: `v != v` is the definition of NaN, and the magnitude bound catches
317// both infinities. 1e38 is inside the float range, so a finite value can
318// exceed it -- which is why the caller must handle large finites separately
319// rather than treating this as "printable".
321
322// `d.ddde+NN` for a magnitude no integer accumulator can hold.
323//
324// The alternative is to print such a value as though it were small, which is
325// what this file used to do: every magnitude at or above 2^31 / 10^precision
326// came out as `-21474836.48`, because `static_cast<int>` of an out-of-range
327// float is undefined behaviour and lands on INT_MIN on the common targets.
328// A number that is wrong without saying so is worse than one in a shape the
329// reader has to decode (FastLED #4156).
330//
331// Precondition: `value` is finite. The normalisation loop divides while the
332// magnitude is at or above ten, which never terminates for an infinity --
333// removing the caller's non-finite check hangs rather than mis-prints, which
334// is how that ordering was confirmed to be load-bearing.
335//
336// Defined in `stdio.cpp.hpp` rather than here: `src/**/*.h` carries
337// declarations, and these two need no template visibility.
339
340// Format floating point with specified precision
341inline fl::string format_float(float value, int precision) FL_NO_EXCEPT {
342 // Non-finite first. These used to reach the integer cast below and print
343 // as `-21474836.48` -- a plausible-looking number for a value that is not
344 // a number at all.
345 if (value != value) {
346 return fl::string("nan");
347 }
348 if (!float_is_finite(value)) {
349 return fl::string(value < 0.0f ? "-inf" : "inf");
350 }
351
352 if (precision < 0) {
353 // Default precision - use sstream's default behavior
354 sstream stream;
355 stream << value;
356 return stream.str();
357 }
358
359 // Past what an integer accumulator can hold, digit-by-digit rendering is
360 // not available at all, so say so in a different shape rather than
361 // printing a wrong number.
363 return format_float_scientific(value, precision > 0 ? precision : 6);
364 }
365
366 const bool negative = value < 0.0f;
367 const float magnitude = negative ? -value : value;
368
369 // Split before scaling, not after. Multiplying the whole value by the
370 // multiplier in float loses the low digits of a large integer part -- 1e9
371 // at precision 2 came out as 999999979.52, because 1e11 is not
372 // representable in a float. Only the fractional residue is scaled; the
373 // integer part goes through as an integer.
374 //
375 // Above 2^24 a float has no fractional part, so the residue is exactly
376 // zero there and the clamp below is for the rounding of the cast back,
377 // not for a value that genuinely has digits to lose.
378 fl::i64 int_part = static_cast<fl::i64>(magnitude);
379 float residue = magnitude - static_cast<float>(int_part);
380 if (residue < 0.0f) {
381 residue = 0.0f;
382 }
383
384 if (precision == 0) {
385 if (residue >= 0.5f) {
386 ++int_part;
387 }
388 sstream stream;
389 if (negative && int_part != 0) {
390 stream << "-";
391 }
392 stream << int_part;
393 return stream.str();
394 }
395
396 fl::i64 multiplier = 1;
397 for (int i = 0; i < precision; ++i) {
398 multiplier *= 10;
399 }
400
401 fl::i64 frac_part =
402 static_cast<fl::i64>(residue * static_cast<float>(multiplier) + 0.5f);
403 if (frac_part >= multiplier) {
404 // Rounded up through the next whole unit.
405 frac_part -= multiplier;
406 ++int_part;
407 }
408
409 sstream stream;
410 // The sign is printed from the sign of the input, not recovered from the
411 // integer part, so -0.5 keeps its sign at precision 1.
412 if (negative && (int_part != 0 || frac_part != 0)) {
413 stream << "-";
414 }
415 stream << int_part;
416 stream << ".";
417
418 // Emit exactly `precision` fractional digits, zero-padded on the left.
419 // The previous implementation stopped padding once temp_multiplier dropped
420 // to 1, which produced "0.0"/"1.0" instead of "0.00"/"1.00" for any
421 // integer-valued input. It also omitted the digits entirely when
422 // frac_part == 0.
423 fl::i64 temp_multiplier = multiplier / 10;
424 while (temp_multiplier > 0) {
425 const fl::i64 digit = (frac_part / temp_multiplier) % 10;
426 stream << static_cast<char>('0' + static_cast<int>(digit));
427 temp_multiplier /= 10;
428 }
429
430 return stream.str();
431}
432
433// Helper to convert pointer to fl::uptr (only instantiated for pointer types)
434template<typename T>
437 const void* vptr = static_cast<const void*>(ptr);
438 return reinterpret_cast<fl::uptr>(vptr); // ok reinterpret cast
439}
440
441// Overload for non-pointer types (never called at runtime, but needed for compilation)
442template<typename T>
445 return 0; // Never executed - runtime check prevents this
446}
447
448// Normalized form of a built-in scalar argument. Every arithmetic type that
449// printf commonly sees is squeezed into this struct so the specifier switch
450// exists once, out of line, instead of once per argument type (#4565).
451struct ScalarArg {
454 fl::u8 size; // sizeof the original type (selects the hex width)
455 fl::i64 s; // integral kinds: value as i64
456 fl::u64 u; // integral kinds: value as u64 (same bits as the original cast)
457 float f; // kFloat
458 double d; // kDouble
459};
460
461// The shared specifier switches, defined in `stdio.cpp.hpp`. Integral and
462// floating kinds get separate functions, chosen at compile time by
463// format_arg<T>, so a program that only formats integers never references
464// float formatting or the soft-float double routines it pulls in. With a
465// single switch, any `%d`/`%s` log linked ~10 KB of float code on FPU-less
466// parts (FastLED#4671).
467void format_integral(sstream& stream, const FormatSpec& spec, const ScalarArg& a) FL_NO_EXCEPT;
468void format_floating(sstream& stream, const FormatSpec& spec, const ScalarArg& a) FL_NO_EXCEPT;
469
470// Maps a built-in type to its ScalarArg::Kind; -1 keeps the generic template.
471template<typename T> struct scalar_kind { enum : int { value = -1 }; };
472template<> struct scalar_kind<bool> { enum : int { value = ScalarArg::kBool }; };
473template<> struct scalar_kind<char> { enum : int { value = ScalarArg::kChar }; };
474template<> struct scalar_kind<signed char> { enum : int { value = ScalarArg::kSigned }; };
475template<> struct scalar_kind<unsigned char> { enum : int { value = ScalarArg::kUnsigned }; };
476template<> struct scalar_kind<short> { enum : int { value = ScalarArg::kSigned }; };
477template<> struct scalar_kind<unsigned short> { enum : int { value = ScalarArg::kUnsigned }; };
478template<> struct scalar_kind<int> { enum : int { value = ScalarArg::kSigned }; };
479template<> struct scalar_kind<unsigned int> { enum : int { value = ScalarArg::kUnsigned }; };
480template<> struct scalar_kind<long> { enum : int { value = ScalarArg::kSigned }; };
481template<> struct scalar_kind<unsigned long> { enum : int { value = ScalarArg::kUnsigned }; };
482template<> struct scalar_kind<long long> { enum : int { value = ScalarArg::kSigned }; };
483template<> struct scalar_kind<unsigned long long> { enum : int { value = ScalarArg::kUnsigned }; };
484template<> struct scalar_kind<float> { enum : int { value = ScalarArg::kFloat }; };
485template<> struct scalar_kind<double> { enum : int { value = ScalarArg::kDouble }; };
486
487// Fill the arg and hand it to the switch for its family. Only the floating
488// overload names format_floating, so only floating instantiations link it.
489template<typename T>
492 const T& arg) FL_NO_EXCEPT {
493 a.s = static_cast<fl::i64>(arg);
494 a.u = static_cast<fl::u64>(arg);
495 format_integral(stream, spec, a);
496}
497
498template<typename T>
501 const T& arg) FL_NO_EXCEPT {
502 a.f = static_cast<float>(arg);
503 a.d = static_cast<double>(arg);
504 format_floating(stream, spec, a);
505}
506
507// Format built-in scalar types: thin adapter onto the shared switches.
508template<typename T>
510format_arg(sstream& stream, const FormatSpec& spec, const T& arg) FL_NO_EXCEPT {
511 ScalarArg a = {};
513 a.size = static_cast<fl::u8>(sizeof(T));
514 format_scalar_arg(stream, spec, a, arg);
515}
516
517// Format other non-pointer types (enums, user types) (d, i, u, o, x, X, f, c, s)
518template<typename T>
520format_arg(sstream& stream, const FormatSpec& spec, const T& arg) FL_NO_EXCEPT {
522 bool is_numeric = false;
523
524 switch (spec.type) {
525 case 'd':
526 case 'i': {
528 result = "<type_error>";
529 break;
530 }
531 is_numeric = true;
532
533 // Convert to string
534 sstream temp;
535 temp << arg;
536 result = temp.str();
537
538 // Handle sign flags
539 bool is_negative = !result.empty() && result[0] == '-';
540 if (!is_negative) {
541 if (spec.show_sign) {
542 result = fl::string("+") + result;
543 } else if (spec.space_sign) {
544 result = fl::string(" ") + result;
545 }
546 }
547 break;
548 }
549
550 case 'u': {
552 result = "<type_error>";
553 break;
554 }
555 is_numeric = true;
556
557 // Convert to string
558 sstream temp;
559 temp << arg;
560 result = temp.str();
561 break;
562 }
563
564 case 'o': {
566 result = "<type_error>";
567 break;
568 }
569 is_numeric = true;
570
571 // Convert to octal
572 result = to_octal(arg);
573
574 // Alternate form: prefix with 0 (but not for zero itself)
575 if (spec.alt_form && arg != 0) {
576 result = fl::string("0") + result;
577 }
578 break;
579 }
580
581 case 'x': {
582 is_numeric = true;
583
584 // Convert to hex
585 result = fl::to_hex(arg, spec.uppercase);
586
587 // Alternate form: prefix with 0x or 0X
588 if (spec.alt_form && arg != 0) {
589 result = fl::string(spec.uppercase ? "0X" : "0x") + result;
590 }
591 break;
592 }
593
594 case 'f': {
596 result = "<type_error>";
597 break;
598 }
599 is_numeric = true;
600
601 if (spec.precision >= 0) {
602 result = format_float(static_cast<float>(arg), spec.precision);
603 } else {
604 sstream temp;
605 temp << static_cast<float>(arg);
606 result = temp.str();
607 }
608 break;
609 }
610
611 case 'c': {
613 result = "<type_error>";
614 break;
615 }
616
617 char ch = static_cast<char>(arg);
618 char temp_str[2] = {ch, '\0'};
619 result = temp_str;
620 break;
621 }
622
623 case 's': {
624 sstream temp;
625 temp << arg;
626 result = temp.str();
627 break;
628 }
629
630 default:
631 result = "<unknown_format>";
632 break;
633 }
634
635 // Apply width and padding
636 result = apply_width(result, spec, is_numeric);
637
638 // Output final result
639 stream << result;
640}
641
642// Format pointer types (only handles 'p' format)
643template<typename T>
645format_arg(sstream& stream, const FormatSpec& spec, const T& arg) FL_NO_EXCEPT {
647 bool is_numeric = false;
648
649 if (spec.type == 'p') {
650 // Pointer format - always prefix with "0x"
651 is_numeric = true;
652
653 // Convert pointer to integer address using SFINAE helper
654 fl::uptr addr = pointer_to_uptr(arg);
655
656 // Format as hex with 0x prefix (always use lowercase for standard compatibility)
657 result = fl::string("0x") + fl::to_hex(addr, false);
658 } else {
659 result = "<type_error>";
660 }
661
662 // Apply width and padding
663 result = apply_width(result, spec, is_numeric);
664
665 // Output final result
666 stream << result;
667}
668
669// Specialized format_arg for const char* (string literals)
670inline void format_arg(sstream& stream, const FormatSpec& spec, const char* arg) FL_NO_EXCEPT {
672
673 bool is_numeric = false;
674
675 switch (spec.type) {
676 case 's':
677 if (arg) {
678 result = arg;
679 } else {
680 result = "(null)";
681 }
682 break;
683 case 'p': {
684 // Pointer format for const char*
685 is_numeric = true;
686 const void* ptr = static_cast<const void*>(arg);
687 fl::uptr addr = reinterpret_cast<fl::uptr>(ptr); // ok reinterpret cast
688 result = fl::string("0x") + fl::to_hex(addr, false);
689 break;
690 }
691 case 'x':
692 result = "<string_not_hex>";
693 break;
694 case 'd':
695 case 'i':
696 case 'u':
697 case 'o':
698 case 'f':
699 case 'c':
700 result = "<type_error>";
701 break;
702 default:
703 result = "<unknown_format>";
704 break;
705 }
706
707 // Apply width and padding
708 result = apply_width(result, spec, is_numeric);
709 stream << result;
710}
711
712void format_arg(sstream& stream, const FormatSpec& spec, const fl::string& arg) FL_NO_EXCEPT;
713void format_arg(sstream& stream, const FormatSpec& spec, const fl::string_view& arg) FL_NO_EXCEPT;
714
716
717// The generic "{}" placeholder saves the caller from hand-picking between
718// %d / %u / %ld / %s / %p and from writing .c_str(): the natural specifier for
719// the argument's C++ type is selected automatically and then handed to the
720// SAME format_arg machinery the '%' path uses. That reuse is deliberate - it
721// shares instantiations with the '%' path: a TU compiled with '{}' emits an
722// identical symbol table to the same TU written with the hand-picked '%'
723// specifier (verified with nm over 10 argument types).
724//
725// Caveat, so nobody is surprised: format_impl's '{}' branch is compiled even
726// for call sites that only use '%', because the format string is a runtime
727// pointer and the branch cannot be discarded. Those call sites therefore emit
728// small format_arg_generic<T> forwarders they did not previously have. Each is
729// a one-line tail call into an already-existing format_arg<T> and inlines at
730// -Os, but the cost is paid tree-wide, not only by '{}' users.
731//
732// "{}" is intentionally constrained to the type set '%' already supports
733// (integral, bool, char, float/double, const char*, fl::string,
734// fl::string_view, pointers). It does NOT fall back to sstream::operator<<
735// for arbitrary types: that would re-introduce the per-call-site operator<<
736// chain instantiations that issue #3158 removed (measured at ~5-10 KB on
737// ESP32-S3, with sstream::operator<<(char) alone at 1.25 KB per #3078).
738// Passing an unsupported type to "{}" is therefore a compile error, not a
739// silent code-size regression.
740
741// Integral types (including bool) format like "%d". The 'd' path stringifies
742// through the same code as %d/%u, so signed and unsigned both render exactly
743// as they do today; bool keeps its "true"/"false" rendering.
744template<typename T>
747 format_arg(stream, FormatSpec('d'), arg);
748}
749
750// Enums format as their underlying integer, like "%d" and like sstream's own
751// is_enum overload. This overload is REQUIRED for correctness, not just
752// convenience: an unscoped enum fails is_integral, but implicitly converts to
753// char, so without it overload resolution silently selects the char overload
754// below and `fl::printf("{}", GREEN)` with `enum Color { GREEN = 66 }` prints
755// "B" instead of "66". Same trap applies to any type with an implicit
756// conversion to char.
757template<typename T>
760 using underlying_t = typename fl::underlying_type<T>::type;
761 format_arg(stream, FormatSpec('d'), static_cast<underlying_t>(arg));
762}
763
764// Floating point types format like "%f" (default precision).
765template<typename T>
768 format_arg(stream, FormatSpec('f'), arg);
769}
770
771// Pointers format like "%p" ("0x" + lowercase hex address).
772// Character pointers are handled by the more specialized overloads below.
773template<typename T>
776 format_arg(stream, FormatSpec('p'), arg);
777}
778
779// char renders as a character, like "%c" (not as its numeric value).
780inline void format_arg_generic(sstream& stream, char arg) FL_NO_EXCEPT {
781 format_arg(stream, FormatSpec('c'), arg);
782}
783
784// C strings render their contents, like "%s" (including the "(null)"
785// sentinel for a null pointer).
786inline void format_arg_generic(sstream& stream, const char* arg) FL_NO_EXCEPT {
787 format_arg(stream, FormatSpec('s'), arg);
788}
789
790inline void format_arg_generic(sstream& stream, char* arg) FL_NO_EXCEPT {
791 format_arg(stream, FormatSpec('s'), static_cast<const char*>(arg));
792}
793
794// NOTE: no const char(&)[N] overload here on purpose. Array-to-pointer is an
795// lvalue transformation excluded from ICS ranking, so a string literal is an
796// Exact Match for BOTH an array template and the non-template const char*
797// overload above -- and the non-template wins the tiebreak. An array overload
798// would be dead code, and adding one would only risk a per-N instantiation.
799
800// fl::string / fl::string_view render like "%s" - no .c_str() needed.
801inline void format_arg_generic(sstream& stream, const fl::string& arg) FL_NO_EXCEPT {
802 format_arg(stream, FormatSpec('s'), arg);
803}
804
805inline void format_arg_generic(sstream& stream, const fl::string_view& arg) FL_NO_EXCEPT {
806 format_arg(stream, FormatSpec('s'), arg);
807}
808
809
810// Handle a '{' or '}' found in the format string.
811// Returns true if a generic "{}" placeholder was consumed, in which case the
812// caller is responsible for emitting an argument. Otherwise the brace text was
813// written to `stream` literally. Either way `format` is advanced past the
814// characters that were consumed, so callers must `continue` (never fall through
815// to an extra ++format) after calling this.
816//
817// Rules:
818// - "{}" is a generic placeholder
819// - "{{" is a literal '{' and "}}" is a literal '}'
820// - a lone '{' or '}' passes through literally rather than being an error
821inline bool parse_brace(sstream& stream, const char*& format) FL_NO_EXCEPT {
822 if (*format == '{') {
823 if (format[1] == '}') {
824 format += 2;
825 return true;
826 }
827 if (format[1] == '{') {
828 stream << "{";
829 format += 2;
830 return false;
831 }
832 stream << "{";
833 ++format;
834 return false;
835 }
836
837 // *format == '}'
838 if (format[1] == '}') {
839 stream << "}";
840 format += 2;
841 return false;
842 }
843 stream << "}";
844 ++format;
845 return false;
846}
847
848// Specialized format_arg for char arrays (string literals like "hello")
849template<fl::size N>
850void format_arg(sstream& stream, const FormatSpec& spec, const char (&arg)[N]) FL_NO_EXCEPT {
851 format_arg(stream, spec, static_cast<const char*>(arg));
852}
853
854// Base case: no more arguments
855inline void format_impl(sstream& stream, const char* format) FL_NO_EXCEPT {
856 while (*format) {
857 if (*format == '%') {
859 if (spec.type == '%') {
860 stream << "%";
861 continue;
862 } else {
863 // No argument for format specifier
864 stream << "<missing_arg>";
865 continue;
866 }
867 } else if (*format == '{' || *format == '}') {
868 if (parse_brace(stream, format)) {
869 // Generic placeholder with no argument left to consume.
870 stream << "<missing_arg>";
871 }
872 continue;
873 } else {
874 // Create a single-character string since sstream treats char as number
875 char temp_str[2] = {*format, '\0'};
876 stream << temp_str;
877 ++format;
878 }
879 }
880}
881
882// Recursive case: process one argument and continue
883template<typename T, typename... Args>
884void format_impl(sstream& stream, const char* format, const T& first, const Args&... rest) FL_NO_EXCEPT {
885 while (*format) {
886 if (*format == '%') {
888 if (spec.type == '%') {
889 stream << "%";
890 continue;
891 } else {
892 // Format the first argument and continue with the rest
893 format_arg(stream, spec, first);
894 format_impl(stream, format, rest...);
895 return;
896 }
897 } else if (*format == '{' || *format == '}') {
898 if (parse_brace(stream, format)) {
899 // Generic placeholder: format the first argument by
900 // picking the natural specifier for its type (NOT through
901 // sstream::operator<<, which is what this design avoids) and
902 // continue with the rest.
903 format_arg_generic(stream, first);
904 format_impl(stream, format, rest...);
905 return;
906 }
907 continue;
908 } else {
909 // Create a single-character string since sstream treats char as number
910 char temp_str[2] = {*format, '\0'};
911 stream << temp_str;
912 ++format;
913 }
914 }
915
916 // If we get here, there are unused arguments
917 // This is not an error in printf, so we just ignore them
918}
919
920}
921
960template<typename... Args>
961void printf(const char* format, const Args&... args) FL_NO_EXCEPT {
962 sstream stream;
964 fl::print(stream.str().c_str());
965}
966
1008template<typename... Args>
1009int snprintf(char* buffer, fl::size size, const char* format, const Args&... args) FL_NO_EXCEPT {
1010 // Handle null buffer or zero size
1011 if (!buffer || size == 0) {
1012 return 0;
1013 }
1014
1015 // Format to internal string stream
1016 sstream stream;
1018 fl::string result = stream.str();
1019
1020 // Get the formatted string length
1021 fl::size formatted_len = result.size();
1022
1023 // Copy to buffer, ensuring null termination
1024 fl::size copy_len = (formatted_len < size - 1) ? formatted_len : size - 1;
1025
1026 // Copy characters
1027 for (fl::size i = 0; i < copy_len; ++i) {
1028 buffer[i] = result[i];
1029 }
1030
1031 // Null terminate
1032 buffer[copy_len] = '\0';
1033
1034 // Return the number of characters actually written (excluding null terminator)
1035 // This respects the buffer size limit instead of returning the full formatted length
1036 return static_cast<int>(copy_len);
1037}
1038
1039} // namespace fl
uint8_t pos
Definition Blur.ino:11
fl::size length() const FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
const char * c_str() const FL_NO_EXCEPT
string str() const FL_NO_EXCEPT
Definition strstream.h:43
fl::enable_if< fl::is_integral< T >::value >::type format_arg_generic(sstream &stream, const T &arg) FL_NO_EXCEPT
Definition stdio.h:746
void format_arg(sstream &stream, const FormatSpec &spec, const fl::string &arg) FL_NO_EXCEPT
fl::string apply_width(const fl::string &str, const FormatSpec &spec, bool is_numeric=false) FL_NO_EXCEPT
Definition stdio.h:247
fl::string to_octal(T value) FL_NO_EXCEPT
Definition stdio.h:228
fl::string format_float(float value, int precision) FL_NO_EXCEPT
Definition stdio.h:341
constexpr float kFloatDecimalLimit
Definition stdio.h:311
void format_impl(sstream &stream, const char *format) FL_NO_EXCEPT
Definition stdio.h:855
bool float_is_finite(float value) FL_NO_EXCEPT
Definition stdio.cpp.hpp:6
FormatSpec parse_format_spec(const char *&format) FL_NO_EXCEPT
Definition stdio.h:130
bool parse_brace(sstream &stream, const char *&format) FL_NO_EXCEPT
Definition stdio.h:821
void format_floating(sstream &stream, const FormatSpec &spec, const ScalarArg &a) FL_NO_EXCEPT
fl::enable_if< fl::is_integral< T >::value >::type format_scalar_arg(sstream &stream, const FormatSpec &spec, ScalarArg &a, const T &arg) FL_NO_EXCEPT
Definition stdio.h:491
fl::string format_float_scientific(float value, int precision) FL_NO_EXCEPT
Definition stdio.cpp.hpp:15
fl::enable_if< fl::is_pointer< T >::value, fl::uptr >::type pointer_to_uptr(const T &ptr) FL_NO_EXCEPT
Definition stdio.h:436
void format_integral(sstream &stream, const FormatSpec &spec, const ScalarArg &a) FL_NO_EXCEPT
Definition stdio.cpp.hpp:93
unsigned char u8
Definition stdint.h:131
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
void print(const char *str) FL_NO_EXCEPT
constexpr int type_rank< T >::value
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
fl::string format(const char *fmt) FL_NO_EXCEPT
Format with no arguments.
Definition format.h:439
void printf(const char *format) FL_NO_EXCEPT
fl::string to_hex(T value, bool uppercase=false, bool pad_to_width=false) FL_NO_EXCEPT
Convert an integer value to hexadecimal string representation.
Definition string.h:526
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
Base definition for an LED controller.
Definition crgb.hpp:179
corkscrew_args args
Definition old.h:149
FormatSpec(char t, int prec) FL_NO_EXCEPT
Definition stdio.h:124
FormatSpec(char t) FL_NO_EXCEPT
Definition stdio.h:123