
競技プログラミング (競プロ) というと、C++ などの言語で複雑なアルゴリズムを実装するイメージを持つ方が多いと思います。
業務システムの開発において、高い生産性を誇る PowerBuilder で競プロを解くとどうなるか気になりませんか?
PowerBuilder の特長である DataWindow (DataStore) を駆使し、実装がどれほど楽になるかご覧ください。
EC サイトの購入ログ (CSV 形式) が N 件あります。
各ログは、
の 3 要素で構成されています。
指定された対象カテゴリ C について、顧客 ID ごとの合計購入金額を計算し、合計購入額が K 円以上の顧客を抽出したランキングを出力してください。
【入力例 (CSV ファイル)】
【入力データの制約】
【条件パラメーター】
【出力ファイル (CSV ファイル)】
PowerBuilder で競プロで出題されるような問題を解くには、メモリー上で強力なデータベース操作を行える DataWindow (DataStore) を活用します!
DataWindow は手動での作成になりますが、これを使うことでプログラミングの手間が劇的に減ります。
データベースに接続せずメモリー上で完結させるため、事前に External (外部) データソース の DataWindow オブジェクトを 2 個作成します。
① 明細用 DataWindow (d_cust_sales)
cust_id (String) : 顧客 ID
category (String): カテゴリ
amount (Long) : 購入金額
② 出力 DataWindow (d_summary)
cust_id (String) : 顧客 ID
total_amount (Long) : 合計購入金額
競プロの要件を満たすコードを関数として実装しました。
DataStore の強力なメソッド群を活用することで、非常にすっきりと書くことができます。
【PowerBuilder ソースコード】
/*
関数 : f_process_leaderboard_csv
引数 :
string as_input_csv - 入力CSVファイルパス (例: "C:\data\input_log.csv")
string as_output_csv - 出力CSVファイルパス (例: "C:\data\output_result.csv")
string as_target_cat - 対象カテゴリ C (例: "Food")
long al_k - 顧客ごとの合計購入金額の閾値 K (例: 5000)
long al_m - 抽出件数 M (例: 2)
戻り値
integer (1: 成功, -1: 処理失敗)
*/
datastore lds_ds, lds_summary
long ll_import_rows, ll_row, ll_new_row, ll_summary_rows
string ls_prev_cust = "", ls_curr_cust
longlong lll_current_sum = 0
// =================================================================
// 1. データの取り込み
// =================================================================
// 入力データ (ログ)CSVファイル用の DataStore 生成
lds_ds = create datastore
lds_ds.dataobject = "d_cust_sales"
// 出力用 (回答) DataStore の生成
lds_summary = create datastore
lds_summary.dataobject = "d_summary"
// 入力データからログデータを一括取り込み
ll_import_rows = lds_ds.ImportFile(CSV!, as_input_csv)
if ll_import_rows <= 0 then
destroy lds_ds
destroy lds_summary
return -1
end if
// =================================================================
// 2. 対象カテゴリの抽出
// =================================================================
// 対象カテゴリ (C) のみ抽出
lds_ds.SetFilter("Trim(category) = '" + as_target_cat + "'")
lds_ds.Filter()
if lds_ds.RowCount() = 0 then
lds_summary.SaveAs(as_output_csv, CSV!, true)
destroy lds_ds
destroy lds_summary
return 1
end if
// 顧客 ID を昇順でソート
lds_ds.SetSort("cust_id A")
lds_ds.Sort()
// =================================================================
// 3. 合計購入金額の集計
// =================================================================
// N 件の入力データをループし、顧客ごとに合計購入金額を集計
for ll_row = 1 to lds_ds.RowCount()
ls_curr_cust = lds_ds.GetItemString(ll_row, "cust_id")
// 読み込む顧客が変更されたとき(2行目以降)、直前顧客の合計購入金額をチェック
if ll_row > 1 and ls_curr_cust <> ls_prev_cust then
// 合計購入金額が K に達した顧客 ID と 合計購入金額を出力用 DataStore に格納する
if lll_current_sum >= al_k then
ll_new_row = lds_summary.InsertRow(0)
lds_summary.SetItem(ll_new_row, "cust_id", ls_prev_cust)
lds_summary.SetItem(ll_new_row, "total_amount", lll_current_sum)
end if
lll_current_sum = 0 // 合計購入金額をリセット
end if
ls_prev_cust = ls_curr_cust
// 購入金額を加算する
lll_current_sum += lds_ds.GetItemNumber(ll_row, "amount")
// 最終行の顧客に対する判定と格納
if ll_row = lds_ds.RowCount() and lll_current_sum >= al_k then
ll_new_row = lds_summary.InsertRow(0)
lds_summary.SetItem(ll_new_row, "cust_id", ls_prev_cust)
lds_summary.SetItem(ll_new_row, "total_amount", lll_current_sum)
end if
next
// =================================================================
// 4. 集計結果をソート、抽出
// =================================================================
// ソート: 合計金額の降順 > 顧客IDの昇順
lds_summary.SetSort("total_amount D, cust_id A")
lds_summary.Sort()
// 上位 M 件を超える余分な行を削除
ll_summary_rows = lds_summary.RowCount()
if ll_summary_rows > al_m then
for ll_row = ll_summary_rows to al_m + 1 step -1
lds_summary.DeleteRow(ll_row)
next
end if
// =================================================================
// 5. 出力
// =================================================================
if lds_summary.SaveAs(as_output_csv, CSV!, true) <> 1 then
destroy lds_ds
destroy lds_summary
return -1
end if
destroy lds_ds
destroy lds_summary
return 1
要件を満たしているかを網羅的に検証するため、以下の境界値・特殊パターンを含めたテストデータで検証を行います。
下記のテスト観点でデータを作成します。
【各データ行のテスト観点】
| 顧客 ID | カテゴリ | 購入金額 | 合計額 | テストの検証目的・境界値観点 |
|---|---|---|---|---|
| C001 | Food | 2,000 + 3,000 |
5,000円 | 先頭行・ちょうど K 円:複数回の合計がちょうど閾値 K (5,000円) に達するパターン。 |
| C002 | Food | 4,999 | 4,999円 | 閾値未満 (K-1 円):1 円足らず閾値未満となるため、集計から除外されるか。 |
| C003 | Food | 5,001 | 5,001円 | 閾値超過 (K+1 円):1 円超えて条件を満たすパターン。 |
| C004 | Food | 1,000 + 4,000 |
5,000円 | 同額タイ (ID昇順比較): C001 と同額。顧客 ID 順 (C001 < C004) で正しくソートされるか。 |
| C005 | Food | 8,000 | 8,000円 | 単発高額: 1 回の購入で大幅超過。単独 1 位として評価されるか。 |
| C006 | Food | 5,000 | 5,000円 | 上位 M 件のカットオフ:同額 (5,000円) グループの中で、上位 M=3 件の枠にあふれて落選するか。 |
| C007 | Book | 10,000 | 10,000円 | 異カテゴリフィルター:金額は高いが対象外カテゴリ (Book) のため除外されるか。 |
| C008 | Food | 1,500 + 3,500 |
5,000円 | 最終行処理:ファイルの末尾に存在するデータが漏れずに集計対象となるか (先ほどの修正箇所の検証)。 |
テスト観点を基にして作成した、テストデータが下記になります。
【入力テストデータ】
条件パラメーター: カテゴリ C = “Food”, 閾値 K = 5000, 抽出数 M = 3
テストデータを作成し、条件パラメーターで関数を実行すると、以下の通り期待値通りの CSV ファイルが出力されます!
【出力結果 (CSV ファイル)】
要件の通り、
最高額の C005 (8,000 円) が 1 位
続いて C003 (5,001 円) が 2 位
同額 5,000 円グループ (C001, C004, C006, C008) の中から、ID 辞書順で最も若い C001 が 3 位として出力されることが確認できました。
アルゴリズムを一から組む競プロ本来の趣旨とは一味異なりますが、
与えられた要件を、少ないコード量と最短の時間で正確に満たすという開発実践の観点において、PowerBuilder の圧倒的な生産性を再確認できる面白い試みとなりました。
みなさんもぜひ、身近な業務ツールでいろいろな課題にチャレンジしてみてください!