簡要理解 - NSPredicate

JABread發表於2017-12-25

簡介

NSPredicate是基礎庫中用來過濾獲取資料的類,類似於SQL中的where語句,但它提供了更為自然且高階的語言,在資料集合的查詢上去定義了邏輯條件語句。

直接展示NSPredicate的用法可能比抽象的講解更容易理解,但在這之前,我們先來學習其基礎的語法。

基礎語法

  • Parser Basics(謂語字串的基本解析)

    謂詞格式字串對關鍵字、括號敏感,對空格不明感,且不會進行語義型別檢測。

    $ :以$符號開頭宣告變數,例如 $VARIABLE_NAME

    %K :屬性名

    %@ :屬性值

    使用%@佔位符只能表示一些表示式,而不能去表示一個謂詞,否則會造成程式異常。

  • Basic Comparisons(基本比較運算)

    =、== : 判斷左邊表示式是否等於右邊表示式。

    >=、 => : 判斷左邊表示式是否大於或等於右邊表示式。

    <=、=< : 判斷左邊表示式是否小於或等於右邊表示式。

    > : 判斷左邊表示式是否大於右邊表示式。

    < : 判斷左邊表示式是否小於右邊表示式。

    !=、<> : 判斷左邊表示式是否不等於右邊表示式。

    BETWEEN : 判斷值是否在區間內,形如 $INPUT BETWEEN { $LOWER, $UPPER }

  • Boolean Value Predicates(Boolen謂語)

    TRUEPREDICATE : 始終返回為真的謂詞。

    FALSEPREDICATE : 始終返回為假的謂詞。

  • Basic Compound Predicates(基本複合謂語)

    AND、&& : 邏輯與。

    OR、|| : 邏輯或。

    NOT、! : 邏輯非。

  • String Comparisons(字串比較)

    字串預設對字母大小寫和發音敏感,但我們可以手動對其進行關閉。例如 firstName BEGINSWITH[cd] $FIRST_NAME。c、d分別對應關閉對字母大小寫與發音的敏感。

    BEGINSWITH : 判斷左邊字串內容是否以右邊字串開始。

    CONTAINS : 判斷字串內容是否包含右邊字串。

    ENDSWITH : 判斷字串內容是否以右邊字串結束。

    LIKE : 判斷左邊字串是否等於右邊字串。* 通常在這裡被用作萬用字元。

    : 表示匹配一個字元。

    * : 表示匹配0個或多個字元。

    注意:與萬用字元 ?* 的組合必須是以字串的形式表示。

    MATCHES : 通過正規表示式判斷左右兩邊表示式是否相等。

    UTI-CONFORMS-TOUTI-EQUALS : 這兩個屬於macOS開發部分,這裡不討論。

  • Aggregate Operations(集合操作)

    ANY、SOME : 指定滿足後面表示式的一些/部分元素。例如ANY children.age < 18

    ALL : 指定滿足後面表示式的所有元素。例如ALL children.age < 18

    NONE : 指定不滿足後面表示式的元素。例如NONE children.age < 18。等同於NOT (ANY ...)

    IN : 等同於SQL的IN操作。判斷左邊元素集合是否在右邊元素集合出現。例如name IN { 'Ben', 'Melissa', 'Nick' }。這些集合可以是array、set、或者是dictionary。如果是dictionary,將取其values值。

    array[index] : 指定陣列array中指定索引代表的元素。

    array[FIRST] : 指定陣列array的第一個元素。

    array[LAST] : 指定陣列array的最後一個元素。

    array[SIZE] : 指定陣列array的大小。

  • Identifiers(識別符號)

    所有C風格的識別符號都不被保留。

    #symbol : 將保留字轉義為使用者識別符號。

    [\]{octaldigit}{3} : 轉義8進位制數字。(\後跟上3個八進位制數字)

    [\][xX]{hexdigit}{2} : 轉義16進位制數字。(\x/\X後跟上2十六進位制數字)

    [\][uU]{hexdigit}{4} : 轉義Unicode。(\u/\U後跟上4個十六進位制數字)

  • Literals(字面量語義)

    單引號與雙引號有著同樣的效果,但是他們不能使對方結束。例如 "abc" 與 `abc` 是等價的,但是 "a`b`c" 卻等同於 a + space + 'b' + space + c.

    FALSE、NO : 邏輯假。

    TRUE、YES : 邏輯真。

    NULL、NIL : 表示null值。

    SELF : 表示被操作的物件。

    "text" : 表示一個字串。

    `text` : 表示一個字串。

  • Reserved Words(保留字)

    AND, OR, IN, NOT, ALL, ANY, SOME, NONE, LIKE, CASEINSENSITIVE, CI, MATCHES, CONTAINS, BEGINSWITH, ENDSWITH, BETWEEN, NULL, NIL, SELF, TRUE, YES, FALSE, NO, FIRST, LAST, SIZE, ANYKEY, SUBQUERY, FETCH, CAST, TRUEPREDICATE, FALSEPREDICATE, UTI-CONFORMS-TO, UTI-EQUALS

