HMDT - Logic and Intuition -

about HMDT

Cocoa Programming Tips 1001

Foundation

NSString

Foundation - NSString

1 行づつ substring を取り出す

Keywords: lineRangeForRange

ある程度の長さの文や、ファイルをあらわしている NSString を考えよう。そこには、改行コードで区切られた、いくつかの行がある。その行を 1 つづつ取り出すにはどうすればいいか?byte 列を取り出して、自分で 1 つづつパースするのもいいよ。だけど、改行コードっていろいろあるじゃない?Mac, UNIX, DOS で違って、めんどうくさい。だから、NSString で用意されている、substringWithRange: を使うのが簡単だ。

Foundation/NSString.h

- (NSRange)lineRangeForRange:(NSRange)range;

これを使うと、range で指定した範囲を含む、最小の行を取り出してくれる。たとえば、引き数の range に最初の一文字(0, 0)を指定すると、一番最初の行を表す NSRange を取り出すことができるんだ。

これを使って、NSString から、1 行づ取り出すコードを書いてみる。

makeParseString (sample)

- (void)makeParsedString:(NSString*)string
{
    NSString* parsedString;
    NSRange range, subrange;
    int length;
    
    length = [string length];
    range = NSMakeRange(0, length);
    while(range.length > 0) {
        subrange = [string lineRangeForRange:
            NSMakeRange(range.location, 0)];
        parsedString = [string substringWithRange:subrange];
        
        printf("line: %s¥n", [parsedString cString]);
        
        range.location = NSMaxRange(subrange);
        range.length -= subrange.length;
    }
}

rangesubrange という 2 つの NSRange を使って、string の中身を走査していくんだ。まず最初は、range は string 全体を表しているんだ。で、lineRangeForRange: を使って、最初の一行を取り出す。引き数として、長さ 0 の NSRange を指定するところがミソだ。subrange が取りだせたら、substringWithRange: を使って、1 行分の string を取り出す。そして、range を更新する。取り出した文字列の文だけ、後ろに移動させてやるんだ。

これで 1 行づつにパースできるよ。

Foundation - NSString

文字列を比較する

Keywords: compare

文字列の比較は、プログラミングの基本中の基本だ!てなわけで、NSString の比較を考えよう。普通、というか standard C library を用いたプログラミングでは、文字列の比較は memcmp() を使うよな。NSString から cString を取り出して、それで比較してもいいけど、NSString でも比較用のメソッドが用意されている。それらを使うと、NSString 同士を比べてくれるんだ。結果は NSComparisonResult って型で返ってくる。そこで定義されている定数は、こんな感じだ。

Foundation/NSObjCRuntime.h

typedef enum _NSComparisonResult {
    NSOrderedAscending = -1, 
    NSOrderedSame, 
    NSOrderedDescending
} NSComparisonResult;

比較した結果、同じ文字列ならば NSOrderedSame = 0 が、そうじゃなければ、それ以外が返る。memcmp() と同じだね。

あと、比較する時に、オプションを指定することができる。オプションの値は、ヘッダでは次のように定義されている。

Foundation/NSString.h

enum {
    NSCaseInseistiiveSearch = 1, 
    NSLiteralSearch = 2, 
    NSbackwardSearch = 4, 
    NSAnchroedSearch = 8
} NSComparisonResult;

だけど、比較する時に指定できるのは、

  • NSCaseInsensitiveSearch
  • NSLiteralSearch

の 2 つだ。気をつけてくれ。OR を取ることによって複数指定できるよ。

さて、いよいよ本題だ。文字列を比較するには、compare: メソッドを使う。

Foundation/NSString.h

- (NSComparisonResult)compare:(NSString*)string;
- (NSComparisonResult)compare:(NSString*)string 
                options:(unsigned)mask;
- (NSComparisonResult)compare:(NSString*)string 
                options:(unsigned)mask 
                range:(NSRange*)compareRanage;

比較する文字列と、オプションと、範囲を指定する。オプションを省略した場合はオプション無しで、範囲を省略した場合は全範囲で実行されるんだ。

Foundation - NSString

トークンに分割する

Keywords: tokenize

文字列をデリミタで区切って、トークンに分割する、っていう処理は、きっとあらゆるところで求められるよな。どういうことかっていうと、たとえば、

"ABC, DEF, GHI"

っていう文字列があったとき、コンマと空白で区切られた部分を順々にとりだしたい。つまり、

{"ABC", "DEF", "GHI"}

っていう配列を得るか、enumerator でアクセスしたい、ってことだ。これを実現して見よう。

