diff --git a/src/Nest/Aggregations/AggregateFormatter.cs b/src/Nest/Aggregations/AggregateFormatter.cs index 5ffa8f9fcb3..921c50c78d9 100644 --- a/src/Nest/Aggregations/AggregateFormatter.cs +++ b/src/Nest/Aggregations/AggregateFormatter.cs @@ -251,27 +251,29 @@ private IAggregate GetMatrixStatsAggregate(ref JsonReader reader, IJsonFormatter private IAggregate GetBoxplotAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary meta) { + var nullableDoubleFormatter = new StringDoubleFormatter(); + var boxplot = new BoxplotAggregate { - Min = reader.ReadDouble(), + Min = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver), Meta = meta }; reader.ReadNext(); // , reader.ReadNext(); // "max" reader.ReadNext(); // : - boxplot.Max = reader.ReadDouble(); + boxplot.Max = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); reader.ReadNext(); // , reader.ReadNext(); // "q1" reader.ReadNext(); // : - boxplot.Q1 = reader.ReadDouble(); + boxplot.Q1 = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); reader.ReadNext(); // , reader.ReadNext(); // "q2" reader.ReadNext(); // : - boxplot.Q2 = reader.ReadDouble(); + boxplot.Q2 = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); reader.ReadNext(); // , reader.ReadNext(); // "q3" reader.ReadNext(); // : - boxplot.Q3 = reader.ReadDouble(); + boxplot.Q3 = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); var token = reader.GetCurrentJsonToken(); if (token != JsonToken.EndObject) @@ -279,11 +281,11 @@ private IAggregate GetBoxplotAggregate(ref JsonReader reader, IJsonFormatterReso reader.ReadNext(); // , reader.ReadNext(); // "lower" reader.ReadNext(); // : - boxplot.Lower = reader.ReadDouble(); + boxplot.Lower = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); reader.ReadNext(); // , reader.ReadNext(); // "upper" reader.ReadNext(); // : - boxplot.Upper = reader.ReadDouble(); + boxplot.Upper = nullableDoubleFormatter.Deserialize(ref reader, formatterResolver); } return boxplot; diff --git a/src/Nest/Aggregations/Metric/Boxplot/BoxplotAggregate.cs b/src/Nest/Aggregations/Metric/Boxplot/BoxplotAggregate.cs index 97c96c7c009..bcebeba77f6 100644 --- a/src/Nest/Aggregations/Metric/Boxplot/BoxplotAggregate.cs +++ b/src/Nest/Aggregations/Metric/Boxplot/BoxplotAggregate.cs @@ -2,22 +2,31 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elasticsearch.Net.Utf8Json; + namespace Nest { public class BoxplotAggregate : MetricAggregateBase { + [JsonFormatter(typeof(StringDoubleFormatter))] public double Min { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Max { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Q1 { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Q2 { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Q3 { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Lower { get; set; } + [JsonFormatter(typeof(StringDoubleFormatter))] public double Upper { get; set; } } } diff --git a/src/Nest/CommonAbstractions/SerializationBehavior/JsonFormatters/NullableStringBooleanFormatter.cs b/src/Nest/CommonAbstractions/SerializationBehavior/JsonFormatters/NullableStringBooleanFormatter.cs index ecef6e29adb..5149a7e786f 100644 --- a/src/Nest/CommonAbstractions/SerializationBehavior/JsonFormatters/NullableStringBooleanFormatter.cs +++ b/src/Nest/CommonAbstractions/SerializationBehavior/JsonFormatters/NullableStringBooleanFormatter.cs @@ -172,6 +172,13 @@ internal class NullableStringDoubleFormatter : IJsonFormatter return null; case JsonToken.String: var s = reader.ReadString(); + + if (s.Equals("Infinity", System.StringComparison.Ordinal)) + return double.PositiveInfinity; + + if (s.Equals("-Infinity", System.StringComparison.Ordinal)) + return double.NegativeInfinity; + if (!double.TryParse(s, out var d)) throw new JsonParsingException($"Cannot parse {typeof(double).FullName} from: {s}"); @@ -194,4 +201,39 @@ public void Serialize(ref JsonWriter writer, double? value, IJsonFormatterResolv writer.WriteDouble(value.Value); } } + + internal class StringDoubleFormatter : IJsonFormatter + { + public double Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) + { + var token = reader.GetCurrentJsonToken(); + switch (token) + { + case JsonToken.Null: + throw new JsonParsingException($"Cannot parse non-nullable double value from: {token}."); + + case JsonToken.String: + var s = reader.ReadString(); + + if (s.Equals("Infinity", System.StringComparison.Ordinal)) + return double.PositiveInfinity; + + if (s.Equals("-Infinity", System.StringComparison.Ordinal)) + return double.NegativeInfinity; + + if (!double.TryParse(s, out var d)) + throw new JsonParsingException($"Cannot parse {typeof(double).FullName} from: {s}"); + + return d; + + case JsonToken.Number: + return reader.ReadDouble(); + + default: + throw new JsonParsingException($"Cannot parse {typeof(double).FullName} from: {token}"); + } + } + + public void Serialize(ref JsonWriter writer, double value, IJsonFormatterResolver formatterResolver) => writer.WriteDouble(value); + } } diff --git a/tests/Tests.Reproduce/GitHubIssue6050.cs b/tests/Tests.Reproduce/GitHubIssue6050.cs new file mode 100644 index 00000000000..6be528c0313 --- /dev/null +++ b/tests/Tests.Reproduce/GitHubIssue6050.cs @@ -0,0 +1,75 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Text; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; + +namespace Tests.Reproduce +{ + public class GitHubIssue6050 + { + private static readonly byte[] ResponseBytes = Encoding.UTF8.GetBytes(@"{ + ""took"" : 1, + ""timed_out"" : false, + ""_shards"" : { + ""total"" : 1, + ""successful"" : 1, + ""skipped"" : 0, + ""failed"" : 0 + }, + ""hits"" : { + ""total"" : { + ""value"" : 0, + ""relation"" : ""eq"" + }, + ""max_score"" : null, + ""hits"" : [ ] + }, + ""aggregations"" : { + ""summary_boxplot"" : { + ""min"" : ""Infinity"", + ""max"" : ""-Infinity"", + ""q1"" : ""NaN"", + ""q2"" : ""NaN"", + ""q3"" : ""NaN"", + ""lower"" : ""NaN"", + ""upper"" : ""-Infinity"" + } + } +}"); + + [U] + public void BoxplotHandlesNaNValues() + { + var pool = new SingleNodeConnectionPool(new Uri($"http://localhost:9200")); + var settings = new ConnectionSettings(pool, new InMemoryConnection(ResponseBytes)); + var client = new ElasticClient(settings); + + var response = client.Search(s => s + .Size(0) + .Index("test") + .Aggregations(a => a + .Boxplot("summary_boxplot", mt => mt.Field(f => f.Population)))); + + var boxplot = response.Aggregations.Boxplot("summary_boxplot"); + + double.IsNaN(boxplot.Lower).Should().BeTrue(); + double.IsNaN(boxplot.Q1).Should().BeTrue(); + double.IsNaN(boxplot.Q2).Should().BeTrue(); + double.IsNaN(boxplot.Q3).Should().BeTrue(); + double.IsInfinity(boxplot.Min).Should().BeTrue(); + double.IsNegativeInfinity(boxplot.Max).Should().BeTrue(); + double.IsNegativeInfinity(boxplot.Upper).Should().BeTrue(); + } + + private class TestData + { + public long Population { get; set; } + } + } +}