共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。前回の投稿で書いたとおり、内部配列を変更しました。その後に、新しい機能を追加したので、また紹介します。
今回追加したのは、ソート機能。説明する必要のないでしょうが、指定された項目値の大きさで、レコードを並び替える機能です。
最初に決めなければならないのは、ソート機能の組み込み方でしょう。ソートしたデータをどのように利用するのか、いろいろと考えました。ソート結果をモデルクラス内で持ち、順番に取り出す方法も考えられます。しかし、そうするとソート以外の機能を用意しなければならず、作る量が増えてしまいます。また、用意した機能で不足する状況も考えられます。
それを避けるために、ソート後のレコード番号を配列の形で渡すことにしました。レコード番号さえあれば、あとは自由に使えます。レコード番号を指定して、レコード内の項目値が得られますし。
ソートには昇順と降順があります。昇順の配列は、逆から読むと降順として使えます。そのため、昇順のソートだけを作ることにしました。
実行に必要なのは、どの項目でソートするかの指定だけですから、引数は項目番号だけで済みます。また、戻り値はレコード番号の配列です。データ型が違っても、まったく同じです。というわけで、メソッドは1つだけになりました。
ただし、Bool型だけは値が2種類しかないので、ソートする意味がありません。無駄な処理をさせないために、エラーメッセージを出して終了することにしました。
では、具体的なコードを見ましょう。次のようなSwiftコードを追加しました。
// ======================================== ソート:レコード番号の配列を返す
func sortF(rFldIdx:Int) -> [Int] {
if cAryType[rFldIdx] == .ZmBool { zSendErrMsgF("ERROR:ZMR_SORT:項目定義のデータ型がBoolでは使用不可"); return [Int]() }
// Step1:指定された項目値とレコード番号のペア配列を要素とする、レコード全体の2次元配列を作る
var iAryAnySort: [[AnyObject]] = [] // ソートで使う2次元配列:データは[[項目値, RecIdx], ...]の形になる
for i in 0..<cRecCount { // i=RecIdx
var iAnyPair: [AnyObject] = [] // 項目値とRecIdxのペアを入れる配列
iAnyPair.append(cAryDataAny[rFldIdx][i]) // 指定された項目の値
iAnyPair.append(i) // RecIdx
iAryAnySort.append(iAnyPair) // ペア配列をソート用配列に追加
}
// Step2:指定された項目値で、2次元配列をソートする:ペア配列の最初の要素を取り出して、大小を比較する
// ペア配列(a0とa1)の最初の要素(a0[0]とa1[0])を取り出して、大小を比較する
switch cAryType[rFldIdx] {
case .ZmInt:
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }
case .ZmFloat:
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Float) < (a1[0] as! Float)) }
case .ZmStr:
// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
(a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }
default:
zSendErrMsgF("ERROR:ZMR_SORT:ここは通らないはず:実行されたらバグあり")
return [Int]()
}
// Step3:2次元配列から、レコード番号だけの配列を作る
var iAryRecIdx: [Int] = []
for i in 0..<cRecCount { // i=RecIdx
let iRexIdx: Int = iAryAnySort[i][1] as! Int // ソートされたRecIdxを取り出す
iAryRecIdx.append(iRexIdx)
}
return iAryRecIdx
}
いつもより長めのコードで、全体が3つの処理に分かれているので、Step1〜3の表記をコメントにつけています。これで、処理の区切りが明確に伝わります。
3つの処理の前に、指定された項目がBool型かを検査します。該当する場合は、エラーメッセージを出して空の配列を返します。空の配列を返すのは、戻り値をOptional型にしたくないからです。
3つのステップを順番に見ていきましょう。
まずは、ステップ1です。1つの配列をソートするだけでは、並び替えたレコード番号が得られません。また、項目値を入れたAnyObject配列には、レコード番号が入っていません。
レコード番号と一緒にソートできる配列を、新しく用意する必要があります。ソートする項目値とレコード番号をペアにして配列に入れ、それをレコード数の分だけ並べます。その集まりを、ペアの中の項目値でソートすると、レコード番号も一緒に並び替えられ、ソート後のレコード番号が得られるというわけです。
つまり、2次元の配列を用意する必要があります。このような配列を作るために、レコード数だけforループさせ、ペアとなる配列iAnyPairを毎回作って、ソート用の配列iAryAnySortへ次々と入れていきます。
続いて、ステップ2です。配列iAryAnySorが出来上がったら、その配列自体をソートします。sortメソッドを使い、値の大きさを判定するための関数(クロージャ)を渡すだけです。ここでは、データ型を指定する必要があるので、項目のデータ型ごとに処理を分けています。
このクロージャの作り方は、少し注意しなければなりません。ソートする配列は、2次元のAnyObject配列です。ですからsortメソッドが取り出して比較するのは、1次元のAnyObject配列になります。そのままだと比較できないので、1次元配列の最初の要素(項目値)を取り出し、Int型にキャストして比べる必要があります。そのため、(a0[0] as! Int)や(a1[0] as! Int)という形になっているわけです。また、引数のデータ型も、ちゃんと[AnyObject]を指定しています。
このsortメソッドが実行されると、2次元配列iAryAnySort内で、ペア配列が項目値の順に並べ替えられます。あとは、順番に取り出すだけです。
ステップ3の取り出しの処理では、戻り値となるレコード番号の配列を作りながら、レコード数の回数だけforループを実行します。ループが終了したら、レコード番号の配列を返して終了です。
今回のメソッドの中で一番理解しづらいのが、ステップ2で使っているsortメソッドのクロージャでしょう。普通の配列をソートするのではなく、2次元配列をソートしていますから。
処理内容が少しでも理解しやすいようにと、クロージャはかなり親切に書いています。何も省略しない書き方なので、これが精一杯でしょう。あとはコメントで補足するしかありません。コメントも、短いながら丁寧に書きました。
// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }
// 省略した書き方
iAryAnySort.sort { ($0[0] as! Int) < ($1[0] as! Int) }
逆に省略した書き方も可能です。でも今回のような使い方では、クロージャ内の引数のデータ型が書かれていないと、前の処理を見ながらデータ型を調べる必要があります。結果として、何をしているのか理解しづらくなります。
少し変わった形でソートする場合は、sortメソッドに渡すクロージャを、できるだけ理解しやすく(省略なしで)書いたほうが良いと思います。もちろん、コメントでの補足も忘れずに。
ステップ2のソートの処理では、String型だけが違う書き方になっています。
他のデータ型のような不等号だけでは、期待したようにソートされず、使いものになりません。英字には大文字と小文字があり、それらを区別してソートしてしまうからです。さらに、日本語にも全角の英字が含まれ、それにも大文字と小文字があります。これらも区別してソートしてしまいます。数字の全角と半角も同様に、区別してソートしてしまいます。
望まれているソートの形は、もっと違うものです。半角英字の大文字と小文字、全角英字の大文字と小文字、それら4つの種類を区別しないで、同じ英字として処理してくれるソートなのです。
このような目的のために、ローカライズされた文字比較の機能が用意されていて、String型で使えます。具体的にはlocalizedCaseInsensitiveCompareメソッドで、比較したい文字列を渡し、判定結果がNSComparisonResult型で返ってきます。
2種類の比較方法を、あえて並べて書くと、次のようになります。
// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて区別するソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! String) < (a1[0] as! String)) }
// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
(a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }
今回のソート機能では、下側のほうを使いました。これで、英字の大文字や小文字が含まれていても、全角の英字や数字が含まれていても、アルファベット順や数字順で正しくソートしてくれます。
String用ソートでも、できるだけ理解しやすいようにと、省略しない形で記述しています。逆に、少し短くした記述方法も可能です。今回の場合は、あまり短くなりませんが。
// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
(a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }
// 省略した書き方
iAryAnySort.sort { ($0[0] as! String).localizedCaseInsensitiveCompare($1[0] as! String) == NSComparisonResult.OrderedAscending }
String用の場合も、少し変わった形のソートですから、省略した記述は避けたほうが良いでしょう。
一応、使用例も紹介します。非常に簡単です。いつものように、iOS実験専用アプリから抜き出しました。
// ソート用メソッドを使ってみる
let iAryKey1: [Int] = modelRecCard25.sortF(fNumber_i) // Int型の項目でソート
let iAryKey2: [Int] = modelRecCard25.sortF(fRate_f) // Float型の項目でソート
let iAryKey3: [Int] = modelRecCard25.sortF(fName_s) // String型の項目でソート
メソッドの引数として、項目番号を指定します。項目番号を間違えないようにと、項目番号に名前をつけた変数(定数)を使うのが基本です。
どの項目のソートでも、たった1行書くだけで、ソート結果が得られます。昇順に並んだレコード番号の配列です。あとは、そのレコード番号を使ってモデルクラスにアクセスするだけです。
今回の機能追加により、記録カードのモデルクラスには、各項目でのソート機能が追加できました。少し前に検索機能も追加していますから、基本的な機能として、検索とソートが揃ったことになります。他に、ファイルへの保存や、タブ区切りテキストの生成もあり、最低限の機能は満たせたと思います。
あとは実際のアプリで使ってみて、不足する機能を追加するだけです。十分に使い物になると判断していますから、積極的に使いたいと思います。過去に作ったアプリでも、機能追加の際に、このモデルクラスと置き換えてみようかと考えています。
ライブラリ化したモデルクラスは2つに増えましたから、3つ目のモデルクラスとなるモデル候補を物色中です。もし変わったモデルクラスが出来上がったら、また紹介したいと思います。
(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)
0 件のコメント:
コメントを投稿