From fc8e8747f6ed3177dfd9216a4876a5942ef96edd Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 18:23:15 +0200 Subject: [PATCH] DateOnly and TimeOnly: retarget net10.0, add conversion samples and tests Retarget both projects from net6.0 to net10.0 and bring the test packages up to the versions a net10.0 test project needs (Microsoft.NET.Test.Sdk 18.9.0, xunit 2.9.3, xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1). Add DemonstrateConversions() covering the DateOnly-to-DateTime direction the sample did not have: ToDateTime(TimeOnly), ToDateTime(TimeOnly.MinValue), ToDateTime(TimeOnly, DateTimeKind), FromDateTime(DateTime.Now) on both types, TimeOnly.FromTimeSpan/ToTimeSpan, DayNumber/FromDayNumber, ParseExact with a fixed format, and AddHours(double, out int). Set CurrentCulture to en-US in Main so the printed output is the same on every machine instead of depending on the host culture. Eleven new xUnit facts cover the additions. The DateTime.Now facts assert round-trip consistency rather than an exact value, because the suite runs at an arbitrary instant. --- .../DateOnlyAndTimeOnlyInCSharpExample.csproj | 2 +- .../Program.cs | 57 +++++++++++- .../Tests/DateOnlyTests.cs | 87 +++++++++++++++++++ .../Tests/Tests.csproj | 10 +-- .../Tests/TimeOnlyTests.cs | 62 +++++++++++++ 5 files changed, 211 insertions(+), 7 deletions(-) diff --git a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/DateOnlyAndTimeOnlyInCSharpExample.csproj b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/DateOnlyAndTimeOnlyInCSharpExample.csproj index 74abf5c976..dfb40caafc 100644 --- a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/DateOnlyAndTimeOnlyInCSharpExample.csproj +++ b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/DateOnlyAndTimeOnlyInCSharpExample.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 enable enable diff --git a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/Program.cs b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/Program.cs index 7154c7263f..0f108c3ed0 100644 --- a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/Program.cs +++ b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/DateOnlyAndTimeOnlyInCSharpExample/Program.cs @@ -1,12 +1,19 @@ -namespace DateOnlyAndTimeOnlyInCSharpExample; +using System.Globalization; + +namespace DateOnlyAndTimeOnlyInCSharpExample; public class Program { public static void Main() { + // The printed output below is culture-dependent, so we state the culture + // instead of letting the host machine decide what the samples look like. + CultureInfo.CurrentCulture = new CultureInfo("en-US"); + DemonstrateDateOnly(); DemonstrateTimeOnly(); DemonstrateFromDateTime(); + DemonstrateConversions(); DemonstrateBasicOperators(); } @@ -62,6 +69,54 @@ public static void DemonstrateFromDateTime() Console.WriteLine(timeOnly); } + public static void DemonstrateConversions() + { + var dateOnly = new DateOnly(2022, 1, 1); + var timeOnly = new TimeOnly(11, 30); + + // DateOnly back to DateTime, in all three shapes. + var combined = dateOnly.ToDateTime(timeOnly); + var midnight = dateOnly.ToDateTime(TimeOnly.MinValue); + var utc = dateOnly.ToDateTime(timeOnly, DateTimeKind.Utc); + + Console.WriteLine($"{combined:O} Kind={combined.Kind}"); + Console.WriteLine($"{midnight:O} Kind={midnight.Kind}"); + Console.WriteLine($"{utc:O} Kind={utc.Kind}"); + + // Neither type has a Now or a Today property. + var today = DateOnly.FromDateTime(DateTime.Now); + var timeOfDay = TimeOnly.FromDateTime(DateTime.Now); + + Console.WriteLine(today); + Console.WriteLine(timeOfDay); + + // TimeSpan in both directions. + var fromTimeSpan = TimeOnly.FromTimeSpan(TimeSpan.FromHours(11.5)); + var sinceMidnight = timeOnly.ToTimeSpan(); + + Console.WriteLine(fromTimeSpan); + Console.WriteLine(sinceMidnight); + + // The underlying day count. + var dayNumber = dateOnly.DayNumber; + var fromDayNumber = DateOnly.FromDayNumber(dayNumber); + + Console.WriteLine(dayNumber); + Console.WriteLine(fromDayNumber); + + // Parsing a string in a known, fixed format. + var parsed = DateOnly.ParseExact("2022-01-01", "yyyy-MM-dd"); + + Console.WriteLine(parsed); + + // AddHours wraps past midnight; the second overload reports the day boundary. + var elevenPM = new TimeOnly(23, 0); + var wrapped = elevenPM.AddHours(2); + var wrappedWithCount = elevenPM.AddHours(2, out var excessDays); + + Console.WriteLine($"{wrapped} / {wrappedWithCount} excessDays={excessDays}"); + } + public static void DemonstrateBasicOperators() { var firstOfJan = new DateOnly(2022, 1, 1); diff --git a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/DateOnlyTests.cs b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/DateOnlyTests.cs index fecbd41c64..33dca3836a 100644 --- a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/DateOnlyTests.cs +++ b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/DateOnlyTests.cs @@ -112,5 +112,92 @@ public void CanUseGreaterThanOperator() // Assert. Assert.True(isAfter); } + [Fact] + public void CanParseExactWithAKnownFormat() + { + // Arrange. + var text = "2022-01-01"; + + // Act. + var date = DateOnly.ParseExact(text, "yyyy-MM-dd"); + + // Assert. + Assert.Equal(new DateOnly(2022, 1, 1), date); + } + + [Fact] + public void CanConvertToDateTimeWithATimeOnly() + { + // Arrange. + var dateOnly = new DateOnly(2022, 1, 1); + var timeOnly = new TimeOnly(11, 30); + + // Act. + var combined = dateOnly.ToDateTime(timeOnly); + + // Assert. + Assert.Equal(new DateTime(2022, 1, 1, 11, 30, 0), combined); + Assert.Equal(DateTimeKind.Unspecified, combined.Kind); + } + + [Fact] + public void CanConvertToDateTimeAtMidnight() + { + // Arrange. + var dateOnly = new DateOnly(2022, 1, 1); + + // Act. + var midnight = dateOnly.ToDateTime(TimeOnly.MinValue); + + // Assert. + Assert.Equal(new DateTime(2022, 1, 1, 0, 0, 0), midnight); + Assert.Equal(DateTimeKind.Unspecified, midnight.Kind); + } + + [Fact] + public void CanConvertToDateTimeWithAKnownKind() + { + // Arrange. + var dateOnly = new DateOnly(2022, 1, 1); + var timeOnly = new TimeOnly(11, 30); + + // Act. + var utc = dateOnly.ToDateTime(timeOnly, DateTimeKind.Utc); + + // Assert. + Assert.Equal(DateTimeKind.Utc, utc.Kind); + Assert.Equal(new DateTime(2022, 1, 1, 11, 30, 0, DateTimeKind.Utc), utc); + } + + [Fact] + public void CanGetTodaysDateFromDateTimeNow() + { + // Arrange. + var now = DateTime.Now; + + // Act. + var today = DateOnly.FromDateTime(now); + + // Assert. CI runs at an arbitrary instant, so we assert consistency, not a value. + Assert.Equal(now.Year, today.Year); + Assert.Equal(now.Month, today.Month); + Assert.Equal(now.Day, today.Day); + } + + [Fact] + public void CanRoundTripThroughDayNumber() + { + // Arrange. + var dateOnly = new DateOnly(2022, 1, 1); + + // Act. + var dayNumber = dateOnly.DayNumber; + var roundTripped = DateOnly.FromDayNumber(dayNumber); + + // Assert. + Assert.Equal(dateOnly, roundTripped); + Assert.Equal(1, dateOnly.DayNumber - new DateOnly(2021, 12, 31).DayNumber); + } + } } \ No newline at end of file diff --git a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/Tests.csproj b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/Tests.csproj index 9f77f5f615..5ea8ec1057 100644 --- a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/Tests.csproj +++ b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/Tests.csproj @@ -1,20 +1,20 @@ - net6.0 + net10.0 enable false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/TimeOnlyTests.cs b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/TimeOnlyTests.cs index 4d8d3edc46..ba3fb00a18 100644 --- a/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/TimeOnlyTests.cs +++ b/dotnet-datetime/DateOnlyAndTimeOnlyInCSharp/Tests/TimeOnlyTests.cs @@ -97,5 +97,67 @@ public void CanUseGreaterThanOperator() // Assert. Assert.True(isAfter); } + [Fact] + public void CanConvertToAndFromTimeSpan() + { + // Arrange. + var timeOnly = new TimeOnly(11, 30); + + // Act. + var sinceMidnight = timeOnly.ToTimeSpan(); + var roundTripped = TimeOnly.FromTimeSpan(sinceMidnight); + + // Assert. + Assert.Equal(TimeSpan.FromHours(11.5), sinceMidnight); + Assert.Equal(timeOnly, roundTripped); + } + + [Fact] + public void CanGetTheCurrentTimeOfDayFromDateTimeNow() + { + // Arrange. + var now = DateTime.Now; + + // Act. + var timeOfDay = TimeOnly.FromDateTime(now); + + // Assert. CI runs at an arbitrary instant, so we assert consistency, not a value. + Assert.Equal(now.Hour, timeOfDay.Hour); + Assert.Equal(now.Minute, timeOfDay.Minute); + Assert.Equal(now.TimeOfDay.Ticks, timeOfDay.Ticks); + } + + [Fact] + public void AddHoursWrapsPastMidnightAndCanReportTheExcessDays() + { + // Arrange. + var elevenPM = new TimeOnly(23, 0); + + // Act. + var wrapped = elevenPM.AddHours(2); + var wrappedWithCount = elevenPM.AddHours(2, out var excessDays); + + // Assert. + Assert.Equal(new TimeOnly(1, 0), wrapped); + Assert.Equal(new TimeOnly(1, 0), wrappedWithCount); + Assert.Equal(1, excessDays); + } + + [Fact] + public void SubtractingTwoValuesIsAlwaysPositive() + { + // Arrange. + var elevenPM = new TimeOnly(23, 0); + var oneAM = new TimeOnly(1, 0); + + // Act. + var forwards = oneAM - elevenPM; + var backwards = elevenPM - oneAM; + + // Assert. + Assert.Equal(TimeSpan.FromHours(2), forwards); + Assert.Equal(TimeSpan.FromHours(22), backwards); + } + } } \ No newline at end of file