まず、NSString にカテゴリを使ってメソッドを加える。

HMDTString.h (sample)

@interface NSString (HMDTString)

- (NSEnumerator*)tokenize:(NSString*)delimiters;

@end

tokenize: っていうメソッドを付け加えてみた。引数にデリミタを取る。複数指定可能。

続いて、HMDTStringEnumerator っていう、外部には公開しないクラスを作ってみた。

HMDTString.m (sample)

@interface HMDTStringEnumerator : NSEnumerator
{
    NSString* _string;
    NSString* _delimiters;
    
    const char* _indicator;
}

- (id)initWithString:(NSString*)string delimiters:(NSString*)delimiters;

- (NSArray*)allObjects;
- (id)nextObject;

@end

このクラスが、実際にトークン化を行うんだ。中心となるメソッド、nextObject: の実装は、こんな感じになる。

HMDTString.m (sample)

- (id)nextObject
{
    const char* delis = [_delimiters cString];
    const char* tmpInd;
    
    // Skip delimiters
    while(_isDelimiter(*_indicator, delis)) {
        _indicator++;
    }
    tmpInd = _indicator;
    
    // Terminator check
    if(*_indicator == '¥0') {
        // There is no more tokens
        return nil;
    }
    
    // Parse token
    while(*_indicator != NULL) {
        if(_isDelimiter(*_indicator, delis)) {
            break;
        }
        _indicator++;
    }
    
    return [NSString 
        stringWithCString:tmpInd length:(_indicator - tmpInd)];
}

まず始めに、デリミタがあったらスキップする。次に、文字の終端かどうかをチェックする(NULL チェック)。そしたら、次のデリミタが来るか、NULL が来るまで読み進めて、最後に NSString を作って return するんだ。めんどくさいけど、複雑な話ではないよね。

これを使ったサンプルコードは、こうなる。

main (sample)

    NSString* string = @"abc, def gh,ijk";
    NSEnumerator* enumerator = [string tokenize:@", "];
    id token;
    
    while((token = [enumerator nextObject])) {
        NSLog(token);
    }

実行結果はこんな感じ。

main (sample)

abc
def
gh
ijk

これはけっこう重宝すると思うよ。いや、おれは欲しかったな。

■サンプルダウンロード:
StringTokenizer.tar.gz

■関連リンク:
トークン分割する (NSScanner) (Apple)

Foundation - NSString

NSString のエンコーディング

Keywords: CFStringEncoding, NSStringEncoding

NSString には、エンコーディング機能があるんだ。NSString 自体は Unicode で文字列を保持しているんだけど、エンコードを指定して、NSData 型に書き出すことができる。このエンコーディング機能は NSString と CoreFoundation とに存在しているんだ。

ちなみに、エンコーディングで気をつけないといけないのは、環境によって可能なエンコーディングの種類が違うこと。たとえば、英語システムでは日本語エンコーディングは利用不可かもしれない。Cocoa でのエンコーディングは、すべてのシステムで利用可能なエンコーディングと、一部のマシンで可能なものとに分かれるんだ。

んじゃ、使用可能なエンコーディングを調べてみよう。まず、エンコーディングは定数で指定するんだ。これは NSString なら NSStringEncoding 型、CoreFoundation なら CFStringEncoding だ。この 2 種類の定数の間では、コンバータをかましてやる必要があるんで、注意。

NSString 側でのエンコーディングの種類は NSString.h で。CoreFoundation では CFString.h と CFStringEncodingExt.h で定義されている。基本的に、CoreFoundation の方がたくさん定義されている。CFString.h で定義されているものは、すべてのシステムで使用可能なことが保証されているんだ。CFStringEncodingExt.h の方は、そうではない。

実際にエンコーディングを使うには、まず最初に、使いたいエンコーディングが、システムで使えるかどうか調べてやらないといけない。それには CFStringIsEncodingAvailable() を使う。

CoreFoundation/CFString.h

CF_EXPORT
Boolean CFStringIsEncodingAvailable(
        CFStringEncoding encoding);

使えるようなら、CFStringEncoding から NSStringEncoding に変換してやる。それには、CoreFoundation の関数 CFStringConvertEncodingToNSStringEncoding() を使う。これで、あのたくさんの CFString のエンコーディングが、NSString で使えるんだ。

CoreFoundation/CFString.h

CF_EXPORT
UInt32 CFStringConvertEncodingToNSStringEncoding(
        CFStringEncoding encoding);

