ReportsDateTimeDisplayHelper.cs 7.38 KB
using System.Globalization;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// Reports Print Log 日期展示:统一为美式短日期 <c>MM/dd/yy</c>(MM/DD/YY)。
/// </summary>
public static class ReportsDateTimeDisplayHelper
{
    /// <summary>列表日期列统一格式(MM/DD/YY)。</summary>
    public const string DateDisplayFormat = "MM/dd/yy";

    /// <summary>含时分时在同日期格式后追加 HH:mm。</summary>
    public const string DateTimeDisplayFormat = "MM/dd/yy HH:mm";

    /// <summary>兼容旧引用。</summary>
    public const string DisplayFormat = DateTimeDisplayFormat;

    private static readonly string[] ExpiryParseFormats =
    {
        DateTimeDisplayFormat,
        DateDisplayFormat,
        "MM/dd/yy",
        "MM/dd",
        "yyyy-MM-dd H:mm",
        "yyyy-MM-dd",
        "yyyy/MM/dd HH:mm",
        "yyyy/MM/dd",
        "MM/dd/yyyy hh:mm tt",
        "MM/dd/yyyy h:mm tt",
        "MM/dd/yyyy HH:mm",
        "MM/dd/yyyy",
        "M/d/yyyy hh:mm tt",
        "M/d/yyyy h:mm tt",
        "M/d/yyyy",
        "dd/MM/yyyy HH:mm",
        "dd/MM/yyyy",
        "d/M/yyyy HH:mm",
        "d/M/yyyy",
    };

    /// <summary>打印时间展示(Printed at 列):统一 MM/DD/YY。</summary>
    public static string FormatPrintedAt(DateTime printedAt)
    {
        if (printedAt == DateTime.MinValue)
        {
            return "无";
        }

        return printedAt.ToString(DateDisplayFormat, CultureInfo.InvariantCulture);
    }

    /// <summary>保质期展示(Expiration 列):解析后统一为 <see cref="DateDisplayFormat"/> 或含时分时 <see cref="DateTimeDisplayFormat"/>。</summary>
    public static string FormatExpiryDisplay(string? raw, DateTime? anchor = null)
    {
        if (string.IsNullOrWhiteSpace(raw) || string.Equals(raw.Trim(), "无", StringComparison.Ordinal))
        {
            return "无";
        }

        var s = raw.Trim();
        if (TryParseToDateTime(s, out var dt))
        {
            return FormatExpiryDateTime(dt);
        }

        var reference = anchor ?? DateTime.Now;
        if (TryParsePartialDateTime(s, reference, out dt))
        {
            return FormatExpiryDateTime(dt);
        }

        return s;
    }

    /// <summary>合并日期片段(如 07/09)与时间片段(如 11:56),缺少年份时用 anchor 补全。</summary>
    public static string MergeDateAndTimeParts(string datePart, string timePart, DateTime anchor)
    {
        var date = datePart.Trim();
        var time = timePart.Trim();
        if (string.IsNullOrWhiteSpace(date))
        {
            return string.IsNullOrWhiteSpace(time) ? "无" : time;
        }

        if (!TryParsePartialDateTime(date, anchor, out var dt) && !TryParseToDateTime(date, out dt))
        {
            return string.IsNullOrWhiteSpace(time) ? date : $"{date} {time}";
        }

        if (!string.IsNullOrWhiteSpace(time))
        {
            var timeMatch = System.Text.RegularExpressions.Regex.Match(
                time,
                @"^(?<h>\d{1,2}):(?<m>\d{2})",
                System.Text.RegularExpressions.RegexOptions.CultureInvariant);
            if (timeMatch.Success &&
                int.TryParse(timeMatch.Groups["h"].Value, out var h) &&
                int.TryParse(timeMatch.Groups["m"].Value, out var m))
            {
                dt = new DateTime(dt.Year, dt.Month, dt.Day, h, m, 0, dt.Kind);
            }
        }

        return FormatExpiryDateTime(dt);
    }

    private static bool TryParsePartialDateTime(string s, DateTime anchor, out DateTime dt)
    {
        dt = default;
        var text = s.Trim();
        var timeMatch = System.Text.RegularExpressions.Regex.Match(
            text,
            @"^(?<date>\d{1,2}/\d{1,2}(?:/\d{2,4})?)\s+(?<time>\d{1,2}:\d{2})$",
            System.Text.RegularExpressions.RegexOptions.CultureInvariant);
        if (timeMatch.Success)
        {
            text = timeMatch.Groups["date"].Value;
            if (TryParsePartialDateOnly(text, anchor, out dt))
            {
                var timePart = timeMatch.Groups["time"].Value;
                var parts = timePart.Split(':');
                if (parts.Length == 2 &&
                    int.TryParse(parts[0], out var h) &&
                    int.TryParse(parts[1], out var m))
                {
                    dt = new DateTime(dt.Year, dt.Month, dt.Day, h, m, 0, dt.Kind);
                    return true;
                }
            }
        }

        return TryParsePartialDateOnly(text, anchor, out dt);
    }

    private static bool TryParsePartialDateOnly(string s, DateTime anchor, out DateTime dt)
    {
        dt = default;
        var parts = s.Split('/');
        if (parts.Length is < 2 or > 3)
        {
            return false;
        }

        if (!int.TryParse(parts[0], out var month) || !int.TryParse(parts[1], out var day))
        {
            return false;
        }

        var year = anchor.Year;
        if (parts.Length == 3)
        {
            if (!int.TryParse(parts[2], out var y))
            {
                return false;
            }

            year = parts[2].Length == 2 ? 2000 + y : y;
        }

        try
        {
            dt = new DateTime(year, month, day, 0, 0, 0, anchor.Kind);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool TryParseToDateTime(string s, out DateTime dt)
    {
        if (DateTime.TryParse(
                s,
                CultureInfo.InvariantCulture,
                DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                out dt))
        {
            return true;
        }

        if (DateTime.TryParse(
                s,
                CultureInfo.GetCultureInfo("en-US"),
                DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                out dt))
        {
            return true;
        }

        foreach (var fmt in ExpiryParseFormats)
        {
            if (DateTime.TryParseExact(
                    s,
                    fmt,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                    out dt))
            {
                return true;
            }

            if (DateTime.TryParseExact(
                    s,
                    fmt,
                    CultureInfo.GetCultureInfo("en-US"),
                    DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                    out dt))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>将已解析的到期时刻格式化为 Print Log 展示文案。</summary>
    public static string FormatExpiryDateTime(DateTime dt)
    {
        if (dt == DateTime.MinValue)
        {
            return "无";
        }

        if (dt.Hour == 0 && dt.Minute == 0 && dt.Second == 0)
        {
            return dt.ToString(DateDisplayFormat, CultureInfo.InvariantCulture);
        }

        return dt.ToString(DateTimeDisplayFormat, CultureInfo.InvariantCulture);
    }

    /// <summary>解析到期字面量(含 anchor 补全年份/时间)。</summary>
    public static bool TryParseExpiryDateTime(string s, DateTime anchor, out DateTime dt)
    {
        if (TryParseToDateTime(s, out dt))
        {
            return true;
        }

        return TryParsePartialDateTime(s, anchor, out dt);
    }
}