NSCharacterSet 簡單用法
NSMutableCharacterSet *base = [NSMutableCharacterSet lowercaseLetterCharacterSet]; //字母
NSCharacterSet *decimalDigit = [NSCharacterSet decimalDigitCharacterSet]; //十進制數字
[base formUnionWithCharacterSet:decimalDigit]; //字母加十進制
NSString *string = @"ax@d5s#@sfn$5`SF$$%x^(#e{]e";
//用上面的base隔開string然后組成一個數組,然后通過componentsJoinedByString,來連接成一個字符串
NSLog(@"%@",[[string componentsSeparatedByCharactersInSet:base] componentsJoinedByString:@"-"]);
[base invert]; //非 字母加十進制
NSLog(@"%@",[[string componentsSeparatedByCharactersInSet:base] componentsJoinedByString:@"-"]);
答應結果:
ax@d-s#@sfn$-`SF$$%x^(#e{]e
---------------------------------------------
正如之前提前過的,基礎類庫(Foundation)擁有最好的、功能也最全的string類的實現。
但是僅當程序員熟練掌握它時,一個string的實現才是真的好。所以本周,我們將瀏覽一些基礎類庫的string生態系統中經常用到且用錯的重要組成部分:NSCharacterSet。
如果你對什么是字符編碼搞不清楚的話(即使你有很好的專業知識),那么你應該抓住這次機會反復閱讀Joel Spolsky的這篇經典的文章"The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)"。在頭腦中保持新鮮感將對你理解我們將要探討的話題非常有幫助。
NSCharacterSet ,以及它的可變版本NSMutableCharacterSet,用面向對象的方式來表示一組Unicode字符。它經常與NSString及NSScanner組合起來使用,在不同的字符上做過濾、刪除或者分割操作。為了給你提供這些字符是哪些字符的直觀印象,請看看NSCharacterSet 提供的類方法:
alphanumericCharacterSetcapitalizedLetterCharacterSetcontrolCharacterSetdecimalDigitCharacterSetdecomposableCharacterSetillegalCharacterSetletterCharacterSetlowercaseLetterCharacterSetnewlineCharacterSetnonBaseCharacterSetpunctuationCharacterSetsymbolCharacterSetuppercaseLetterCharacterSetwhitespaceAndNewlineCharacterSetwhitespaceCharacterSet
與它的名字所表述的相反,NSCharacterSet 跟 NSSet 一點關系都沒有。
雖然底層實現不太一樣,但是 NSCharacterSet 在概念上跟 NSIndexSet 還有點相似的。NSIndexSet,之前提到過,表示一個有序的不重復的無符號整數的集合。Unicode字符跟無符號整數類似,大致對應一些拼寫表示。所以,一個 NSCharacterSet +lowercaseCharacterSet 字符集與一個包含97到122范圍的 NSIndexSet 是等價的。
現在我們對理解 NSCharacterSet 的基本概念已經有了少許自信,讓我們來看一些它的模式與反模式吧:
去掉空格
NSString -stringByTrimmingCharactersInSet: 是個你需要牢牢記住的方法。它經常會傳入NSCharacterSet +whitespaceCharacterSet 或 +whitespaceAndNewlineCharacterSet 來刪除輸入字符串的頭尾的空白符號。
需要重點注意的是,這個方法 僅僅 去除了 開頭 和 結尾 的指定字符集中連續字符。這就是說,如果你想去除單詞之間的額外空格,請看下一步。
擠壓空格
假設你去掉字符串兩端的多余空格之后,還想去除單詞之間的多余空格,這里有個非常簡便的方法:
NSString *string = @"Lorem ipsum dolar sit amet."; string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSArray *components = [string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; components = [components filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self <> ''"]]; string = [components componentsJoinedByString:@" "];
首先,刪除字符串首尾的空格;然后用 NSString -componentsSeparatedByCharactersInSet: 在空格處將字符串分割成一個 NSArray;再用一個 NSPredicate 去除空串;最后,用 NSArray -componentsJoinedByString: 用單個空格符將數組重新拼成字符串。注意:這種方法僅適用於英語這種用空格分割的語言。
現在看看反模式吧。請先看看 the answers to this question on StackOverflow。
在寫這篇文章的時候,排行第二的正確答案有 58 個頂和 2 個踩。排行第一的有 84 個頂和 24 個踩。
如今,排名第一的答案卻不是正確答案是不太正常的,但是這個問題已經破了不重復答案數(10個)的記錄,同時也破了不重復、完全錯誤的答案數(9個)的記錄。
言歸正傳,這里有 9 個錯誤答案:
- "Use
stringByTrimmingCharactersInSet" - 正如你所知道的,它只去掉首尾的空格。 - "Replace ' ' with ''" - 這個去除了所有的空格,勞而無功。
- "Use a regular expression" - 有點用,但它沒有處理首尾的空格。用正則表達式有點大材小用了。
- "Use Regexp Lite" - 說真的,正則表達式真心沒必要。同時為了這點功能增加第三方庫很不值。
- "Use OgreKit" - 同上,添加了第三方庫。
- "Split the string into components, iterate over them to find components with non-zero length, and then re-combine" - 很接近了,但是
componentsSeparatedByCharactersInSet:已經讓遍歷變得沒必要。 - "Replace two-space strings with single-space strings in a while loop" - 錯誤且浪費計算資源。
- "Manually iterate over each
unicharin the string and useNSCharacterSet -characterIsMember:" - 用了一個復雜到讓人吃驚的程度的方法,卻忘了標准庫中已經有方法可以用。 - "Find and remove all of the tabs" - 有誰提到了制表符了?不過還是謝謝了吧。
我個人並不是想責怪回答問題的人——只是指出完成這個功能有多少種不同的方法,而這些方法有多少是完全錯誤的。
字符串分詞
不要用 NSCharacterSet 來分詞。 用 CFStringTokenizer 來替代它。
你用 componentsSeparatedByCharactersInSet: 來清理用戶輸入是可以諒解的,但是用它來做更復雜的事情,你將陷入痛苦的深淵。
為什么?請記住,語言並不是都用空格作為詞的分界。雖然實際上以空格分界的語言使用非常廣泛。但哪怕只算上中國和日本就已經有十多億人,占了世界人口總量的 16%。
……即使是用空格分隔的語言,分詞也有一些模棱兩可的邊界條件,特別是復合詞匯和標點符號。
以上只為說明:如果你想將字符串分成有意義的單詞,那么請用 CFStringTokenizer (或者enumerateSubstringsInRange:options:usingBlock:)吧。
從字符串解析數據
NSScanner 是個用以解析任意或半結構化的字符串的數據的類。當你為一個字符串創建一個掃描器時,你可以指定忽略哪些字符,這樣可以避免那些字符以各種各樣的方式被包含到解析出來的結果中。
例如,你想從這樣一個字符串中解析出開門時間:
Mon-Thurs: 8:00 - 18:00 Fri: 7:00 - 17:00 Sat-Sun: 10:00 - 15:00
你會 enumerateLinesUsingBlock: 並像這樣用一個 NSScanner 來解析:
let skippedCharacters = NSMutableCharacterSet() skippedCharacters.formIntersectionWithCharacterSet(NSCharacterSet.punctuationCharacterSet()) skippedCharacters.formIntersectionWithCharacterSet(NSCharacterSet.whitespaceCharacterSet()) string.enumerateLines { (line, _) in let scanner = NSScanner(string: line) scanner.charactersToBeSkipped = skippedCharacters var startDay, endDay: NSString? var startHour: Int = 0 var startMinute: Int = 0 var endHour: Int = 0 var endMinute: Int = 0 scanner.scanCharactersFromSet(NSCharacterSet.letterCharacterSet(), intoString: &startDay) scanner.scanCharactersFromSet(NSCharacterSet.letterCharacterSet(), intoString: &endDay) scanner.scanInteger(&startHour) scanner.scanInteger(&startMinute) scanner.scanInteger(&endHour) scanner.scanInteger(&endMinute) }
我們首先從空格字符集和標點符號字符集的並集構造了一個 NSMutableCharacterSet。告訴 NSScanner忽略這些字符以極大地減少解析這些字符的必要邏輯。
scanCharactersFromSet: 傳入字母字符集得到每項中一星期內的開始和結束(可選)的天數。scanInteger 類似地,得到下一個連續的整型值。
NSCharacterSet 和 NSScanner 讓你可以快速而充滿自信地編碼。這兩者真是完美組合。
NSCharacterSet 是基礎類庫中字符串處理系統中的一員,可能是最容易被用錯或是誤解的一員。在腦中記住這些模式與反模式,你將不僅能做一些很有用的諸如管理空格及從字符串中讀信息之類的事情,更重要的是,你將避免誤入歧途。
如果“不出錯”對一個 NSHipster 來說不是最重要的事情,那我也不想成為正確的了!
Ed. Speaking of (not) being wrong, the original version of this article contained errors in both code samples. These have since been corrected.
