読者です 読者をやめる 読者になる 読者になる

forの二重ループではなくLINQを使うように、昨日のコードを書きなおしてみた

C#

昨日の記事を書き上げた後、forの二重ループをどうするか考えていたところ、以下の記事を目にしました。
StringとStringBuilderと、in C# - 亀岡的プログラマ日記


その中でループはLINQにするという記載があり、LINQのコードをメモ的に残していなかったこともあり、forの二重ループをLINQで書きなおしてみました(合わせて、string.Format()部分も修正)。

■元々のソース

var contents = new List<string>();
for (int row = 1; row < 10000; row++)
{
    var startNo = GetCellValue(row, START_COLUMN);
    var lastNo = GetCellValue(row, LAST_COLUMN);

    if (startNo == 0 && lastNo == 0) break;


    var amount = lastNo - startNo + 1;
    for (int i = 0; i < amount; i++)
    {
        contents.Add("x" + string.Format("{0:D4}", (startNo + i)) + "x");
    }
}

■string.Format部分の修正

一部にしかstring.Formatを使えていなかったので、まとめるようにしました。
(以前のバージョンと区別するように、prefix・suffixを変えています。以下同じ。)

var contents = new List<string>();
for (int row = 1; row < 10000; row++)
{
    var startNo = GetCellValue(row, START_COLUMN);
    var lastNo = GetCellValue(row, LAST_COLUMN);

    if (startNo == 0 && lastNo == 0) break;


    var amount = lastNo - startNo + 1;
    for (int i = 0; i < amount; i++)
    {
        //  string.Formatをまとめる
        contents.Add(string.Format("{0}{1:D4}{0}", "w", (startNo + i)));
    }
}

■内側のforループをLINQに置き換える

AggregateでListを返すようにして、内側のforループを置き換えました。
参考:c# - LINQやRxのAggregateの初期値 - Qiita

var contents = new List<string>();
for (int row = 1; row < 10000; row++)
{
    var startNo = GetCellValue(row, START_COLUMN);
    var lastNo = GetCellValue(row, LAST_COLUMN);

    if (startNo == 0 && lastNo == 0) break;

    //  以降をforからLINQへと置き換え
    contents.AddRange(Enumerable.Range(0, lastNo - startNo + 1).Aggregate(new List<string>(), (list, i) =>
    {
        list.Add(string.Format("{0}{1:D4}{0}", "y", (startNo + i)));
        return list;
    }));
}

■外側のforループもLINQに置き換える

空白行を挟んでまたデータがある場合の挙動を考え、WhereではなくTakeWhileを使っています。
また、GetCellValue()メソッドをSelectとTakeWhileの両方で呼ぶのは処理が遅くなると考え、今回は最初にSelectを持ってきています。

var contents = new List<string>();
var items = Enumerable.Range(1, 10000)
                      .Select((i) => new { startNo = GetCellValue(i, START_COLUMN), lastNo = GetCellValue(i, LAST_COLUMN) })
                      .TakeWhile(j => j.startNo != 0 || j.lastNo != 0);

foreach (var item in items)
{
    var amount = item.lastNo - item.startNo + 1;
    contents.AddRange(Enumerable.Range(0, amount).Aggregate(new List<string>(), (list, k) =>
    {
        list.Add(string.Format("{0}{1:D4}{0}", "z", (item.startNo + k)));
        return list;
    }));
}

■ソース全体

Gistにも上げてありますが、上記の全4パターンを記載しているため、出力されるデータは4倍になっています。
ExcelDNASkype.csの、for二重ループをLINQで置き換えたサンプル (出力されるテキストファイルは4倍になっていることに注意)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// Add
using System.Windows.Forms;
using ExcelDna.Integration;
using SKYPE4COMLib;

namespace ExcelDNASkype
{
    public class ExcelDNASkype : IExcelAddIn
    {
        const int START_COLUMN = 0;
        const int LAST_COLUMN = 1;

        const string MENU_NORMAL = "一般";
        const string MENU_SPECIAL = "特殊";

        const string SKYPE_USER = "";


        /// <summary>
        /// アドインメニュー:一般
        /// </summary>
        [ExcelCommand(MenuName = "AddIn", MenuText = "Normal")]
        public static void DisplayNormalMenu()
        {
            Run(MENU_NORMAL);
        }

        /// <summary>
        /// アドインメニュー:特殊
        /// </summary>
        [ExcelCommand(MenuName = "AddIn", MenuText = "Special")]
        public static void DisplaySpecialMenu()
        {
            Run(MENU_SPECIAL);
        }


        /// <summary>
        /// メイン処理
        /// </summary>
        /// <param name="menuName">メニュー名</param>
        private static void Run(string menuName)
        {
            //  テキストファイル出力
            var contents = CreateContents();
            //  .NET4 から登場した、Path.Combineのオーバーロードを利用
            var txtpath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
                                                 menuName + "_content.txt");