基礎用法

Creating Predicate(建立謂詞)

  1. Creating a Predicate Using a Format String

    通常我們使用NSPredicate的類方法+ (NSPredicate *)predicateWithFormat:(NSString *)predicateFormat, ...;來定義一個謂詞,因為編譯器是不會對字串進行語義型別的檢測,導致錯誤在編譯時無法被察覺,所以我們必須嚴格進行編寫,否則在替換變數的一些情況下會造成執行時錯誤。

  • String Constants, Variables, and Wildcards(字串常量/變數,萬用字元)

    在謂詞格式字串中插入字串常量,需要用到成對的單引號或者雙引號且使用轉義字元\,例如NSPredicate *predicate = [NSPredicate predicateWithFormat:@"lastName like[c] \"S*\""];

    在謂詞格式字串中插入佔位符%@,那麼%@表示的內容會被自動的新增上引號。這裡要特別注意的是%@不能與萬用字元*或者?直接使用,而必須使用拼接的方式。例如

    NSString *prefix = @"prefix";
    NSString *suffix = @"suffix";
    NSPredicate *predicate = [NSPredicate
    predicateWithFormat:@"SELF like[c] %@",
    [[prefix stringByAppendingString:@"*"] stringByAppendingString:suffix]];
    BOOL ok = [predicate evaluateWithObject:@"prefixxxxxxsuffix"];
    複製程式碼

    在謂詞格式字串中插入變數,例如

    predicate = [NSPredicate
    predicateWithFormat:@"lastName like[c] $LAST_NAME"];
    複製程式碼
  • Boolean Values(Boolean值)

    指定和測試Boolean值的相等性。例如

    NSPredicate *newPredicate =
    [NSPredicate predicateWithFormat:@"anAttribute == %@", [NSNumber numberWithBool:aBool]];
    NSPredicate *testForTrue =
    [NSPredicate predicateWithFormat:@"anAttribute == YES"];
    複製程式碼
  • Dynamic Property Names(動態屬性名)

    我們不能使用%@表示屬性名,因為使用%@佔位符表示後的值會被引號包裹成字串變數。例如

    NSString *attributeName = @"firstName";
    NSString *attributeValue = @"Adam";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%@ like %@",
    attributeName, attributeValue];
    複製程式碼

    該謂詞格式字串是 "firstName" LIKE "Adam"

    如果要指定一個動態屬性名,則需要用到%K。例如

    predicate = [NSPredicate predicateWithFormat:@"%K like %@",
        attributeName, attributeValue];
    複製程式碼

    該謂語格式字串則變成我們想要的 firstName LIKE "Adam"

  1. Creating Predicates Directly in Code(用程式碼定義一個謂詞)

    下面是一段表示(revenue >= 1000000) and (revenue < 100000000)的程式碼例子。

    NSExpression *lhs = [NSExpression expressionForKeyPath:@"revenue"];
    
    NSExpression *greaterThanRhs = [NSExpression expressionForConstantValue:[NSNumber numberWithInt:1000000]];
    NSPredicate *greaterThanPredicate = [NSComparisonPredicate
                                         predicateWithLeftExpression:lhs
                                         rightExpression:greaterThanRhs
                                         modifier:NSDirectPredicateModifier
                                         type:NSGreaterThanOrEqualToPredicateOperatorType
                                         options:0];
    
    NSExpression *lessThanRhs = [NSExpression expressionForConstantValue:[NSNumber numberWithInt:100000000]];
    NSPredicate *lessThanPredicate = [NSComparisonPredicate
                                      predicateWithLeftExpression:lhs
                                      rightExpression:lessThanRhs
                                      modifier:NSDirectPredicateModifier
                                      type:NSLessThanPredicateOperatorType
                                      options:0];
    
    NSCompoundPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
                                      @[greaterThanPredicate, lessThanPredicate]];
    複製程式碼
  2. Creating Predicates Using Predicate Templates(使用謂詞模板定義謂詞)

    這是對通過硬編碼定義謂詞易造成程式出錯與通過程式碼定義謂詞帶來的繁瑣之間折中之後的方法。例如

    NSPredicate *predicateTemplate = [NSPredicate
    predicateWithFormat:@"lastName like[c] \$LAST_NAME"];
    複製程式碼

    將上述定義成謂詞模板為

    NSExpression *lhs = [NSExpression expressionForKeyPath:@"lastName"];
    
    NSExpression *rhs = [NSExpression expressionForVariable:@"LAST_NAME"];
    
    NSPredicate *predicateTemplate = [NSComparisonPredicate
                                      predicateWithLeftExpression:lhs
                                      rightExpression:rhs
                                      modifier:NSDirectPredicateModifier
                                      type:NSLikePredicateOperatorType
                                      options:NSCaseInsensitivePredicateOption];
    複製程式碼

    這時我們可以這樣來使用它

    NSPredicate *predicate = [predicateTemplate predicateWithSubstitutionVariables:
    [NSDictionary dictionaryWithObject:@"Turner" forKey:@"LAST_NAME"]];
    複製程式碼

    那麼現在這個新的謂詞將變成lastName LIKE[c] "Turner"

    使用dictionary進行替換的前提是dictionary中必須包含謂詞所指定變數的鍵值對,所以當我們想要匹配一個null值時,我們必須在dictionary中提供一個null值,例如

    NSPredicate *predicate = [NSPredicate
    predicateWithFormat:@"date = $DATE"];
    predicate = [predicate predicateWithSubstitutionVariables:
    [NSDictionary dictionaryWithObject:[NSNull null] forKey:@"DATE"]];
    複製程式碼

    這時謂詞變成date == <null>

    利用程式碼直接定義一個謂詞其實就是系統幫我們將前面學習的基礎語法轉換成列舉供我們選擇進行建立,避免發生硬編碼錯誤。例如options

    typedef NS_OPTIONS(NSUInteger, NSComparisonPredicateOptions) {
        NSCaseInsensitivePredicateOption, // 字母大小寫不敏感,即[c]
        NSDiacriticInsensitivePredicateOption, // 發音不敏感,即[d]
        NSNormalizedPredicateOption, // 即[cd],且系統會對該選項進行效能優化。
    };
    複製程式碼

    當我們需要組合幾個謂詞時,使用NSPredicate的子類NSCompoundPredicate會更加方便。

  3. Format String Summary(格式字串小結)

    被引號包裹的%@%K$VARIABLE會被解釋成字串,因而會阻止任何替換的行為。

    • @"attributeName == %@" : 該謂詞會檢查屬性名attributeNamed的值是否會等於%@所指代的值,可以是NSDate、NSNumber、NSString等。

    • @"%K == %@" : 該謂詞會檢查鍵%K的值是否等於%@的值。

    • @"name IN $NAME_LIST" : 該謂詞模板會檢查鍵name是否出現在變數$NAME_LIST中。

    • @"`name` IN $NAME_LIST" : 該謂詞模板會檢查字串常量`name`是否出現在變數$NAME_LIST中。

    • @"$name IN $NAME_LIST" : 該謂詞模板會檢查變數$name是否出現在變數$NAME_LIST中。

    • @"%K == `%@`" : 該謂詞會檢查%K的值是否等於字串%@的值。

