大化通信

大化物流開発合同会社の社員から技術発信をしていきます

成績ランク算出プログラムを設計・製造してみた(プログラム実装編)

はじめに

どうも、迷走星人です。 2回目はプログラム実装の話を出来たらと思います。 尚、この段階では以下の工程が完了しているものとします。

  • プログラムの設計&レビュー

開発環境

今回のプログラム開発の環境を以下にまとめます。

ツール一覧 用途
Visual Studio 2019 Commnuity ソースコードの編集
・プログラムのデバッグ
・プログラムテスト
WinMerge 差分確認
TortoiseSVN バージョン管理

いずれのツールもプログラム開発をする時に開発者が重用しているソフトになりますので、各ツールの詳細はこの記事では省略します。
一応、それぞれの公式HPを以下に掲載しておきます。

開発準備

  1. レポジトリの作成 開発するプロジェクトのソースコードのバージョン管理のためのレポジトリを作成します。 任意の空のフォルダを作成し、その下でTortoiseSVNの「ここにレポジトリを作成」を選択します。 レポジトリを作成すると、下図のようなフォルダ・ファイルが生成されます。

  2. チェックアウト レポジトリが作成出来たら、次は開発するプログラム用にレポジトリをチェックアウトします。 開発用のフォルダを用意し、その下に1.で作成したレポジトリをチェックアウトします。

  3. プロジェクトの作成 チェックアウトが出来たら、開発用のプロジェクトを作成します。 今回の開発では空のプロジェクトを作成し、その中にソースコード等を追加していきます。

コーディング

開発準備が整ったら、いよいよコーディング作業に入ります。 ここからは実際にプログラムの機能を実現するためにソースコードを追加・編集していきます。 尚、ソースコードを追加・編集した時はその都度SVNにコミットし、変更履歴として残していくようにします。 後になって手戻り作業が発生した時、残していた履歴を辿ってリカバリ出来るようにするためです。 コミットする目途としては、

  • 新しいファイル・関数の追加
  • ロジックの大きな変更

を行った時等です。勿論、自分の好みのタイミングでコミットしても問題ありません。 今回のプログラムでは以下のソースコードを追加・編集していきます。

ソースコード ファイル名称 内容
main.cpp プログラム本体ファイル プログラム本体を実装する。
GetGradeRankCom.h 共通ヘッダーファイル プログラム共通で使用する定義、構造体を記載する。
ClassFileReadWrite.h ファイル読み書き機能クラスファイル(*.h) ファイル読み書き機能の関数を宣言する。
ClassFileReadWrite.cpp ファイル読み書き機能クラスファイル(*.cpp) ファイル読み書き機能の関数の実態部分を記載する。
ClassCalcGradeRank.h 成績ランク算出機能クラスファイル(*.h) 成績ランク算出機能の関数を宣言する。
ClassCalcGradeRank.cpp 成績ランク算出機能クラスファイル(*.cpp) 成績ランク算出機能の関数の実態部分を記載する。

変数定義

プログラムの中で使う定義値を以下の様に設定します。

定義名 定義値 用途
COM_SUCCESS 0 戻り値(成功)
COM_FAILED -1 戻り値(失敗)
COM_MODER 0 読込
COM_MODEW 1 書込

構造体宣言

プログラムの中で使う構造体を以下の様に設定しています。

structGradeRank
メンバー変数 データ型 用途
name char配列 名前
grade char配列 点数
rank char配列 成績

構造体のメンバ変数は全部char配列の形で持たせ、各機能の内部処理で必要に応じてキャストして使用していきます。

関数実装

定義値、構造体が設定できたら、次は関数の実装に入ります。 プログラム設計に書いた仕様に基づき、ソースコード上に実装していきます。

