Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Test.Utilities;
using Xunit;

namespace Microsoft.Health.Fhir.Core.UnitTests.Extensions
{
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
[Trait(Traits.Category, Categories.Search)]
public class DateTimeSafeExtensionsTests
{
// SafeAddTicks - DateTime

[Fact]
public void GivenNormalDateTime_WhenSafeAddTicksCalled_ThenReturnsExpectedResult()
{
var dt = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(dt.AddTicks(TimeSpan.TicksPerMillisecond), result);
}

[Fact]
public void GivenDateTimeNearMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
var dt = DateTime.MaxValue.AddTicks(-100);
DateTime result = dt.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
DateTime result = DateTime.MaxValue.SafeAddTicks(1);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeNearMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
var dt = DateTime.MinValue.AddTicks(100);
DateTime result = dt.SafeAddTicks(-TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTime.MinValue, result);
}

[Fact]
public void GivenDateTimeMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
DateTime result = DateTime.MinValue.SafeAddTicks(-1);
Assert.Equal(DateTime.MinValue, result);
}

[Fact]
public void GivenDateTime_WhenSafeAddTicksZero_ThenReturnsOriginal()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(0);
Assert.Equal(dt, result);
}

[Fact]
public void GivenAnyDateTime_WhenSafeAddTicksLongMinValue_ThenClampsToMinValue()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(long.MinValue);
Assert.Equal(DateTime.MinValue, result);
}

[Fact]
public void GivenUtcDateTime_WhenSafeAddTicksClampsToMaxValue_ThenPreservesKind()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
DateTime result = DateTime.MaxValue.AddTicks(-1).SafeAddTicks(TimeSpan.TicksPerDay);
Assert.Equal(DateTimeKind.Unspecified, result.Kind);
Assert.Equal(DateTime.MaxValue.Ticks, result.Ticks);
}
Comment thread
Copilot marked this conversation as resolved.

[Fact]
public void GivenLocalDateTime_WhenSafeAddTicksClampsToMinValue_ThenPreservesKind()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Local);
DateTime result = dt.AddTicks(1).SafeAddTicks(-TimeSpan.TicksPerDay);
Assert.Equal(DateTimeKind.Local, result.Kind);
Assert.Equal(DateTime.MinValue.Ticks, result.Ticks);
}

// SafeAddTicks - DateTimeOffset

[Fact]
public void GivenNormalDateTimeOffset_WhenSafeAddTicksCalled_ThenReturnsExpectedResult()
{
var dto = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(dto.AddTicks(TimeSpan.TicksPerMillisecond), result);
}

[Fact]
public void GivenDateTimeOffsetNearMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
var dto = DateTimeOffset.MaxValue.AddTicks(-100);
DateTimeOffset result = dto.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTimeOffset.MaxValue, result);
}

[Fact]
public void GivenDateTimeOffsetNearMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
var dto = DateTimeOffset.MinValue.AddTicks(100);
DateTimeOffset result = dto.SafeAddTicks(-TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTimeOffset.MinValue, result);
}

[Fact]
public void GivenDateTimeOffset_WhenSafeAddTicksZero_ThenReturnsOriginal()
{
var offset = TimeSpan.FromHours(5);
var dto = new DateTimeOffset(2024, 6, 15, 12, 0, 0, offset);
DateTimeOffset result = dto.SafeAddTicks(0);
Assert.Equal(dto, result);
Assert.Equal(offset, result.Offset);
}

[Fact]
public void GivenDateTimeOffset_WhenSafeAddTicksClampsWithNonZeroOffset_ThenPreservesOffset()
{
var offset = TimeSpan.FromHours(5);
var dto = new DateTimeOffset(9999, 12, 31, 23, 59, 59, offset);
DateTimeOffset result = dto.SafeAddTicks(TimeSpan.TicksPerDay);
Assert.Equal(offset, result.Offset);
Assert.Equal(DateTimeOffset.MaxValue.Ticks, result.Ticks);
}

// SafeAddDays - DateTime

[Fact]
public void GivenNormalDateTime_WhenSafeAddDaysCalled_ThenReturnsExpectedResult()
{
var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddDays(1);
Assert.Equal(dt.AddDays(1), result);
}

