[C#] 文字列の中のyyyyMMdd(20201016等)とtimezone(+0900等)を取り出してDateTimeに変換する


もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

ファイル名に日付が入ってるログファイルがあって、それがあるフォルダにまとめて入れられている。
そういうログファイルたちの中から、ファイルの更新日付ではなく「ファイル名に付いてる日付」が一番新しいものの日付を取り出したい、ということがあった。

※下図のようなファイルがある場合は、「testlog_21201017+0900.log」の日付が一番新しいので、DateTimeの「2120/10/17」を取り出したい。

今回の場合は、ファイル名にyyyyMMddだけではなくタイムゾーンを示す+0900というのが入った形式だった。

調べた結果、日付の文字列(20201016など)を一旦DateTimeに直して、その値でどれが新しいか判断することにしたが、特にタイムゾーンのところをDateTimeに直すときに少しクセがあったのでメモを残しておく。

やったこと

下記のようなことをした。

  1. Directory.GetFiles()で拡張子を指定して、フォルダ内のログファイルをすべて列挙
  2. その中から、Linqと正規表現を使って日付の部分を取り出す
  3. 取り出した日付をDateTime.ParseExact()でDateTimeに変換
  4. その中の一番新しいものを取り出し

具体的な流れは、下のサンプルコードを参照だが、
3番のDateTime.ParseExact()をするところに少しクセがあった。

DateTime.ParseExact()のクセ

DateTime.ParseExact()は、書式を指定して、文字列からDateTimeに変換するものだが、
上の図にあったようなファイル名に含まれる、20201016+0900をDateTimeに変換する際、そのままの文字列と書式指定のyyyyMMddzzzDateTime.ParseExact()に渡してもうまくいかなかった。

対策は、
20201016+090020201016 +09:00に直したうえで、DateTime.ParseExact()に渡すということ。

// こういうイメージ
DateTime.ParseExact("20201016+0900", "yyyyMMdd zzz", null)

これで、タイムゾーンも込みで、DateTimeに変換することができた。

DateTime.ParseExact()のクセはそれほどでもなかった(20/10/31追記)

@htsignさんからコメント頂いた。

// こうではなく
DateTime.ParseExact("20201016+0900", "yyyyMMdd zzz", null)
// こうだとInsertとかしなくてもうまくいく様子
DateTime.ParseExact("20201016+0900", "yyyyMMddzzz", null) // ←「zzz」の前のスペースが余計だった

上記だと、特に特殊なことをしなくてもOKということが判明。ありがとうございます。
この記事の肝だと思ってた部分が崩れる感じでなんだかお恥ずかしいですが、シンプルにできることがわかってすっきりです。

実験コード

using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace ConsoleApp19
{
    class Program
    {
        static void Main(string[] args)
        {
            // フォルダ内のログファイルを列挙
            string targetDirPath = @"C:\Users\masa\Desktop\test";
            string targetExt = "*.log";
            string[] logs = Directory.GetFiles(targetDirPath, targetExt, SearchOption.TopDirectoryOnly);

            // ファイル名についてる「時間+タイムゾーン」の文字列(「20191011+0900」みたいなの=「yyyyMMddzzz」を取り出すための正規表現)
            string reg = "[0-9]{8}[+|-][0-9]{4}";
            string format = "yyyyMMdd zzz";

            var latestLogDate = logs.Where(x => Regex.Match(x, reg).Value != "")            // 指定の正規表現に一致するものに絞って、
                                    .Select(x => Regex.Match(x, reg))                       // 一致した部分の文字列だけ取り出し、
                                    .Select(x => x.Value.Insert(11, ":").Insert(8, " "))    // +0900の+09と00の間に「:」を挿入し、さらに日付とタイムゾーンの間に半角スペースを入れ、
                                    .Select(x => DateTime.ParseExact(x, format, null))      // ParseExactでDateTimeにパースして、
                                    .Max();                                                 // 一番新しい日付のものを取り出す
            Console.WriteLine("一番新しいログの日付は " + latestLogDate.ToString("yyyy/MM/dd") + " のログです。");
            Console.ReadLine();
        }
    }
}

※ファイル名から日付を取り出す正規表現は、改善の余地ありかも。

実験コードその②(20/10/31追記、クセ考慮部分の見直し)

ParseExact()のクセがそうでもないことが分かったので、その辺を考慮していた部分を外してすっきりさせ、かつLinqについてもコメント頂いていたのでその部分もちょっと手直ししたのが下記。だいぶわかりやすくなった。

static void Main(string[] args)
{
    // フォルダ内のログファイルを列挙
    string targetDirPath = @"C:\Users\masa\Desktop\test";
    string targetExt = "*.log";
    string[] logs = Directory.GetFiles(targetDirPath, targetExt, SearchOption.TopDirectoryOnly);

    // ファイル名についてる「時間+タイムゾーン」の文字列(「20191011+0900」みたいなの=「yyyyMMddzzz」を取り出すための正規表現)
    string reg = "[0-9]{8}[+|-][0-9]{4}";
    string format = "yyyyMMddzzz";

    var latestLogDate = logs.Select(x => Regex.Match(x, reg))                       // 一致した部分の文字列だけ取り出し、
                            .Where(x => x.Success)                                  // 一致するものがあったものだけに絞り、
                            .Select(x => DateTime.ParseExact(x.Value, format, null))// ParseExactでDateTimeにパースして、
                            .Max();                                                 // 一番新しい日付のものを取り出す
    Console.WriteLine("一番新しいログの日付は " + latestLogDate.ToString("yyyy/MM/dd") + " のログです。");
    Console.ReadLine();
}

参考

■標準の日時の書式指定文字列
https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/standard-date-and-time-format-strings

■カスタムの日時の書式指定文字列
ParseExactのフォーマット指定(yyyyMMdd zzzとか)に何を指定すればよいかがここで分かる。
https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/custom-date-and-time-format-strings
→これによると、zzzは標準の書式指定文字列ではなく、DateTimeOffset用のカスタム書式指定文字列らしい。

■標準の日時の書式指定文字列の日本語解説ページ
https://dotnet.programmer-reference.com/csharp-date-format/