NSString

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;
 }
}

range と subrange という 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

■関連リンク:
トークン分割する (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




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 で試したところ、以下の通りだ。

japaneseEncoding.jpg


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

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



Foundation - NSString

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

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

AppDelegate.m (sample)

CF_EXPORT
CFStringRef CFStringConvertEncodingToIANACharSetName(
    CFStringEncoding encoding);

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

IANAjpg.png




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 回検索してやる。テキストを取り扱うアプリケーションを作る時に参考になるかも。