diff --git a/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs b/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs index f0ec4cb0..e2e86998 100644 --- a/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs +++ b/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs @@ -15,9 +15,8 @@ namespace Newtonsoft.Json.Tests.Issues /// Here's where we test the sandbox for needles and broken glass /// [TestFixture] - public class EqualityTests + public class Issue1837 { - [Test] public void AllStrictEqualityTests() { @@ -28,68 +27,81 @@ namespace Newtonsoft.Json.Tests.Issues // For all tests, if Type(x) is different from Type(y), return false. // given x === y, if Type(x) is Null, return true var target = lhs.Null; - AssertAll(strictEquality, target, rhs.Null); - AssertNone(strictEquality, target, rhs.ErrybodyButNull); + AssertAll(StrictEquality, target, rhs.Null); + AssertNone(StrictEquality, target, rhs.ErrybodyButNull); // given x === y, if x is the same Number value as y, return true. target = lhs.One; - AssertAll(strictEquality, target, rhs.One, rhs.OneDotZero); - Assert.IsFalse(BooleanQueryExpression.IsStrictMatch(target, rhs.Two)); + AssertAll(StrictEquality, target, rhs.One, rhs.OneDotZero); + Assert.IsFalse(BooleanQueryExpression.EqualsWithStrictMatch(target, rhs.Two)); target = lhs.Scientific; - Assert.IsTrue(BooleanQueryExpression.IsStrictMatch(target, rhs.Scientific)); + Assert.IsTrue(BooleanQueryExpression.EqualsWithStrictMatch(target, rhs.Scientific)); // given x === y, if Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions); otherwise, return false. target = lhs.DerpString; - AssertNone(strictEquality, target, rhs.HerpString); - AssertAll(strictEquality, target, rhs.DerpString); + AssertNone(StrictEquality, target, rhs.HerpString); + AssertAll(StrictEquality, target, rhs.DerpString); // given x === y, if Type(x) is Boolean, return true if x and y are both true or both false; otherwise, return false. target = lhs.True; - AssertAll(strictEquality, target, rhs.True); - AssertNone(strictEquality, target, new[] { rhs.False }, rhs.Nopes, rhs.Numbers, rhs.Strings, rhs.Dates); + AssertAll(StrictEquality, target, rhs.True); + AssertNone(StrictEquality, target, new[] { rhs.False }, rhs.Nopes, rhs.Numbers, rhs.Strings, rhs.Dates); target = lhs.False; - AssertAll(strictEquality, target, rhs.False); - AssertNone(strictEquality, target, new[] { rhs.True }, rhs.Nopes, rhs.Numbers, rhs.Strings, rhs.Dates); + AssertAll(StrictEquality, target, rhs.False); + AssertNone(StrictEquality, target, new[] { rhs.True }, rhs.Nopes, rhs.Numbers, rhs.Strings, rhs.Dates); //Dates target = lhs.DateYearMonth; - AssertAll(strictEquality, target, rhs.DateYearMonth); - AssertNone(strictEquality, target, rhs.DateYear); + AssertAll(StrictEquality, target, rhs.DateYearMonth); + AssertNone(StrictEquality, target, rhs.DateYear); target = lhs.DateYear; - AssertNone(strictEquality, target, rhs.DateYearMonth); - AssertAll(strictEquality, target, rhs.DateYear); + AssertNone(StrictEquality, target, rhs.DateYearMonth); + AssertAll(StrictEquality, target, rhs.DateYear); target = lhs.DateISO; - Assert.IsTrue(BooleanQueryExpression.IsStrictMatch(target, rhs.DateISO)); - Assert.IsFalse(BooleanQueryExpression.IsStrictMatch(target, rhs.OtherISODate)); + Assert.IsTrue(BooleanQueryExpression.EqualsWithStrictMatch(target, rhs.DateISO)); + Assert.IsFalse(BooleanQueryExpression.EqualsWithStrictMatch(target, rhs.OtherISODate)); } + #region helpers // used by asserters to perform the comparison - public delegate bool Comparator(JToken lhs, JToken rhs); + public delegate bool Comparator(JValue lhs, JValue rhs); // there was going to be an abstractEquality, but check the exception for it's implementation for why that's skipped for now - readonly Comparator strictEquality = (lhs, rhs) => BooleanQueryExpression.IsStrictMatch(lhs, rhs); + private readonly Comparator StrictEquality = (lhs, rhs) => BooleanQueryExpression.EqualsWithStrictMatch(lhs, rhs); + // a bunch of convenience methods for the test belwo // these make sure the comparator returns false for all do not wants - private void AssertNone(Comparator comparator, JToken token, params JToken[][] doNotWant) + private void AssertNone(Comparator comparator, JValue token, params JValue[][] doNotWant) { - foreach(var group in doNotWant) + foreach (var group in doNotWant) + { AssertNone(comparator, token, group); + } } - private void AssertNone(Comparator comparator, JToken token, params JToken[] doNotWant) + + private void AssertNone(Comparator comparator, JValue token, params JValue[] doNotWant) { - foreach(var item in doNotWant) - Assert.IsTrue(!comparator(token, item)); + foreach (var item in doNotWant) + { + Assert.IsTrue(!comparator(token, item)); + } } + // these make sure the comparator returns true for all do not wants - private void AssertAll(Comparator comparator, JToken token, params JToken[][] want) + private void AssertAll(Comparator comparator, JValue token, params JValue[][] want) { - foreach(var group in want) + foreach (var group in want) + { AssertAll(comparator, token, group); + } } - private void AssertAll(Comparator comparator, JToken token, params JToken[] want) + + private void AssertAll(Comparator comparator, JValue token, params JValue[] want) { - foreach(var item in want) + foreach (var item in want) + { Assert.IsTrue(comparator(token, item)); + } } #endregion } @@ -99,60 +111,61 @@ namespace Newtonsoft.Json.Tests.Issues /// public class TestData { - public readonly JToken Null; + public readonly JValue Null; //JSON.stringify({"undef": undefined}) returns {} //public readonly JToken Undefined; - public readonly JToken[] Nopes; + public readonly JValue[] Nopes; - public readonly JToken One; - public readonly JToken OneDotZero; - public readonly JToken Two; - public readonly JToken Scientific; + public readonly JValue One; + public readonly JValue OneDotZero; + public readonly JValue Two; + public readonly JValue Scientific; // stringify returns these as 0 //public readonly JToken NegativeZero; //public readonly JToken PositiveZero; // JSON.stringify({"lol": NaN}) returns "{"lol":null}" //public readonly JToken NaN; - public readonly JToken[] Numbers; + public readonly JValue[] Numbers; - public readonly JToken HerpString; - public readonly JToken DerpString; - public readonly JToken[] Strings; + public readonly JValue HerpString; + public readonly JValue DerpString; + public readonly JValue[] Strings; - public readonly JToken True; - public readonly JToken False; - public readonly JToken[] Boolies; + public readonly JValue True; + public readonly JValue False; + public readonly JValue[] Boolies; // JSON.stringify({"lol": new Date("2018-09-02") - new Date("2018-09-01")}) returns "{"lol":86400000}", and so is indistinguishable from a number //public readonly JToken Timespan1; //public readonly JToken Timespan2; //public readonly JToken[] Timespans; - public readonly JToken DateYearMonth; - public readonly JToken DateYear; + public readonly JValue DateYearMonth; + public readonly JValue DateYear; // stringify only ever uses the ISO 8601 zulu date format, so let's just bother with that one. - public readonly JToken DateISO; - public readonly JToken OtherISODate; - public readonly JToken[] Dates; + public readonly JValue DateISO; + public readonly JValue OtherISODate; + public readonly JValue[] Dates; + + public readonly JValue[][] Errybody; + public readonly JValue[][] ErrybodyButNull; - public readonly JToken[][] Errybody; - public readonly JToken[][] ErrybodyButNull; public TestData() { var shebang = JObject.Parse("{\"null\":null,\"NaN\":null,\"true\":true,\"false\":false,\"two\":2,\"int\":1,\"float\":1.0,\"scifloat\":-1.3e+70,\"herp\":\"herp\",\"derp\":\"derp\",\"timespan\":86400000,\"dateYearMonth\":\"2018-09-01T00: 00:00.000Z\",\"dateYear\":\"2018-01-01T00: 00:00.000Z\",\"dateJSONAndISOZulu\":\"2018-09-20T20:38:59.463Z\", \"otherDate\": \"2018-09-20T20:41:14.821Z\"}"); - Null = shebang["null"]; - One = shebang["int"]; - OneDotZero = shebang["float"]; - Two = shebang["two"]; - Scientific = shebang["scifloat"]; - True = shebang["true"]; - False = shebang["false"]; - HerpString = shebang["herp"]; - DerpString = shebang["derp"]; - DateYearMonth = shebang["dateYearMonth"]; - DateYear = shebang["dateYear"]; - DateISO = shebang["dateJSONAndISOZulu"]; - OtherISODate = shebang["otherDate"]; + Null = (JValue)shebang["null"]; + One = (JValue)shebang["int"]; + OneDotZero = (JValue)shebang["float"]; + Two = (JValue)shebang["two"]; + Scientific = (JValue)shebang["scifloat"]; + True = (JValue)shebang["true"]; + False = (JValue)shebang["false"]; + HerpString = (JValue)shebang["herp"]; + DerpString = (JValue)shebang["derp"]; + DateYearMonth = (JValue)shebang["dateYearMonth"]; + DateYear = (JValue)shebang["dateYear"]; + DateISO = (JValue)shebang["dateJSONAndISOZulu"]; + OtherISODate = (JValue)shebang["otherDate"]; Dates = new[] { DateYearMonth, DateYear, DateISO, OtherISODate }; Boolies = new[] { True, False }; Strings = new[] @@ -175,5 +188,4 @@ namespace Newtonsoft.Json.Tests.Issues ErrybodyButNull = new[] { Numbers, Strings, Boolies, Dates }; } } - } diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs index 2a608bf3..db8461dc 100644 --- a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs +++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs @@ -35,6 +35,7 @@ using Newtonsoft.Json.Tests.Bson; using Xunit; using Test = Xunit.FactAttribute; using Assert = Newtonsoft.Json.Tests.XUnitAssert; +using TestCaseSource = Xunit.MemberDataAttribute; #else using NUnit.Framework; #endif @@ -1453,6 +1454,7 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath List result = rootObject.SelectTokens("$.dateObjectsArray[?(@.date == $.referenceDate)]").ToList(); Assert.AreEqual(2, result.Count); } + [Test] public void IdentityOperator() { @@ -1486,5 +1488,69 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath Assert.AreEqual("Number", mustBeNumber2.Single()); Assert.AreEqual("String", mustBeString2.Single()); } + + [Test] + public void Equals_FloatWithInt() + { + JToken t = JToken.Parse(@"{ + ""Values"": [ + { + ""Property"": 1 + } + ] +}"); + + Assert.IsNotNull(t.SelectToken(@"Values[?(@.Property == 1.0)]")); + } + +#if DNXCORE50 + [Theory] +#endif + [TestCaseSource(nameof(StrictMatchWithInverseTestData))] + public static void EqualsStrict(string value1, string value2, bool matchStrict) + { + string completeJson = @"{ + ""Values"": [ + { + ""Property"": " + value1 + @" + } + ] +}"; + string completeStrictPath = "$.Values[?(@.Property === " + value2 + ")]"; + + JToken t = JToken.Parse(completeJson); + + bool hasMatchStrict = t.SelectTokens(completeStrictPath).Any(); + Assert.AreEqual( + matchStrict, + hasMatchStrict, + $"Expected {value1} and {value2} to match: {matchStrict}" + + Environment.NewLine + completeJson + Environment.NewLine + completeStrictPath); + } + + public static IEnumerable StrictMatchWithInverseTestData() + { + foreach (var item in StrictMatchTestData()) + { + yield return new object[] { item[0], item[1], item[2] }; + yield return new object[] { item[1], item[0], item[2] }; + } + } + + private static IEnumerable StrictMatchTestData() + { + yield return new object[] { "1", "1", true }; + yield return new object[] { "1", "1.0", true }; + yield return new object[] { "1", "true", false }; + yield return new object[] { "1", "'1'", false }; + yield return new object[] { "'1'", "'1'", true }; + yield return new object[] { "false", "false", true }; + yield return new object[] { "true", "false", false }; + yield return new object[] { "1", "1.1", false }; + yield return new object[] { "1", "null", false }; + yield return new object[] { "null", "null", true }; + yield return new object[] { "null", "'null'", false }; + yield return new object[] { "null", "1", false }; + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs index c559ac42..a65bcbfb 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; +using System.Diagnostics; #if !HAVE_LINQ using Newtonsoft.Json.Utilities.LinqBridge; #else @@ -123,7 +124,7 @@ namespace Newtonsoft.Json.Linq.JsonPath return false; } - + private bool MatchTokens(JToken leftResult, JToken rightResult) { if (leftResult is JValue leftValue && rightResult is JValue rightValue) @@ -143,7 +144,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } break; case QueryOperator.StrictEquals: - if (EqualsWithoutStringCoercion(leftValue, rightValue)) + if (EqualsWithStrictMatch(leftValue, rightValue)) { return true; } @@ -155,7 +156,7 @@ namespace Newtonsoft.Json.Linq.JsonPath } break; case QueryOperator.StrictNotEquals: - if (!EqualsWithoutStringCoercion(leftValue, rightValue)) + if (!EqualsWithStrictMatch(leftValue, rightValue)) { return true; } @@ -219,13 +220,21 @@ namespace Newtonsoft.Json.Linq.JsonPath return Regex.IsMatch((string)input.Value, patternText, MiscellaneousUtils.GetRegexOptions(optionsText)); } - private bool EqualsWithStringCoercion(JValue value, JValue queryValue) + internal static bool EqualsWithStringCoercion(JValue value, JValue queryValue) { if (value.Equals(queryValue)) { return true; } + // Handle comparing an integer with a float + // e.g. Comparing 1 and 1.0 + if ((value.Type == JTokenType.Integer && queryValue.Type == JTokenType.Float) + || (value.Type == JTokenType.Float && queryValue.Type == JTokenType.Integer)) + { + return JValue.Compare(value.Type, value.Value, queryValue.Value) == 0; + } + if (queryValue.Type != JTokenType.String) { return false; @@ -272,65 +281,26 @@ namespace Newtonsoft.Json.Linq.JsonPath return string.Equals(currentValueString, queryValueString, StringComparison.Ordinal); } - private bool EqualsWithoutStringCoercion(JValue value, JValue queryValue) + internal static bool EqualsWithStrictMatch(JValue value, JValue queryValue) { - return IsStrictMatch(value, queryValue); - } - internal static bool IsStrictMatch(JToken value, JToken other) - { - // I've made this internal and static for testing purposes because I really don't get how to call the IsMatch method :/ - /* - * If Type(x) is different from Type(y), return false. - * If Type(x) is Undefined, return true. - * If Type(x) is Null, return true. - * If Type(x) is Number, then - * If x is NaN, return false. - * If y is NaN, return false. - * If x is the same Number value as y, return true. - * If x is +0 and y is −0, return true. - * If x is −0 and y is +0, return true. - * Return false. - * If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions); otherwise, return false. - * If Type(x) is Boolean, return true if x and y are both true or both false; otherwise, return false. - * Return true if x and y refer to the same object. Otherwise, return false. - */ - if(value == null) - throw new ArgumentNullException(nameof(value)); - if (other == null) - throw new ArgumentNullException(nameof(other)); + Debug.Assert(value != null); + Debug.Assert(queryValue != null); + + // Handle comparing an integer with a float + // e.g. Comparing 1 and 1.0 + if ((value.Type == JTokenType.Integer && queryValue.Type == JTokenType.Float) + || (value.Type == JTokenType.Float && queryValue.Type == JTokenType.Integer)) + { + return JValue.Compare(value.Type, value.Value, queryValue.Value) == 0; + } // we handle floats and integers the exact same way, so they are pseudo equivalent - if (value.Type != other.Type && - ((value.Type != JTokenType.Integer && value.Type != JTokenType.Float) || - (other.Type != JTokenType.Integer && other.Type != JTokenType.Float))) return false; - - switch (value.Type) + if (value.Type != queryValue.Type) { - case JTokenType.Null: - case JTokenType.Undefined: - return true; - case JTokenType.Integer: - case JTokenType.Float: - return value.Value() == other.Value(); - case JTokenType.String: - return string.Equals(value.Value(), other.Value(), StringComparison.Ordinal); - case JTokenType.Boolean: - return value.Value() == other.Value(); - case JTokenType.Date: - return value.Value() == other.Value(); - // How the heck could this happen? Ain't no guids in ecmascript. - case JTokenType.Guid: - return new Guid(value.Value()) == new Guid(other.Value()); - case JTokenType.TimeSpan: - return new TimeSpan(value.Value()) == new TimeSpan(other.Value()); - // unsure of Uri; it appears (from ad hoc testing) that javascript does not consider equivalent URLs to be equal no matter what - // new URL("http://lol.com") === new URL("http://lol.com") returns false always - // also, JSON.stringify renders {} for urls. JSON.stringify({lol: new URL("http://lol.com")}) renders "{"lol":{}}" - // so I don't know how we could get this kind of token type - case JTokenType.Uri: - default: - throw new InvalidOperationException($"Unexpected or unsupported JTokenType {value.Type}"); + return false; } + + return value.Equals(queryValue); } } } \ No newline at end of file