C# テキストファイルを最終行から指定行数だけ読み出す

テキストファイルの最終行から指定行数だけ読み出して返します。
バッファやポジションの扱いは苦手で、もう一度書きたくなかったので残します。

- 本記事のすべての情報は利用者の責任において利用ください。
- 本記事の情報が正しいこと、誤っていることと実行効率を検証していません。
- 動作することも、動作しないことも保証しません。

誤りや改善案はご指摘いただければ感謝します。が、修正できない場合があることをご了承ください。

```csharp:Program.cs
internal class Program
{
    static void Main(string[] _)
    {
        Console.Write(
            ReadTailLines(
                @"C:\temp\新規 テキスト ドキュメント.txt",
                Encoding.UTF8,
                3
                ));
    }

    static string? ReadTailLines(
        string filePath,
        Encoding encoding,
        int lineCount,
        int bufferBytes = 10,
        string delimiter = "\r\n"
        )
    {
        using FileStream stream = new(
            filePath,
            FileMode.Open,
            FileAccess.Read
            );

        return ReadTailLines(stream, encoding, lineCount, bufferBytes, delimiter);
    }

    static string? ReadTailLines(
        FileStream stream,
        Encoding encoding,
        int lineCount,
        int bufferBytes = 10,
        string delimiter = "\r\n"
        )
    {
        if (bufferBytes <= 0) throw new ArgumentException($"bufferBytes <= 0");
        if (string.IsNullOrEmpty(delimiter)) throw new ArgumentException($"empty delimiter");

        Stack<IEnumerable<byte>> stack = new();

        // 読み出すバイト数
        int readBytes = (int)Math.Min(bufferBytes, stream.Length);

        stream.Position = stream.Length - readBytes;

        while (stream.Position >= 0 && readBytes > 0)
        {
            var buffer = new byte[readBytes];

            if (stream.Read(buffer, 0, readBytes) > 0)
            {
                // 読み出したバイト列の評価
                {
                    stack.Push(buffer);

                    var s = encoding.GetString(
                        stack.SelectMany(x => x).ToArray()
                        ).TrimEnd();

                    var lastIndex = s.LastIndexOfTimes(delimiter, lineCount);

                    if (lastIndex > -1)
                    {
                        return s[(lastIndex + delimiter.Length)..];
                    }
                }

                // 次の周回の Position 合わせ
                {
                    int beforeReadPosition = (int)stream.Position - readBytes;

                    readBytes = (beforeReadPosition - bufferBytes >= 0)
                        ? bufferBytes
                        : beforeReadPosition;

                    stream.Position = beforeReadPosition - readBytes;
                }
            }
        }

        return null;
    }

}
```

```csharp:StringExtension.cs
static class StringExtension
{
    public static int LastIndexOfTimes(this string self, string search, int times)
    {
        int foundCount = 0;

        int index = self.LastIndexOf(search, self.Length - 1);

        while (index > -1)
        {
            foundCount++;

            if (foundCount >= times)
            {
                return index;
            }
            else
            {
                index = self.LastIndexOf(search, index);
            }
        }

        return -1;
    }
}
```

```text:新規 テキスト ドキュメント.txt
A,B,C,D,E
F,G,H,I,J,K,L,M,N,O,P,Q,R,

ぎゃはS,T,U,V,W,X,Y,Z
```

```text:出力
F,G,H,I,J,K,L,M,N,O,P,Q,R,

ぎゃはS,T,U,V,W,X,Y,Z
```

## 確認した環境
- Windows 11 Home 22H2
- Microsoft Visual Studio Community 2022 (64 ビット) Version 17.4.3
- .NET Core 6.0

## 関連記事
https://qiita.com/monta31538/items/741706c4d531b1c1ae6b