Using Predicate(使用謂詞)

  1. Evaluating Predicates(執行謂詞)

    這一個簡單的例子

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF     IN %@", @[@"Stig", @"Shaffiq", @"Chris"]];
    BOOL result = [predicate evaluateWithObject:@"Shaffiq"];
    複製程式碼

    注意: 只有支援KVC的類才能使用謂詞。

  2. Using Predicates with Arrays(在集合中使用謂詞)

    陣列與可變陣列都支援過濾陣列元素的操作。但它們是有區別的。

    • NSArray: 使用filteredArrayUsingPredicate:方法將過濾獲取的元素通過一個新的陣列返回。
    • NSMutableArray: 使用filterUsingPredicate:方法操作的物件是原陣列,只有符合謂詞要求的元素才會被保留下來。

    例如

    NSMutableArray *names = [@[@"Nick", @"Ben", @"Adam", @"Melissa"] mutableCopy];
    
    NSPredicate *bPredicate = [NSPredicate predicateWithFormat:@"SELF beginswith[c] 'b'"];
    NSArray *beginWithB = [names filteredArrayUsingPredicate:bPredicate];
    // beginWithB contains { @"Ben" }.
    
    NSPredicate *ePredicate = [NSPredicate predicateWithFormat:@"SELF contains[c] 'e'"];
    [names filterUsingPredicate:ePredicate];
    // names now contains { @"Ben", @"Melissa" }
    複製程式碼
  3. Using Predicates with Key-Paths(通過鍵路徑使用謂詞)

    例如

    NSString *departmentName = ... ;
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"department.name like %@", departmentName];
    複製程式碼

    如果是一對多關係,謂詞結構會有些許不同。如果想要獲取名字的first name是"Matthew"的所有員工的公寓,我們可以使用ANY:

    NSPredicate *predicate = [NSPredicate predicateWithFormat:
    @"ANY employees.firstName like 'Matthew'"];
    複製程式碼

    如果我們想要知道員工工資大於一定值的員工所在的是哪些部門:

    float salary = ... ;
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ANY employees.salary > %f", salary];
    複製程式碼

