NSArrayをFinder風の並び順でソートする

こんなファイルがあったとする。

file-a
file-1
file-01
file-2
file-10
file-b
file-ab

辞書式(普通の昇順ソート)で並べ替えると

file-01
file-1
file-10
file-2
file-a
file-ab
file-b

となるわけだが、Finderでソートすると

file-1
file-01
file-2
file-10
file-a
file-ab
file-b

となる。連続した数字部分を数値と見なし、数値順で並べてくれる。個人的にはこの方が分かりやすくて好きだ。

で、この数値順ソートをCocoaで実現するにはどーしたらいいのかなー?と思って調べていたら、Technical Q&A QA1159: Sorting Like the Finderという、そのまんまの記事がADCにあった。流石林檎様、分かっていらっしゃる。

上記Q&Aのコードを改造してNSMutableArrayのカテゴリメソッドにするとスマートに使えて良い感じ。

NSString配列のソートはもちろん、任意のオブジェクトの場合はソートに使うNSStringインスタンス変数名をsortByFinderOrderWithStringObjectKey:に渡せばおk。

#include <CoreServices/CoreServices.h>
#include <sys/param.h>
 
static CFComparisonResult CompareLikeTheFinder(const void *val1, const void *val2, void *context)
{
    SInt32          compareResult;
    CFStringRef     lhsStr;
    CFStringRef     rhsStr;
    CFIndex         lhsLen;
    CFIndex         rhsLen;
    UniChar         lhsBuf[MAXPATHLEN];
    UniChar         rhsBuf[MAXPATHLEN];
 
    // val1 is the left-hand side CFString.
    // val2 is the right-hand side CFString.
    if (context)
    {
        NSString *key = (NSString *)context;
        lhsStr = (CFStringRef)[(NSObject *)val1 valueForKey:key];
        rhsStr = (CFStringRef)[(NSObject *)val2 valueForKey:key];
    }
    else
    {
        lhsStr = (CFStringRef)val1;
        rhsStr = (CFStringRef)val2;
    }
    lhsLen = CFStringGetLength(lhsStr);
    rhsLen = CFStringGetLength(rhsStr);
 
    // Get the actual Unicode characters (UTF-16) for each string.
    CFStringGetCharacters(lhsStr, CFRangeMake(0, lhsLen), lhsBuf);
    CFStringGetCharacters(rhsStr, CFRangeMake(0, rhsLen), rhsBuf);
 
    // Do the comparison.
    UCCompareTextDefault(
                         kUCCollateComposeInsensitiveMask
                         | kUCCollateWidthInsensitiveMask
                         | kUCCollateCaseInsensitiveMask
                         | kUCCollateDigitsOverrideMask
                         | kUCCollateDigitsAsNumberMask
                         | kUCCollatePunctuationSignificantMask,
                         lhsBuf,
                         lhsLen,
                         rhsBuf,
                         rhsLen,
                         NULL,
                         &compareResult
                         );
 
    // Return the result. Conveniently, UCCompareTextDefault 
    // returns -1, 0, or +1, which matches the values for 
    // CFComparisonResult exactly.
    return (CFComparisonResult)compareResult;
}
 
static void SortCFMutableArrayLikeTheFinder(CFMutableArrayRef array, CFStringRef key)
{
    CFArraySortValues(
                      array, 
                      CFRangeMake(0, CFArrayGetCount(array)),
                      CompareLikeTheFinder,
                      key
                      );
}
 
@interface NSMutableArray (PKAdditions)
- (void)sortByFinderOrder;
- (void)sortByFinderOrderWithStringObjectKey:(NSString *)key;
@end
 
@implementation NSMutableArray (PKAdditions)
- (void)sortByFinderOrder
{
    [self sortByFinderOrderWithStringObjectKey:nil];
}
 
- (void)sortByFinderOrderWithStringObjectKey:(NSString *)key
{
    SortCFMutableArrayLikeTheFinder(self, key);
}
@end

実際のファイル名やディレクトリ名をソートする場合は、- [NSFileManager displayNameAtPath:(NSString *)path]で得られる名前をソートしないとFinder順にはならない。なぜかというと、Finderから見えるディレクトリ名はローカライズ(~/Documents → 書類 みたいなの)された物なので。