diff --git a/PredicateKit/Predicate.swift b/PredicateKit/Predicate.swift index 610eb98..7617284 100644 --- a/PredicateKit/Predicate.swift +++ b/PredicateKit/Predicate.swift @@ -383,7 +383,6 @@ public func <= (lhs: E, rhs: T) -> Predicat .comparison(.init(lhs, .lessThanOrEqual, rhs.rawValue)) } - public func == (lhs: E, rhs: T) -> Predicate where E.Value == T { .comparison(.init(lhs, .equal, rhs)) } @@ -406,6 +405,20 @@ public func != (lhs: E, rhs: T) -> Pre .comparison(.init(lhs, .notEqual, rhs)) } +public func != (lhs: E, rhs: T) -> Predicate where E.Value == T, T.RawValue: Equatable & Primitive { + .comparison(.init(lhs, .notEqual, rhs.rawValue)) +} + +@available(iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public func != (lhs: E, rhs: T) -> Predicate where E.Value == T, T.ID: Primitive { + .comparison(.init(ObjectIdentifier(root: lhs), .notEqual, rhs.id)) +} + +@_disfavoredOverload +public func != (lhs: E, rhs: Nil) -> Predicate where E.Value: OptionalType { + .comparison(.init(lhs, .notEqual, rhs)) +} + public func >= (lhs: E, rhs: T) -> Predicate where E.Value == T { .comparison(.init(lhs, .greaterThanOrEqual, rhs)) } diff --git a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift index b3dc8b7..178fbf2 100644 --- a/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift +++ b/PredicateKitTests/CoreDataTests/NSFetchRequestBuilderTests.swift @@ -283,6 +283,61 @@ final class NSFetchRequestBuilderTests: XCTestCase { XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) } + @available(iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func testNotEqualWithIdentifiable() throws { + guard let identifiable = makeIdentifiable() else { + XCTFail("could not initialize IdentifiableData") + return + } + + identifiable.id = "42" + + let request = makeRequest(\Data.identifiable != identifiable) + let builder = makeRequestBuilder() + + let result: NSFetchRequest = builder.makeRequest(from: request) + + let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate) + XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "identifiable.id")) + XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: "42")) + XCTAssertEqual(comparison.predicateOperatorType, .notEqualTo) + XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) + } + + @available(iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func testNotEqualWithOptionalIdentifiable() throws { + guard let identifiable = makeIdentifiable() else { + XCTFail("could not initialize IdentifiableData") + return + } + + identifiable.id = "42" + + let request = makeRequest(\Data.optionalIdentifiable != identifiable) + let builder = makeRequestBuilder() + + let result: NSFetchRequest = builder.makeRequest(from: request) + + let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate) + XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "optionalIdentifiable.id")) + XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: "42")) + XCTAssertEqual(comparison.predicateOperatorType, .notEqualTo) + XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) + } + + func testNotEqualWithRawRepresentable() throws { + let request = makeRequest(\Data.dataType != .two) + let builder = makeRequestBuilder() + + let result: NSFetchRequest = builder.makeRequest(from: request) + + let comparison = try XCTUnwrap(result.predicate as? NSComparisonPredicate) + XCTAssertEqual(comparison.leftExpression, NSExpression(forKeyPath: "dataType")) + XCTAssertEqual(comparison.rightExpression, NSExpression(forConstantValue: DataType.two.rawValue)) + XCTAssertEqual(comparison.predicateOperatorType, .notEqualTo) + XCTAssertEqual(comparison.comparisonPredicateModifier, .direct) + } + func testArrayElementNotEqualPredicate() throws { let request = makeRequest((\Data.relationships).last(\.count) != 42) let builder = makeRequestBuilder() diff --git a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift index 3535524..a29dce1 100644 --- a/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift +++ b/PredicateKitTests/CoreDataTests/NSManagedObjectContextExtensionsTests.swift @@ -137,6 +137,27 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertEqual(notes.first?.numberOfViews, 42) } + @available(iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func testFetchWithObjectComparison2() throws { + let attachment1 = try container.viewContext.insertAttachment("1") + let attachment2 = try container.viewContext.insertAttachment("2") + + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], attachment: attachment1 ), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"], attachment: attachment2 ), + (text: "See ya!", creationDate: Date(), numberOfViews: 3, tags: ["greeting"], attachment: attachment2 ) + ) + + let notes: [Note] = try container.viewContext + .fetch(where: \Note.attachment != attachment2) + .result() + + XCTAssertEqual(notes.count, 1) + XCTAssertEqual(notes.first?.text, "Hello, World!") + XCTAssertEqual(notes.first?.tags, ["greeting"]) + XCTAssertEqual(notes.first?.numberOfViews, 42) + } + func testFetchWithEnumComparison() throws { try container.viewContext.insertNotes( (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], type: .freeForm), @@ -153,6 +174,22 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertEqual(notes.first?.numberOfViews, 42) } + func testFetchWithEnumComparison2() throws { + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"], type: .freeForm), + (text: "Goodbye!", creationDate: Date(), numberOfViews: 122, tags: ["greeting"], type: .structured) + ) + + let notes: [Note] = try container.viewContext + .fetch(where: \Note.type != .structured) + .result() + + XCTAssertEqual(notes.count, 1) + XCTAssertEqual(notes.first?.text, "Hello, World!") + XCTAssertEqual(notes.first?.tags, ["greeting"]) + XCTAssertEqual(notes.first?.numberOfViews, 42) + } + func testFetchAll() throws { try container.viewContext.insertNotes( (text: "Hello, World!", creationDate: Date(), numberOfViews: 42, tags: ["greeting"]), @@ -684,7 +721,25 @@ final class NSManagedObjectContextExtensionsTests: XCTestCase { XCTAssertEqual(notes.first?.numberOfViews, 3) } - func testFetchWithArrayNilEqualityNilEquality() throws { + func testFetchWithNilInequality() throws { + let now = Date() + + try container.viewContext.insertNotes( + (text: "Hello, World!", creationDate: .distantFuture, updateDate: now, numberOfViews: 42, tags: ["greeting"]), + (text: "Goodbye!", creationDate: .distantPast, updateDate: nil, numberOfViews: 3, tags: ["greeting"]) + ) + + let notes: [Note] = try container.viewContext + .fetch(where: \Note.updateDate != nil) + .result() + + XCTAssertEqual(notes.count, 1) + XCTAssertEqual(notes.first?.text, "Hello, World!") + XCTAssertEqual(notes.first?.tags, ["greeting"]) + XCTAssertEqual(notes.first?.numberOfViews, 42) + } + + func testFetchWithArrayNilEquality() throws { try container.viewContext.insertUsers( (name: "John Doe", billingAccountType: "Pro", purchases: [35.0, 120.0]), (name: "Jane Doe", billingAccountType: "Default", purchases: nil) diff --git a/PredicateKitTests/OperatorTests.swift b/PredicateKitTests/OperatorTests.swift index a81ca54..70fcc1a 100644 --- a/PredicateKitTests/OperatorTests.swift +++ b/PredicateKitTests/OperatorTests.swift @@ -268,6 +268,37 @@ final class OperatorTests: XCTestCase { XCTAssertEqual(value, "1") } + func testKeyPathEqualRawRepresentable() throws { + struct Data { + let rawRepresentable: RawRepresentableValue + } + + enum RawRepresentableValue: Int { + case zero + case one + } + + let predicate = \Data.rawRepresentable == .zero + + guard case let .comparison(comparison) = predicate else { + XCTFail("rawRepresentable == .zero should result in a comparison") + return + } + + guard + let expression = comparison.expression.as(KeyPath.self) + else { + XCTFail("the left side of the comparison should be a key path") + return + } + + let value = try XCTUnwrap(comparison.value as? RawRepresentableValue.RawValue) + + XCTAssertEqual(expression, \Data.rawRepresentable) + XCTAssertEqual(comparison.operator, .equal) + XCTAssertEqual(value, 0) + } + func testOptionalKeyPathEqualToNil() throws { let predicate: Predicate = \Data.optionalRelationship == nil @@ -472,6 +503,104 @@ final class OperatorTests: XCTestCase { XCTAssertEqual(value, 5) } + @available(iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func testKeyPathNotEqualIdentifiable() throws { + struct Data { + let identifiable: IdentifiableData + } + + struct IdentifiableData: Identifiable, Equatable { + let id: String + } + + let predicate = \Data.identifiable != IdentifiableData(id: "1") + + guard case let .comparison(comparison) = predicate else { + XCTFail("identifiable.id != 1 should result in a comparison") + return + } + + guard + let expression = comparison.expression.as(ObjectIdentifier, String>.self) + else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + let value = try XCTUnwrap(comparison.value as? IdentifiableData.ID) + + XCTAssertEqual(expression.root, \Data.identifiable) + XCTAssertEqual(comparison.operator, .notEqual) + XCTAssertEqual(value, "1") + } + + func testKeyPathNotEqualRawRepresentable() throws { + struct Data { + let rawRepresentable: RawRepresentableValue + } + + enum RawRepresentableValue: Int { + case zero + case one + } + + let predicate = \Data.rawRepresentable != .zero + + guard case let .comparison(comparison) = predicate else { + XCTFail("rawRepresentable != .zero should result in a comparison") + return + } + + guard + let expression = comparison.expression.as(KeyPath.self) + else { + XCTFail("the left side of the comparison should be a key path") + return + } + + let value = try XCTUnwrap(comparison.value as? RawRepresentableValue.RawValue) + + XCTAssertEqual(expression, \Data.rawRepresentable) + XCTAssertEqual(comparison.operator, .notEqual) + XCTAssertEqual(value, 0) + } + + func testOptionalKeyPathNotEqualToNil() throws { + let predicate: Predicate = \Data.optionalRelationship != nil + + guard case let .comparison(comparison) = predicate else { + XCTFail("optionalRelationship != nil should result in a comparison") + return + } + + guard let keyPath = comparison.expression.as(KeyPath.self) else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + XCTAssertEqual(keyPath, \Data.optionalRelationship) + XCTAssertEqual(comparison.operator, .notEqual) + XCTAssertNotNil(comparison.value as? Nil) + } + + func testOptionalArrayKeyPathNotEqualToNil() throws { + let predicate: Predicate = \Data.optionalRelationships != nil + + guard case let .comparison(comparison) = predicate else { + XCTFail("optionalRelationships != nil should result in a comparison") + return + } + + guard let keyPath = comparison.expression.as(KeyPath.self) else { + XCTFail("the left side of the comparison should be a key path expression") + return + } + + XCTAssertEqual(keyPath, \Data.optionalRelationships) + XCTAssertEqual(comparison.operator, .notEqual) + XCTAssertNotNil(comparison.value as? Nil) + } + // MARK: - >= func testKeyPathGreaterThanOrEqualPrimitive() throws {