feat: add DateOnly extensions

This commit is contained in:
Oliver Booth 2023-04-04 10:10:55 +01:00
parent c2bb08a9f3
commit b8c3a5121a
No known key found for this signature in database
GPG Key ID: 20BEB9DC87961025
3 changed files with 441 additions and 0 deletions

View File

@ -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`.

View File

@ -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

View File

@ -0,0 +1,208 @@
#if NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace X10D.Time;
/// <summary>
/// Time-related extension methods for <see cref="DateOnly" />.
/// </summary>
public static class DateOnlyExtensions
{
/// <summary>
/// Returns the rounded-down integer number of years since a given date as of today.
/// </summary>
/// <param name="value">The date from which to calculate.</param>
/// <returns>The rounded-down integer number of years since <paramref name="value" /> as of today.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
[ExcludeFromCodeCoverage]
public static int Age(this DateOnly value)
{
return value.Age(DateOnly.FromDateTime(DateTime.Today));
}
/// <summary>
/// Returns the rounded-down integer number of years since a given date as of another specified date.
/// </summary>
/// <param name="value">The date from which to calculate.</param>
/// <param name="referenceDate">The date to use as the calculation reference.</param>
/// <returns>
/// The rounded-down integer number of years since <paramref name="value" /> as of the date specified by
/// <paramref name="referenceDate" />.
/// </returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int Age(this DateOnly value, DateOnly referenceDate)
{
return value.ToDateTime(default).Age(referenceDate.ToDateTime(default));
}
/// <summary>
/// Deconstructs the current <see cref="DateOnly" /> into its year, month, and day.
/// </summary>
/// <param name="value">The date to deconstruct.</param>
/// <param name="year">When this method returns, contains the year.</param>
/// <param name="month">When this method returns, contains the month.</param>
/// <param name="day">When this method returns, contains the day.</param>
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;
}
/// <summary>
/// Gets a date representing the first occurence of a specified day of the week in the current month.
/// </summary>
/// <param name="value">The current date.</param>
/// <param name="dayOfWeek">The day of the week.</param>
/// <returns>A <see cref="DateTimeOffset" /> representing the first occurence of <paramref name="dayOfWeek" />.</returns>
[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;
}
/// <summary>
/// Gets a date representing the first day of the current month.
/// </summary>
/// <param name="value">The current date.</param>
/// <returns>A <see cref="DateTimeOffset" /> representing the first day of the current month.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static DateOnly FirstDayOfMonth(this DateOnly value)
{
return value.AddDays(1 - value.Day);
}
/// <summary>
/// Gets the ISO-8601 week number of the year for the current date.
/// </summary>
/// <param name="value">The date whose week number to return.</param>
/// <returns>The ISO-8601 week number of the year.</returns>
/// <author>Shawn Steele, Microsoft</author>
/// <remarks>
/// This implementation is directly inspired from a
/// <a href="https://docs.microsoft.com/en-gb/archive/blogs/shawnste/iso-8601-week-of-year-format-in-microsoft-net">
/// blog post
/// </a>.
/// about this subject.
/// </remarks>
[Pure]
public static int GetIso8601WeekOfYear(this DateOnly value)
{
return value.ToDateTime(default).GetIso8601WeekOfYear();
}
/// <summary>
/// Returns a value indicating whether the year represented by the current <see cref="DateOnly" /> is a leap year.
/// </summary>
/// <param name="value">The date whose year to check.</param>
/// <returns>
/// <see langword="true" /> if the year represented by <paramref name="value" /> is a leap year; otherwise,
/// <see langword="false" />.
/// </returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static bool IsLeapYear(this DateOnly value)
{
return DateTime.IsLeapYear(value.Year);
}
/// <summary>
/// Gets a date representing the final occurence of a specified day of the week in the current month.
/// </summary>
/// <param name="value">The current date.</param>
/// <param name="dayOfWeek">The day of the week.</param>
/// <returns>A <see cref="DateTimeOffset" /> representing the final occurence of <paramref name="dayOfWeek" />.</returns>
[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);
}
/// <summary>
/// Gets a date representing the last day of the current month.
/// </summary>
/// <param name="value">The current date.</param>
/// <returns>A <see cref="DateTimeOffset" /> representing the last day of the current month.</returns>
[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);
}
/// <summary>
/// Gets a date representing the next occurence of a specified day of the week in the current month.
/// </summary>
/// <param name="value">The current date.</param>
/// <param name="dayOfWeek">The day of the week.</param>
/// <returns>A <see cref="DateTimeOffset" /> representing the next occurence of <paramref name="dayOfWeek" />.</returns>
[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);
}
/// <summary>
/// Returns the number of milliseconds that have elapsed since 1970-01-01T00:00:00.000Z.
/// </summary>
/// <param name="value">The current date.</param>
/// <param name="time">A reference time to use with the current date.</param>
/// <returns>The number of milliseconds that have elapsed since 1970-01-01T00:00:00.000Z.</returns>
[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();
}
/// <summary>
/// Returns the number of seconds that have elapsed since 1970-01-01T00:00:00.000Z.
/// </summary>
/// <param name="value">The current date.</param>
/// <param name="time">A reference time to use with the current date.</param>
/// <returns>The number of seconds that have elapsed since 1970-01-01T00:00:00.000Z.</returns>
[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