LoginSignup
4

More than 5 years have passed since last update.

posted at

updated at

【paiza】出題解答用の自分用ユーティリティを作った【C#】

paizaを始めたので、自分用のユーティリティを作ってみました。(C#用)

改訂メモ:

  • 【version1】標準入力からのパラメータ取得を yield return でスルッと書ける機能を実装。
  • 【version2】標準入力を介さずテストデータを流し込む為のドライバ実装に差し替えられるように機能改良。
  • 【version3】複雑なパラメータ入力を処理する為の追加機能を実装。
  • 【version4】良く出て来る入力パラメータの文字列分割、及びパースの為のユーティリティを追加。

【version1】標準入力から出題パラメータを取得する処理(yield return 方式)

実際はクラスファイル分け出来ないので、この static class を問題解答クラスの inner class としてコピペして使用してます。
inner class に持って行っても PaizaUtility.XxxXxx() って呼び出し形式は変わらないので、ローカルのIDEである程度コーディングしてからそれを張り付けて回答してます。

PaizaUtility.cs

    /// <summary>
    /// paiza の出題回答に使用する汎用的なユーティリティ処理を纏める。
    /// </summary>
    public static class PaizaUtility
    {
        /// <summary>
        /// 問題の入力データ取得(入力データ行数が未定の場合)
        /// </summary>
        /// <remarks>
        /// 入力データ数が予め定まっていない(入力データ数が可変)場合、
        /// 一行目にデータ数が入力され、その後、指定された数だけデータ行の入力が行われる。
        /// 一行目が数値に parse 出来なかった場合は、エラーとする。
        /// なお、一行目で入力された数と異なる行数が入力されるケースは出題の本旨と異なるため想定しない。
        /// </remarks>
        /// <returns>入力データの列挙</returns>
        public static IEnumerable<string> ReadArgs()
        {
            // 最初の入力が、後続するデータの行数になるらしい。
            string x = Console.ReadLine();
            int n = int.Parse(x);

            // 入力された行数ぶん、データ行の読み込みを行う。
            return ReadArgs( n );
        }
        /// <summary>
        /// 問題の入力データ取得(予め入力行数が解っているパターン)
        /// </summary>
        /// <param name="n">入力行数</param>
        /// <returns>入力データの列挙</returns>
        public static IEnumerable<string> ReadArgs(int n)
        {
            // 入力された行数ぶん、データ行の読み込みを行う。
            for ( int i = 0; i < n; i++ )
            {
                string s = Console.ReadLine();

                yield return s;
            }
        }
    }

【version2】ReadLine、WriteLineをプロキシするため、内部実装を変更。

PaizaUtility.cs(ITestIOインタフェースを追加、内部処理をリファクタリング)
    /// <summary>
    /// paiza の出題回答に使用する汎用的なユーティリティ処理を纏める。
    /// </summary>
    public static class PaizaUtility
    {
        /// <summary>
        /// プロキシパターンでIOの向き先を変える為のインタフェース
        /// </summary>
        public interface ITestIO
        {
            string ReadLine();
            void WriteLine( string line );
        }
        /// <summary>
        /// ITestIO のデフォルト実装。
        /// </summary>
        public class ConsoleProxy : ITestIO
        {
            string ITestIO.ReadLine()
            {
                return Console.ReadLine();
            }

            void ITestIO.WriteLine( string line )
            {
                Console.WriteLine( line );
            }
        }

        public static ITestIO IO { get; set; } = new ConsoleProxy();

        public static string ReadLine()
        {
            return IO.ReadLine();
        }

        public static void WriteLine( string line )
        {
            IO.WriteLine( line );
        }

        /// <summary>
        /// 問題の入力データ取得(入力データ行数が未定の場合)
        /// </summary>
        /// <remarks>
        /// 入力データ数が予め定まっていない(入力データ数が可変)場合、
        /// 一行目にデータ数が入力され、その後、指定された数だけデータ行の入力が行われる。
        /// 一行目が数値に parse 出来なかった場合は、エラーとする。
        /// なお、一行目で入力された数と異なる行数が入力されるケースは出題の本旨と異なるため想定しない。
        /// </remarks>
        /// <returns>入力データの列挙</returns>
        public static IEnumerable<string> ReadArgs()
        {
            // 最初の入力が、後続するデータの行数になるらしい。
            string x = IO.ReadLine();
            int n = int.Parse(x);

            // 入力された行数ぶん、データ行の読み込みを行う。
            return ReadArgs( n );
        }
        /// <summary>
        /// 問題の入力データ取得(予め入力行数が解っているパターン)
        /// </summary>
        /// <param name="n">入力行数</param>
        /// <returns>入力データの列挙</returns>
        public static IEnumerable<string> ReadArgs(int n)
        {
            // 入力された行数ぶん、データ行の読み込みを行う。
            for ( int i = 0; i < n; i++ )
            {
                string s = IO.ReadLine();

                yield return s;
            }
        }
    }

つかいかた。

Program.cs

    class Program
    {
        /// <summary>
        /// <seealso cref="Console.ReadLine"/> の替わりに、固定のテストデータを返す <seealso cref="PaizaUtility.ITestIO"/> 実装
        /// </summary>
        private class TestData : PaizaUtility.ITestIO
        {
            /// <summary>
            /// テストデータ
            /// </summary>
            private readonly string[] lines = @"
// ▼▼ここにテストデータをコピペするのじゃ▼▼

// ▲▲ '//' 開始と空行は無視するから気にするな▲▲
"
                // ↓不要な LINQメソッド式 があれば適当にコメントアウトしてね↓
                .Split(new [] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries )
                .AsEnumerable()
                .Select( s => s.Trim() )
                .Where( s => !string.IsNullOrEmpty(s) )
                .Where( s => !s.StartsWith("//") )
                .ToArray()
            ;

            private int index = 0;

            string PaizaUtility.ITestIO.ReadLine()
            {
                return index < lines.Length ? lines[index++] : null;
            }

            void PaizaUtility.ITestIO.WriteLine( string line )
            {
                Console.WriteLine( line );
            }
        }

        /// <summary>
        /// デフォルトでConsoleに向いているIOを↑のデバッグ用プロキシ実装に差し替える。
        /// </summary>
        static Program()
        {
            // もうちょっとカッコイイ実装(JavaのCDIみたいな)にしたかったけど、paiza用なんでこれで良いよね。
            PaizaUtility.IO = new TestData();
        }

        /* Mainは省略 */
    }

こんな感じで、ITestIOインタフェースを実装したモノをPaizaUtility.IOに設定することで、任意のReadLine/WriteLineにアダプタする事が出来る。
デフォルト実装(PaizaUtility.IOのフィールド初期化)ではConsoleへのプロキシが実装されているので、何もしなければ標準入出力で動作する。

このサンプル実装では、IO.ReadLine呼び出し時に固定のテストデータを順番に返してくれる。
ローカルのIDE環境でのみこの差し替えを実装しておき、paiza回答コードには含めないようにすれば良い。

これで、イチイチコンソールからの手入力でテストデータを流す事なく、テストコード的に組み込む事が出来るようになります。

(*'▽')やったね!

【version3】複雑なパラメータ入力に対応するため、ヘッダレコードのパーサ指定を追加。

登録二日目にランクAの問題を解きに行ったんですが、意外と複雑な入力パラメータ渡して来やがってversion1ベースの実装では対応できなかったので、独自実装したのを抽象化して汎用機能として昇格させてやりました。
後はもう「ヘッダレコードがN行になる」とか「入力パラメータがブロックに分かれてて複数ブロック連続してやって来る」みたいな事さえなければ更なる機能拡張はしなくて良いでしょう、、、。

(*'▽')きっと、たぶん。

PaizaUtility.cs(追加したメソッドのみ)

        /// <summary>
        /// 問題の入力データ取得(複雑なヘッダ形式をしている場合)
        /// </summary>
        /// <param name="parseHeaderRecord">ヘッダレコードを解析し、その後読み込むべきデータ行数を返すコールバックメソッド</param>
        /// <returns>入力データの列挙</returns>
        public static IEnumerable<string> ReadArgs( Func<string, int> parseHeaderRecord )
        {
            string header = IO.ReadLine();
            int n = parseHeaderRecord( header );

            return ReadArgs( n );
        }

上記追加機能を利用したサンプル実装。
※サンプル実装とかは解り易さ重視で実装しているので、実行コストを多少犠牲にして見た目重視でコーディングしてますのよ。

(*'▽')こういうコードが書けるからC#のラムダ式はやめられないぜ!!

Do.cs(出題解答ロジックを実装するだけのクラス)

    public static class Do
    {
        /// <summary>
        /// paiza の出題に回答するロジックを実装する
        /// </summary>
        public static void Answer()
        {
            // 【サンプル実装】ヘッダパーサを独自に指定するパターン。
            // ヘッダで2種類のデータ数が指定され、
            // それらが連続したデータ行として与えられる、みたいなケースを想定。
            // (実際にそんな問題があるのかどうかは知らん)
            int a = 0;
            int b = 0;
            Func<string, int> parser = ( header ) =>
            {
                int[] token = header
                        .Split( new []{ " " }, StringSplitOptions.RemoveEmptyEntries )
                        .AsEnumerable()
                        .Select( x => int.Parse(x) )
                        .ToArray();

                // 二種類のデータ数をそれぞれ控える。
                a = token[0];
                b = token[1];

                // 二種類のデータの合計数を返す(読み込み行数)
                return a + b;
            };
            // ヘッダのパーサを指定して ReadArgs を呼び出す。
            var args = PaizaUtility.ReadArgs( parser ).ToList();

            List<string> argsA = args
                    .Take( a )
                    .ToList();
            List<string> argsB = args
                    .Skip( a )
                    .Take( b )
                    .ToList();

            foreach ( var arg in argsA )
            {
                PaizaUtility.IO.WriteLine( "arg-a: " + arg );
            }
            foreach ( var arg in argsB )
            {
                PaizaUtility.IO.WriteLine( "arg-b: " + arg );
            }
        }
    }
Program.cs

    class Program
    {
        /// <summary>
        /// <seealso cref="Console.ReadLine"/> の替わりに、固定のテストデータを返す <seealso cref="PaizaUtility.ITestIO"/> 実装
        /// </summary>
        private class TestData : PaizaUtility.ITestIO
        {
            /// <summary>
            /// テストデータ
            /// </summary>
            private readonly string[] lines = @"

// ▼▼ここにテストデータをコピペするのじゃ▼▼
2 3
アフリカオオコノハズク
ワシミミズク
そうです
我々は
賢いので。
// ▲▲ '//' 開始と空行は無視するから気にするな▲▲

"
                // ↓不要な LINQメソッド式 があれば適当にコメントアウトしてね↓
                .Split(new [] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries )
                .AsEnumerable()
                .Select( s => s.Trim() )
                .Where( s => !string.IsNullOrEmpty(s) )
                .Where( s => !s.StartsWith("//") )
                .ToArray()
            ;

            private int index = 0;

            string PaizaUtility.ITestIO.ReadLine()
            {
                return index < lines.Length ? lines[index++] : null;
            }

            void PaizaUtility.ITestIO.WriteLine( string line )
            {
                Console.WriteLine( line );
            }
        }

        /// <summary>
        /// デフォルトでConsoleに向いているIOを↑のデバッグ用プロキシ実装に差し替える。
        /// </summary>
        static Program()
        {
            // もうちょっとカッコイイ実装(JavaのCDIみたいな)にしたかったけど、paiza用なんでこれで良いよね。
            PaizaUtility.IO = new TestData();
        }

        /// <summary>
        /// ぶっちゃけ例外処理要らないみたいだから Do.Answer() だけ移植でも良い。
        /// </summary>
        static void Main( string[] args )
        {
            try
            {
                Do.Answer();
            }
            catch ( Exception ex )
            {
                Console.WriteLine( ex.Message );
            }
        }
    }

Doクラスは特に深い意味は無いんだけど、version2で追加したIOのデフォルト実装差し替えを回答コードに紛れ込ませないように、回答コードだけを独立して実装する場所が欲しかったので作りました。
回答時は、DoクラスとPaizaUtilityクラスを両方 inner class としてコピペして、Do.Answer();呼び出しだけ組み込めば良いと言う手順になります。

ちなみに上記サンプルコードを実行すると、以下のような出力が得られます。

arg-a: アフリカオオコノハズク
arg-a: ワシミミズク
arg-b: そうです
arg-b: 我々は
arg-b: 賢いので。

【version4】入力パラメータのスペース分割、及び分割トークンのパーサを楽に指定するためのユーティリティ拡張。

かなり頻繁に出て来るっぽい定型コードなので固めといたよ。(*'▽')g

PaizaUtility(追加分のみ抜粋、コメント削除)

        #region 追加コード
        public static string[] SplitBy(
                string s, 
                params string[] by )
        {
            return SplitBy( s, StringSplitOptions.None, by );
        }
        public static string[] SplitBy( 
                string s, 
                StringSplitOptions option,
                params string[] splitter )
        {
            return s.Split( splitter, option );
        }

        public static string[] SplitSpace( string s )
        {
            return SplitBy( s, " " );
        }
        public static IEnumerable<T> SplitAs<T>( string s, Func<string, T> parser )
        {
            return SplitSpace( s ).Select( parser );
        }
        public static IEnumerable<int> SplitAsInt( string s )
        {
            return SplitAs( s, int.Parse );
        }
        #endregion

実装コード見れば大体用途は解ると思うので、解説は特にしないよ。

(*'▽')もし必要なら編集リクエストでも送ってね!!

ソリューション一式はこちら

VS-Git のクローンURL:
https://ellnore-git.visualstudio.com/_git/PaizaFramework

GitHubのリポジトリURL:
https://github.com/sugaryo/CS-PaizaFramework

paiza 規約に関して【重要】

paiza の利用規約(本稿記載の2017/17/25時点のもの)に関して。

利用規約「第3章 paiza」の「第12条(禁止行為)」より引用抜粋。

(3)ブログ・SNS等の各種媒体(インターネット媒体に限られず、不特定多数が閲覧可能なものを全て含む。)上において、当社がpaizaで出題した問題の内容、当該問題に対する解答、解答へのヒント等の示唆およびカンニング等の不正を助長する内容等を掲載する行為。

本禁止事項に関して、ぼくとしては下記に示す通り、このQiita記事投稿(と、ユーティリティのソース公開)に関しては問題ないと判断していますが、もし万一問題があれば記事とgitリポジトリのソース一式まとめて削除します。

  • 公開したコードの内容は、汎用的かつ共通的な標準入出力に対するユーティリティ処理に過ぎない。
  • 特定の設問に関する解法などを示すものではなく、また特定の出題に関して特化した内容でもない。
  • このユーティリティコードを利用したとして、コーディングが楽になる程度の恩恵しかなく、解答のヒントやその他それに準ずる情報の示唆にも当たらない。
  • 不正行為を助長しようと言う目的は一切ない。

正直、本来であれば素直にコマンドライン引数からargsで出題パラメータ渡してくれよって思うけど、ブラウザ経由で色んな言語のコードを受け取ってテストコードに食わせる必要があるコードコンテスト系のサービスなので標準入出力を介してやり取りしている訳で。
単にこの制約というか、弊害というか、その辺の不便な所だけクリアにしたいと言うだけのものなので、これはまぁセーフかな、と判断しました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4