Skip to content

Commit

Permalink
Merge pull request #2600 from almostchristian/develop
Browse files Browse the repository at this point in the history
Add subtract operator for Date and DateTime.
  • Loading branch information
marcovisserFurore authored Sep 28, 2023
2 parents f623ba5 + 14da458 commit 4e6dcca
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 26 deletions.
35 changes: 24 additions & 11 deletions src/Hl7.Fhir.Base/ElementModel/Types/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,35 +141,48 @@ private static bool tryParse(string representation, out Date value)
return success;
}

public static Date operator -(Date dateValue, Quantity subtractValue)
{
if (dateValue is null) throw new ArgumentNullException(nameof(dateValue));
if (subtractValue is null) throw new ArgumentNullException(nameof(subtractValue));

return Add(dateValue, -subtractValue.Value, subtractValue.Unit);
}

public static Date operator +(Date dateValue, Quantity addValue)
{
if (dateValue is null) throw new ArgumentNullException(nameof(dateValue));
if (addValue is null) throw new ArgumentNullException(nameof(addValue));

return Add(dateValue, addValue.Value, addValue.Unit);
}

private static Date Add(Date dateValue, decimal value, string unit)
{
// Based on the discussion on equality/comparisons here:
// https://chat.fhir.org/#narrow/stream/179266-fhirpath/topic/Date.2FTime.20comparison.20vs.20equality
// We have also allowed addition to use the definitve UCUM units of 'wk', 'd' as if they are a calendar unit of
// 'week'/'day' respectively.
var dto = addValue.Unit switch
var dto = 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._value.AddYears((int)addValue.Value),
"years" or "year" => dateValue._value.AddYears((int)value),
"month" or "months" => dateValue.Precision == DateTimePrecision.Year
? dateValue._value.AddYears((int)(addValue.Value / 12))
: dateValue._value.AddMonths((int)addValue.Value),
? dateValue._value.AddYears((int)(value / 12))
: dateValue._value.AddMonths((int)value),
"week" or "weeks" or "wk" => dateValue.Precision switch
{
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)
DateTimePrecision.Year => dateValue._value.AddYears((int)(value / 52)),
DateTimePrecision.Month => dateValue._value.AddMonths((int)(value * 7 / 30)),
_ => dateValue._value.AddDays(((int)value) * 7)
},
"day" or "days" or "d" => dateValue.Precision switch
{
DateTimePrecision.Year => dateValue._value.AddYears((int)(addValue.Value / 365)),
DateTimePrecision.Month => dateValue._value.AddMonths((int)(addValue.Value / 30)),
_ => dateValue._value.AddDays((int)addValue.Value)
DateTimePrecision.Year => dateValue._value.AddYears((int)(value / 365)),
DateTimePrecision.Month => dateValue._value.AddMonths((int)(value / 30)),
_ => dateValue._value.AddDays((int)value)
},
_ => throw new ArgumentException($"'{addValue.Unit}' is not a valid time-valued unit", nameof(addValue)),
_ => throw new ArgumentException($"'{unit}' is not a valid time-valued unit", nameof(unit)),
};
var result = FromDateTimeOffset(dto, dateValue.Precision);
return result;
Expand Down
43 changes: 28 additions & 15 deletions src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,50 +150,63 @@ private static bool tryParse(string representation, out DateTime value)
return success;
}

public static DateTime operator -(DateTime dateTimeValue, Quantity subtractValue)
{
if (dateTimeValue is null) throw new ArgumentNullException(nameof(dateTimeValue));
if (subtractValue is null) throw new ArgumentNullException(nameof(subtractValue));

return Add(dateTimeValue, -subtractValue.Value, subtractValue.Unit);
}

public static DateTime operator +(DateTime dateTimeValue, Quantity addValue)
{
if (dateTimeValue is null) throw new ArgumentNullException(nameof(dateTimeValue));
if (addValue is null) throw new ArgumentNullException(nameof(addValue));

return Add(dateTimeValue, addValue.Value, addValue.Unit);
}

