Skip to content

Commit

Permalink
Add BeCloseTo() / NotBeCloseTo() to TimeOnlyAssertions (#2030)
Browse files Browse the repository at this point in the history
  • Loading branch information
IT-VBFK committed Nov 28, 2022
1 parent 512165b commit 57f11d4
Show file tree
Hide file tree
Showing 5 changed files with 572 additions and 0 deletions.
30 changes: 30 additions & 0 deletions Src/FluentAssertions/Common/TimeOnlyExtensions.cs
@@ -0,0 +1,30 @@
#if NET6_0_OR_GREATER

using System;

namespace FluentAssertions.Common;

internal static class TimeOnlyExtensions
{
/// <summary>
/// Determines if <paramref name="subject"/> is close to <paramref name="other"/> within a given <paramref name="precision"/>.
/// </summary>
/// <param name="subject">The time to check</param>
/// <param name="other">The time to be close to</param>
/// <param name="precision">The precision that <paramref name="other"/> may differ from <paramref name="subject"/></param>
/// <remarks>
/// <see cref="TimeOnly.IsBetween(TimeOnly, TimeOnly)" /> checks the right-open interval, whereas this method checks the closed interval.
/// </remarks>
public static bool IsCloseTo(this TimeOnly subject, TimeOnly other, TimeSpan precision)
{
long startTicks = other.Add(-precision).Ticks;
long endTicks = other.Add(precision).Ticks;
long ticks = subject.Ticks;

return startTicks <= endTicks
? (startTicks <= ticks && endTicks >= ticks)
: (startTicks <= ticks || endTicks >= ticks);
}
}

#endif
91 changes: 91 additions & 0 deletions Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using FluentAssertions.Common;
using FluentAssertions.Execution;

#if NET6_0_OR_GREATER
Expand Down Expand Up @@ -127,6 +128,96 @@ public AndConstraint<TAssertions> Be(TimeOnly? expected, string because = "", pa
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <see cref="TimeOnly"/> is within the specified time
/// from the specified <paramref name="nearbyTime"/> value.
/// </summary>
/// <param name="nearbyTime">
/// The expected time to compare the actual value with.
/// </param>
/// <param name="precision">
/// The maximum amount of time which the two values may differ.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> BeCloseTo(TimeOnly nearbyTime, TimeSpan precision, string because = "",
params object[] becauseArgs)
{
if (precision < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(precision), $"The value of {nameof(precision)} must be non-negative.");
}

TimeSpan? difference = (Subject != null)
? MinimumDifference(Subject.Value, nearbyTime)
: null;

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}, ", precision, nearbyTime)
.ForCondition(Subject is not null)
.FailWith("but found <null>.")
.Then
.ForCondition(Subject?.IsCloseTo(nearbyTime, precision) == true)
.FailWith("but {0} was off by {1}.", Subject, difference)
.Then
.ClearExpectation();

return new AndConstraint<TAssertions>((TAssertions)this);
}

private static TimeSpan? MinimumDifference(TimeOnly a, TimeOnly b)
{
var diff1 = a - b;
var diff2 = b - a;

return diff1 < diff2 ? diff1 : diff2;
}

/// <summary>
/// Asserts that the current <see cref="TimeOnly"/> is not within the specified time
/// from the specified <paramref name="distantTime"/> value.
/// </summary>
/// <param name="distantTime">
/// The time to compare the actual value with.
/// </param>
/// <param name="precision">
/// The maximum amount of time which the two values must differ.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> NotBeCloseTo(TimeOnly distantTime, TimeSpan precision, string because = "",
params object[] becauseArgs)
{
if (precision < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(precision), $"The value of {nameof(precision)} must be non-negative.");
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Did not expect {context:the time} to be within {0} from {1}{reason}, ", precision, distantTime)
.ForCondition(Subject is not null)
.FailWith("but found <null>.")
.Then
.ForCondition(!Subject?.IsCloseTo(distantTime, precision) == true)
.FailWith("but it was {0}.", Subject)
.Then
.ClearExpectation();

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <see cref="TimeOnly"/> is before the specified value.
/// </summary>
Expand Down
Expand Up @@ -2312,6 +2312,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(System.TimeOnly? expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeAfter(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeBefore(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeCloseTo(System.TimeOnly nearbyTime, System.TimeSpan precision, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOnOrAfter(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOnOrBefore(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(params System.Nullable<System.TimeOnly>[] validValues) { }
Expand All @@ -2327,6 +2328,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> NotBe(System.TimeOnly? unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeAfter(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeBefore(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeCloseTo(System.TimeOnly distantTime, System.TimeSpan precision, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOnOrAfter(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotBeOnOrBefore(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> NotHaveHours(int unexpected, string because = "", params object[] becauseArgs) { }
Expand Down

0 comments on commit 57f11d4

Please sign in to comment.