2015年7月2日木曜日

共通モデルのライブラリ化を試作(機能追加編2)

共通で使えるモデルクラスをライブラリ化する第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 件のコメント:

コメントを投稿