また、使用可能なすべてのエンコーディングを調べるには CFStringGetListOfAvailableEncoding() を使う。あと、エンコーディングから名前を得ることもできる。それには CFStringGetNameOfEncoding() だ。

CoreFoundation/CFString.h

CF_EXPORT
const CFStringEncoding *CFStringGetListOfAvailableEncodings();
CF_EXPORT
CFStringRef CFStringGetNameOfEncoding(
        CFStringEncoding encoding);

続いては NSString 側での話。エンコーディングが使えるようだったら、実際にエンコードしてやろう。dataUsingEncoding: を使う。

CoreFoundation/CFString.h

- (NSData *)dataUsingEncoding:(NSStringEncoding)encoding;
- (NSData *)dataUsingEncoding:(NSStringEncoding)encoding 
        allowLossyConversion:(BOOL)flag;

後の方の、dataUsingEncoding:allowLossyConversion: では、コンバートする際に、いくつかの情報が失われてもいいか?ってことを指定できるんだ。たとえば、アクセントとか大文字小文字とか。日本語の場合はなにかあるのか?

NSString でも使用可能なすべてのエンコーディングを調べることができる。avaialbeStringEncodings だ。名前を得るには localizedNameOfStringEncoding: ね。

CoreFoundation/CFString.h

+ (const NSStringEncoding *)availableStringEncodings;
+ (NSString *)localizedNameOfStringEncoding:(NSStringEncoding)encoding;

で、これらを利用してエンコーディングメニューを作ってみた。使用可能なエンコーディングを、すべて表示したメニューだ。

AppDelegate.m (sample)

- (NSArray*)allAvailableEncodings
{
    NSMutableArray*	array = [NSMutableArray array];
    const NSStringEncoding*	encoding = [NSString availableStringEncodings];
    
    while (*encoding) {
        [array addObject:[NSString localizedNameOfStringEncoding:*encoding]];
        [array addObject:[NSNumber numberWithInt:*encoding]];
        encoding++;
    }
    
    return array;
}

上のコードは、サンプルの一部。使用可能なエンコーディングと、その値を調べるためのメソッドだ。Mac OS X 10.1.5 の日本語版では、80 を超えるエンコーディングが使用可能。いっぱいあるのぉ。

■サンプルダウンロード:
EncodingMenu.tar.gz

Foundation - NSString

NSString での日本語エンコーディング

Keywords: JIS, Shift-JIS, EUC

次に、日本語のエンコーディングにしぼって話をしよう。Cocoa で使用可能な日本語エンコーディングはどれだけあるのか?CFStringEncodingExt.h から、それらしいものを集めてみた。

AppDelegate.m (sample)

static CFStringEncoding	_japaneseEncodings[] = {
    kCFStringEncodingMacJapanese, 
    kCFStringEncodingDOSJapanese, 
    kCFStringEncodingJIS_X0201_76, 
    kCFStringEncodingJIS_X0208_83, 
    kCFStringEncodingJIS_X0208_90, 
    kCFStringEncodingJIS_X0212_90, 
    kCFStringEncodingJIS_C6226_78, 
    0x0628, // JIS_X0213
    kCFStringEncodingISO_2022_JP, 
    kCFStringEncodingISO_2022_JP_2, 
    kCFStringEncodingEUC_JP, 
    kCFStringEncodingShiftJIS, 
    kCFStringEncodingInvalidId
};

これだけの定数が定義されている。JIS_X0213 を現すと思われる 0x0628 は、ヘッダファイルには定義がなかったよ。で、これらの定数の内、実際に使用可能なものはどれだけか?Mac OS X 10.1.5 で試したところ、以下の通りだ。

Japanese encoding

これらが使用可能なわけね。その他のエンコーディングについては、obsolete 扱いということかな?

■サンプルダウンロード:
EncodingMenu.tar.gz

Foundation - NSString

日本語エンコーディングの IANA 表現

Keywords: IANA

CoreFoundation の API CFStringConvertEncodingToIANACharSetName() を使うと、エンコーディングの IANA 表現を得ることができるんだ。

AppDelegate.m (sample)

CF_EXPORT
CFStringRef CFStringConvertEncodingToIANACharSetName(
        CFStringEncoding encoding);

Cocoa で使える日本語エンコーディングのうち、これを使って得ることができる IANA 表現を調べてみた。以下の表の通り。

定数 IANA 表現
kCFStringEncodingMacJapanese X-MAC-JAPANESE
kCFStringEncodingDOSJapanese CP932
kCFStringEncodingJIS_X0208_90 JIS_C6226-1983
kCFStringEncodingISO_2022_JP ISO-2022-JP
kCFStringEncodingEUC_JP EUC-JP
kCFStringEncodingShiftJIS SHIFT_JIS

