Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BeCloseTo() / NotBeCloseTo() to TimeOnlyAssertions #2030

Merged
merged 29 commits into from Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
22e4793
Add `BeCloseTo()` to TimeOnlyAssertions
IT-VBFK Nov 7, 2022
fff16c0
Add `NotBeCloseTo()` to TimeOnlyAssertions
IT-VBFK Nov 8, 2022
497eb8f
Update releases.md
IT-VBFK Nov 8, 2022
9ac6dcf
Update Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs
IT-VBFK Nov 19, 2022
8bdeb04
Fix small typing error in tests
IT-VBFK Nov 19, 2022
4585714
Remove redundant test
IT-VBFK Nov 19, 2022
315d385
Improve test names
IT-VBFK Nov 20, 2022
16a5872
Simplified time difference calculation
IT-VBFK Nov 20, 2022
157e4f4
Refactor by using IsBetween and add missing tests
IT-VBFK Nov 21, 2022
b22b678
Handle possible `TimeSpan` overflow
IT-VBFK Nov 22, 2022
74f5e4b
Optimize calculation of smaller difference
IT-VBFK Nov 22, 2022
83aa879
Improve test name at overflow test
IT-VBFK Nov 22, 2022
313e515
Introduce new extension `AddTicks()`
IT-VBFK Nov 22, 2022
dcbc45a
Add missing overflow protection to `NotBeCloseTo()`
IT-VBFK Nov 22, 2022
3ca49d5
Accept API changes
IT-VBFK Nov 22, 2022
01fc1e4
Fix code formatting issues
IT-VBFK Nov 22, 2022
7636c20
Update Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
IT-VBFK Nov 24, 2022
2a06e6a
Keep the helper extension internal for now
IT-VBFK Nov 24, 2022
d56dddb
Fix issue with assertion scope
IT-VBFK Nov 24, 2022
c2b9ab7
Make maximum time be right-closed
IT-VBFK Nov 24, 2022
174bde6
Revert 3ca49d5
IT-VBFK Nov 24, 2022
347b42e
Merge branch 'issue-2029' of github.com:IT-VBFK/fluentassertions into…
IT-VBFK Nov 24, 2022
35444f6
Add missing test for null subject inside an assertion scope
IT-VBFK Nov 24, 2022
d2ef03e
Use safer approach for calculating the time span
IT-VBFK Nov 28, 2022
e96450f
Added `BeOneOf` for `IComparable`s and object comparisons. (#2028)
jez9999 Nov 27, 2022
641b095
Merge branch 'develop' into issue-2029
IT-VBFK Nov 28, 2022
74169b2
Fix some typos
IT-VBFK Nov 28, 2022
f3407c1
Merge branch 'issue-2029' of github.com:IT-VBFK/fluentassertions into…
IT-VBFK Nov 28, 2022
034d0a9
Avoid both checked and unchecked overflow
IT-VBFK Nov 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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