應用場景

  • 測試程式碼
        // MARK: - 字串
        let str = "hello holy! it's so cold today!"
        let p01 = NSPredicate(format: "SELF CONTAINS 'cold'")
        if p01.evaluate(with: str) {
            print("p01: ")
        }

        let p02 = NSPredicate(format: "SELF LIKE[c] 'hello'")
        if p02.evaluate(with: str) {
            print("p02: ")
        }

        let p03 = NSPredicate(format: "SELF LIKE[c] '*ello'")
        if p03.evaluate(with: str) {
            print("p03: ")
        }

        let p04 = NSPredicate(format: "SELF LIKE[c] '?ello'")
        if p04.evaluate(with: str) {
            print("p04: ")
        }

        let p05 = NSPredicate(format: "SELF LIKE '?Ello*'")
        if p05.evaluate(with: str) {
            print("p05: ")
        }

        let p06 = NSPredicate(format: "SELF LIKE[c] 'hello*!'")
        if p06.evaluate(with: str) {
            print("p06: ")
        }
        let p07 = NSPredicate(format: "SELF IN %@", str)
        if p07.evaluate(with: "hello") {
            print("p07: ")
        }

        // MARK: - 集合
        let alice = Person(firstName: "Alice", lastName: "Smith", age: 24, departmentName: "A")
        let bob = Person(firstName: "Bob", lastName: "Jones", age: 13, departmentName: "B")
        let charlie = Person(firstName: "Charlie", lastName: "Smith", age: 20, departmentName: "A")
        let quentin = Person(firstName: "Quentin", lastName: "Alberts", age: 20, departmentName: "C")
        let jack = Person(firstName: "Jack", lastName: "J", age: 18, departmentName: "C")
        let people: NSMutableArray = [alice, bob, charlie, quentin, jack]
        self.people = people

        // 1. 查詢lastName為Smith的人
        let p1 = NSPredicate(format: "lastName = 'Smith'")
        let arr1 = people.filtered(using: p1)
        print("arr1: \(arr1)");
        // 2. 查詢firstName為某變數的人
        let p2 = NSPredicate(format: "firstName = %@", "Bob")
        let arr2 = people.filtered(using: p2)
        print("arr2: \(arr2)")
        // 3. 查詢age >= 18的人
        let p3 = NSPredicate(format: "age >= 18")
        let arr3 = people.filtered(using: p3)
        print("arr3: \(arr3)")
        // 4. 使用可數陣列`filter`方法修改原陣列
