ズンドコキヨシ with C#


C# の特徴である LINQ と yield を活かしてズンドコしてみた。

「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了

ソースコード

Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace ZunDokoKiyoshi
{
    class Program
    {
        static readonly string Zun = "ズン";
        static readonly string Doko = "ドコ";
        static readonly string Kiyoshi = "キ・ヨ・シ!";

        static readonly IReadOnlyList<string> Elements = Array.AsReadOnly(new string[] { Zun, Doko });
        static readonly IReadOnlyList<string> Pattern = Array.AsReadOnly(new string[] { Zun, Zun, Zun, Zun, Doko });
        static readonly IReadOnlyList<string> Appendix = Array.AsReadOnly(new string[] { Kiyoshi });

        static void Main(string[] args)
        {
            var items = Elements.RepeatRandomChoice()
                .TakeUntilMatchingPattern(Pattern)
                .Concat(Appendix);

            foreach (var item in items)
            {
                Console.WriteLine(item);
            }
        }
    }
}
Enumerable.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace ZunDokoKiyoshi
{
    static class Enumerable
    {
        private static readonly string EmptyExceptionMessage = "要素が1つ以上含まれている必要があります。";

        /// <summary>
        /// シーケンスからランダムに要素を選択して返すことを繰り返します。
        /// </summary>
        /// <typeparam name="TSource"><paramref name="source"/>の要素の型。</typeparam>
        /// <param name="source">要素を返すシーケンス。</param>
        /// <returns><paramref name="source"/>からランダムに選択された要素を含む<see cref="IEnumerable{T}"/>。</returns>
        public static IEnumerable<TSource> RepeatRandomChoice<TSource>(this IEnumerable<TSource> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            if (!source.Any())
            {
                throw new ArgumentException(EmptyExceptionMessage, nameof(source));
            }

            // 要素取得時ではなく関数呼び出し時に上記の引数チェックが行われるように、
            // yield を用いた処理は別関数にする。
            return RepeatRandomChoiceInternal(source);
        }

        private static IEnumerable<TSource> RepeatRandomChoiceInternal<TSource>(IEnumerable<TSource> source)
        {
            var items = source as IReadOnlyList<TSource> ?? source.ToArray();
            var random = new Random();

            while (true)
            {
                var index = random.Next(items.Count);
                yield return items[index];
            }
        }

        /// <summary>
        /// 指定されたパターンどおりに要素が現れるまで、シーケンスから要素を返します。
        /// </summary>
        /// <typeparam name="TSource"><paramref name="source"/>の要素の型。</typeparam>
        /// <param name="source">要素を返すシーケンス。</param>
        /// <param name="pattern">パターン。</param>
        /// <returns>指定されたパターンが現れるまでに出現する入力シーケンスの要素を含む<see cref="IEnumerable{T}"/>。</returns>
        public static IEnumerable<TSource> TakeUntilMatchingPattern<TSource>(
            this IEnumerable<TSource> source, IEnumerable<TSource> pattern)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            if (pattern == null)
            {
                throw new ArgumentNullException(nameof(pattern));
            }
            if (!pattern.Any())
            {
                throw new ArgumentException(EmptyExceptionMessage, nameof(source));
            }

            // 要素取得時ではなく関数呼び出し時に上記の引数チェックが行われるように、
            // yield を用いた処理は別関数にする。
            return TakeUntilMatchingPatternInternal(source, pattern);
        }

        private static IEnumerable<TSource> TakeUntilMatchingPatternInternal<TSource>(
            IEnumerable<TSource> source, IEnumerable<TSource> pattern)
        {
            var patternLength = pattern.Count();
            var queue = new Queue<TSource>(patternLength);

            foreach (var item in source)
            {
                yield return item;

                if (queue.Count >= patternLength)
                {
                    queue.Dequeue();
                }
                queue.Enqueue(item);

                if (queue.SequenceEqual(pattern))
                {
                    yield break;
                }
            }
        }
    }
}

解説

  • 「ズン」/「ドコ」をランダムに繰り返す処理は汎用化して関数 RepeatRandomChoice にした。
  • 「ズンズンズンズンドコ」を検出して繰り返しを止める処理は汎用化して関数 TakeUntilMatchingPattern にした。
  • どちらの関数も yield を用いたことで簡潔な実装になっている。
  • Main 関数での処理も、(コンソール出力部分を除けば)LINQ により簡潔な1文のみになっている。
  • 文字列等のリソースファイルへの切り出しは省略。

まとめ

キ・ヨ・シ!