From 0b8db4527beebfa1816d279eb24dcfe962646ed0 Mon Sep 17 00:00:00 2001 From: sachinkale246 <157940644+sachinkale246@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:19:38 -0700 Subject: [PATCH] Commit changes --- .../JsonTextReaderTests/FloatTests.cs | 126 ++++++++++++++++++ Src/Newtonsoft.Json/JsonTextReader.cs | 65 +++++++++ 2 files changed, 191 insertions(+) diff --git a/Src/Newtonsoft.Json.Tests/JsonTextReaderTests/FloatTests.cs b/Src/Newtonsoft.Json.Tests/JsonTextReaderTests/FloatTests.cs index 05874fe03..0fdba4cbe 100644 --- a/Src/Newtonsoft.Json.Tests/JsonTextReaderTests/FloatTests.cs +++ b/Src/Newtonsoft.Json.Tests/JsonTextReaderTests/FloatTests.cs @@ -365,5 +365,131 @@ public void ReadFloatingPointNumber() Assert.AreEqual(JsonToken.EndArray, jsonReader.TokenType); } } + + [Test] + public void PreserveNumberText_Float_ReturnsRawText() + { + const string testJson = "[1.5, 42, 3.14]"; + using JsonTextReader reader = new JsonTextReader(new StringReader(testJson)); + reader.PreserveNumberText = true; + + reader.Read(); // [ + + reader.Read(); // 1.5 + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.AreEqual("1.5", reader.NumberText); + + reader.Read(); // 42 + Assert.AreEqual(JsonToken.Integer, reader.TokenType); + Assert.AreEqual("42", reader.NumberText); + + reader.Read(); // 3.14 + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.AreEqual("3.14", reader.NumberText); + + reader.Read(); // ] + Assert.AreEqual(JsonToken.EndArray, reader.TokenType); + Assert.IsNull(reader.NumberText); + } + + [Test] + public void PreserveNumberText_NegativeDoubleMaxValue_RawTextExact() + { + const string testJson = "[-1.7976931348623157E+308]"; + using JsonTextReader reader = new JsonTextReader(new StringReader(testJson)); + reader.PreserveNumberText = true; + + reader.Read(); // [ + reader.Read(); // -1.7976931348623157E+308 + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.AreEqual("-1.7976931348623157E+308", reader.NumberText); + Assert.AreEqual(-double.MaxValue, (double)reader.Value!); + } + + [Test] + public void PreserveNumberText_Disabled_NumberTextIsNull() + { + const string testJson = "[1.5]"; + using JsonTextReader reader = new JsonTextReader(new StringReader(testJson)); + + reader.Read(); // [ + reader.Read(); // 1.5 + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.IsNull(reader.NumberText); + } + + [Test] + public void PreserveNumberText_DoubleMaxValue_RawTextExact() + { + string doubleMax = double.MaxValue.ToString("R", CultureInfo.InvariantCulture); + string testJson = "[" + doubleMax + "]"; + using JsonTextReader reader = new JsonTextReader(new StringReader(testJson)); + reader.PreserveNumberText = true; + + reader.Read(); + reader.Read(); + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.AreEqual(doubleMax, reader.NumberText); + } + + [Test] + public void OnNumberParsed_Override_CanChangeParsedValue() + { + // Verify that a subclass can intercept number tokens and replace the value + string testJson = "[1.7976931348623157E+308]"; // double.MaxValue — overflows decimal + using SmartNumberReader reader = new SmartNumberReader(new StringReader(testJson)); + + reader.Read(); // [ + reader.Read(); // number + + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.IsInstanceOf(typeof(double), reader.Value); + Assert.AreEqual(double.MaxValue, (double)reader.Value!); + } + + [Test] + public void OnNumberParsed_Override_HighPrecisionDecimal_PreservesExactValue() + { + // 29 significant digits — exact as decimal, lossy as double (~15-16 sig figs) + const string rawDecimal = "1.23456789012345678901234567890"; + const string testJson = "[" + rawDecimal + "]"; + using SmartNumberReader reader = new SmartNumberReader(new StringReader(testJson)); + + reader.Read(); // [ + reader.Read(); // number + + // Default FloatParseHandling.Double would lose precision; SmartNumberReader picks decimal + Assert.AreEqual(JsonToken.Float, reader.TokenType); + Assert.IsInstanceOf(typeof(decimal), reader.Value); + Assert.AreEqual(decimal.Parse(rawDecimal, CultureInfo.InvariantCulture), (decimal)reader.Value!); + } + + // Minimal subclass demonstrating Option C: override OnNumberParsed to pick the + // best .NET numeric type for each float token based on the raw source text. + private sealed class SmartNumberReader : JsonTextReader + { + public SmartNumberReader(TextReader reader) : base(reader) + { + PreserveNumberText = true; + } + + protected override void OnNumberParsed(string rawText, ref JsonToken tokenType, ref object value) + { + if (tokenType != JsonToken.Float) + { + return; + } + + // Try decimal first (higher precision), fall back to double for out-of-range values. + if (decimal.TryParse(rawText, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal d)) + { + value = d; + } + else if (double.TryParse(rawText, NumberStyles.Float, CultureInfo.InvariantCulture, out double dbl)) + { + value = dbl; + } + } + } } } diff --git a/Src/Newtonsoft.Json/JsonTextReader.cs b/Src/Newtonsoft.Json/JsonTextReader.cs index dd8094f96..6783a8b8d 100644 --- a/Src/Newtonsoft.Json/JsonTextReader.cs +++ b/Src/Newtonsoft.Json/JsonTextReader.cs @@ -76,6 +76,10 @@ public partial class JsonTextReader : JsonReader, IJsonLineInfo private StringBuffer _stringBuffer; private StringReference _stringReference; private IArrayPool? _arrayPool; + private bool _preserveNumberText; + private string? _numberText; + // cached at construction time so hot paths avoid repeated GetType() virtual calls + private readonly bool _hasNumberOverride; /// /// Initializes a new instance of the class with the specified . @@ -90,6 +94,7 @@ public JsonTextReader(TextReader reader) _reader = reader; _lineNumber = 1; + _hasNumberOverride = GetType() != typeof(JsonTextReader); #if HAVE_ASYNC _safeAsync = GetType() == typeof(JsonTextReader); @@ -128,6 +133,53 @@ public IArrayPool? ArrayPool } } + /// + /// Gets or sets a value indicating whether the raw source text of each number token is + /// preserved and accessible via after returns. + /// When false (the default), is always null. + /// Enabling this causes one string allocation per number token parsed. + /// + public bool PreserveNumberText + { + get => _preserveNumberText; + set => _preserveNumberText = value; + } + + /// + /// Gets the raw source text of the current number token, or null if the current token + /// is not or , or if + /// is false. + /// + public string? NumberText + { + get + { + if (TokenType != JsonToken.Integer && TokenType != JsonToken.Float) + { + return null; + } + + return _numberText; + } + } + + /// + /// Called after a number token has been parsed and its default value determined, but before + /// the token is committed via . + /// Override to replace and/or with a + /// more precise representation (e.g. choose between and + /// based on the raw text). + /// + /// The exact source text of the number as it appeared in the JSON. + /// + /// The resolved token type ( or ). + /// May be changed by the override. + /// + /// The resolved boxed value. May be changed by the override. + protected virtual void OnNumberParsed(string rawText, ref JsonToken tokenType, ref object value) + { + } + private void EnsureBufferNotEmpty() { if (_stringBuffer.IsEmpty) @@ -2236,8 +2288,21 @@ private void ParseReadNumber(ReadType readType, char firstChar, int initialPosit throw JsonReaderException.Create(this, "Cannot read number value as type."); } + // Capture raw text before the string reference is cleared. + // Only pay the allocation cost when PreserveNumberText is set or a subclass may + // override OnNumberParsed (detected once at construction time via _hasNumberOverride). + string? rawText = (_preserveNumberText || _hasNumberOverride) + ? _stringReference.ToString() + : null; + + _numberText = rawText; ClearRecentString(); + if (rawText != null) + { + OnNumberParsed(rawText, ref numberType, ref numberValue); + } + // index has already been updated SetToken(numberType, numberValue, false); }