Skip to content

Commit

Permalink
Use DateOnly as our base type for Date Input (#249)
Browse files Browse the repository at this point in the history
Up until now we've had our own Date type for use with the Date Input
component. Now that we have access to a built-in one - DateOnly - our's
is going away. For now it's [Obsolete]d with a view to removing it in
the final 1.0 release.
  • Loading branch information
gunndabad authored Dec 9, 2022
1 parent ba5688b commit 42817a5
Show file tree
Hide file tree
Showing 17 changed files with 173 additions and 86 deletions.
8 changes: 4 additions & 4 deletions samples/Samples.DateInput/LocalDateDateInputModelConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ namespace Samples.DateInput
{
public class LocalDateDateInputModelConverter : DateInputModelConverter
{
public override bool CanConvertModelType(Type modelType) => modelType == typeof(LocalDate) || modelType == typeof(LocalDate?);
public override bool CanConvertModelType(Type modelType) => modelType == typeof(LocalDate);

public override object CreateModelFromDate(Type modelType, Date date)
public override object CreateModelFromDate(Type modelType, DateOnly date)
{
return new LocalDate(date.Year, date.Month, date.Day);
}

public override Date? GetDateFromModel(Type modelType, object model)
public override DateOnly? GetDateFromModel(Type modelType, object model)
{
var localDate = (LocalDate?)model;

return localDate.HasValue ?
new Date(localDate.Value.Year, localDate.Value.Month, localDate.Value.Day) :
new DateOnly(localDate.Value.Year, localDate.Value.Month, localDate.Value.Day) :
null;
}
}
Expand Down
37 changes: 23 additions & 14 deletions src/GovUk.Frontend.AspNetCore/Date.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace GovUk.Frontend.AspNetCore
{
/// <summary>
/// Represents a date.
/// </summary>
[Obsolete("Use DateOnly instead.", error: false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[StructLayout(LayoutKind.Auto)]
public readonly struct Date : IComparable, IComparable<Date>, IEquatable<Date>
{
private readonly DateTime _dt;
private readonly DateOnly _d;

/// <summary>
/// Creates a new instance of the <see cref="Date"/> structure to the specified year, month and day.
Expand All @@ -18,34 +21,40 @@ namespace GovUk.Frontend.AspNetCore
/// <param name="month">The month (1 through 12).</param>
/// <param name="day">The day (1 through the number of days in <paramref name="month" />).</param>
public Date(int year, int month, int day)
: this(new DateTime(year, month, day))
: this(new DateOnly(year, month, day))
{
}

private Date(DateTime dateTime)
private Date(DateOnly date)
{
_dt = dateTime;
_d = date;
}

/// <summary>
/// Gets the current date.
/// </summary>
public static Date Today => new(DateTime.Today);
public static Date Today => new(DateOnly.FromDateTime(DateTime.Today));

/// <summary>
/// Gets the day component of the date.
/// </summary>
public int Day => _dt.Day;
public int Day => _d.Day;

/// <summary>
/// Gets the month component of the date.
/// </summary>
public int Month => _dt.Month;
public int Month => _d.Month;

/// <summary>
/// Gets the year component of the date.
/// </summary>
public int Year => _dt.Year;
public int Year => _d.Year;

/// <summary>
/// Converts the specified <see cref="Date"/> to a <see cref="DateOnly"/>.
/// </summary>
/// <param name="date">The <see cref="Date"/> to convert.</param>
public static implicit operator DateOnly(Date date) => date._d;

/// <summary>
/// Determines whether two specified instances of <see cref="Date"/> are equal.
Expand All @@ -69,31 +78,31 @@ private Date(DateTime dateTime)
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns><see langword="true"/> if left is earlier than right; otherwise, <see langword="false"/>.</returns>
public static bool operator <(Date left, Date right) => left._dt < right._dt;
public static bool operator <(Date left, Date right) => left._d < right._d;

/// <summary>
/// Determines whether one specified <see cref="Date"/> represents a date that is the same as or earlier than another specified <see cref="Date"/>.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns><see langword="true"/> if left is the same as or earlier than right; otherwise, <see langword="false"/>.</returns>
public static bool operator <=(Date left, Date right) => left._dt <= right._dt;
public static bool operator <=(Date left, Date right) => left._d <= right._d;

/// <summary>
/// Determines whether one specified <see cref="Date"/> is later than another specified <see cref="Date"/>.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns><see langword="true"/> if left is later than right; otherwise, <see langword="false"/>.</returns>
public static bool operator >(Date left, Date right) => left._dt > right._dt;
public static bool operator >(Date left, Date right) => left._d > right._d;

/// <summary>
/// Determines whether one specified <see cref="Date"/> represents a date that is the same as or later than another specified <see cref="Date"/>.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns><see langword="true"/> if left is the same as or later than right; otherwise, <see langword="false"/>.</returns>
public static bool operator >=(Date left, Date right) => left._dt >= right._dt;
public static bool operator >=(Date left, Date right) => left._d >= right._d;

/// <inheritdoc/>
public int CompareTo(object? value)
Expand All @@ -112,7 +121,7 @@ public int CompareTo(object? value)
}

/// <inheritdoc/>
public int CompareTo(Date value) => _dt.CompareTo(value);
public int CompareTo(Date value) => _d.CompareTo(value);

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Date date && Equals(date);
Expand All @@ -128,7 +137,7 @@ public int CompareTo(object? value)
/// </summary>
/// <param name="value">The <see cref="DateTime"/> instance.</param>
/// <returns>The <see cref="Date"/> instance composed of the date part of the specified input time <see cref="DateTime"/> instance.</returns>
public static Date FromDateTime(DateTime value) => new(value);
public static Date FromDateTime(DateTime value) => new(value.Year, value.Minute, value.Day);

/// <summary>
/// Returns a <see cref="DateTime"/> that is set to the date of this <see cref="Date"/> instance.
Expand Down
16 changes: 12 additions & 4 deletions src/GovUk.Frontend.AspNetCore/DateDateInputModelConverter.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
#pragma warning disable CS0618 // Type or member is obsolete
using System;

namespace GovUk.Frontend.AspNetCore
{
internal class DateDateInputModelConverter : DateInputModelConverter
{
public override bool CanConvertModelType(Type modelType) =>
modelType == typeof(Date) || modelType == typeof(Date?);
public override bool CanConvertModelType(Type modelType) => modelType == typeof(Date);

public override object CreateModelFromDate(Type modelType, Date date) => date;
public override object CreateModelFromDate(Type modelType, DateOnly date) => new Date(date.Year, date.Month, date.Day);

public override Date? GetDateFromModel(Type modelType, object model) => (Date?)model;
public override DateOnly? GetDateFromModel(Type modelType, object model)
{
if (model is null)
{
return null;
}

return (Date)model;
}
}
}
4 changes: 2 additions & 2 deletions src/GovUk.Frontend.AspNetCore/DateInputModelConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public abstract class DateInputModelConverter
/// <param name="modelType">The model type to convert to.</param>
/// <param name="date">The <see cref="Date"/> instance to convert.</param>
/// <returns>An instance of <paramref name="modelType"/> that represents the <paramref name="date"/> argument.</returns>
public abstract object CreateModelFromDate(Type modelType, Date date);
public abstract object CreateModelFromDate(Type modelType, DateOnly date);

/// <summary>
/// Converts <paramref name="model"/> to instance of <see cref="Date"/>.
/// </summary>
/// <param name="modelType">The model type to convert from.</param>
/// <param name="model">The model instance to convert.</param>
/// <returns>The converted model instance.</returns>
public abstract Date? GetDateFromModel(Type modelType, object model);
public abstract DateOnly? GetDateFromModel(Type modelType, object model);

/// <summary>
/// Creates an instance of the specified model type from a set of parse errors.
Expand Down
14 changes: 14 additions & 0 deletions src/GovUk.Frontend.AspNetCore/DateOnlyDateInputModelConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable
using System;

namespace GovUk.Frontend.AspNetCore
{
internal class DateOnlyDateInputModelConverter : DateInputModelConverter
{
public override bool CanConvertModelType(Type modelType) => modelType == typeof(DateOnly);

public override object CreateModelFromDate(Type modelType, DateOnly date) => date;

public override DateOnly? GetDateFromModel(Type modelType, object model) => (DateOnly?)model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ namespace GovUk.Frontend.AspNetCore
{
internal class DateTimeDateInputModelConverter : DateInputModelConverter
{
public override bool CanConvertModelType(Type modelType) =>
modelType == typeof(DateTime) || modelType == typeof(DateTime?);
public override bool CanConvertModelType(Type modelType) => modelType == typeof(DateTime);

public override object CreateModelFromDate(Type modelType, Date date) => new DateTime(date.Year, date.Month, date.Day);
public override object CreateModelFromDate(Type modelType, DateOnly date) => new DateTime(date.Year, date.Month, date.Day);

public override Date? GetDateFromModel(Type modelType, object model)
public override DateOnly? GetDateFromModel(Type modelType, object model)
{
if (model is null)
{
return null;
}

return Date.FromDateTime((DateTime)model);
return DateOnly.FromDateTime((DateTime)model);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public GovUkFrontendAspNetCoreOptions()
DateInputModelConverters = new List<DateInputModelConverter>()
{
new DateDateInputModelConverter(),
new DateTimeDateInputModelConverter()
new DateTimeDateInputModelConverter(),
new DateOnlyDateInputModelConverter()
};

PrependErrorSummaryToForms = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
{
Guard.ArgumentNotNull(nameof(bindingContext), bindingContext);

var modelType = bindingContext.ModelType;
var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
if (!_dateInputModelConverter.CanConvertModelType(modelType))
{
throw new InvalidOperationException($"Cannot bind {modelType.Name}.");
Expand Down Expand Up @@ -118,7 +118,7 @@ internal static string GetModelStateErrorMessage(DateInputParseErrors parseError
}

// internal for testing
internal static DateInputParseErrors Parse(string? day, string? month, string? year, out Date? date)
internal static DateInputParseErrors Parse(string? day, string? month, string? year, out DateOnly? date)
{
day ??= string.Empty;
month ??= string.Empty;
Expand Down
13 changes: 7 additions & 6 deletions src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using GovUk.Frontend.AspNetCore.HtmlGeneration;
using GovUk.Frontend.AspNetCore.ModelBinding;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -244,7 +245,7 @@ private TagBuilder GenerateDateInput(DateInputContext dateInputContext, bool hav
DateInputAttributes.ToAttributeDictionary());

DateInputItem CreateDateInputItem(
Func<Date?, string?> getComponentFromValue,
Func<DateOnly?, string?> getComponentFromValue,
string defaultLabel,
string defaultName,
string defaultClass,
Expand Down Expand Up @@ -316,7 +317,7 @@ DateInputItem CreateDateInputItem(
}
}

Date? GetValueAsDate()
DateOnly? GetValueAsDate()
{
if (Value is null)
{
Expand All @@ -337,12 +338,12 @@ DateInputItem CreateDateInputItem(
return null;
}

Date? GetValueFromModel()
DateOnly? GetValueFromModel()
{
Debug.Assert(AspFor != null);

var modelValue = AspFor!.Model;
var modelType = AspFor.ModelExplorer.ModelType;
var underlyingModelType = Nullable.GetUnderlyingType(AspFor.ModelExplorer.ModelType) ?? AspFor.ModelExplorer.ModelType;

if (modelValue is null)
{
Expand All @@ -353,9 +354,9 @@ DateInputItem CreateDateInputItem(

foreach (var converter in dateInputModelConverters)
{
if (converter.CanConvertModelType(modelType))
if (converter.CanConvertModelType(underlyingModelType))
{
return converter.GetDateFromModel(modelType, modelValue);
return converter.GetDateFromModel(underlyingModelType, modelValue);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ bool IsModelExpressionForDate()
{
Debug.Assert(AspFor != null);

var modelType = AspFor!.Metadata.ModelType;
var modelType = Nullable.GetUnderlyingType(AspFor!.Metadata.ModelType) ?? AspFor!.Metadata.ModelType;
return _options.DateInputModelConverters.Any(c => c.CanConvertModelType(modelType));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public class DateInputsTestsModel
{
[Display(Name = "Date of birth")]
[Required(ErrorMessage = "Enter your date of birth")]
public Date? Date { get; set; }
public DateOnly? Date { get; set; }

[Display(Name = "Date of birth")]
[Required(ErrorMessage = "Enter your date of birth")]
Expand All @@ -272,17 +272,17 @@ public class CustomDateTypeConverter : DateInputModelConverter
{
public override bool CanConvertModelType(Type modelType) => modelType == typeof(CustomDateType);

public override object CreateModelFromDate(Type modelType, Date date) => new CustomDateType(date.Year, date.Month, date.Day);
public override object CreateModelFromDate(Type modelType, DateOnly date) => new CustomDateType(date.Year, date.Month, date.Day);

public override Date? GetDateFromModel(Type modelType, object model)
public override DateOnly? GetDateFromModel(Type modelType, object model)
{
if (model is null)
{
return null;
}

var cdt = (CustomDateType)model;
return new Date(cdt.Y, cdt.M, cdt.D);
return new DateOnly(cdt.Y, cdt.M, cdt.D);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#pragma warning disable CS0618 // Type or member is obsolete
using System;
using Xunit;

Expand All @@ -9,7 +10,7 @@ public class DateDateInputModelConverterTests
[MemberData(nameof(CreateDateFromElementsData))]
public void CreateDateFromComponents_ReturnsExpectedResult(
Type modelType,
Date date,
DateOnly date,
object expectedResult)
{
// Arrange
Expand All @@ -27,7 +28,7 @@ public void CreateDateFromComponents_ReturnsExpectedResult(
public void GetDateFromModel_ReturnsExpectedResult(
Type modelType,
object model,
Date? expectedResult)
DateOnly? expectedResult)
{
// Arrange
var converter = new DateDateInputModelConverter();
Expand All @@ -39,16 +40,16 @@ public void GetDateFromModel_ReturnsExpectedResult(
Assert.Equal(expectedResult, result);
}

public static TheoryData<Type, Date?, object> CreateDateFromElementsData { get; } = new TheoryData<Type, Date?, object>()
public static TheoryData<Type, DateOnly?, object> CreateDateFromElementsData { get; } = new()
{
{ typeof(Date), new Date(2020, 4, 1), new Date(2020, 4, 1) },
{ typeof(Date?), new Date(2020, 4, 1), (Date?)new Date(2020, 4, 1) }
{ typeof(Date), new DateOnly(2020, 4, 1), new Date(2020, 4, 1) },
{ typeof(Date?), new DateOnly(2020, 4, 1), (Date?)new Date(2020, 4, 1) }
};

public static TheoryData<Type, object, Date?> GetDateFromModelData { get; } = new TheoryData<Type, object, Date?>()
public static TheoryData<Type, object, DateOnly?> GetDateFromModelData { get; } = new()
{
{ typeof(Date), new Date(2020, 4, 1), new Date(2020, 4, 1) },
{ typeof(Date?), (Date?)new Date(2020, 4, 1), new Date(2020, 4, 1) }
{ typeof(Date), new Date(2020, 4, 1), new DateOnly(2020, 4, 1) },
{ typeof(Date?), (Date?)new Date(2020, 4, 1), new Date(2020, 4, 1) },
};
}
}
Loading

0 comments on commit 42817a5

Please sign in to comment.