Skip to content

Commit

Permalink
Merge pull request #2990 from FirelyTeam/6.0/2900-align-null-behaviou…
Browse files Browse the repository at this point in the history
…r-for-datetimes

Align null behaviour for DateTimes
  • Loading branch information
ewoutkramer authored Dec 9, 2024
2 parents c684103 + 39eb672 commit 742bae6
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 321 deletions.
7 changes: 7 additions & 0 deletions src/Hl7.Fhir.Base/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,13 @@
<Right>lib/net8.0/Hl7.Fhir.Base.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Hl7.Fhir.Model.Date.ToDateTimeOffset</Target>
<Left>lib/net8.0/Hl7.Fhir.Base.dll</Left>
<Right>lib/net8.0/Hl7.Fhir.Base.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Hl7.Fhir.Model.Meta.get_ProfileElement</Target>
Expand Down
184 changes: 88 additions & 96 deletions src/Hl7.Fhir.Base/Model/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,129 +27,121 @@ POSSIBILITY OF SUCH DAMAGE.
*/
#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using P = Hl7.Fhir.ElementModel.Types;

#nullable enable
namespace Hl7.Fhir.Model;

namespace Hl7.Fhir.Model
public partial class Date
{
public partial class Date
public Date(int year, int month, int day)
: this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTHDAY, year, month, day))
{
public Date(int year, int month, int day)
: this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTHDAY, year, month, day))
{
}
}

public Date(int year, int month)
: this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTH, year, month))
{
}
public Date(int year, int month)
: this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTH, year, month))
{
}

public Date(int year) : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEAR, year))
{
}
public Date(int year) : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEAR, year))
{
}

public static Date FromDateTimeOffset(DateTimeOffset date) => new(date.Year, date.Month, date.Day);
public static Date FromDateTimeOffset(DateTimeOffset date) => new(date.Year, date.Month, date.Day);

/// <summary>
/// Gets the current date in the local timezone.
/// </summary>
public static Date Today() => FromDateTimeOffset(DateTimeOffset.Now);
/// <summary>
/// Gets the current date in the local timezone.
/// </summary>
public static Date Today() => FromDateTimeOffset(DateTimeOffset.Now);

/// <summary>
/// Gets the current date in UTC.
/// </summary>
public static Date UtcToday() => FromDateTimeOffset(DateTimeOffset.UtcNow);
/// <summary>
/// Gets the current date in UTC.
/// </summary>
public static Date UtcToday() => FromDateTimeOffset(DateTimeOffset.UtcNow);

[NonSerialized] // To prevent binary serialization from serializing this field
private P.Date? _parsedValue = null;
[NonSerialized] // To prevent binary serialization from serializing this field
private P.Date? _parsedValue = null;

// This is a sentinel value that marks that the current string representation is
// not parseable, so we don't have to try again. It's value is never used, it's just
// checked by reference.
private static readonly P.Date INVALID_VALUE = P.Date.FromDateTimeOffset(DateTimeOffset.MinValue);
// This is a sentinel value that marks that the current string representation is
// not parseable, so we don't have to try again. It's value is never used, it's just
// checked by reference.
private static readonly P.Date INVALID_VALUE = P.Date.FromDateTimeOffset(DateTimeOffset.MinValue);

/// <summary>
/// Converts a Fhir Date to a <see cref="P.Date"/>.
/// </summary>
/// <returns>true if the Fhir Date contains a valid date string, false otherwise.</returns>
public bool TryToDate([NotNullWhen(true)] out P.Date? date)
/// <summary>
/// Converts a Fhir Date to a <see cref="P.Date"/>.
/// </summary>
/// <returns>true if the Fhir Date contains a valid date string, false otherwise.</returns>
public bool TryToDate([NotNullWhen(true)] out P.Date? date)
{
if (_parsedValue is null)
{
if (_parsedValue is null)
{
if (Value is not null && !(P.Date.TryParse(Value, out _parsedValue) && !_parsedValue!.HasOffset))
_parsedValue = INVALID_VALUE;
}

if (hasInvalidParsedValue())
{
date = null;
return false;
}
else
{
date = _parsedValue!;
return true;
}

bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE);
if (Value is not null && !(P.Date.TryParse(Value, out _parsedValue) && !_parsedValue!.HasOffset))
_parsedValue = INVALID_VALUE;
}

