From b8c3a5121adaf9555eb31ee2fc4970eb4e86d9f7 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Tue, 4 Apr 2023 10:10:55 +0100 Subject: [PATCH] feat: add DateOnly extensions --- CHANGELOG.md | 3 + X10D.Tests/src/Time/DateOnlyTests.cs | 230 +++++++++++++++++++++++++++ X10D/src/Time/DateOnlyExtensions.cs | 208 ++++++++++++++++++++++++ 3 files changed, 441 insertions(+) create mode 100644 X10D.Tests/src/Time/DateOnlyTests.cs create mode 100644 X10D/src/Time/DateOnlyExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d7323..e5d5ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 4.0.0 - [Unreleased] +### Added +- X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`. + ### Changed - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. diff --git a/X10D.Tests/src/Time/DateOnlyTests.cs b/X10D.Tests/src/Time/DateOnlyTests.cs new file mode 100644 index 0000000..d58b9dc --- /dev/null +++ b/X10D.Tests/src/Time/DateOnlyTests.cs @@ -0,0 +1,230 @@ +#if NET6_0_OR_GREATER +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Time; + +namespace X10D.Tests.Time; + +[TestClass] +public class DateOnlyTests +{ + [TestMethod] + public void Age_ShouldBe17_Given31December1991Birthday_And30December2017Date() + { + var reference = new DateOnly(2017, 12, 30); + var birthday = new DateOnly(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(17, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And1January2018Date() + { + var reference = new DateOnly(2018, 1, 1); + var birthday = new DateOnly(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); + } + + [TestMethod] + public void Age_ShouldBe18_Given31December1991Birthday_And31December2017Date() + { + var reference = new DateOnly(2017, 12, 31); + var birthday = new DateOnly(1999, 12, 31); + + int age = birthday.Age(reference); + + Assert.AreEqual(18, age); + } + + [TestMethod] + public void Deconstruct_ShouldDeconstructToTuple_GivenDateOnly() + { + var date = new DateOnly(2017, 12, 31); + + (int year, int month, int day) = date; + + Assert.AreEqual(2017, year); + Assert.AreEqual(12, month); + Assert.AreEqual(31, day); + } + + [TestMethod] + public void First_ShouldBeSaturday_Given1Jan2000() + { + var date = new DateOnly(2000, 1, 1); + + Assert.AreEqual(new DateOnly(2000, 1, 1), date.First(DayOfWeek.Saturday)); + Assert.AreEqual(new DateOnly(2000, 1, 2), date.First(DayOfWeek.Sunday)); + Assert.AreEqual(new DateOnly(2000, 1, 3), date.First(DayOfWeek.Monday)); + Assert.AreEqual(new DateOnly(2000, 1, 4), date.First(DayOfWeek.Tuesday)); + Assert.AreEqual(new DateOnly(2000, 1, 5), date.First(DayOfWeek.Wednesday)); + Assert.AreEqual(new DateOnly(2000, 1, 6), date.First(DayOfWeek.Thursday)); + Assert.AreEqual(new DateOnly(2000, 1, 7), date.First(DayOfWeek.Friday)); + } + + [TestMethod] + public void FirstDayOfMonth_ShouldBe1st_GivenToday() + { + DateOnly today = DateOnly.FromDateTime(DateTime.Now.Date); + + Assert.AreEqual(new DateOnly(today.Year, today.Month, 1), today.FirstDayOfMonth()); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given4January1970() + { + var date = new DateOnly(1970, 1, 4); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn1_Given31December1969() + { + var date = new DateOnly(1969, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(1, iso8601WeekOfYear); + } + + [TestMethod] + public void GetIso8601WeekOfYear_ShouldReturn53_Given31December1970() + { + var date = new DateOnly(1970, 12, 31); + int iso8601WeekOfYear = date.GetIso8601WeekOfYear(); + + Assert.AreEqual(53, iso8601WeekOfYear); + } + + [TestMethod] + public void IsLeapYear_ShouldBeFalse_Given1999() + { + var date = new DateOnly(1999, 1, 1); + Assert.IsFalse(date.IsLeapYear()); + } + + [TestMethod] + public void IsLeapYear_ShouldBeTrue_Given2000() + { + var date = new DateOnly(2000, 1, 1); + Assert.IsTrue(date.IsLeapYear()); + } + + [TestMethod] + public void IsLeapYear_ShouldBeFalse_Given2001() + { + var date = new DateOnly(2001, 1, 1); + Assert.IsFalse(date.IsLeapYear()); + } + + [TestMethod] + public void IsLeapYear_ShouldBeFalse_Given2100() + { + var date = new DateOnly(2100, 1, 1); + Assert.IsFalse(date.IsLeapYear()); + } + + [TestMethod] + public void LastSaturday_ShouldBe29th_Given1Jan2000() + { + var date = new DateOnly(2000, 1, 1); + + Assert.AreEqual(new DateOnly(2000, 1, 29), date.Last(DayOfWeek.Saturday)); + Assert.AreEqual(new DateOnly(2000, 1, 30), date.Last(DayOfWeek.Sunday)); + Assert.AreEqual(new DateOnly(2000, 1, 31), date.Last(DayOfWeek.Monday)); + Assert.AreEqual(new DateOnly(2000, 1, 25), date.Last(DayOfWeek.Tuesday)); + Assert.AreEqual(new DateOnly(2000, 1, 26), date.Last(DayOfWeek.Wednesday)); + Assert.AreEqual(new DateOnly(2000, 1, 27), date.Last(DayOfWeek.Thursday)); + Assert.AreEqual(new DateOnly(2000, 1, 28), date.Last(DayOfWeek.Friday)); + } + + [TestMethod] + public void LastDayOfMonth_ShouldBe28th_GivenFebruary1999() + { + var february = new DateOnly(1999, 2, 1); + + Assert.AreEqual(new DateOnly(february.Year, february.Month, 28), february.LastDayOfMonth()); + } + + [TestMethod] + public void LastDayOfMonth_ShouldBe29th_GivenFebruary2000() + { + var february = new DateOnly(2000, 2, 1); + + Assert.AreEqual(new DateOnly(february.Year, february.Month, 29), february.LastDayOfMonth()); + } + + [TestMethod] + public void LastDayOfMonth_ShouldBe28th_GivenFebruary2001() + { + var february = new DateOnly(2001, 2, 1); + + Assert.AreEqual(new DateOnly(february.Year, february.Month, 28), february.LastDayOfMonth()); + } + + [TestMethod] + public void LastDayOfMonth_ShouldBe30th_GivenAprilJuneSeptemberNovember() + { + var april = new DateOnly(2000, 4, 1); + var june = new DateOnly(2000, 6, 1); + var september = new DateOnly(2000, 9, 1); + var november = new DateOnly(2000, 11, 1); + + Assert.AreEqual(new DateOnly(april.Year, april.Month, 30), april.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(june.Year, june.Month, 30), june.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(september.Year, september.Month, 30), september.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(november.Year, november.Month, 30), november.LastDayOfMonth()); + } + + [TestMethod] + public void LastDayOfMonth_ShouldBe31st_GivenJanuaryMarchMayJulyAugustOctoberDecember() + { + var january = new DateOnly(2000, 1, 1); + var march = new DateOnly(2000, 3, 1); + var may = new DateOnly(2000, 5, 1); + var july = new DateOnly(2000, 7, 1); + var august = new DateOnly(2000, 8, 1); + var october = new DateOnly(2000, 10, 1); + var december = new DateOnly(2000, 12, 1); + + Assert.AreEqual(new DateOnly(january.Year, january.Month, 31), january.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(march.Year, march.Month, 31), march.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(may.Year, may.Month, 31), may.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(july.Year, july.Month, 31), july.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(august.Year, august.Month, 31), august.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(october.Year, october.Month, 31), october.LastDayOfMonth()); + Assert.AreEqual(new DateOnly(december.Year, december.Month, 31), december.LastDayOfMonth()); + } + + [TestMethod] + public void NextSaturday_ShouldBe8th_Given1Jan2000() + { + var date = new DateOnly(2000, 1, 1); + + Assert.AreEqual(new DateOnly(2000, 1, 8), date.Next(DayOfWeek.Saturday)); + } + + [TestMethod] + public void ToUnixTimeMilliseconds_ShouldBe946684800000_Given1Jan2000() + { + var date = new DateOnly(2000, 1, 1); + var time = new TimeOnly(0, 0, 0); + + Assert.AreEqual(946684800000, date.ToUnixTimeMilliseconds(time)); + } + + [TestMethod] + public void ToUnixTimeSeconds_ShouldBe946684800_Given1Jan2000() + { + var date = new DateOnly(2000, 1, 1); + var time = new TimeOnly(0, 0, 0); + + Assert.AreEqual(946684800, date.ToUnixTimeSeconds(time)); + } +} +#endif diff --git a/X10D/src/Time/DateOnlyExtensions.cs b/X10D/src/Time/DateOnlyExtensions.cs new file mode 100644 index 0000000..032af08 --- /dev/null +++ b/X10D/src/Time/DateOnlyExtensions.cs @@ -0,0 +1,208 @@ +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace X10D.Time; + +/// +/// Time-related extension methods for . +/// +public static class DateOnlyExtensions +{ + /// + /// Returns the rounded-down integer number of years since a given date as of today. + /// + /// The date from which to calculate. + /// The rounded-down integer number of years since as of today. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + [ExcludeFromCodeCoverage] + public static int Age(this DateOnly value) + { + return value.Age(DateOnly.FromDateTime(DateTime.Today)); + } + + /// + /// Returns the rounded-down integer number of years since a given date as of another specified date. + /// + /// The date from which to calculate. + /// The date to use as the calculation reference. + /// + /// The rounded-down integer number of years since as of the date specified by + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static int Age(this DateOnly value, DateOnly referenceDate) + { + return value.ToDateTime(default).Age(referenceDate.ToDateTime(default)); + } + + /// + /// Deconstructs the current into its year, month, and day. + /// + /// The date to deconstruct. + /// When this method returns, contains the year. + /// When this method returns, contains the month. + /// When this method returns, contains the day. + public static void Deconstruct(this DateOnly value, out int year, out int month, out int day) + { + year = value.Year; + month = value.Month; + day = value.Day; + } + + /// + /// Gets a date representing the first occurence of a specified day of the week in the current month. + /// + /// The current date. + /// The day of the week. + /// A representing the first occurence of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static DateOnly First(this DateOnly value, DayOfWeek dayOfWeek) + { + DateOnly first = value.FirstDayOfMonth(); + + if (first.DayOfWeek != dayOfWeek) + { + first = first.Next(dayOfWeek); + } + + return first; + } + + /// + /// Gets a date representing the first day of the current month. + /// + /// The current date. + /// A representing the first day of the current month. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static DateOnly FirstDayOfMonth(this DateOnly value) + { + return value.AddDays(1 - value.Day); + } + + /// + /// Gets the ISO-8601 week number of the year for the current date. + /// + /// The date whose week number to return. + /// The ISO-8601 week number of the year. + /// Shawn Steele, Microsoft + /// + /// This implementation is directly inspired from a + /// + /// blog post + /// . + /// about this subject. + /// + [Pure] + public static int GetIso8601WeekOfYear(this DateOnly value) + { + return value.ToDateTime(default).GetIso8601WeekOfYear(); + } + + /// + /// Returns a value indicating whether the year represented by the current is a leap year. + /// + /// The date whose year to check. + /// + /// if the year represented by is a leap year; otherwise, + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static bool IsLeapYear(this DateOnly value) + { + return DateTime.IsLeapYear(value.Year); + } + + /// + /// Gets a date representing the final occurence of a specified day of the week in the current month. + /// + /// The current date. + /// The day of the week. + /// A representing the final occurence of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static DateOnly Last(this DateOnly value, DayOfWeek dayOfWeek) + { + DateOnly last = value.LastDayOfMonth(); + var lastDayOfWeek = last.DayOfWeek; + + int diff = dayOfWeek - lastDayOfWeek; + int offset = diff > 0 ? diff - 7 : diff; + + return last.AddDays(offset); + } + + /// + /// Gets a date representing the last day of the current month. + /// + /// The current date. + /// A representing the last day of the current month. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static DateOnly LastDayOfMonth(this DateOnly value) + { + int daysInMonth = DateTime.DaysInMonth(value.Year, value.Month); + return new DateOnly(value.Year, value.Month, daysInMonth); + } + + /// + /// Gets a date representing the next occurence of a specified day of the week in the current month. + /// + /// The current date. + /// The day of the week. + /// A representing the next occurence of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static DateOnly Next(this DateOnly value, DayOfWeek dayOfWeek) + { + int offsetDays = dayOfWeek - value.DayOfWeek; + + if (offsetDays <= 0) + { + offsetDays += 7; + } + + return value.AddDays(offsetDays); + } + + /// + /// Returns the number of milliseconds that have elapsed since 1970-01-01T00:00:00.000Z. + /// + /// The current date. + /// A reference time to use with the current date. + /// The number of milliseconds that have elapsed since 1970-01-01T00:00:00.000Z. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long ToUnixTimeMilliseconds(this DateOnly value, TimeOnly time) + { + return value.ToDateTime(time).ToUnixTimeMilliseconds(); + } + + /// + /// Returns the number of seconds that have elapsed since 1970-01-01T00:00:00.000Z. + /// + /// The current date. + /// A reference time to use with the current date. + /// The number of seconds that have elapsed since 1970-01-01T00:00:00.000Z. + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static long ToUnixTimeSeconds(this DateOnly value, TimeOnly time) + { + return value.ToDateTime(time).ToUnixTimeSeconds(); + } +} +#endif