[Fact]
public void GivenDateTimeNearMaxValue_WhenSafeAddDaysPositive_ThenClampsToMaxValue()
{
var dt = DateTime.MaxValue.AddDays(-0.5);
DateTime result = dt.SafeAddDays(1);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeNearMinValue_WhenSafeAddDaysNegative_ThenClampsToMinValue()
{
var dt = DateTime.MinValue.AddDays(0.5);
DateTime result = dt.SafeAddDays(-1);
Assert.Equal(DateTime.MinValue, result);
}

// SafeAddDays - DateTimeOffset

[Fact]
public void GivenNormalDateTimeOffset_WhenSafeAddDaysCalled_ThenReturnsExpectedResult()
{
var dto = new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddDays(1);
Assert.Equal(dto.AddDays(1), result);
}

[Fact]
public void GivenDateTimeOffsetNearMaxValue_WhenSafeAddDaysPositive_ThenClampsToMaxValue()
{
var dto = DateTimeOffset.MaxValue.AddDays(-0.5);
DateTimeOffset result = dto.SafeAddDays(1);
Assert.Equal(DateTimeOffset.MaxValue, result);
}

[Fact]
public void GivenDateTimeOffsetNearMinValue_WhenSafeAddDaysNegative_ThenClampsToMinValue()
{
var dto = DateTimeOffset.MinValue.AddDays(0.5);
DateTimeOffset result = dto.SafeAddDays(-1);
Assert.Equal(DateTimeOffset.MinValue, result);
}

[Fact]
public void GivenDateTime_WhenSafeAddDaysIntMaxValue_ThenClampsToMaxValue()
{
var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddDays(int.MaxValue);
Assert.Equal(DateTime.MaxValue.Ticks, result.Ticks);
}

[Fact]
public void GivenDateTime_WhenSafeAddDaysIntMinValue_ThenClampsToMinValue()
{
var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddDays(int.MinValue);
Assert.Equal(DateTime.MinValue.Ticks, result.Ticks);
}

[Fact]
public void GivenUtcDateTime_WhenSafeAddDaysClampsToMaxValue_ThenPreservesKind()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddDays(int.MaxValue);
Assert.Equal(DateTimeKind.Utc, result.Kind);
Assert.Equal(DateTime.MaxValue.Ticks, result.Ticks);
}

[Fact]
public void GivenLocalDateTime_WhenSafeAddDaysClampsToMinValue_ThenPreservesKind()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Local);
DateTime result = dt.SafeAddDays(int.MinValue);
Assert.Equal(DateTimeKind.Local, result.Kind);
Assert.Equal(DateTime.MinValue.Ticks, result.Ticks);
}

[Fact]
public void GivenDateTimeOffset_WhenSafeAddDaysIntMaxValue_ThenClampsToMaxValue()
{
var dto = new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddDays(int.MaxValue);
Assert.Equal(DateTimeOffset.MaxValue.Ticks, result.Ticks);
}

[Fact]
public void GivenDateTimeOffset_WhenSafeAddDaysIntMinValue_ThenClampsToMinValue()
{
var dto = new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddDays(int.MinValue);
Assert.Equal(DateTimeOffset.MinValue.Ticks, result.Ticks);
}

[Fact]
public void GivenDateTimeOffset_WhenSafeAddDaysClampsToMaxValue_ThenPreservesOffset()
{
var offset = TimeSpan.FromHours(5);
var dto = new DateTimeOffset(2024, 1, 1, 0, 0, 0, offset);
DateTimeOffset result = dto.SafeAddDays(int.MaxValue);
Assert.Equal(offset, result.Offset);
Assert.Equal(DateTimeOffset.MaxValue.Ticks, result.Ticks);
}
}
}
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
111 changes: 111 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Extensions/DateTimeSafeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;