ファイル読み書き機能(関数名:FileReadWrite)
項目 名称 データ型 用途 IN・OUT
引数 1 file char * ファイルパス IN
2 mode int モード
・COM_MODER:読込
・COM_MODEW:書込
IN
3 list vector<structGradeRank> 配列データ
(名前、点数、ランク)
IN/OUT
戻り値 COM_SUCCESS int 成功 OUT
COM_FAILED 失敗
成績ランク算出機能(関数名:CalcGradeRank)
項目 名称 データ型 用途 IN・OUT
引数 1 list vector<structGradeRank> 配列データ
(名前、点数、ランク)
IN/OUT
戻り値 COM_SUCCESS int 成功 OUT
COM_FAILED 失敗
実際の実装(main.cpp)
/// <summary>
/// 成績ランク算出プログラムメイン関数
/// </summary>
/// <param name="argc">コマンドライン引数の数</param>
/// <param name="argv">コマンドライン引数(結果ファイル)</param>
/// <returns>戻り値(COM_SUCCESS:成功、COM_FAILED:失敗)</returns>
int main(int argc, char **argv)
{
    // ファイル名
    char FPath[BUFSIZE1024];   memset(FPath, 0x00, BUFSIZE1024);
    // 配列データ
    vector<structGradeRank> ArrGradeRank;    ArrGradeRank.clear();
    // 戻り値
    int Ret = COM_SUCCESS;

    cout << "大化技術通信用プログラム開始" << endl;

    // インスタンスを生成
    ClassFileReadWrite clsFileReadWrite;
    ClassCalcGradeRank clsCalcGradeRank;

    // カレントディレクトリパスを取得
    TCHAR DName[MAX_PATH];
    GetCurrentDirectory(MAX_PATH, DName);
    WideCharToMultiByte(CP_ACP, 0, DName, -1, FPath, BUFSIZE1024, NULL, NULL);
    cout << "カレントディレクトリ:"  << FPath << endl;

    // ファイルの存在チェック
    string FAbsPath = FPath;
    FAbsPath += "\\";
    FAbsPath += argv[1];   // 結果ファイル名
    struct stat statFile;
    cout << "FAbsPath:" << FAbsPath << endl;
    // 結果ファイルのステータス情報取得結果で判定
    if (stat(FAbsPath.c_str(), &statFile) != 0)
    {
        cout << "ファイルがプログラムと同じ場所にありません。" << endl;
        return COM_FAILED;
    }
    memset(FPath, 0x00, BUFSIZE1024);
    strcpy_s(FPath, FAbsPath.c_str());

    // ファイル読込
    Ret = clsFileReadWrite.FileReadWrite(FPath, COM_MODER, ArrGradeRank);
    if (COM_SUCCESS != Ret)
    {
        cout << "ファイル読込に失敗しました。" << endl;
        return Ret;
    }

    // 成績ランク算出
    Ret = clsCalcGradeRank.CalcGradeRank(ArrGradeRank);
    if (Ret != COM_SUCCESS)
    {
        cout << "成績ランク算出に失敗しました。" << endl;
        return Ret;
    }

    // ファイル書込
    Ret = clsFileReadWrite.FileReadWrite(FPath, COM_MODEW, ArrGradeRank);
    if (COM_SUCCESS != Ret)
    {
        cout << "ファイル書込みに失敗しました。" << endl;
        return Ret;
    }

    cout << "大化技術通信用プログラム終了" << endl;

    return Ret;
}
ソースコードの解説
  • 引数で結果ファイル名を指定し、GetCurrentDirectory関数でカレントディレクトリのパスを取得しています。
    ディレクトリのパスはTCHAR型で取得しているので、char型配列のFPathに設定するためにWideCharToMultiByte関数を呼出して設定しています。
  • カレントディレクトリのパスとコマンドライン引数で指定した結果ファイルを結合させ、stat関数を呼出して結果ファイルのステータス情報取得結果の判定でプログラムと同じディレクトリに格納されているか確認しています。
  • ファイル読み書き機能、成績ランク算出機能はクラスファイルで実装しているので、インスタンスを生成後に関数を呼び出しています。
