diff --git a/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs b/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs
new file mode 100644
index 00000000..f0ec4cb0
--- /dev/null
+++ b/Src/Newtonsoft.Json.Tests/Issues/Issue1837 .cs
@@ -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
+{
+ ///
+ /// Here's where we test the sandbox for needles and broken glass
+ ///
+ [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
+ }
+
+ ///
+ /// Holds (practically) all the different possible javascript types and variants of possible values gathered from the algorithm and (imperfect) observation
+ ///
+ 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 };
+ }
+ }
+
+}
diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs
index 43fb0a97..2a608bf3 100644
--- a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs
+++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs
@@ -1453,5 +1453,38 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath
List 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 sanity1 = o.SelectTokens("Values[?(@.Coercible == '1')].Name").Select(x => (string)x);
+ IEnumerable sanity2 = o.SelectTokens("Values[?(@.Coercible != '1')].Name").Select(x => (string)x);
+ // new behavior
+ IEnumerable mustBeNumber1 = o.SelectTokens("Values[?(@.Coercible === 1)].Name").Select(x => (string)x);
+ IEnumerable mustBeString1 = o.SelectTokens("Values[?(@.Coercible !== 1)].Name").Select(x => (string)x);
+ IEnumerable mustBeString2 = o.SelectTokens("Values[?(@.Coercible === '1')].Name").Select(x => (string)x);
+ IEnumerable 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());
+ }
}
}
\ No newline at end of file
diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs
index 792aa62c..b61ec4c7 100644
--- a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs
+++ b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs
@@ -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;
diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs
index 7cfcf3f3..c559ac42 100644
--- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs
+++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryExpression.cs
@@ -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() == 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}");
+ }
+ }
}
}
\ No newline at end of file