配列のソート

JavaプログラマのためのObjective-Cにおける配列ソート解説。

いくつかのソート方法がある。
そのうち簡単なのは、以下のようにNSSortDescriptorクラスを使うことだ。

NSSortDescriptor* dec = [NSSortDescriptor sortDescriptorWithKey:@"key" ascending:YES];
array = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:dec]];

@"key" の部分に比較に用いるプロパティの名前を指定する。*1このキーで指定されたプロパティは、自然な順序付けをもつ型でなければならない。「自然な順序付けをもつ型」とは、compare: というセレクタを実装していなければならないということである。Java的にいえば、Comparableインターフェイスを実装していなければならない、ということであるが、Objective-Cでは、単に、compare: というセレクタが存在すればよい。比較に使用するキーはint, floatなどのプリミティブ型でもきちんと動作したが、仕様書上でプリミティブ型でもよいという記述は見つけられなかった。


コード例を以下に示す。

// サンプル用ひとクラス
@interface Person : NSObject
@property(copy, nonatomic, readwrite) NSString* name;     // 名前
@property(copy, nonatomic, readwrite) NSDate* birth;      // 生年月日
@property(copy, nonatomic, readwrite) NSString* number;   // 背番号
@end

@implementation Person
@synthesize name;
@synthesize birth;
@synthesize number;

-(void)dealloc {
    self.name = nil;
    self.birth = nil;
    self.number = nil;
    [super dealloc];
}

-(NSString*)description {
    NSDate* now = [NSDate date];
    NSCalendar* cal = [NSCalendar currentCalendar];
    NSDateComponents* comp = [cal components:NSYearCalendarUnit fromDate:self.birth toDate:now options:0];
    NSInteger age = [comp year];
    
    return [NSString stringWithFormat:@"Number:%@ %@(%ld)", self.number, self.name, (long)age];
}

@end

// テスト用メイン関数
int main(int argc, const char** argv)
{
    @autoreleasepool {
        NSCalendar* cal = [NSCalendar currentCalendar];
        NSDateComponents* comp = [[[NSDateComponents alloc] init] autorelease];
        
        // 人データサンプルを作成
        Person* p1 = [[[Person alloc] init] autorelease];
        p1.name = @"Abe";
        [comp setYear:1979];
        [comp setMonth:3];
        [comp setDay:20];
        p1.birth = [cal dateFromComponents:comp];
        p1.number = @"10";
        
        Person* p2 = [[[Person alloc] init] autorelease];
        p2.name = @"Sakamoto";
        [comp setYear:1988];
        [comp setMonth:12];
        [comp setDay:14];
        p2.birth = [cal dateFromComponents:comp];
        p2.number = @"6";

        Person* p3 = [[[Person alloc] init] autorelease];
        p3.name = @"Yamaguchi";
        [comp setYear:1983];
        [comp setMonth:11];
        [comp setDay:11];
        p3.birth = [cal dateFromComponents:comp];
        p3.number = @"47";

        // サンプルデータを配列にする
        NSArray* array = [NSArray arrayWithObjects:p1, p2, p3, nil];
        
        // 年齢の高い順にソート
        // (生年月日の古い(小さい)順なのでASC)
        NSSortDescriptor* dec = [NSSortDescriptor sortDescriptorWithKey:@"birth" ascending:YES];
        array = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:dec]];
        
        // 配列要素トレース
        for (Person* p in array) {
            NSLog(@"%@", p);
        }
    }
}

【出力】

Number:10 Abe(34)
Number:47 Yamaguchi(30)
Number:6 Sakamoto(25)

もし、自然な順序付けを使用せずに、独自の比較を行いたい場合は、
+ sortDescriptorWithKey:ascending:selector:
にカスタムセレクタを渡してやる。

たとえば、キーが文字列で、大文字小文字を区別せずに比較させたい場合は、以下のようにすることも可能だ。

NSSortDescriptor* dec = [NSSortDescriptor sortDescriptorWithKey:@"key" ascending:YES selector:@selector(caseInsensitiveCompare:)];
array = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:dec]];

これを応用すれば、比較に用いるキーが独自に作成したクラスで、かつ、そいつが自然な順序付け(compare:)を持っていなかったとしても、NSSortDescriptorを使ってソートすることができる。また、ご覧のように、sortedArrayUsingDescriptorsに渡すNSSortDescriptorは配列で指定するため、重複する(比較結果がNSOrderedSameである)オブジェクトに対して、追加のソートをかけることができる。


と、ここまでがNSSortDescriptorを使った配列のソート方法であった。
しかしながら、Javaから入って、最近Objective-Cを学んでいるプログラマからすれば、慣れ親しんだ java.util.Collections.sort(List, Comparator) をやりたい! というニーズもあるのではないだろうか。OK、そのやり方もある。だが、記事が長くなったので、次回へ続く。

*1:この指定方法については、Key-Valueコーディングの規格に従う。https://developer.apple.com/library/mac/documentation/cocoa/Conceptual/KeyValueCoding/Articles/KeyValueCoding.html