From a2bd306e3056c15f9c98699f7ad1d7467c41922e Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 11:53:19 +0200 Subject: [PATCH 1/4] Add DateTime extensions --- CHANGELOG.md | 1 + .../Extensions/DateTimeExtensionsTests.cs | 181 ++++++++++++++++++ .../Extensions/DateTimeExtensions.cs | 50 +++++ 3 files changed, 232 insertions(+) create mode 100644 Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs create mode 100644 Neolution.Utilities/Extensions/DateTimeExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6851ca2..80adfe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,5 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `IEnumerable` extension methods - `IConfiguration` extension methods - `IServiceCollection` extension methods +- `DateTime` extension methods - `IFormFile` extension method (AspNetCore package only) - `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only) diff --git a/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs new file mode 100644 index 0000000..3271b37 --- /dev/null +++ b/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs @@ -0,0 +1,181 @@ +namespace Neolution.Utilities.UnitTests.Extensions; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Unit tests for the class. +/// +public class DateTimeExtensionsTests +{ + /// + /// Tests the parameter-less method which requires the time to be exactly 00:00:00. + /// + /// The year component. + /// The month component. + /// The day component. + /// The hour component. + /// The minute component. + /// The second component. + /// Expected result indicating whether it is exactly midnight. + [Theory] + [InlineData(2025, 1, 10, 0, 0, 0, true)] // Exact midnight + [InlineData(2025, 1, 10, 0, 0, 1, false)] // Midnight + 1 second + [InlineData(2025, 1, 10, 0, 1, 0, false)] // 00:01 + [InlineData(2025, 1, 10, 1, 0, 0, false)] // 01:00 + [InlineData(2025, 1, 10, 23, 59, 59, false)] // End of day just before midnight + public void GivenDateTime_WhenIsMidnightCalled_ThenReturnsExpected( + int year, int month, int day, int hour, int minute, int second, bool expected) + { + // Arrange + var dateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified); + + // Act + var result = dateTime.IsMidnight(); + + // Assert + result.ShouldBe(expected); + } + + /// + /// Tests with the ignoreSeconds flag. + /// Ensures midnight detection works both strictly and when seconds are ignored. + /// + /// Hour component. + /// Minute component. + /// Second component. + /// Flag indicating whether to ignore seconds. + /// Expected result given the flag. + [Theory] + [InlineData(0, 0, 0, false, true)] // Exact midnight strict + [InlineData(0, 0, 1, false, false)] // Not exact midnight strict + [InlineData(0, 0, 1, true, true)] // Midnight with seconds ignored + [InlineData(0, 1, 0, true, false)] // Minute > 0 cannot be midnight + [InlineData(1, 0, 0, true, false)] // Hour > 0 cannot be midnight + public void GivenDateTimeAndIgnoreSecondsFlag_WhenIsMidnightCalled_ThenReturnsExpected( + int hour, int minute, int second, bool ignoreSeconds, bool expected) + { + // Arrange + var dateTime = new DateTime(2025, 5, 20, hour, minute, second, DateTimeKind.Unspecified); + + // Act + var result = dateTime.IsMidnight(ignoreSeconds); + + // Assert + result.ShouldBe(expected); + } + + /// + /// Tests with various months including leap year February. + /// + /// Year component. + /// Month component. + /// Day component. + /// Expected result (true if end of month). + [Theory] + [InlineData(2025, 1, 31, true)] // 31-day month end + [InlineData(2025, 1, 30, false)] + [InlineData(2024, 2, 29, true)] // Leap year February + [InlineData(2024, 2, 28, false)] + [InlineData(2025, 2, 28, true)] // Non-leap year February + [InlineData(2025, 2, 27, false)] + [InlineData(2025, 4, 30, true)] // 30-day month end + [InlineData(2025, 4, 29, false)] + public void GivenDateTime_WhenIsEndOfMonthCalled_ThenReturnsExpected( + int year, int month, int day, bool expected) + { + // Arrange + var dateTime = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified); + + // Act + var result = dateTime.IsEndOfMonth(); + + // Assert + result.ShouldBe(expected); + } + + /// + /// Gets the test data for method. + /// + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "It's better to have test data beside the test method")] + public static TheoryData GivenTargetAndRange_WhenIsInRangeCalled_ThenReturnsExpected_TestData => new() + { + { + // Inside range + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + true + }, + { + // Equal to start + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + true + }, + { + // Equal to end + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + true + }, + { + // Before start + new DateTime(2025, 5, 13, 22, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + false + }, + { + // After end + new DateTime(2025, 5, 16, 22, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + false + }, + { + // Single point range (match) + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + true + }, + { + // Single point range (mismatch) + new DateTime(2025, 5, 14, 21, 36, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + false + }, + { + // Reversed range (start > end) always false + new DateTime(2025, 5, 15, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 16, 0, 0, 0, DateTimeKind.Unspecified), + new DateTime(2025, 5, 14, 0, 0, 0, DateTimeKind.Unspecified), + false + }, + }; + + /// + /// Tests ensuring: + /// - Inclusive start and end boundaries. + /// - Dates before and after the range are excluded. + /// - Single-point range behavior (start == end). + /// - Reversed range (start > end) returns false. + /// + /// The target date to check. + /// The start of the range. + /// The end of the range. + /// Expected result indicating if target is in range. + [Theory] + [MemberData(nameof(GivenTargetAndRange_WhenIsInRangeCalled_ThenReturnsExpected_TestData))] + public void GivenTargetAndRange_WhenIsInRangeCalled_ThenReturnsExpected(DateTime target, DateTime startDate, DateTime endDate, bool expectedResult) + { + // Act + var result = target.IsInRange(startDate, endDate); + + // Assert + result.ShouldBe(expectedResult); + } +} diff --git a/Neolution.Utilities/Extensions/DateTimeExtensions.cs b/Neolution.Utilities/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..7b828db --- /dev/null +++ b/Neolution.Utilities/Extensions/DateTimeExtensions.cs @@ -0,0 +1,50 @@ +namespace Neolution.Utilities.Extensions; + +using static System.Runtime.InteropServices.JavaScript.JSType; + +/// +/// The DateTime extension methods. +/// +public static class DateTimeExtensions +{ + /// + /// Determines whether the specified value is exactly at midnight (00:00:00). + /// + /// The to evaluate. + /// true if the time component is exactly 00:00:00; otherwise false. + public static bool IsMidnight(this DateTime dateTime) + => dateTime.IsMidnight(false); + + /// + /// Determines whether the specified value represents midnight (00:00), + /// optionally ignoring the seconds component. + /// + /// The to evaluate. + /// + /// If true, evaluates midnight as any time where hour and minute are zero (00:00), regardless of seconds. + /// If false, requires the time to be exactly 00:00:00. + /// + /// + /// true if the time is midnight according to the rule; otherwise false. + /// + public static bool IsMidnight(this DateTime dateTime, bool ignoreSeconds) + => dateTime.Hour == 0 && dateTime.Minute == 0 && (ignoreSeconds || dateTime.Second == 0); + + /// + /// Determines whether the specified value is the end of the month. + /// + /// The to evaluate. + /// true if the specified value is the end of the month; otherwise, false. + public static bool IsEndOfMonth(this DateTime dateTime) + => dateTime.Day == DateTime.DaysInMonth(dateTime.Year, dateTime.Month); + + /// + /// Determines whether the specified value is in range. + /// + /// The to evaluate. + /// The start . + /// The end . + /// true if the specified value is in range; otherwise, false. + public static bool IsInRange(this DateTime dateTime, DateTime startDate, DateTime endDate) + => dateTime >= startDate && dateTime <= endDate; +} From 1b1f38f9e62f75f0d2ab032018dc6d9bfde741ff Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 11:56:34 +0200 Subject: [PATCH 2/4] fix --- Neolution.Utilities/Extensions/DateTimeExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Neolution.Utilities/Extensions/DateTimeExtensions.cs b/Neolution.Utilities/Extensions/DateTimeExtensions.cs index 7b828db..72ef4f5 100644 --- a/Neolution.Utilities/Extensions/DateTimeExtensions.cs +++ b/Neolution.Utilities/Extensions/DateTimeExtensions.cs @@ -1,7 +1,5 @@ namespace Neolution.Utilities.Extensions; -using static System.Runtime.InteropServices.JavaScript.JSType; - /// /// The DateTime extension methods. /// From 010df5fa326fb14360e2a86db8264a7a9bcb5ac7 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 14:09:42 +0200 Subject: [PATCH 3/4] fix --- Neolution.Utilities/Extensions/DateTimeExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neolution.Utilities/Extensions/DateTimeExtensions.cs b/Neolution.Utilities/Extensions/DateTimeExtensions.cs index 72ef4f5..ec8cb44 100644 --- a/Neolution.Utilities/Extensions/DateTimeExtensions.cs +++ b/Neolution.Utilities/Extensions/DateTimeExtensions.cs @@ -26,7 +26,7 @@ public static bool IsMidnight(this DateTime dateTime) /// true if the time is midnight according to the rule; otherwise false. /// public static bool IsMidnight(this DateTime dateTime, bool ignoreSeconds) - => dateTime.Hour == 0 && dateTime.Minute == 0 && (ignoreSeconds || dateTime.Second == 0); + => ignoreSeconds ? dateTime.Hour == 0 && dateTime.Minute == 0 : dateTime.TimeOfDay.Ticks == 0; /// /// Determines whether the specified value is the end of the month. From 7c3b6f902fa0124ac02f36f9da553c973e152d1e Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 14:11:44 +0200 Subject: [PATCH 4/4] Update Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/DateTimeExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs index 3271b37..e11d7f4 100644 --- a/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/DateTimeExtensionsTests.cs @@ -96,7 +96,7 @@ public void GivenDateTime_WhenIsEndOfMonthCalled_ThenReturnsExpected( /// /// Gets the test data for method. /// - [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "It's better to have test data beside the test method")] + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "It's better to have test data next to the test method")] public static TheoryData GivenTargetAndRange_WhenIsInRangeCalled_ThenReturnsExpected_TestData => new() { {