Repository navigation
Description
Background and motivation
Initial discussion in #2339 and #2064
I would like to add some overloads to most string equivalency assertions to tackle various use cases (e.g. ignore white space, ignore casing, ignore newline, etc.) with the goal to tackle most existing use cases and to allow for extensibility of the provided mechanism. In order to achieve this, I would add an overload with a Func<EquivalencyAssertionOptions<string>, EquivalencyAssertionOptions<string>> to specify additional options on the string comparison like ignoring leading or trailing white space or newlines to the following string assertions:
- BeEquivalentTo
- ContainEquivalentOf
- EndWithEquivalentOf
- MatchEquivalentOf
- NotBeEquivalentTo
- NotContainEquivalentOf
- NotEndWithEquivalentOf
- NotMatchEquivalentOf
- NotStartWithEquivalentOf
- StartWithEquivalentOf
This also allows specifying an IEqualityComparer<string> as requested in #2339 via the .Using method on the assertion options.
API Proposal
Example for BeEquivalentTo:
public AndConstraint<TAssertions> BeEquivalentTo(string expected,
Func<EquivalencyAssertionOptions<string>, EquivalencyAssertionOptions<string>> config,
string because = "", params object[] becauseArgs)
{
// ...
}
public AndConstraint<TAssertions> NotBeEquivalentTo(string unexpected,
Func<EquivalencyAssertionOptions<string>, EquivalencyAssertionOptions<string>> config,
string because = "", params object[] becauseArgs)
{
// ...
}and add the following methods to the EquivalencyAssertionOptions<T> class:
public EquivalencyAssertionOptions<T> IgnoringLeadingWhitespace()
{
// ...
return this;
}
public EquivalencyAssertionOptions<T> IgnoringTrailingWhitespace()
{
// ...
return this;
}
public EquivalencyAssertionOptions<T> IgnoringCase()
{
// ...
return this;
}API Usage
subject.Should().BeEquivalentTo(expected, o => o
.IgnoringLeadingWhitespace()
.IgnoringTrailingWhitespace()
.IgnoringCase());subject.Should().BeEquivalentTo(expected, o => o
.Using(StringComparer.InvariantCultureIgnoreCase));Alternative Designs
No response
Risks
As I would support arbitrary IEqualityComparer<string>, I can no longer use the IndexOfFirstMismatch extension method, but would have to rewrite it, e.g. as follows:
public static int IndexOfFirstMismatch(this string value, string expected, IEqualityComparer<string> comparer)
{
for (int index = 0; index < value.Length; index++)
{
if (index >= expected.Length || !comparer.Equals(value[index..(index + 1)], expected[index..(index + 1)]))
{
return index;
}
}
return -1;
}The difference is, that I have to slice the strings and get substrings, instead of comparing char by char. This could have a performance impact...
Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?
Yes, please assign this issue to me.
Activity
[-][API Proposal]: New overloads for `StringAssertions.BeEquivalentTo`[/-][+]New string-specific options for `StringAssertions.BeEquivalentTo`[/+]dennisdoomen commentedon Oct 6, 2023
I like 1 and 3, but I don't like 2. Instead, we also need to add these options to the general
BeEquivalentTo. Otherwise, we would not offer an inconsistent surface area.vbreuss commentedon Oct 6, 2023
@dennisdoomen : I adjusted the API proposal to exclude point 2.
I am not sure, I understand what you mean with "to add these options to the general
BeEquivalentTo". Do you refer to theObjectAssertions.BeEquivalentTomethod?dennisdoomen commentedon Oct 8, 2023
Yes, and all the others that take a
EquivalencyAssertionOptions<T>. If you add those options to that options calls it'll become available everywhere.vbreuss commentedon Oct 9, 2023
@dennisdoomen :
I see your point and adjusted the API proposal accordingly.
I would split it into two pull requests:
IEqualityCompareroverload (as requested in Allow IEqualityComparer in string equality assertion #2339)EquivalencyAssertionOptions<string>and adds additional options for string comparison there.Is this okay for you?
dennisdoomen commentedon Oct 9, 2023
Yeah, that sounds like a great plan.
dennisdoomen commentedon Oct 14, 2023
@jnyrup I'm fine marking this as "api approved"
IT-VBFK commentedon Oct 14, 2023
Love this proposal :)
vbreuss commentedon Oct 14, 2023
@dennisdoomen : As requested in the #2372 I also added the two API changes for
NotBeEquivalentTo.jnyrup commentedon Oct 22, 2023
I've been thinking about if this needs some more upfront designing here?
I.e. how this should (not) influence the other
Equivalentmethods inStringAssertionsDoesn't one currently has do some extra work to make the general
BeEquivalentTouse case-insensitive string comparison?The first thing that comes to my mind is this:
Which with the options overload to
StringAssertions.BeEquivalentTowould becomeOr are you saying that one should have the same/similar
EquivalencyAssertionOptionsdirectly available no matter if you're asserting directly on a string or an object having a string member?vbreuss commentedon Oct 23, 2023
@jnyrup :
The idea is exactly this, that I add the additional methods (
IgnoringNewLines,IgnoringLeadingWhitespace, ...) directly to theEquivalencyAssertionOptions, so that they can be used everywhere and are applied, when comparing astring.vbreuss commentedon Oct 23, 2023
@jnyrup : Do you mean, that I should also add the corresponding overload with
IEqualityComparer<string> comparerandIEqualityComparer<string> comparerto these methods?jnyrup commentedon Oct 23, 2023
It was meant as an open question.
In general have avoided overloads taking
IEqualityComparer<T>,StringComparer,StringComparisonor similar.#2243 Adding
IEqualityComparer<T>and #1145+#1869+#1880 addingOccurrenceConstraintparameters are notable exceptions to this.So instead of looking solely at
BeEquivalentToI wanted to draw attention to the similar string assertions to include more brains.I also just recalled that with #1284 we have support for passing a custom
IEqualityComparer<T>to the non-stringBeEquivalentTo, such that one can writevbreuss commentedon Oct 24, 2023
I often had the case, that I needed to compare strings case sensitive or case insensitive and for me it would read better to specify this explicitely than to rely on the difference between
.Beand.BeEquivalentTo.vs.
In the latter example I often got review comments, why this works, as it is not immediately clear, that "BeEquivalentTo" is case-insensitive.
So for me adding the
IEqualityComparer<string>to the string assertions is a good idea and I am happy to expand it also to the other string assertion methods you mention.13 remaining items
jnyrup commentedon Nov 4, 2023
In the draft PR
StringAssertions.BeandStringAssertions.BeEquivalentToare identical.Would it be enough with just the new
BeEquivalentTo?If we add
NotContainEquivalentOf(unexpected, occurrenceConstraint, config)(which I approve), let's also addNotContainEquivalentOf(unexpected, occurrenceConstraint)to have the same four overloads asContainEquivalentOf.vbreuss commentedon Nov 4, 2023
I think so, but then I would have to also change
StringEqualityEquivalencyStepto useBeEquivalentToinstead ofBe, but I don't think this is a problem...vbreuss commentedon Nov 4, 2023
I agree, but as this has nothing to do with the config changes, I implemented it in a separate pull request (#2436), to keep the changes in #2413 more focused.
vbreuss commentedon Nov 4, 2023
I changed it accordingly in #2413!
jnyrup commentedon Nov 4, 2023
Some more thinking on the behavior of
IgnoreNewlines.#1247 is about considering "\n" and
"\r\n"equivalent, i.e. ignoring OS newline style.My example in #2339 (comment) was going one step further and considered
\n,\r\nandthe same.E.g. ignoring line breaks on spaces.
The current implementation removes newlines, such that
is is considered equivalent to
Any proper line-breaking algorithm would insert a hyphen when breaking a line inside a word
What do you think?
vbreuss commentedon Nov 4, 2023
I think, that when I read
IgnoringNewlines, I would expect all newlines to be removed, as it is implemented.However, I believe that the most common use case would be to ignore OS-specific differences.
So, what about renaming the method from
IgnoringNewlinestoIgnoringNewlineStyleorUsingCrossPlatformNewlineStyleor similar and the implementation just replaces"\r\n"with"\n"?BTW, when I look at the example in #1247, it could also be fixed with
IgnoringTrailingWhitespace, as this also removes newlines, tabs and blanks.dennisdoomen commentedon Nov 8, 2023
I'm not entirely sure what you mean to say with the last example (about the hyphen).
jnyrup commentedon Nov 8, 2023
That I don't think
"cash\r\new"and"cashew"should be considered equivalent.If
"cashew"had been split across a line by some line-splitting algorithm, it should have inserted a hyphen such that it becomes e.g. `"cash-\r\new".eNeRGy164 commentedon Nov 8, 2023
So the question is, does
IgnoreNewLinesmeancompare as if there are no newlines at all
or
compare but replace 1+ newline characters as the same single newline char on both sides
vbreuss commentedon Nov 8, 2023
What about adding two options:
IgnoreAllNewlinesWhich compares as if there are no newlines (current behavior)
IgnoreNewlineStyleWhich replaces
"\r\n"with"\n"so that OS-Specific newline differences are ignoreddennisdoomen commentedon Nov 8, 2023
I honestly don't know 🤷♂️
vbreuss commentedon Dec 4, 2023
As this issue is already quite large, I removed the option
IgnoringNewlinesfrom this issue and the corresponding pull request #2413.I hope that this allows us to finish this issue and then focus the discussion of how to work with newlines in a focused thread (see #2496).