namespace Microsoft.Health.Fhir.Core.Extensions
{
/// <summary>
/// Extension methods for <see cref="DateTime"/> and <see cref="DateTimeOffset"/> that clamp
/// results to <see cref="DateTime.MinValue"/>/<see cref="DateTime.MaxValue"/> instead of
/// throwing on overflow.
/// </summary>
public static class DateTimeSafeExtensions
{
private const long MaxDaysBeforeTicksOverflow = long.MaxValue / TimeSpan.TicksPerDay;

/// <summary>
/// Adds the specified number of ticks to a <see cref="DateTime"/>, clamping the result
/// to <see cref="DateTime.MinValue"/> or <see cref="DateTime.MaxValue"/> on overflow.
/// </summary>
public static DateTime SafeAddTicks(this DateTime value, long ticks)
{
if (ticks == 0)
{
return value;
}

if (ticks == long.MinValue)
{
// Can't negate long.MinValue; adding it always underflows within DateTime's range.
return new DateTime(DateTime.MinValue.Ticks, value.Kind);
}

if (ticks > 0 && value.Ticks > DateTime.MaxValue.Ticks - ticks)
{
return new DateTime(DateTime.MaxValue.Ticks, value.Kind);
}

if (ticks < 0 && value.Ticks < DateTime.MinValue.Ticks - ticks)
{
return new DateTime(DateTime.MinValue.Ticks, value.Kind);
}

return value.AddTicks(ticks);
}

/// <summary>
/// Adds the specified number of ticks to a <see cref="DateTimeOffset"/>, clamping the result
/// to <see cref="DateTimeOffset.MinValue"/> or <see cref="DateTimeOffset.MaxValue"/> on overflow.
/// </summary>
Comment thread
Copilot marked this conversation as resolved.
public static DateTimeOffset SafeAddTicks(this DateTimeOffset value, long ticks)
{
if (ticks == 0)
{
return value;
}

if (ticks == long.MinValue)
{
// Can't negate long.MinValue; adding it always underflows within DateTimeOffset's range.
return new DateTimeOffset(DateTimeOffset.MinValue.Ticks, value.Offset);
}

if (ticks > 0 && value.Ticks > DateTimeOffset.MaxValue.Ticks - ticks)
{
return new DateTimeOffset(DateTimeOffset.MaxValue.Ticks, value.Offset);
}

if (ticks < 0 && value.Ticks < DateTimeOffset.MinValue.Ticks - ticks)
{
return new DateTimeOffset(DateTimeOffset.MinValue.Ticks, value.Offset);
}

return value.AddTicks(ticks);
}
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Outdated

/// <summary>
/// Adds the specified number of days to a <see cref="DateTime"/>, clamping the result
/// to <see cref="DateTime.MinValue"/> or <see cref="DateTime.MaxValue"/> on overflow.
/// </summary>
public static DateTime SafeAddDays(this DateTime value, int days)
{
// Detect if days * TimeSpan.TicksPerDay would overflow long.
if (days > MaxDaysBeforeTicksOverflow || days < -MaxDaysBeforeTicksOverflow)
{
return days > 0 ? new DateTime(DateTime.MaxValue.Ticks, value.Kind) : new DateTime(DateTime.MinValue.Ticks, value.Kind);
}

long ticks = days * TimeSpan.TicksPerDay;
return value.SafeAddTicks(ticks);
}

/// <summary>
/// Adds the specified number of days to a <see cref="DateTimeOffset"/>, clamping the result
/// to <see cref="DateTimeOffset.MinValue"/> or <see cref="DateTimeOffset.MaxValue"/> on overflow.
/// </summary>
public static DateTimeOffset SafeAddDays(this DateTimeOffset value, int days)
{
// Detect if days * TimeSpan.TicksPerDay would overflow long.
if (days > MaxDaysBeforeTicksOverflow || days < -MaxDaysBeforeTicksOverflow)
{
return days > 0 ? new DateTimeOffset(DateTimeOffset.MaxValue.Ticks, value.Offset) : new DateTimeOffset(DateTimeOffset.MinValue.Ticks, value.Offset);
}

long ticks = days * TimeSpan.TicksPerDay;
return value.SafeAddTicks(ticks);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ void ISearchValueVisitor.Visit(DateTimeSearchValue dateTime)

var differenceTicks = (long)((Clock.UtcNow.Ticks - Math.Max(startTicks, endTicks)) * ApproximateMultiplier);

var approximateStart = dateTime.Start.AddTicks(-differenceTicks);
var approximateEnd = dateTime.End.AddTicks(differenceTicks);
var approximateStart = dateTime.Start.SafeAddTicks(-differenceTicks);
var approximateEnd = dateTime.End.SafeAddTicks(differenceTicks);
Comment thread
v-isyamauchi-gh marked this conversation as resolved.

// Spec (ap): the search range overlaps the target range. Emit overlap directly
// (Start <= approxEnd AND End >= approxStart) rather than the eq-shaped containment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.ValueSets;

Expand Down Expand Up @@ -41,7 +42,7 @@ expression.Expressions[0] is BinaryExpression isLong &&
return Expression.And(
Expression.Equals(SqlFieldName.DateTimeIsLongerThanADay, left.ComponentIndex, false),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeEnd, left.ComponentIndex, left.Value),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeStart, left.ComponentIndex, ((DateTimeOffset)left.Value).AddTicks(-TimeSpan.TicksPerDay)),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeStart, left.ComponentIndex, ((DateTimeOffset)left.Value).SafeAddTicks(-TimeSpan.TicksPerDay)),
new BinaryExpression(right.BinaryOperator, FieldName.DateTimeStart, right.ComponentIndex, right.Value));
}

Expand Down
Loading
Loading