//        let p4 = NSPredicate(format: "age = 18")
//        people.filter(using: p4)
//        print("people: \(people)")
        // 5. 查詢住在公寓A的人
        let p5 = NSPredicate(format: "department.name = 'A'")
        let arr5 = people.filtered(using: p5)
        print("arr5: \(arr5)")
        // 6. 是否有人的年齡大於25
        let p6 = NSPredicate(format: "ANY people.age > 25 ")
        if p6.evaluate(with: self) {
            print("p6: 有")
        } else {
            print("p6: 沒有")
        }
        // 7. 年齡大於等於20的人
        let p7 = NSPredicate { (evaluatedObject, _) -> Bool in
            return (evaluatedObject as! Person).age >= 20
        }
        let arr7 = people.filtered(using: p7)
        print("arr7: \(arr7)")
        // 8. "%K == %@"
        let p8 = NSPredicate(format: "%K == %@", "lastName", "Smith")
        let arr8 = people.filtered(using: p8)
        print("arr8: \(arr8)")
        // 9.
        let p9t = NSPredicate(format: "lastName = $NAME")
        let p9 = p9t.withSubstitutionVariables(["NAME": "Smith"])
        let arr9 = people.filtered(using: p9)
        print("arr9: \(arr9)")
        // 10. 大於18歲小於20歲
        let lhs = NSExpression(forKeyPath: "age")
        let greaterThanRhs = NSExpression(forConstantValue: 18)
        let greaterP = NSComparisonPredicate(leftExpression: lhs, rightExpression: greaterThanRhs, modifier: NSComparisonPredicate.Modifier.direct, type: NSComparisonPredicate.Operator.greaterThan, options: NSComparisonPredicate.Options.normalized)

        let lessThanRhs = NSExpression(forConstantValue: 20)
        let lessP = NSComparisonPredicate(leftExpression: lhs, rightExpression: lessThanRhs, modifier: NSComparisonPredicate.Modifier.direct, type: NSComparisonPredicate.Operator.lessThan, options: NSComparisonPredicate.Options.normalized)

        let p10 = NSCompoundPredicate(andPredicateWithSubpredicates: [greaterP, lessP])
        let arr10  = people.filtered(using: p10)
        print("arr10: \(arr10)")

        // MARK: - 驗證
        let testPhone = "13422222222"
        let phoneRegex = "^((13[0-9])|(15[^4,\\D])|(18[0,0-9]))\\d{8}$"
        let p21 = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
        if p21.evaluate(with: testPhone) {
            print("是手機號!")
        }

        let testEmail = "jabread007@yahoo.com"
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"
        let p22 = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        if p22.evaluate(with: testEmail) {
            print("是郵箱號!")
        }

    }
    
    // 用到的兩個類
    class Person: NSObject {
        @objc var firstName: String = ""
        @objc var lastName: String = ""
        @objc var age: Int = 0
        @objc var department: Department

        convenience init(firstName: String, lastName: String, age: Int, departmentName: String) {
            self.init()
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
            self.department.name = departmentName
        }

        override init() {
            department = Department()
            super.init()
        }

        override var description: String {
            return firstName + " " + lastName
        }
    }

    class Department: NSObject {
        @objc var name: String
        init(name: String = "") {
            self.name = name
        }
    }
複製程式碼
  • Core Data

    NSFetchRequest中有predicate屬性,用來對資料進行過濾獲取。

  • 驗證格式

    主要結合正規表示式的使用。

    1. 郵箱號正規表示式:

    [A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}

    1. 手機號正規表示式:

    ^((13[0-9])|(15[^4,\\D])|(18[0,0-9]))\\d{8}$

相關文章