private static DateTime Add(DateTime dateTimeValue, decimal value, string unit)
{
// Based on the discussion on equality/comparisons here:
// https://chat.fhir.org/#narrow/stream/179266-fhirpath/topic/Date.2FTime.20comparison.20vs.20equality
// We have also allowed addition to use the definitve UCUM units of 'wk', 'd', 'h', 'min' as if they are a calendar unit of
// 'week'/'day'/'hour'/'minute' respectively.
var dto = addValue.Unit switch
var dto = 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" => dateTimeValue._value.AddYears((int)addValue.Value),
"years" or "year" => dateTimeValue._value.AddYears((int)value),
"month" or "months" => dateTimeValue.Precision == DateTimePrecision.Year
? dateTimeValue._value.AddYears((int)(addValue.Value / 12))
: dateTimeValue._value.AddMonths((int)addValue.Value),
? dateTimeValue._value.AddYears((int)(value / 12))
: dateTimeValue._value.AddMonths((int)value),
"week" or "weeks" or "wk" => dateTimeValue.Precision switch
{
DateTimePrecision.Year => dateTimeValue._value.AddYears((int)(addValue.Value / 52)),
DateTimePrecision.Month => dateTimeValue._value.AddMonths((int)(addValue.Value * 7 / 30)),
_ => dateTimeValue._value.AddDays(((int)addValue.Value) * 7)
DateTimePrecision.Year => dateTimeValue._value.AddYears((int)(value / 52)),
DateTimePrecision.Month => dateTimeValue._value.AddMonths((int)(value * 7 / 30)),
_ => dateTimeValue._value.AddDays(((int)value) * 7)
},
"day" or "days" or "d" => dateTimeValue.Precision switch
{
DateTimePrecision.Year => dateTimeValue._value.AddYears((int)(addValue.Value / 365)),
DateTimePrecision.Month => dateTimeValue._value.AddMonths((int)(addValue.Value / 30)),
_ => dateTimeValue._value.AddDays((int)addValue.Value)
DateTimePrecision.Year => dateTimeValue._value.AddYears((int)(value / 365)),
DateTimePrecision.Month => dateTimeValue._value.AddMonths((int)(value / 30)),
_ => dateTimeValue._value.AddDays((int)value)
},

// NOT ignoring precision on time based stuff if there is no time component
// if no time component, don't modify result
"hour" or "hours" or "h" => dateTimeValue.Precision > DateTimePrecision.Day
? dateTimeValue._value.AddHours((double)addValue.Value)
? dateTimeValue._value.AddHours((double)value)
: dateTimeValue._value,
"minute" or "minutes" or "min" => dateTimeValue.Precision > DateTimePrecision.Day
? dateTimeValue._value.AddMinutes((double)addValue.Value)
? dateTimeValue._value.AddMinutes((double)value)
: dateTimeValue._value,
"s" or "second" or "seconds" => dateTimeValue.Precision > DateTimePrecision.Day
? dateTimeValue._value.AddSeconds((double)addValue.Value)
? dateTimeValue._value.AddSeconds((double)value)
: dateTimeValue._value,
"ms" or "millisecond" or "milliseconds" => dateTimeValue.Precision > DateTimePrecision.Day
? dateTimeValue._value.AddMilliseconds((double)addValue.Value)
? dateTimeValue._value.AddMilliseconds((double)value)
: dateTimeValue._value,
_ => throw new ArgumentException($"'{addValue.Unit}' is not a valid time-valued unit", nameof(addValue)),
_ => throw new ArgumentException($"'{unit}' is not a valid time-valued unit", nameof(unit)),
};

var resultRepresentation = dto.ToString(FMT_FULL);
Expand Down
2 changes: 2 additions & 0 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public static SymbolTable AddStandardFP(this SymbolTable t)
t.Add("binary.-", (object f, int a, int b) => a - b, doNullProp: true);
t.Add("binary.-", (object f, long a, long b) => a - b, doNullProp: true);
t.Add("binary.-", (object f, decimal a, decimal b) => a - b, doNullProp: true);
t.Add("binary.-", (object f, P.DateTime a, P.Quantity b) => a - b, doNullProp: true);
t.Add("binary.-", (object f, P.Date a, P.Quantity b) => a - b, doNullProp: true);
t.Add("binary.-", (object f, P.Quantity a, P.Quantity b) => a - b, doNullProp: true);

t.Add("binary.div", (object f, int a, int b) => b != 0 ? a / b : (int?)null, doNullProp: true);
Expand Down
9 changes: 9 additions & 0 deletions src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ public void CompilationIsCached()

}

[TestMethod]
public void TestDateTimeArithmetic()
{
fixture.IsTrue(@"(Patient.birthDate + 100 years) > @2000");
fixture.IsTrue(@"(Patient.birthDate - 100 years) < @2000");
fixture.IsTrue(@"(now() - 100 seconds) < now()");
fixture.IsTrue(@"(now() + 100 seconds) > now()");
}

private ConcurrentDictionary<string, CacheItem<CompiledExpression>> getCache()
{
var cache = typeof(FhirPathExtensions)
Expand Down

0 comments on commit 4e6dcca

Please sign in to comment.