Re: NSString が整数値かどうか調べる関数

お題: 「NSString が整数値かどうか調べる関数 - 宇宙行きたい

とりあえずFoundation Frameworkにあるようなクラスを使って書いてみるよ。
但し仕事でObjective-C/Foundation使ってたのは前世紀なので今はもっと良いやり方あるかも。
あとiOSだったら正規表現とか使えるらしいのでそれで良い気も。

方法その1. NSCharacterSet + NSScanner

元のソースを見ると文字種のチェックをしているようなのでまずはその線で実装してみる。
NSScannerで文字列を指定した文字セットでスキャンする。指定した文字セット以外の文字が含まれてなければ、末尾までスキャン出来ているはずなのでそれで判定する。

+(BOOL)isDigit:(NSString *)text
{
    NSCharacterSet *digitCharSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];

    NSScanner *aScanner = [NSScanner localizedScannerWithString:text];
    [aScanner setCharactersToBeSkipped:nil];
    
    [aScanner scanCharactersFromSet:digitCharSet intoString:NULL];
    return [aScanner isAtEnd];
}

NSCharacterSetというのは文字の集合を表すクラスで、上の場合'0'〜'9'の文字で構成される文字セットを生成している。*1 毎回作るのが嫌ならstatic変数とかにしてもよい。
NSScannerというのは文字列のスキャンをするクラスで、ちょっとJavaのStringTokenizerと似たような機能。引数の文字列から、+localizedScannerWithString:でNSScannerのインスタンスを生成する。
NSScannerはデフォルトでは前後のスペースなどを読み飛ばしてくれるのだが、あえて-setCharactersToBeSkipped:でnilを渡して抑制している。
scanCharactersFromSet:intoString:で指定したNSCharacterSetに含まれる文字をスキャンする。読み取った文字列自体は使わないのでintoString:にNULLを渡して捨てている。
数字のみで構成されている場合、最後まで読み取っているはずなので、-isAtEndがYESになるはず。数字以外の文字が存在すれば、そこで止まっているはずなので、-isAtEndはNOになる。
この場合空文字の場合もYESになるので、空文字をNOにしたければreturn [aScanner scanCharactersFromSet:characterSet intoString:NULL] && [aScanner isAtEnd];にするか、別に空文字チェックを入れる。

方法その2. NSCharacterSetの包含関係で判定

引数の文字列からNSCharacterSetを生成し、数字の文字セットのサブセットになっていれば、数字のみで構成されていると判定できる。

+(BOOL)isDigit:(NSString *)text
{
    NSCharacterSet *digitCharSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];

    NSCharacterSet *aCharacterSet = [NSCharacterSet characterSetWithCharactersInString:text];
    
    return [digitCharSet isSupersetOfSet:aCharacterSet];
}

-isSupersetOfSet:は集合が一致する場合もYESを返すのでこれを使うことができる。
このやり方はあんまり使ったことが無い。

方法その3. NSScannerで数値をスキャンする

文字種のチェックじゃなくて数値のバリデーションがしたいのなら、NSScannerで直接数値を読み取っても良い。

+(BOOL)isInt(NSString *text)
{
    NSScanner *aScanner = [NSScanner localizedScannerWithString:text];
    [aScanner setCharactersToBeSkipped:nil];

    [aScanner scanInt:NULL];
    return [aScanner isAtEnd];
}

この場合は全角数字も通ってしまう。あと負の数とかも。
@"1234567890123"のようなintの範囲を超えるものの場合も通ってしまう。(その場合、scanInt:でintへのポインタを渡して値を取得するとMAX_INTになっており、-isAtEndがYESになる。ドキュメントによると仕様らしい。)
空文字がYESになってしまうので、空文字の場合にNOにしたいなら[aScanner scanInt:NULL] && [aScanner isAtEnd]にするか別に空文字チェックを付ける。

NSStringに判定メソッドを追加する

カテゴリを使って、その1.の方針でNSStringに-isDigitメソッドを追加する。

#import <Foundation/Foundation.h>

@interface NSString(Validation)
// 数字のみで構成されていればYESを戻す
- (BOOL)isDigit;
// characterSetに含まれる文字のみで構成されていればYESを戻す
- (BOOL)consistsOf:(NSCharacterSet *)characterSet;
@end