/// <summary>
/// Converts a Fhir Date to a <see cref="P.Date"/>.
/// </summary>
/// <returns>The Date, or null if the <see cref="Value"/> is null.</returns>
/// <exception cref="FormatException">Thrown when the Value does not contain a valid FHIR Date.</exception>
public P.Date? ToDate() => TryToDate(out var dt) ? dt : throw new FormatException($"String '{Value}' was not recognized as a valid date.");

protected override void OnObjectValueChanged()
if (hasInvalidParsedValue())
{
_parsedValue = null;
base.OnObjectValueChanged();
date = null;
return false;
}

/// <summary>
/// Converts this Fhir Fhir Date to a <see cref="DateTimeOffset"/>.
/// </summary>
/// <returns>A DateTimeOffset filled out to midnight, january 1 (UTC) in case of a partial date.</returns>
public DateTimeOffset? ToDateTimeOffset()
{
if (Value == null) return null; // Note: this behaviour is inconsistent with ToDateTimeOffset() in FhirDateTime
date = _parsedValue!;
return true;

// ToDateTimeOffset() will convert partial date/times by filling out to midnight/january 1 UTC
if (!TryToDate(out var dt))
throw new FormatException($"Date '{Value}' was not recognized as a valid datetime.");
bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE);
}

// Since Value is not null and the parsed value is valid, dto will not be null
return dt!.ToDateTimeOffset(TimeSpan.Zero);
}
/// <summary>
/// Converts a Fhir Date to a <see cref="P.Date"/>.
/// </summary>
/// <returns>The Date, or null if the <see cref="Value"/> is null.</returns>
/// <exception cref="FormatException">Thrown when the Value does not contain a valid FHIR Date.</exception>
public P.Date ToDate() => TryToDate(out var dt) ? dt : throw new FormatException($"String '{Value}' was not recognized as a valid date.");

protected override void OnObjectValueChanged()
{
_parsedValue = null;
base.OnObjectValueChanged();
}

/// <summary>
/// Convert this Fhir Date to a <see cref="DateTimeOffset"/>.
/// </summary>
/// <returns>True if the value of the Fhir Date is not null and can be parsed as a DateTimeOffset, false otherwise.</returns>
public bool TryToDateTimeOffset(out DateTimeOffset dto)
/// <summary>
/// Converts this Fhir Fhir Date to a <see cref="DateTimeOffset"/>.
/// </summary>
/// <returns>A DateTimeOffset filled out to midnight, january 1 (UTC) in case of a partial date.</returns>
public DateTimeOffset ToDateTimeOffset()
{
if (Value == null) throw new InvalidOperationException("Date's value is null.");

// TryToDate() will convert partial date/times by filling out to midnight/january 1 UTC
if (!TryToDate(out var dt))
throw new FormatException($"Date '{Value}' was not recognized as a valid datetime.");

// Since Value is not null and the parsed value is valid, dto will not be null
return dt.ToDateTimeOffset(TimeSpan.Zero);
}

/// <summary>
/// Convert this Fhir Date to a <see cref="DateTimeOffset"/>.
/// </summary>
/// <returns>True if the value of the Fhir Date is not null and can be parsed as a DateTimeOffset, false otherwise.</returns>
public bool TryToDateTimeOffset(out DateTimeOffset dto)
{
if (Value is not null && TryToDate(out var dt))
{
if (Value is not null && TryToDate(out var dt))
{
dto = dt.ToDateTimeOffset(TimeSpan.Zero);
return true;
}
else
{
dto = default;
return false;
}
dto = dt.ToDateTimeOffset(TimeSpan.Zero);
return true;
}

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
public static bool IsValidValue(string value) => P.Date.TryParse(value, out var parsed) && !parsed.HasOffset;
dto = default;
return false;
}
}

#nullable restore
/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
public static bool IsValidValue(string value) => P.Date.TryParse(value, out var parsed) && !parsed.HasOffset;
}
Loading

0 comments on commit 742bae6

Please sign in to comment.