|
3 | 3 | // Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors) |
4 | 4 | #include "System/String.hpp" |
5 | 5 | #include "System/detail/CompositeFormat.hpp" |
| 6 | +#include "System/detail/FloatTextFormat.hpp" |
| 7 | +#include "System/Double.hpp" |
| 8 | +#include "System/Int32.hpp" |
| 9 | +#include "System/Single.hpp" |
6 | 10 | #include "System/ArgumentOutOfRangeException.hpp" |
7 | 11 | #include "System/FormatException.hpp" |
8 | 12 | #include "System/OutOfMemoryException.hpp" |
@@ -150,16 +154,22 @@ namespace System |
150 | 154 | // format item. `text` points at the caller's own std::string parameter, which outlives |
151 | 155 | // the call; nothing here owns or copies it. |
152 | 156 | struct FormatArg { |
153 | | - enum class Kind { Int, Long, Double, Text }; |
| 157 | + enum class Kind { Int, Long, Float, Double, Text }; |
154 | 158 | Kind kind = Kind::Int; |
155 | 159 | SharpRuntime::intcs i = 0; |
156 | 160 | SharpRuntime::longcs l = 0; |
| 161 | + float f = 0.0f; |
157 | 162 | double d = 0.0; |
158 | 163 | const std::string* text = nullptr; |
159 | 164 | }; |
160 | 165 |
|
161 | 166 | FormatArg argOf(SharpRuntime::intcs v) { FormatArg a; a.kind = FormatArg::Kind::Int; a.i = v; return a; } |
162 | 167 | FormatArg argOf(SharpRuntime::longcs v) { FormatArg a; a.kind = FormatArg::Kind::Long; a.l = v; return a; } |
| 168 | + // A float is NOT widened to double. Single and Double round-trip through a different |
| 169 | + // number of digits, so Format("{0}", 59.4f) came out as "59.400001525878906" -- the |
| 170 | + // double text of the widened float -- where .NET prints "59.4", because .NET formats the |
| 171 | + // argument with its OWN Single.ToString(). |
| 172 | + FormatArg argOf(float v) { FormatArg a; a.kind = FormatArg::Kind::Float; a.f = v; return a; } |
163 | 173 | FormatArg argOf(double v) { FormatArg a; a.kind = FormatArg::Kind::Double; a.d = v; return a; } |
164 | 174 | FormatArg argOf(const std::string& v) { FormatArg a; a.kind = FormatArg::Kind::Text; a.text = &v; return a; } |
165 | 175 |
|
@@ -211,6 +221,19 @@ namespace System |
211 | 221 | // Format integer with .NET-style specifier (X/x=hex, D=decimal padded, else plain). |
212 | 222 | std::string fmtInt(SharpRuntime::intcs value, std::string_view spec) { |
213 | 223 | if (spec.empty()) return std::to_string(value); |
| 224 | + // A format that is not one letter plus an optional precision is a CUSTOM numeric |
| 225 | + // format string -- "00", "0.00", "#,##0" -- a separate .NET grammar the standard |
| 226 | + // specifiers below cannot express. This function knew only X/x/D/d, so every custom |
| 227 | + // format fell through to a plain decimal and was silently DROPPED: |
| 228 | + // Format("{0:00}:{1:00}", 3, 7) returned "3:7" where .NET returns "03:07". The |
| 229 | + // grammar is already implemented once, in the type's own ToString, so this defers to |
| 230 | + // it rather than growing a second copy. Standard specifiers keep taking the path |
| 231 | + // below unchanged, which is what preserves the specifier-tail hardening of tickets |
| 232 | + // #1847/#1849 -- ToString's own tail parsing is stricter in ways Format's callers |
| 233 | + // are already tested against ("{0:DX}" must yield "42", not throw). |
| 234 | + if (System::detail::isCustomNumericPlaceholderFormat(std::string(spec))) { |
| 235 | + return System::Int32::ToString(value, std::string(spec)); |
| 236 | + } |
214 | 237 | const char sc = spec[0]; |
215 | 238 | const SpecNumber num = parseSpecNumber(spec); |
216 | 239 | if (num.kind == SpecNumberKind::TooLarge) throwBadFormatSpecifier(); |
@@ -270,6 +293,11 @@ namespace System |
270 | 293 | auto [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), value); |
271 | 294 | return ec == std::errc{} ? std::string(buf.data(), ptr) : std::to_string(value); |
272 | 295 | } |
| 296 | + // Same custom-format gap as fmtInt above, in the floating-point half: |
| 297 | + // Format("{0:0.00}", 59.4) returned "59.4" instead of "59.40". |
| 298 | + if (System::detail::isCustomNumericPlaceholderFormat(std::string(spec))) { |
| 299 | + return System::Double::ToString(value, std::string(spec)); |
| 300 | + } |
273 | 301 | const char sc = spec[0]; |
274 | 302 | const SpecNumber num = parseSpecNumber(spec); |
275 | 303 | if (num.kind == SpecNumberKind::TooLarge) throwBadFormatSpecifier(); |
@@ -298,9 +326,20 @@ namespace System |
298 | 326 | } |
299 | 327 | } |
300 | 328 |
|
| 329 | + // Format a float as .NET does: through Single's own ToString, never through Double's. |
| 330 | + // The specifier tail is validated here first, so an oversized one is still the |
| 331 | + // FormatException ticket #1849 pinned rather than a 10^9-digit precision request. |
| 332 | + std::string fmtFloat(float value, std::string_view spec) { |
| 333 | + if (spec.empty()) return System::Single::ToString(value); |
| 334 | + const SpecNumber num = parseSpecNumber(spec); |
| 335 | + if (num.kind == SpecNumberKind::TooLarge) throwBadFormatSpecifier(); |
| 336 | + return System::Single::ToString(value, std::string(spec)); |
| 337 | + } |
| 338 | + |
301 | 339 | std::string renderArg(const FormatArg& arg, std::string_view spec) { |
302 | 340 | switch (arg.kind) { |
303 | 341 | case FormatArg::Kind::Int: return fmtInt(arg.i, spec); |
| 342 | + case FormatArg::Kind::Float: return fmtFloat(arg.f, spec); |
304 | 343 | case FormatArg::Kind::Double: return fmtDouble(arg.d, spec); |
305 | 344 | case FormatArg::Kind::Long: return std::to_string(arg.l); |
306 | 345 | case FormatArg::Kind::Text: return *arg.text; |
@@ -838,7 +877,8 @@ namespace System |
838 | 877 |
|
839 | 878 | std::string String::Format(const std::string& format, float arg0) |
840 | 879 | { |
841 | | - return Format(format, static_cast<double>(arg0)); |
| 880 | + const FormatArg args[] = {argOf(arg0)}; |
| 881 | + return formatCore(format, args, 1); |
842 | 882 | } |
843 | 883 |
|
844 | 884 | std::string String::Format(const std::string& format, SharpRuntime::longcs arg0, SharpRuntime::longcs arg1) |
|
0 commit comments