Skip to content

Commit

Permalink
Merge pull request #2541 from FirelyTeam/feature/2534-improve-date-pe…
Browse files Browse the repository at this point in the history
…rformance

2534 Improve performance of Date handling
  • Loading branch information
marcovisserFurore authored Jul 19, 2023
2 parents d2a0b16 + d452708 commit 286cf55
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 231 deletions.
1 change: 0 additions & 1 deletion src/Benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>Benchmarks</AssemblyName>
<AssemblyName>Firely.Sdk.Benchmarks</AssemblyName>
<RootNamespace>Firely.Sdk.Benchmarks</RootNamespace>
</PropertyGroup>

Expand Down
62 changes: 62 additions & 0 deletions src/Benchmarks/DateTimeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using BenchmarkDotNet.Attributes;
using Hl7.Fhir.Model;
using System;

namespace Firely.Sdk.Benchmarks
{
public class DateTimeBenchmarks
{
[GlobalSetup]
public void BenchmarkSetup()
{
_dateTimeInstance = new FhirDateTime(DATETIME);
_ = _dateTimeInstance.TryToDateTime(out var _); // trigger initial compile of regex

_dateInstance = new Date(DATE);
_ = _dateInstance.TryToDate(out var _); // trigger initial compile of regex
}

private const string DATETIME = "2023-07-11T13:00:00";
private FhirDateTime _dateTimeInstance;

[Benchmark]
public DateTimeOffset DateTimeToDTO_Uncached()
{
// Clear the cache each invocation
_dateTimeInstance.Value = DATETIME;
_ = _dateTimeInstance.TryToDateTimeOffset(TimeSpan.Zero, out var result);
_dateTimeInstance.Value = null;

return result;
}

[Benchmark]
public DateTimeOffset DateTimeToDTO_Cached()
{
_ = _dateTimeInstance.TryToDateTimeOffset(TimeSpan.Zero, out var result);
return result;
}

private const string DATE = "2023-07-11";
private Date _dateInstance;


[Benchmark]
public DateTimeOffset DateToDTO_Uncached()
{
// Clear the cache each invocation
_dateInstance.Value = DATETIME;
_ = _dateInstance.TryToDateTimeOffset(out var result);
_dateInstance.Value = null;

return result;
}

[Benchmark]
public DateTimeOffset DateToDTO_Cached()
{
_ = _dateInstance.TryToDateTimeOffset(out var result);
return result;
}
}
}
92 changes: 48 additions & 44 deletions src/Hl7.Fhir.Base/ElementModel/Types/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ namespace Hl7.Fhir.ElementModel.Types
{
public class Date : Any, IComparable, ICqlEquatable, ICqlOrderable, ICqlConvertible
{
private Date(string original, DateTimeOffset parsedValue, DateTimePrecision precision, bool hasOffset)
private Date(DateTimeOffset value, DateTimePrecision precision, bool hasOffset)
{
_original = original;
_parsedValue = parsedValue;
if (precision > DateTimePrecision.Day) throw new ArgumentException($"Invalid precision {precision}, cannot be more than {nameof(DateTimePrecision.Day)}.", nameof(precision));

_value = DateTime.RoundToPrecision(value, precision, hasOffset);
Precision = precision;
HasOffset = hasOffset;
}
Expand All @@ -31,22 +32,10 @@ public static Date Parse(string representation) =>
public static bool TryParse(string representation, out Date value) => tryParse(representation, out value);

public static Date FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision prec = DateTimePrecision.Day,
bool includeOffset = false)
{
string formatString = prec switch
{
DateTimePrecision.Year => "yyyy",
DateTimePrecision.Month => "yyyy-MM",
_ => "yyyy-MM-dd",
};
if (includeOffset) formatString += "K";

var representation = dto.ToString(formatString);
return Parse(representation);
}
bool includeOffset = false) => new(dto, prec, includeOffset);

public DateTime ToDateTime() => new(_parsedValue, Precision, HasOffset);

public DateTime ToDateTime() => DateTime.FromDateTimeOffset(_value, Precision, HasOffset);

public static Date Today(bool includeOffset = false) => FromDateTimeOffset(DateTimeOffset.Now, includeOffset: includeOffset);

Expand All @@ -55,27 +44,33 @@ public static Date FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision prec
/// </summary>
public DateTimePrecision Precision { get; private set; }

public int? Years => Precision >= DateTimePrecision.Year ? _parsedValue.Year : (int?)null;
public int? Months => Precision >= DateTimePrecision.Month ? _parsedValue.Month : (int?)null;
public int? Days => Precision >= DateTimePrecision.Day ? _parsedValue.Day : (int?)null;
public int? Years => Precision >= DateTimePrecision.Year ? _value.Year : (int?)null;
public int? Months => Precision >= DateTimePrecision.Month ? _value.Month : (int?)null;
public int? Days => Precision >= DateTimePrecision.Day ? _value.Day : (int?)null;

/// <summary>
/// The span of time ahead/behind UTC
/// </summary>
public TimeSpan? Offset => HasOffset ? _parsedValue.Offset : (TimeSpan?)null;

private readonly string _original;
private readonly DateTimeOffset _parsedValue;
public TimeSpan? Offset => HasOffset ? _value.Offset : null;

/// <summary>
/// Whether the time specifies an offset to UTC
/// </summary>
public bool HasOffset { get; private set; }

private static readonly string DATEFORMAT =
$"(?<year>[0-9]{{4}}) ((?<month>-[0-9][0-9]) ((?<day>-[0-9][0-9]) )?)? {Time.OFFSETFORMAT}?";
public static readonly Regex PARTIALDATEREGEX = new("^" + DATEFORMAT + "$",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.ExplicitCapture);
/// <summary>
/// If this instance was constructed using Parse(), this is the original
/// raw input to the parse. Used to guarantee roundtrippability.
/// </summary>
private string? _originalParsedString { get; init; }

