Simplify strict equals
This commit is contained in:
@@ -15,9 +15,8 @@ namespace Newtonsoft.Json.Tests.Issues
|
||||
/// Here's where we test the sandbox for needles and broken glass
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<JToken> 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<object[]> 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<object[]> 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<float>() == other.Value<float>();
|
||||
case JTokenType.String:
|
||||
return string.Equals(value.Value<string>(), other.Value<string>(), StringComparison.Ordinal);
|
||||
case JTokenType.Boolean:
|
||||
return value.Value<bool>() == other.Value<bool>();
|
||||
case JTokenType.Date:
|
||||
return value.Value<DateTime>() == other.Value<DateTime>();
|
||||
// How the heck could this happen? Ain't no guids in ecmascript.
|
||||
case JTokenType.Guid:
|
||||
return new Guid(value.Value<string>()) == new Guid(other.Value<string>());
|
||||
case JTokenType.TimeSpan:
|
||||
return new TimeSpan(value.Value<long>()) == new TimeSpan(other.Value<long>());
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user