Strict operator support added
This commit is contained in:
committed by
James Newton-King
parent
700444a996
commit
3a9dfebc2d
@@ -0,0 +1,179 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq.JsonPath;
|
||||
using System;
|
||||
#if DNXCORE50
|
||||
using Xunit;
|
||||
using Test = Xunit.FactAttribute;
|
||||
using Assert = Newtonsoft.Json.Tests.XUnitAssert;
|
||||
#else
|
||||
using NUnit.Framework;
|
||||
#endif
|
||||
|
||||
namespace Newtonsoft.Json.Tests.Issues
|
||||
{
|
||||
/// <summary>
|
||||
/// Here's where we test the sandbox for needles and broken glass
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class EqualityTests
|
||||
{
|
||||
|
||||
[Test]
|
||||
public void AllStrictEqualityTests()
|
||||
{
|
||||
// this is a bit cargo-culty; making absolutely sure no false positives caused by instance equivalence
|
||||
var lhs = new TestData();
|
||||
var rhs = new TestData();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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));
|
||||
target = lhs.Scientific;
|
||||
Assert.IsTrue(BooleanQueryExpression.IsStrictMatch(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);
|
||||
|
||||
// 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);
|
||||
target = lhs.False;
|
||||
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);
|
||||
target = lhs.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));
|
||||
}
|
||||
#region helpers
|
||||
// used by asserters to perform the comparison
|
||||
public delegate bool Comparator(JToken lhs, JToken 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);
|
||||
// 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)
|
||||
{
|
||||
foreach(var group in doNotWant)
|
||||
AssertNone(comparator, token, group);
|
||||
}
|
||||
private void AssertNone(Comparator comparator, JToken token, params JToken[] doNotWant)
|
||||
{
|
||||
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)
|
||||
{
|
||||
foreach(var group in want)
|
||||
AssertAll(comparator, token, group);
|
||||
}
|
||||
private void AssertAll(Comparator comparator, JToken token, params JToken[] want)
|
||||
{
|
||||
foreach(var item in want)
|
||||
Assert.IsTrue(comparator(token, item));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds (practically) all the different possible javascript types and variants of possible values gathered from the algorithm and (imperfect) observation
|
||||
/// </summary>
|
||||
public class TestData
|
||||
{
|
||||
public readonly JToken Null;
|
||||
//JSON.stringify({"undef": undefined}) returns {}
|
||||
//public readonly JToken Undefined;
|
||||
public readonly JToken[] Nopes;
|
||||
|
||||
public readonly JToken One;
|
||||
public readonly JToken OneDotZero;
|
||||
public readonly JToken Two;
|
||||
public readonly JToken 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 JToken HerpString;
|
||||
public readonly JToken DerpString;
|
||||
public readonly JToken[] Strings;
|
||||
|
||||
public readonly JToken True;
|
||||
public readonly JToken False;
|
||||
public readonly JToken[] 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;
|
||||
// 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 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"];
|
||||
Dates = new[] { DateYearMonth, DateYear, DateISO, OtherISODate };
|
||||
Boolies = new[] { True, False };
|
||||
Strings = new[]
|
||||
{
|
||||
HerpString,
|
||||
DerpString
|
||||
};
|
||||
Numbers = new[]
|
||||
{
|
||||
One,
|
||||
OneDotZero,
|
||||
Two,
|
||||
Scientific
|
||||
};
|
||||
Nopes = new[]
|
||||
{
|
||||
Null
|
||||
};
|
||||
Errybody = new[] { Nopes, Numbers, Strings, Boolies, Dates };
|
||||
ErrybodyButNull = new[] { Numbers, Strings, Boolies, Dates };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1453,5 +1453,38 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath
|
||||
List<JToken> result = rootObject.SelectTokens("$.dateObjectsArray[?(@.date == $.referenceDate)]").ToList();
|
||||
Assert.AreEqual(2, result.Count);
|
||||
}
|
||||
[Test]
|
||||
public void IdentityOperator()
|
||||
{
|
||||
JObject o = JObject.Parse(@"{
|
||||
'Values': [{
|
||||
|
||||
'Coercible': 1,
|
||||
'Name': 'Number'
|
||||
|
||||
}, {
|
||||
'Coercible': '1',
|
||||
'Name': 'String'
|
||||
}]
|
||||
}");
|
||||
|
||||
// just to verify expected behavior hasn't changed
|
||||
IEnumerable<string> sanity1 = o.SelectTokens("Values[?(@.Coercible == '1')].Name").Select(x => (string)x);
|
||||
IEnumerable<string> sanity2 = o.SelectTokens("Values[?(@.Coercible != '1')].Name").Select(x => (string)x);
|
||||
// new behavior
|
||||
IEnumerable<string> mustBeNumber1 = o.SelectTokens("Values[?(@.Coercible === 1)].Name").Select(x => (string)x);
|
||||
IEnumerable<string> mustBeString1 = o.SelectTokens("Values[?(@.Coercible !== 1)].Name").Select(x => (string)x);
|
||||
IEnumerable<string> mustBeString2 = o.SelectTokens("Values[?(@.Coercible === '1')].Name").Select(x => (string)x);
|
||||
IEnumerable<string> mustBeNumber2 = o.SelectTokens("Values[?(@.Coercible !== '1')].Name").Select(x => (string)x);
|
||||
|
||||
// FAILS-- JPath returns { "String" }
|
||||
//CollectionAssert.AreEquivalent(new[] { "Number", "String" }, sanity1);
|
||||
// FAILS-- JPath returns { "Number" }
|
||||
//Assert.IsTrue(!sanity2.Any());
|
||||
Assert.AreEqual("Number", mustBeNumber1.Single());
|
||||
Assert.AreEqual("String", mustBeString1.Single());
|
||||
Assert.AreEqual("Number", mustBeNumber2.Single());
|
||||
Assert.AreEqual("String", mustBeString2.Single());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -786,6 +786,11 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
throw new JsonException("Path ended with open query.");
|
||||
}
|
||||
|
||||
if (Match("==="))
|
||||
{
|
||||
return QueryOperator.StrictEquals;
|
||||
}
|
||||
|
||||
if (Match("=="))
|
||||
{
|
||||
return QueryOperator.Equals;
|
||||
@@ -796,6 +801,11 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
return QueryOperator.RegexEquals;
|
||||
}
|
||||
|
||||
if (Match("!=="))
|
||||
{
|
||||
return QueryOperator.StrictNotEquals;
|
||||
}
|
||||
|
||||
if (Match("!=") || Match("<>"))
|
||||
{
|
||||
return QueryOperator.NotEquals;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -24,7 +24,9 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
GreaterThanOrEquals = 7,
|
||||
And = 8,
|
||||
Or = 9,
|
||||
RegexEquals = 10
|
||||
RegexEquals = 10,
|
||||
StrictEquals = 11,
|
||||
StrictNotEquals = 12
|
||||
}
|
||||
|
||||
internal abstract class QueryExpression
|
||||
@@ -121,7 +123,7 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private bool MatchTokens(JToken leftResult, JToken rightResult)
|
||||
{
|
||||
if (leftResult is JValue leftValue && rightResult is JValue rightValue)
|
||||
@@ -140,12 +142,24 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case QueryOperator.StrictEquals:
|
||||
if (EqualsWithoutStringCoercion(leftValue, rightValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case QueryOperator.NotEquals:
|
||||
if (!EqualsWithStringCoercion(leftValue, rightValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case QueryOperator.StrictNotEquals:
|
||||
if (!EqualsWithoutStringCoercion(leftValue, rightValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case QueryOperator.GreaterThan:
|
||||
if (leftValue.CompareTo(rightValue) > 0)
|
||||
{
|
||||
@@ -257,5 +271,66 @@ namespace Newtonsoft.Json.Linq.JsonPath
|
||||
|
||||
return string.Equals(currentValueString, queryValueString, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool EqualsWithoutStringCoercion(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));
|
||||
|
||||
// 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)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user