private readonly DateTimeOffset _value;

/// <summary>
/// Converts the date to a full DateTimeOffset instance.
/// </summary>
public DateTimeOffset ToDateTimeOffset(TimeSpan defaultOffset) =>
HasOffset ? _value : new(_value.Ticks, defaultOffset);

/// <summary>
/// Converts the date to a full DateTimeOffset instance.
Expand All @@ -98,8 +93,13 @@ public DateTimeOffset ToDateTimeOffset(int hours, int minutes, int seconds, Time
/// <param name="defaultOffset">Offset used when the datetime does not specify one.</param>
/// <returns></returns>
public DateTimeOffset ToDateTimeOffset(int hours, int minutes, int seconds, int milliseconds, TimeSpan defaultOffset) =>
new(_parsedValue.Year, _parsedValue.Month, _parsedValue.Day, hours, minutes, seconds, milliseconds,
HasOffset ? _parsedValue.Offset : defaultOffset);
new(_value.Year, _value.Month, _value.Day, hours, minutes, seconds, milliseconds,
HasOffset ? _value.Offset : defaultOffset);

private static readonly string DATEFORMAT =
$"(?<year>[0-9]{{4}}) ((?<month>-[0-9][0-9]) ((?<day>-[0-9][0-9]) )?)? {Time.OFFSETFORMAT}?";
public static readonly Regex PARTIALDATEREGEX = new("^" + DATEFORMAT + "$",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.ExplicitCapture);

/// <summary>
/// Converts the date to a full DateTimeOffset instance.
Expand All @@ -112,7 +112,7 @@ private static bool tryParse(string representation, out Date value)
var matches = PARTIALDATEREGEX.Match(representation);
if (!matches.Success)
{
value = new Date(representation, default, default, default);
value = new Date(default, default, default);
return false;
}

Expand All @@ -133,7 +133,11 @@ private static bool tryParse(string representation, out Date value)
(offset.Success ? offset.Value : "Z");

var success = DateTimeOffset.TryParse(parseableDT, out var parsedValue);
value = new Date(representation, parsedValue, prec, offset.Success);
value = new Date(parsedValue, prec, offset.Success)
{
_originalParsedString = representation
};

return success;
}

Expand All @@ -149,21 +153,21 @@ private static bool tryParse(string representation, out Date value)
var dto = addValue.Unit switch
{
// we can ignore precision, as the precision will "trim" it anyway, and if we add 13 months, then the year can tick over nicely
"years" or "year" => dateValue._parsedValue.AddYears((int)addValue.Value),
"years" or "year" => dateValue._value.AddYears((int)addValue.Value),
"month" or "months" => dateValue.Precision == DateTimePrecision.Year
? dateValue._parsedValue.AddYears((int)(addValue.Value / 12))
: dateValue._parsedValue.AddMonths((int)addValue.Value),
? dateValue._value.AddYears((int)(addValue.Value / 12))
: dateValue._value.AddMonths((int)addValue.Value),
"week" or "weeks" or "wk" => dateValue.Precision switch
{
DateTimePrecision.Year => dateValue._parsedValue.AddYears((int)(addValue.Value / 52)),
DateTimePrecision.Month => dateValue._parsedValue.AddMonths((int)(addValue.Value * 7 / 30)),
_ => dateValue._parsedValue.AddDays(((int)addValue.Value) * 7)
DateTimePrecision.Year => dateValue._value.AddYears((int)(addValue.Value / 52)),
DateTimePrecision.Month => dateValue._value.AddMonths((int)(addValue.Value * 7 / 30)),
_ => dateValue._value.AddDays(((int)addValue.Value) * 7)
},
"day" or "days" or "d" => dateValue.Precision switch
{
DateTimePrecision.Year => dateValue._parsedValue.AddYears((int)(addValue.Value / 365)),
DateTimePrecision.Month => dateValue._parsedValue.AddMonths((int)(addValue.Value / 30)),
_ => dateValue._parsedValue.AddDays((int)addValue.Value)
DateTimePrecision.Year => dateValue._value.AddYears((int)(addValue.Value / 365)),
DateTimePrecision.Month => dateValue._value.AddMonths((int)(addValue.Value / 30)),
_ => dateValue._value.AddDays((int)addValue.Value)
},
_ => throw new ArgumentException($"'{addValue.Unit}' is not a valid time-valued unit", nameof(addValue)),
};
Expand Down Expand Up @@ -210,7 +214,7 @@ public Result<int> TryCompareTo(Any other)
return other switch
{
null => 1,
Date p => DateTime.CompareDateTimeParts(_parsedValue, Precision, HasOffset, p._parsedValue, p.Precision, p.HasOffset),
Date p => DateTime.CompareDateTimeParts(_value, Precision, HasOffset, p._value, p.Precision, p.HasOffset),
_ => throw NotSameTypeComparison(this, other)
};
}
Expand All @@ -221,8 +225,8 @@ public Result<int> TryCompareTo(Any other)
public static bool operator >=(Date a, Date b) => a.CompareTo(b) >= 0;


public override int GetHashCode() => _original.GetHashCode();
public override string ToString() => _original;
public override int GetHashCode() => _value.GetHashCode();
public override string ToString() => _originalParsedString is not null ? _originalParsedString : DateTime.ToStringWithPrecision(_value, Precision, HasOffset);

public static implicit operator DateTime(Date pd) => pd.ToDateTime();
public static explicit operator Date(DateTimeOffset dto) => FromDateTimeOffset(dto);
Expand Down
Loading

0 comments on commit 286cf55

Please sign in to comment.