Foundation - NSString

文字列を検索する

Keywords: rangeOfString:

検索ってのは文字列操作の中でも重要なものの 1 つだ。NSString では、ある文字列から部分文字列を探し出すためのメソッド、rangeOfString: を提供してるんだ。

Foundation/NSString.h

- (NSRange)rangeOfString:(NSString *)aString;
- (NSRange)rangeOfString:(NSString *)aString 
                options:(unsigned)mask;
- (NSRange)rangeOfString:(NSString *)aString 
                options:(unsigned)mask 
                range:(NSRange)searchRange;

基本は aString が最初に出てくる NSRange を返すんだ。mask を指定すると、いろいろな条件がつけられる。可能なマスクは以下の通り。

  • NSCaseInsensitiveSearch
    大文字、小文字を無視する
  • NSLiteralSearch
    byte ごとに比較する。Unicode だと、合成文字(たとえば「か」と「゛」で「が」になる)のコードと、これと同一の文字をあらわす合成済みのコードとが用意されてるんだ。これのマッチングをやめるのが、このオプション。これを指定すると、スピードがとても速くなることがある。
  • NSBackwardsSearch
    指定された範囲の先頭から探すか、終わりから探すか
  • NSAnchoredSearch
    文字列の一番先頭か、一番終わりかしか探さない

searchRange は検索する範囲を指定する。こんなところ。

で、これだけじゃ面白くないんで、実践的な検索メソッドの例を見てみよう。それは、テキストエディタの検索コマンドで使われるような検索の仕方だ。以下で見ていくのは、Developer Tools のサンプルでついてくる TextEdit での検索方法だ。

エディタで求められるような検索の仕様を考えてみよう。まず、全体の文字列と、検索する文字列とが与えられる。文字列の一部が選択されているかもしれない。選択されていなかったら、先頭(または終わり)から普通に探す。選択されていたら、まず選択されているところから後ろの部分を探す。そこになかったら、文字列の先頭から選択されている部分までを探す。つまりラップして探すんだ。

と、いう仕様の検索メソッドだ。普通のテキストエディタに求められるようなタイプだね。ではコードを。

TextEdit/TextFinder.m (Developer kit example)

@implementation NSString (NSStringTextFinding)

- (NSRange)findString:(NSString *)string 
                selectedRange:(NSRange)selectedRange 
                options:(unsigned)options 
                wrap:(BOOL)wrap
{
    BOOL forwards = (options & NSBackwardsSearch) == 0;
    unsigned length = [self length];
    NSRange searchRange, range;

    // In case of find forward
    if (forwards) {
        searchRange.location = NSMaxRange(selectedRange);
        searchRange.length = length - searchRange.location;

        // Find string
        range = [self rangeOfString:string 
                        options:options 
                        range:searchRange];
        // If not found look at the first part of the string
        if ((range.length == 0) && wrap) {
	        searchRange.location = 0;
            searchRange.length = selectedRange.location;

            // Find again
            range = [self rangeOfString:string 
                            options:options 
                            range:searchRange];
        }
    }
    // In case of find backward
    else {
        searchRange.location = 0;
        searchRange.length = selectedRange.location;

        // Find string
        range = [self rangeOfString:string 
                        options:options 
                        range:searchRange];
        // If not found look at the first part of the string
        if ((range.length == 0) && wrap) {
            searchRange.location = NSMaxRange(selectedRange);
            searchRange.length = length - searchRange.location;

            // Find again
            range = [self rangeOfString:string 
                            options:options 
                            range:searchRange];
        }
    }

    return range;
}        

@end

このソースコードは、Developer Tools の Examples/AppKit/TextEdit/TextFinder.m から抜き出しました。検束メソッドを NSString のカテゴリっていう形で実装している。引数には、検索する文字列と、いま選択されている箇所、オプション、に加えてラップして検索するかどうかを指定する。文字列の千択か所を指定する、っていうことは、NSTextView とかを使っている、っていう前提が暗黙にあるんだ。

実装自体は普通にやっている。仕様に合うように検索する範囲を決定して、rangeOfString:options:range: を使って検索する。見つからなくて、かつラップが指定してある場合は、範囲を設定し直してもう 1 回検索してやる。テキストを取り扱うアプリケーションを作る時に参考になるかも。

back to top content

Copyright © 2002-2006 HMDT. All rights reserved.