From 4f61326413f1a35ccfc4e3b93b5c304bb06c3d0a Mon Sep 17 00:00:00 2001 From: Zoltan Csizmadia Date: Fri, 22 Nov 2024 11:27:56 -0600 Subject: [PATCH] AVRO-2032: [C#] Add support for NaN, Infinity and -Infinity in JsonDecoder (#3070) * Add support for IEEE 754 strings in json decoder * Remove redundent casting --------- Co-authored-by: Zoltan Csizmadia --- lang/csharp/src/apache/main/IO/JsonDecoder.cs | 55 +++++++++++++++++-- .../src/apache/test/IO/JsonCodecTests.cs | 54 ++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/lang/csharp/src/apache/main/IO/JsonDecoder.cs b/lang/csharp/src/apache/main/IO/JsonDecoder.cs index 0e45ebc50a9..549ead26d33 100644 --- a/lang/csharp/src/apache/main/IO/JsonDecoder.cs +++ b/lang/csharp/src/apache/main/IO/JsonDecoder.cs @@ -190,10 +190,25 @@ public override float ReadFloat() reader.Read(); return result; } - else + else if (reader.TokenType == JsonToken.String) { - throw TypeError("float"); + string str = Convert.ToString(reader.Value); + reader.Read(); + if (IsNaNString(str)) + { + return float.NaN; + } + else if (IsPositiveInfinityString(str)) + { + return float.PositiveInfinity; + } + else if (IsNegativeInfinityString(str)) + { + return float.NegativeInfinity; + } } + + throw TypeError("float"); } /// @@ -206,10 +221,25 @@ public override double ReadDouble() reader.Read(); return result; } - else + else if (reader.TokenType == JsonToken.String) { - throw TypeError("double"); + string str = Convert.ToString(reader.Value); + reader.Read(); + if (IsNaNString(str)) + { + return double.NaN; + } + else if (IsPositiveInfinityString(str)) + { + return double.PositiveInfinity; + } + else if (IsNegativeInfinityString(str)) + { + return double.NegativeInfinity; + } } + + throw TypeError("double"); } /// @@ -766,6 +796,23 @@ public override bool Read() } } + private bool IsNaNString(string str) + { + return str.Equals("NaN", StringComparison.Ordinal); + } + + private bool IsPositiveInfinityString(string str) + { + return str.Equals("Infinity", StringComparison.Ordinal) || + str.Equals("INF", StringComparison.Ordinal); + } + + private bool IsNegativeInfinityString(string str) + { + return str.Equals("-Infinity", StringComparison.Ordinal) || + str.Equals("-INF", StringComparison.Ordinal); + } + private AvroTypeException TypeError(string type) { return new AvroTypeException("Expected " + type + ". Got " + reader.TokenType); diff --git a/lang/csharp/src/apache/test/IO/JsonCodecTests.cs b/lang/csharp/src/apache/test/IO/JsonCodecTests.cs index 5b05c93e563..fe2183a2f1f 100644 --- a/lang/csharp/src/apache/test/IO/JsonCodecTests.cs +++ b/lang/csharp/src/apache/test/IO/JsonCodecTests.cs @@ -268,6 +268,60 @@ public void TestJsonDecoderNumeric(string type, object value) } } + [Test] + [TestCase("float", "0", (float)0)] + [TestCase("float", "1", (float)1)] + [TestCase("float", "1.0", (float)1.0)] + [TestCase("double", "0", (double)0)] + [TestCase("double", "1", (double)1)] + [TestCase("double", "1.0", 1.0)] + [TestCase("float", "\"NaN\"", float.NaN)] + [TestCase("float", "\"Infinity\"", float.PositiveInfinity)] + [TestCase("float", "\"INF\"", float.PositiveInfinity)] + [TestCase("float", "\"-Infinity\"", float.NegativeInfinity)] + [TestCase("float", "\"-INF\"", float.NegativeInfinity)] + [TestCase("double", "\"NaN\"", double.NaN)] + [TestCase("double", "\"Infinity\"", double.PositiveInfinity)] + [TestCase("double", "\"INF\"", double.PositiveInfinity)] + [TestCase("double", "\"-Infinity\"", double.NegativeInfinity)] + [TestCase("double", "\"-INF\"", double.NegativeInfinity)] + [TestCase("float", "\"\"", null)] + [TestCase("float", "\"unknown\"", null)] + [TestCase("float", "\"nan\"", null)] + [TestCase("float", "\"infinity\"", null)] + [TestCase("float", "\"inf\"", null)] + [TestCase("float", "\"-infinity\"", null)] + [TestCase("float", "\"-inf\"", null)] + [TestCase("double", "\"\"", null)] + [TestCase("double", "\"unknown\"", null)] + [TestCase("double", "\"nan\"", null)] + [TestCase("double", "\"infinity\"", null)] + [TestCase("double", "\"inf\"", null)] + [TestCase("double", "\"-infinity\"", null)] + [TestCase("double", "\"-inf\"", null)] + [TestCase("double", "\"-inf\"", null)] + public void TestJsonDecodeFloatDouble(string typeStr, string valueStr, object expected) + { + string def = $"{{\"type\":\"record\",\"name\":\"X\",\"fields\":[{{\"type\":\"{typeStr}\",\"name\":\"Value\"}}]}}"; + Schema schema = Schema.Parse(def); + DatumReader reader = new GenericDatumReader(schema, schema); + + string record = $"{{\"Value\":{valueStr}}}"; + Decoder decoder = new JsonDecoder(schema, record); + try + { + GenericRecord r = reader.Read(null, decoder); + Assert.AreEqual(expected, r["Value"]); + } + catch (AvroTypeException) + { + if (expected != null) + { + throw; + } + } + } + [TestCase("{ \"s\": \"1900-01-01T00:00:00Z\" }", "1900-01-01T00:00:00Z")] [TestCase("{ \"s\": \"1900-01-01T00:00:00.0000000Z\" }", "1900-01-01T00:00:00.0000000Z")] [TestCase("{ \"s\": \"1900-01-01T00:00:00\" }", "1900-01-01T00:00:00")]