            MessageBox.Show(txtpath);

            if (ExportTextFile(contents, txtpath)) MessageBox.Show(contents.Count.ToString() + " 件、出力しました。");



            //  Excelファイル保存
            if (SaveAsExcelFile(txtpath)) MessageBox.Show("Excelファイルを保存しました。");



            //  Skype通知
            if (SendSkypeMessage(menuName)) MessageBox.Show("Skype通知をしました。");
        }




        /// <summary>
        /// 出力するテキストファイルデータの作成
        /// </summary>
        /// <returns>テキストファイルデータ</returns>
        private static List<string> CreateContents()
        {
            var contents = new List<string>();


            //  最初
            for (int row = 1; row < 10000; row++)
            {
                var startNo = GetCellValue(row, START_COLUMN);
                var lastNo = GetCellValue(row, LAST_COLUMN);

                if (startNo == 0 && lastNo == 0) break;


                var amount = lastNo - startNo + 1;
                for (int i = 0; i < amount; i++)
                {
                    contents.Add("x" + string.Format("{0:D4}", (startNo + i)) + "x");
                }
            }


            //  string.Formatを使う
            for (int row = 1; row < 10000; row++)
            {
                var startNo = GetCellValue(row, START_COLUMN);
                var lastNo = GetCellValue(row, LAST_COLUMN);

                if (startNo == 0 && lastNo == 0) break;


                var amount = lastNo - startNo + 1;
                for (int i = 0; i < amount; i++)
                {
                    contents.Add(string.Format("{0}{1:D4}{0}", "w", (startNo + i)));
                }
            }


            //  内側のforループをLINQに変更
            for (int row = 1; row < 10000; row++)
            {
                var startNo = GetCellValue(row, START_COLUMN);
                var lastNo = GetCellValue(row, LAST_COLUMN);

                if (startNo == 0 && lastNo == 0) break;


                contents.AddRange(Enumerable.Range(0, lastNo - startNo + 1).Aggregate(new List<string>(), (list, i) =>
                {
                    list.Add(string.Format("{0}{1:D4}{0}", "y", (startNo + i)));
                    return list;
                }));
            }


            //  外側・内側ともforループを削除
            var items = Enumerable.Range(1, 10000)
                                  .Select((i) => new { startNo = GetCellValue(i, START_COLUMN), lastNo = GetCellValue(i, LAST_COLUMN) })
                                  .TakeWhile(j => j.startNo != 0 || j.lastNo != 0);

            foreach (var item in items)
            {
                var amount = item.lastNo - item.startNo + 1;
                contents.AddRange(Enumerable.Range(0, amount).Aggregate(new List<string>(), (list, k) =>
                {
                    list.Add(string.Format("{0}{1:D4}{0}", "z", (item.startNo + k)));
                    return list;
                }));
            }



            return contents;
        }


        /// <summary>
        /// セルの値を取得する
        /// </summary>
        /// <param name="row">セルの行(0始まり)</param>
        /// <param name="column">セルの列(0始まり)</param>
        /// <returns>セルの値、取得できない場合は、セルの値は0を返す</returns>
        private static int GetCellValue(int row, int column)
        {
            var cell = new ExcelReference(row, column);

            var value = 0;
            if (int.TryParse(cell.GetValue().ToString(), out value)) return value;
            else return 0;
        }


        /// <summary>
        /// テキストファイルの出力
        /// </summary>
        /// <param name="contents">テキストファイルの内容</param>
        /// <param name="fullpath">出力先のパス</param>
        /// <returns></returns>
        private static bool ExportTextFile(List<string> contents, string fullpath)
        {
            System.Text.Encoding encode = System.Text.Encoding.GetEncoding("SHIFT_JIS");

            using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fullpath, true, encode))
            {
                foreach (var content in contents)
                {
                    sw.WriteLine(content);
                }
            }

            return true;
        }


        /// <summary>
        /// Excelファイルとして保存
        /// </summary>
        /// <param name="txtpath">テキストファイルの出力先</param>
        /// <returns></returns>
        private static bool SaveAsExcelFile(string txtpath)
        {
            var regex = new System.Text.RegularExpressions.Regex("txt$");
            var xlspath = regex.Replace(txtpath, "xls");

            //  Excel-DNAの機能を使用
            XlCall.Excel(XlCall.xlcSaveAs, xlspath);

            return true;
        }


        /// <summary>
        /// Skypeメッセージの送信
        /// </summary>
        /// <returns></returns>
        private static bool SendSkypeMessage(string menu)
        {
            //  Skype4COMを使用
            SKYPE4COMLib.Skype skype = new Skype();
            skype.SendMessage(SKYPE_USER, menu);
            return true;
        }



        //  以下、IExcelAddIn用で、今回は使用しない
        public void AutoOpen() { }
        public void AutoClose() { }

    }
}