実際の実装(ClassFileReadWrite.cpp)
/// <summary>
/// ファイル読み書き機能
/// </summary>
/// <param name="fname">結果ファイルのファイル名</param>
/// <param name="mode">モード(0:読込、1:書込)</param>
/// <param name="list">配列データ(点数, 成績ランク)</param>
/// <returns>戻り値(0:成功、-1:失敗)</returns>
int ClassFileReadWrite::FileReadWrite(char* file, int mode, std::vector<structGradeRank>& list)
{
    int i;
    char fname[BUFSIZE1024];
    std::string tmpBuf;

    // NULLチェック
    if (NULL == file)
    {
        return COM_FAILED;
    }
    strcpy_s(fname, BUFSIZE1024, file);

    if (COM_MODER == mode)
    {
        // 読込
        std::ifstream ifs;
        std::string r_ss;
        std::vector < std::string > vec;    // 切り出し文字列格納関数
        size_t current;                     // 文字列切り出し開始位置
        size_t found;                       // 文字列切り出し終了位置

        // ファイルオープン
        ifs.open(fname, std::ios::in);
        if (!ifs)
        {
            // ファイルオープン失敗
            return COM_FAILED;
        }

        // データ読み取り
        while (std::getline(ifs, tmpBuf))
        {
            if (tmpBuf.empty()) continue;
            structGradeRank data;
            memset(&data, 0x00, sizeof(structGradeRank));

            // 行データ分割
            // 文字列から半角カンマ「,」を検索し、見つかった箇所までの文字列を抜き出す
            vec.clear();
            current = 0;
            found = tmpBuf.find_first_of(',');
            while (current < found)
            {
                // 文字列切り出し
                std::string subStr(tmpBuf, current, found - current);
                vec.push_back(subStr);

                current = found + 1;
                found = tmpBuf.find_first_of(',', current);

                if (std::string::npos == found)
                {
                    found = tmpBuf.size();
                }
            }

            // 分割成功(リストの要素数が2以上)
            if (2 <= vec.size())
            {
                // リストに格納
                strcpy_s(data.name, vec.at(0).c_str());
                strcpy_s(data.grade, vec.at(1).c_str());
                list.push_back(data);
            }
        }

        // ファイルクローズ
        ifs.close();
    }
    else if (COM_MODEW == mode)
    {
        // 書込
        std::ofstream ofs;

        // ファイルオープン
        ofs.open(fname, std::ios::out);
        if (!ofs)
        {
            // ファイルオープン失敗
            return COM_FAILED;
        }

        // データ書込み
        for (i = 0; i < list.size(); i++)
        {
            ofs << list.at(i).name << "," << list.at(i).grade << "," << list.at(i).rank << std::endl;
        }

        // ファイルクローズ
        ofs.close();
    }
    else
    {
        return COM_FAILED;
    }

    return COM_SUCCESS;
}
ソースコードの解説
  • ファイルのオープンクローズは「open/close」関数を用いています。
  • 実装した関数の第2引数(変数名:mode)で読込/書込を制御するようにしています。
  • 読込時、ファイル中のデータは「getline」関数を使い、行単位でデータを取得しています。データが取れなくなれば、読込処理を終了します。
  • 読込時のファイル中の行方向の分割は半角カンマ「,」を区切り文字として以下の処理フローで行っています。区切り文字の見つけ方には「find_first_of」関数を使用しています。

行データ分割処理フロー図初期化現在位置(current=0)検出位置(found)を取得(find_first_of関数)文字列切り出し(subStr関数)現在位置更新検出位置(found)を取得(find_first_of関数)検出位置(found)を文字列のサイズに更新yes行の末尾に到達?検出ループリストに格納名前成績yes分割成功?

  • 書込時は、リストに格納したデータを「名前」「成績」「ランク」の順に半角カンマ「,」を間に挟みながら書込を行います。
実際の実装(ClassCalcGradeRank.cpp)
/// <summary>
/// 成績ランク算出機能
/// </summary>
/// <param name="list">配列データ(点数, 成績ランク)</param>
/// <returns>戻り値(0:成功、-1:失敗)</returns>
int ClassCalcGradeRank::CalcGradeRank(std::vector<structGradeRank>& list)
{
    int i;

    // 配列チェック
    if (0 == list.size())
    {
        return COM_FAILED;
    }

    // 成績ランク算出
    for (i = 0; i < list.size(); i++)
    {
        std::string strVal = list.at(i).grade;
        try
        {
            // string->doubleに変換
            double dVal = stod(strVal);

            if ((90 <= dVal) && (dVal <= 100))
            {
                list.at(i).rank = 'A';
            }
            else if ((75 <= dVal) && (dVal < 90))
            {
                list.at(i).rank = 'B';
            }
            else if ((60 <= dVal) && (dVal < 75))
            {
                list.at(i).rank = 'C';
            }
            else if ((0 <= dVal) && (dVal < 60))
            {
                list.at(i).rank = 'D';
            }
            else
            {
                list.at(i).rank = 'E';
            }
        }
        catch (...)
        {
            list.at(i).rank = 'E';
        }
    }
    
    return COM_SUCCESS;
}
ソースコードの解説
  • 配列リストがNULL(サイズが0)の時、成績ランク機能は処理を中断し、戻り値「COM_FAILED」を返すようにします。
  • 成績ランク算出機能のループ処理の中で、string型からdouble型に変換する時、0~100点の範囲内にある実数の場合は値に応じて成績ランクを算出します。それ以外は全て例外処理で成績ランク「E」を付けるように実装しています。

コードレビュー

ソースコードの実装が一通り完了したら、ソースコードのレビューをしてもらいます。 レビュー時はソースコード実装時にコミットしていた変更分、あるいは、変更前後のソースコードを比較してその差分を説明しながらレビューアーに説明していきます。 そこでレビューアーから指摘を受けた場合は、その指摘事項を記録し、レビュー後ソースコードに反映します。 大抵は専用のレビューシートが用意され、指摘事項はそこに記述されていきます。 指摘事項を全て反映した後、再レビューの依頼を発行し、指摘事項の反映を確認してもらえたらシートにレビューアーからのフォロー(承認)を記載してもらいます。

次回予告

ここまでプログラム実装について発表しました。 次回は以下のタイトルで続きを発表して行こうと思います。

  • 成績ランク算出プログラムを設計・製造してみた(プログラム試験設計編)