From 83308328d64898809e017b663e8b46b1ae157b64 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 11:39:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20DateTimeHelper?= =?UTF-8?q?=20=E8=BE=85=E5=8A=A9=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Utils/DateTimeHelper.cs | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/BootstrapBlazor/Utils/DateTimeHelper.cs diff --git a/src/BootstrapBlazor/Utils/DateTimeHelper.cs b/src/BootstrapBlazor/Utils/DateTimeHelper.cs new file mode 100644 index 00000000000..1c5bd7e6976 --- /dev/null +++ b/src/BootstrapBlazor/Utils/DateTimeHelper.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Globalization; + +namespace BootstrapBlazor.Components; + +static class DateTimeHelper +{ + /// + /// 无分隔符等标准解析无法识别的紧凑格式,需要显式列出 + /// + private static readonly string[] CompactFormats = + [ + "yyyyMMdd", + "yyyyMMddHHmm", + "yyyyMMddHHmmss", + "yyyyMMddHHmmssfff", + "yyyyMMdd HHmmss", + "yyyyMMdd HH:mm:ss", + "yyyyMMdd HH:mm", + "yyyy-M-d", + "yyyy/M/d" + ]; + + /// + /// 将字符串解析为 ,无法解析时返回 + /// + public static DateTime? ToDateTime(string value) + => TryToDateTime(value, out var result) ? result : null; + + /// + /// 将字符串解析为 ,无法解析时返回 + /// + /// 要解析的字符串 + /// 解析失败时返回的默认值 + public static DateTime ToDateTime(string value, DateTime defaultValue) + => TryToDateTime(value, out var result) ? result : defaultValue; + + /// + /// 尝试将字符串解析为 + /// + /// 要解析的字符串 + /// 解析成功时的结果,失败时为 + public static bool TryToDateTime(string value, out DateTime result) + { + result = DateTime.MinValue; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var text = value.Trim(); + + // 先尝试无分隔符等紧凑格式(如 20260501),这类格式标准解析无法识别 + if (DateTime.TryParseExact(text, CompactFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + { + return true; + } + + // 再回退到通用解析,覆盖 2026-5-1、2026/5/1、2026-05-01 13:04:05 等带分隔符写法 + return DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out result); + } + + /// + /// 将字符串解析为 ,无法解析时返回 + /// + /// + /// 适用于 DateTimePicker 绑定 的场景; + /// 不含时区信息时按本地时区处理。 + /// + /// 要解析的字符串 + /// 解析成功返回对应的 ,否则返回 + public static DateTimeOffset? ToDateTimeOffset(string value) + { + if (TryToDateTime(value, out var dateTime)) + { + // 紧凑/无时区字符串解析出的 Kind 为 Unspecified,按本地时区补齐偏移 + return dateTime.Kind == DateTimeKind.Unspecified + ? new DateTimeOffset(DateTime.SpecifyKind(dateTime, DateTimeKind.Local)) + : new DateTimeOffset(dateTime); + } + return null; + } +} From bcc1092406db843d75da16bb51a95b831849a099 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 11:39:36 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=BD=95=E5=85=A5=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=85=BC=E5=AE=B9=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimePicker/DateTimePicker.razor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index e0500380ce6..1f7a774fd46 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -4,7 +4,6 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Localization; -using System.Globalization; namespace BootstrapBlazor.Components; @@ -569,10 +568,9 @@ private async Task OnClear() /// protected override bool TryParseValueFromString(string value, [MaybeNullWhen(false)] out TValue result, out string? validationErrorMessage) { - var format = ViewMode == DatePickerViewMode.DateTime ? DateTimeFormat : DateFormat; result = default; validationErrorMessage = null; - var ret = DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var val); + var ret = DateTimeHelper.TryToDateTime(value, out var val); if (ret) { result = (TValue)(object)val; From c756ac990d2565965755364e8eec4b2e74195103 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 12:01:51 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=E8=A1=A5=E5=85=A8=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Utils/DateTimeHelperTest.cs | 148 ++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 test/UnitTest/Utils/DateTimeHelperTest.cs diff --git a/test/UnitTest/Utils/DateTimeHelperTest.cs b/test/UnitTest/Utils/DateTimeHelperTest.cs new file mode 100644 index 00000000000..a1db225c7c4 --- /dev/null +++ b/test/UnitTest/Utils/DateTimeHelperTest.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Runtime.CompilerServices; + +namespace UnitTest.Utils; + +/// +/// DateTimeHelper 为内部类,通过 配合 +/// 直接访问其静态方法进行测试 +/// +public class DateTimeHelperTest +{ + /// + /// 内部类型的程序集限定名 + /// + private const string HelperTypeName = "BootstrapBlazor.Components.DateTimeHelper, BootstrapBlazor"; + + [Fact] + public void ToDateTime_Ok() + { + // 紧凑格式解析成功 + Assert.Equal(new DateTime(2026, 5, 1), ToDateTime(null, "20260501")); + + // 带分隔符格式解析成功 + Assert.Equal(new DateTime(2026, 5, 1, 13, 4, 5), ToDateTime(null, "2026-05-01 13:04:05")); + } + + [Fact] + public void ToDateTime_Null() + { + // 解析失败返回 null + Assert.Null(ToDateTime(null, "test")); + Assert.Null(ToDateTime(null, null!)); + } + + [Fact] + public void ToDateTime_DefaultValue_Ok() + { + var defaultValue = new DateTime(2020, 1, 1); + + // 解析成功返回解析值 + Assert.Equal(new DateTime(2026, 5, 1), ToDateTimeWithDefaultValue(null, "20260501", defaultValue)); + + // 解析失败返回默认值 + Assert.Equal(defaultValue, ToDateTimeWithDefaultValue(null, "test", defaultValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public void TryToDateTime_NullOrWhiteSpace(string? value) + { + // 空字符串直接返回 false 且结果为 DateTime.MinValue + Assert.False(TryToDateTime(null, value!, out var result)); + Assert.Equal(DateTime.MinValue, result); + } + + [Fact] + public void TryToDateTime_Invalid() + { + // 两种解析方式均失败 + Assert.False(TryToDateTime(null, "test", out var result)); + Assert.Equal(DateTime.MinValue, result); + + Assert.False(TryToDateTime(null, "20261301", out _)); + } + + [Theory] + [InlineData("20260501", 2026, 5, 1, 0, 0, 0, 0)] + [InlineData("202605011304", 2026, 5, 1, 13, 4, 0, 0)] + [InlineData("20260501130405", 2026, 5, 1, 13, 4, 5, 0)] + [InlineData("20260501130405123", 2026, 5, 1, 13, 4, 5, 123)] + [InlineData("20260501 130405", 2026, 5, 1, 13, 4, 5, 0)] + [InlineData("20260501 13:04:05", 2026, 5, 1, 13, 4, 5, 0)] + [InlineData("20260501 13:04", 2026, 5, 1, 13, 4, 0, 0)] + [InlineData("2026-5-1", 2026, 5, 1, 0, 0, 0, 0)] + [InlineData("2026/5/1", 2026, 5, 1, 0, 0, 0, 0)] + public void TryToDateTime_CompactFormats(string value, int year, int month, int day, int hour, int minute, int second, int millisecond) + { + // 紧凑格式由 TryParseExact 分支解析 + Assert.True(TryToDateTime(null, value, out var result)); + Assert.Equal(new DateTime(year, month, day, hour, minute, second, millisecond), result); + } + + [Theory] + [InlineData("2026-05-01")] + [InlineData("2026/05/01")] + public void TryToDateTime_Fallback(string value) + { + // 标准格式由 TryParse 回退分支解析 + Assert.True(TryToDateTime(null, value, out var result)); + Assert.Equal(new DateTime(2026, 5, 1), result); + } + + [Fact] + public void TryToDateTime_Trim() + { + // 前后空白被裁剪后可正常解析 + Assert.True(TryToDateTime(null, " 20260501 ", out var result)); + Assert.Equal(new DateTime(2026, 5, 1), result); + } + + [Fact] + public void ToDateTimeOffset_Unspecified() + { + // 无时区信息按本地时区补齐偏移 + var value = new DateTime(2026, 5, 1, 13, 4, 5); + var expected = TimeZoneInfo.Local.GetUtcOffset(value); + + var actual = ToDateTimeOffset(null, "20260501 130405"); + Assert.NotNull(actual); + Assert.Equal(expected, actual.Value.Offset); + Assert.Equal(value, actual.Value.DateTime); + } + + [Fact] + public void ToDateTimeOffset_Local() + { + // 带时区信息的字符串解析出的 Kind 不是 Unspecified,直接构造 + var actual = ToDateTimeOffset(null, "2026-05-01T13:04:05Z"); + Assert.NotNull(actual); + Assert.Equal(new DateTime(2026, 5, 1, 13, 4, 5, DateTimeKind.Utc), actual.Value.UtcDateTime); + } + + [Fact] + public void ToDateTimeOffset_Null() + { + // 解析失败返回 null + Assert.Null(ToDateTimeOffset(null, "test")); + } + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTime")] + static extern DateTime? ToDateTime([UnsafeAccessorType(HelperTypeName)] object? @this, string value); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTime")] + static extern DateTime ToDateTimeWithDefaultValue([UnsafeAccessorType(HelperTypeName)] object? @this, string value, DateTime defaultValue); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "TryToDateTime")] + static extern bool TryToDateTime([UnsafeAccessorType(HelperTypeName)] object? @this, string value, out DateTime result); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ToDateTimeOffset")] + static extern DateTimeOffset? ToDateTimeOffset([UnsafeAccessorType(HelperTypeName)] object? @this, string value); +}