@implementation NSString(Validation)
static NSCharacterSet *digitCharacterSet = nil;
+ (void)load
{
    if (! digitCharacterSet) {
        NSAutoreleasePool *innerPool = [[NSAutoreleasePool alloc] init];
        digitCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] retain];
        [innerPool release];
    }
}
- (BOOL)isDigit
{
    return [self consistsOf:digitCharacterSet];
}
- (BOOL)consistsOf:(NSCharacterSet *)characterSet
{
    NSScanner *aScanner = [NSScanner localizedScannerWithString:self];
    [aScanner setCharactersToBeSkipped:nil];
    
    [aScanner scanCharactersFromSet:characterSet intoString:NULL];
    return [aScanner isAtEnd];
}
@end

+loadはカテゴリがロードされた際に呼ばれるので、その際にstatic変数を初期化する。別に-isDigitが呼ばれる度にチェックしても良いけど。


呼んでみる。

#define printIsDigit(x) NSLog(@"'%@' %@ digit", (x), [(x) isDigit] ? @"is" : @"is not");
...
    printIsDigit(@"");
    printIsDigit(@"123");
    printIsDigit(@"12a3");
    printIsDigit(@" 1233 ");
    printIsDigit(@"0123");
    printIsDigit(@"1234567890123");
    printIsDigit(@"1");

結果

2011-07-16 03:40:36.431 ObjcSample[60717:a0f] '' is digit
2011-07-16 03:40:36.433 ObjcSample[60717:a0f] '123' is digit
2011-07-16 03:40:36.434 ObjcSample[60717:a0f] '12a3' is not digit
2011-07-16 03:40:36.435 ObjcSample[60717:a0f] ' 1233 ' is not digit
2011-07-16 03:40:36.435 ObjcSample[60717:a0f] '0123' is digit
2011-07-16 03:40:36.436 ObjcSample[60717:a0f] '1234567890123' is digit
2011-07-16 03:40:36.436 ObjcSample[60717:a0f] '1' is not digit

一応パフォーマンス測定

100万回繰り返して@"1234567890"を判定して計測してみるよ。
その1を+isDigit1:、その2の方を+isDigit2:という名前にしてNSStringクラスに入れました。
折角なのでBlocks使ってみたり。

// ベンチマーク測定用関数
void
benchmark(int count,  void (^f)(void), NSString *title)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSDate *start = [NSDate date];
    int counter = count;
    while(counter-- > 0) {
        f();
    }
    NSTimeInterval interval = [start timeIntervalSinceNow];
    NSLog(@"[%@](x %d) takes %f sec.", title, count, -interval);

    [pool drain];
}

...
// 呼び出し
    NSString *text = @"1234567890";
    int count = 1000000;
    benchmark(count, ^{[NSString isDigit1:text];} , @"+isDigit1:");
    benchmark(count, ^{[NSString isDigit2:text];} , @"+isDigit2:");
    benchmark(count, ^{[NSString isInt:text];} , @"+isInt:");
    benchmark(count, ^{[text isDigit];} , @"-isDigit:");

出力結果

2011-07-17 01:30:16.066 ObjcSample[70315:a0f] [+isDigit1:](x 1000000) takes 2.625714 sec.
2011-07-17 01:30:33.349 ObjcSample[70315:a0f] [+isDigit2:](x 1000000) takes 16.010035 sec.
2011-07-17 01:30:35.105 ObjcSample[70315:a0f] [+isInt:](x 1000000) takes 0.995496 sec.
2011-07-17 01:30:37.410 ObjcSample[70315:a0f] [-isDigit:](x 1000000) takes 1.910755 sec.

1つ目と4つ目の違いはNSCharacterSetの初期化分と思われる。
環境はMac OS X 10.6.6 (2.66GHz Core 2 Duo)でx86_64のReleaseビルド。

*1:NSCharacterSetクラスには+decimalDigitCharacterSetというクラスメソッドもあるのだが、全角数字とかも入ってくるので上の例では自前で作っている。逆に全角数字を許容するならdecimalDigitCharacterSetでも良いかも。