Cocoaの機能を使って1次元リストをソートするAppleScriptハンドラ


Cocoaの機能を使ったソートはいくつか方法がある。

1次元のリストをソートするときは`NSArray`クラスの`sortedArrayUsingSelector:`メソッドが簡単。


## sortedArrayUsingSelector:に最適なselectorを考える

`sortedArrayUsingSelector:`の引数は`NSComparisonResult`クラスを返す比較用メソッドの`selector`。

比較用メソッドとして最も汎用性が高いのは`compare:`。数字でも日付でも使える。

```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{5, 1, 2, 4, 3}
return (array's sortedArrayUsingSelector:"compare:") as list
--> {1, 2, 3, 4, 5}
```

```AppleScript
use framework "Foundation"

set dateList to {date ("2015-01-01"), date ("2014-01-01"), date ("2016-01-01")}
set array to current application's NSArray's arrayWithArray:dateList
return (array's sortedArrayUsingSelector:"compare:") as list
--> {date "2014年1月1日水曜日 0:00:00", date "2015年1月1日木曜日 0:00:00", date "2016年1月1日金曜日 0:00:00"}
```


__ただし、文字列に対しての結果はイマイチ__。

文字列に`compare:`メソッドを使うとASCIIコードの番号で比較される。そのため、例えばアルファベットでは大文字(A-Z)、小文字(a-z)の順になる。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"}
return (array's sortedArrayUsingSelector:"compare:") as list
--> {"Apple", "Dog", "blue", "cat", "あか", "あし", "アオ", "アザ", "ｱｲ"}
```


文字列をソートするときには大文字と小文字を区別せず辞書順の方が自然。

文字列の比較に適したメソッドは[Identifying and Comparing Strings](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/index.html#//apple_ref/doc/uid/20000154-SW29)のうちのひとつ、`localizedCaseInsensitiveCompare:`。大文字と小文字を区別せず、さらにシステムの設定言語でローカライズされる。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"}
return (array's sortedArrayUsingSelector:"localizedCaseInsensitiveCompare:") as list
--> {"Apple", "blue", "cat", "Dog", "ｱｲ", "アオ", "あか", "アザ", "あし"}
```


ただ、こちらにも欠点がある。 __`localizedCaseInsensitiveCompare:`が比較できるのは文字列のみで、数字などには使えない。__


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{5, 1, 2, 4, 3}
return (array's sortedArrayUsingSelector:"localizedCaseInsensitiveCompare:") as list
--> error "-[__NSCFNumber localizedCaseInsensitiveCompare:]: unrecognized selector sent to instance 0x527" number -10000
```

つまり`compare:`と`localizedCaseInsensitiveCompare:`は一長一短。

__「リストの要素が文字列かどうか」によって`selector`を切り替える__のが最適。リストの要素が文字列なら`localizedCaseInsensitiveCompare:`、そうでなければ`compare:`を`selector`にする。


## 「リストの要素が文字列かどうか」を判別する方法

ここではリストの全要素が同じクラス、または`compare:`メソッドがあるスーパークラスのサブクラス同士と仮定。

この仮定に基づき、1番目の要素だけをチェックする。したがって「リストの1番目の要素が文字列かどうか」を判別することになる。

`NSArray`クラスの1番目の要素は`firstObject`プロパティで取得。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"}
set anObject to array's firstObject()
return anObject's |description|() as text
--> "blue"
```


あるオブジェクトが文字列かどうかを判別するには`isSubclassOfClass:`メソッドを使う。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"}
set anObject to array's firstObject()
return anObject's |class|'s isSubclassOfClass:(current application's NSString)
--> true
```

さらにリストが空である可能性を考慮して「リストの1番目の要素が文字列かどうか」を取得すると下のようになる。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"}
set anObject to array's firstObject()
return anObject ≠ missing value and (anObject's |class|'s isSubclassOfClass:(current application's NSString))
--> true
```


リストの要素が文字列でない場合はもちろん`false`。


```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{5, 1, 2, 4, 3}
set anObject to array's firstObject()
return anObject ≠ missing value and (anObject's |class|'s isSubclassOfClass:(current application's NSString))
--> false
```

```AppleScript
use framework "Foundation"

set array to current application's NSArray's arrayWithArray:{}
set anObject to array's firstObject()
return anObject ≠ missing value and (anObject's |class|'s isSubclassOfClass:(current application's NSString))
--> false
```


## ハンドラ化

以上をまとめてハンドラ化。


```AppleScript:sortList.scpt
use framework "Foundation"

my sortList({5, 1, 2, 4, 3})
--> {1, 2, 3, 4, 5}

my sortList({date ("2015-01-01"), date ("2014-01-01"), date ("2016-01-01")})
--> {date "2014年1月1日水曜日 0:00:00", date "2015年1月1日木曜日 0:00:00", date "2016年1月1日金曜日 0:00:00"}

my sortList({"blue", "Apple", "cat", "Dog", "あし", "アオ", "あか", "ｱｲ", "アザ"})
--> {"Apple", "blue", "cat", "Dog", "ｱｲ", "アオ", "あか", "アザ", "あし"}

my sortList({})
--> {}

on sortList(aList as list)
	set array to current application's NSArray's arrayWithArray:aList
	set anObject to array's firstObject()
	if anObject ≠ missing value and (anObject's |class|'s isSubclassOfClass:(current application's NSString)) then
		set comparator to "localizedCaseInsensitiveCompare:"
	else
		set comparator to "compare:"
	end if
	return (array's sortedArrayUsingSelector:comparator) as list
end sortList
```



## 更新履歴

* 2016-01-14: Cocoaの機能を使って作成
* 2016-01-15: リストの要素が文字列かどうかで`selector`を切り替えるように変更
* 2016-01-17: リストの要素が文字列のときの`selector`を`localizedCaseInsensitiveCompare:`に変更