ReportsDateTimeDisplayHelper.cs 6.77 KB
using System.Globalization;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// Reports Print Log 日期时间展示:统一为美式短日期 <c>MM/dd/yy</c>,含时间时精确到分钟。
/// </summary>
public static class ReportsDateTimeDisplayHelper
{
    /// <summary>Print Log 日期部分格式(MM/DD/YY)</summary>
    public const string PrintLogDateFormat = "MM/dd/yy";

    /// <summary>Print Log 日期+时间格式(精确到分钟)</summary>
    public const string PrintLogDateTimeFormat = "MM/dd/yy HH:mm";

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

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

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

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

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

        return s;
    }

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

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

    /// <summary>尝试将字面量解析为 <see cref="DateTime"/>(支持仅日期、仅时间、缺少年份等)。</summary>
    public static bool TryParseToDateTime(string s, DateTime? referenceTime, out DateTime dt)
    {
        dt = default;
        var reference = referenceTime ?? DateTime.Now;

        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 NormalizePartialDateTime(dt, fmt, reference);
            }

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

        if (TryParseTimeOnly(s, reference, out dt))
        {
            return true;
        }

        if (TryParseMonthDayOnly(s, reference, out dt))
        {
            return true;
        }

        return false;
    }

    private static bool NormalizePartialDateTime(DateTime dt, string format, DateTime reference)
    {
        if (format is "MM/dd" or "MM/dd HH:mm")
        {
            var year = reference.Year;
            var candidate = new DateTime(year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
            if (candidate.Date < reference.Date)
            {
                candidate = candidate.AddYears(1);
            }

            dt = candidate;
            return true;
        }

        if (format is "HH:mm" or "H:mm" or "hh:mm tt" or "h:mm tt")
        {
            dt = new DateTime(reference.Year, reference.Month, reference.Day, dt.Hour, dt.Minute, dt.Second);
            return true;
        }

        return true;
    }

    private static bool TryParseTimeOnly(string s, DateTime reference, out DateTime dt)
    {
        dt = default;
        foreach (var fmt in new[] { "HH:mm", "H:mm", "hh:mm tt", "h:mm tt" })
        {
            if (DateTime.TryParseExact(
                    s,
                    fmt,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                    out var parsed))
            {
                dt = new DateTime(reference.Year, reference.Month, reference.Day, parsed.Hour, parsed.Minute, 0);
                return true;
            }

            if (DateTime.TryParseExact(
                    s,
                    fmt,
                    CultureInfo.GetCultureInfo("en-US"),
                    DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                    out parsed))
            {
                dt = new DateTime(reference.Year, reference.Month, reference.Day, parsed.Hour, parsed.Minute, 0);
                return true;
            }
        }

        return false;
    }

    private static bool TryParseMonthDayOnly(string s, DateTime reference, out DateTime dt)
    {
        dt = default;
        foreach (var fmt in new[] { "MM/dd", "M/d" })
        {
            if (DateTime.TryParseExact(
                    s,
                    fmt,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
                    out var parsed))
            {
                var year = reference.Year;
                var candidate = new DateTime(year, parsed.Month, parsed.Day, 0, 0, 0);
                if (candidate.Date < reference.Date)
                {
                    candidate = candidate.AddYears(1);
                }

                dt = candidate;
                return true;
            }
        }

        return false;
    }
}