CLOSE

競技プログラミング (競プロ) というと、C++ などの言語で複雑なアルゴリズムを実装するイメージを持つ方が多いと思います。
業務システムの開発において、高い生産性を誇る PowerBuilder で競プロを解くとどうなるか気になりませんか?
PowerBuilder の特長である DataWindow (DataStore) を駆使し、実装がどれほど楽になるかご覧ください。

問題:顧客別購入ログのランキング集計

EC サイトの購入ログ (CSV 形式) が N 件あります。
各ログは、

  • 顧客 ID
  • カテゴリ
  • 購入金額

の 3 要素で構成されています。
指定された対象カテゴリ C について、顧客 ID ごとの合計購入金額を計算し、合計購入額が K 円以上の顧客を抽出したランキングを出力してください。

【入力例 (CSV ファイル)】

  • C002,Food,3000
  • C001,Food,4000
  • C002,Food,3000
  • C003,Book,9000
  • C001,Food,2000
  • C004,Food,1000

【入力データの制約】

  • 1 ≤ N ≤ 10,000
  • 1 ≤ K ≤ 1,000,000
  • 顧客 ID, カテゴリは半角英数字 (10 文字以内)
  • 購入金額は 1 以上 100,000 以下の整数

【条件パラメーター】

  • 対象カテゴリ C: “Food”
  • 閾値 K: 5000
  • 抽出件数 M: 3

【出力ファイル (CSV ファイル)】

  • cust_id,total_amount
  • C002,6000
  • C001,6000

PowerBuilder ならどう解く?

PowerBuilder で競プロで出題されるような問題を解くには、メモリー上で強力なデータベース操作を行える DataWindow (DataStore) を活用します!
DataWindow は手動での作成になりますが、これを使うことでプログラミングの手間が劇的に減ります。

  • CSV パース → ImportFile() 1 行で一括メモリ読み込み (ループパース不要)
  • カテゴリ抽出 → SetFilter() + Filter() で一括絞り込み
  • ソート → SetSort() + Sort() で一括並び替え
  • CSV 出力 → SaveAs() 1 行でファイル書き出し完了

事前準備 (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) : 合計購入金額

PowerBuilder での実装

競プロの要件を満たすコードを関数として実装しました。
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

  • C001,Food,2000
  • C002,Food,4999
  • C007,Book,10000
  • C005,Food,8000
  • C001,Food,3000
  • C004,Food,1000
  • C004,Food,4000
  • C006,Food,5000
  • C003,Food,5001
  • C008,Food,1500
  • C008,Food,3500

出力結果

テストデータを作成し、条件パラメーターで関数を実行すると、以下の通り期待値通りの CSV ファイルが出力されます!

【出力結果 (CSV ファイル)】

  • cust_id, total_amount
  • C005, 8000
  • C003, 5001
  • C001, 5000

要件の通り、
最高額の C005 (8,000 円) が 1 位
続いて C003 (5,001 円) が 2 位
同額 5,000 円グループ (C001, C004, C006, C008) の中から、ID 辞書順で最も若い C001 が 3 位として出力されることが確認できました。

まとめ

アルゴリズムを一から組む競プロ本来の趣旨とは一味異なりますが、
与えられた要件を、少ないコード量と最短の時間で正確に満たすという開発実践の観点において、PowerBuilder の圧倒的な生産性を再確認できる面白い試みとなりました。

みなさんもぜひ、身近な業務ツールでいろいろな課題にチャレンジしてみてください!

x instagram facebook youtube