Simplify strict equals

This commit is contained in:
James Newton-King
2018-10-07 16:22:13 +13:00
parent 3a9dfebc2d
commit 416a96bf4e
3 changed files with 170 additions and 122 deletions
+76 -64
View File
@@ -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);
}
}
}