Wire: write doubles and floats with .NET Framework's round-trip digits
The original ran Newtonsoft.Json on .NET Framework, whose "R" format writes 15 significant digits when they read back exactly and 17 otherwise (floats: 7, else 9). Modern .NET writes the shortest round trip instead, so a value like -0.066666670143604279 came out as -0.06666667014360428: equal when parsed, different bytes. Every frame now goes through a converter that repeats the Framework rule, with Newtonsoft's '.0' suffix and non-finite handling kept. Goldens that encoded the modern digits are corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
82a518dcfb
commit
5a94e1cf0b
7 changed files with 148 additions and 7 deletions
|
|
@ -0,0 +1,82 @@
|
|||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Backend;
|
||||
|
||||
/// <summary>
|
||||
/// Writes doubles and floats with the digits the original plugin wrote.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Newtonsoft.Json writes a double as <c>value.ToString("R")</c> plus <c>.0</c>
|
||||
/// when that has no decimal point. The original ran on .NET Framework, where
|
||||
/// "R" meant: fifteen significant digits if they read back as the same
|
||||
/// double, otherwise seventeen (a float: seven, otherwise nine). Modern .NET
|
||||
/// prints the shortest text that reads back instead, so a value such as
|
||||
/// -0.066666670143604279 would come out as -0.06666667014360428: the same
|
||||
/// number, different bytes. This converter repeats the Framework rule.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Non-finite values keep Newtonsoft's own handling.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class FrameworkNumberConverter : JsonConverter
|
||||
{
|
||||
public static FrameworkNumberConverter Instance { get; } = new();
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanConvert(Type objectType) =>
|
||||
objectType == typeof(double) || objectType == typeof(double?)
|
||||
|| objectType == typeof(float) || objectType == typeof(float?);
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
writer.WriteNull();
|
||||
break;
|
||||
case double d when double.IsFinite(d):
|
||||
writer.WriteRawValue(EnsureDecimalPlace(FrameworkRoundTrip(d)));
|
||||
break;
|
||||
case float f when float.IsFinite(f):
|
||||
writer.WriteRawValue(EnsureDecimalPlace(FrameworkRoundTrip(f)));
|
||||
break;
|
||||
case double d:
|
||||
writer.WriteValue(d);
|
||||
break;
|
||||
case float f:
|
||||
writer.WriteValue(f);
|
||||
break;
|
||||
default:
|
||||
writer.WriteValue(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
/// <summary>.NET Framework's "R" for a double: G15 if it reads back exactly, else G17.</summary>
|
||||
internal static string FrameworkRoundTrip(double value)
|
||||
{
|
||||
string text = value.ToString("G15", CultureInfo.InvariantCulture);
|
||||
if (double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture) == value)
|
||||
return text;
|
||||
return value.ToString("G17", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>.NET Framework's "R" for a float: G7 if it reads back exactly, else G9.</summary>
|
||||
internal static string FrameworkRoundTrip(float value)
|
||||
{
|
||||
string text = value.ToString("G7", CultureInfo.InvariantCulture);
|
||||
if (float.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture) == value)
|
||||
return text;
|
||||
return value.ToString("G9", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>Newtonsoft's rule: a number with no point and no exponent gets ".0".</summary>
|
||||
private static string EnsureDecimalPlace(string text) =>
|
||||
text.Contains('.') || text.Contains('E') || text.Contains('e') ? text : text + ".0";
|
||||
}
|
||||
|
|
@ -16,7 +16,14 @@ internal static class Wire
|
|||
// A private serializer rather than JsonConvert.SerializeObject: that one
|
||||
// picks up JsonConvert.DefaultSettings, which any code in this load
|
||||
// context could change.
|
||||
private static readonly JsonSerializer Serializer = JsonSerializer.Create();
|
||||
private static readonly JsonSerializer Serializer = CreateSerializer();
|
||||
|
||||
private static JsonSerializer CreateSerializer()
|
||||
{
|
||||
JsonSerializer serializer = JsonSerializer.Create();
|
||||
serializer.Converters.Add(FrameworkNumberConverter.Instance);
|
||||
return serializer;
|
||||
}
|
||||
|
||||
public static string Serialize(object? payload)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue