Skip to content

Commit

Permalink
(#20) Comparison: implement and use the IEqualityComparers
Browse files Browse the repository at this point in the history
  • Loading branch information
ForNeVeR committed Oct 6, 2024
1 parent 74bca6c commit 4e9a54e
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 65 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
Thanks to @babaruh and @Kataane for working on this improvement.

### Added
- `Equals` method on `AbsolutePath` and `LocalPath` that accepts an alternate comparer (see `TruePath.Comparers.PlatformDefaultPathComparer` and `StrictStringPathComparer`). Thanks to @babaruh and @Kataane for working on this improvement.
- `Equals` method on `AbsolutePath` and `LocalPath` that accepts an alternate comparer (see `PlatformDefaultComparer` and `StrictStringComparer` static comparers on both types). Thanks to @babaruh and @Kataane for working on this improvement.

## [1.5.0] - 2024-09-22
### Fixed
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,22 @@ Another related issue is canonical path status (for the lack of a better term).

TruePath allows the user to control certain aspects of how their paths are presented and compared, and offers a set of defaults _that prefer max performance over correctness_ — they should work for the most practical cases, but may break in certain situations.

When comparing the path objects via either `==` operator or the standard `Equals(object)` method, the library uses a `TruePath.Comparers.PlatformDefaultPathComparer`, meaning that
When comparing the path objects via either `==` operator or the standard `Equals(object)` method, the library uses the `AbsolutePath.PlatformDefaultComparer` or the `LocalPath.PlatformDefaultComparer`, meaning that
- paths are compared as strings (no canonicalization performed),
- paths are compared in either case-sensitive (Linux) or case-insensitive/ordinal mode (Windows, macOS).

For cases when you want to always perform strict case-sensitive comparison (more performant yet not platform-aware), pass a `StrictStrictPathComparer` to the overload of `Equals` method:
For cases when you want to always perform strict case-sensitive comparison (more performant yet not platform-aware), pass the `AbsolutePath.StrictStringComparer` or the `LocalPath.StrictStringComparer` to the overload of the `Equals` method:
```csharp
var path1 = new LocalPath("a/b/c");
var path2 = new LocalPath("A/B/C");
var result1 = path1.Equals(path2, StrictStrictPathComparer.Instance); // guaranteed to be false on all platforms
var result1 = path1.Equals(path2, LocalPath.StrictStringComparer); // guaranteed to be false on all platforms
var result2 = path1.Equals(path2); // might be true or false, depends on the current platform
```

The advantage of the current implementations is that they will never do any IO: they don't need to ask the OS about path features to compare them. This comes at cost of incorrect comparisons for paths that use unusual semantics (say, a folder that's marked as case-sensitive on a platform with case-insensitive default). We are planning to offer an option for a comparer that will take particular path case sensitivity into account; follow the [issue #20][issue.20] for details.

To convert the path to the canonical form, use `AbsolutePath::Canonicalize`.

### Other Features
Aside from the strict types, the following features are supported for the paths.

Expand Down
16 changes: 13 additions & 3 deletions TruePath.Tests/AbsolutePathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT

using System.Diagnostics;
using TruePath.Comparers;

namespace TruePath.Tests;

Expand Down Expand Up @@ -229,6 +228,17 @@ public void CanonicalizationCaseOnMacOs()
Assert.Equal(newDirectory.Value, result.Value);
}

[Fact]
public void PlatformDefaultPathComparerTest()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;

var path1 = new AbsolutePath(@"C:\Windows");
var path2 = new AbsolutePath(@"C:\WINDOWS");

Assert.True(path1.Equals(path2, AbsolutePath.PlatformDefaultComparer));
}

[Fact]
public void EqualsUseStrictStringPathComparer_SamePaths_True()
{
Expand All @@ -240,7 +250,7 @@ public void EqualsUseStrictStringPathComparer_SamePaths_True()
var path2 = new AbsolutePath(nonCanonicalPath);

// Act
var equals = path1.Equals(path2, StrictStringPathComparer.Instance);
var equals = path1.Equals(path2, AbsolutePath.StrictStringComparer);

// Assert
Assert.True(equals);
Expand All @@ -257,7 +267,7 @@ public void EqualsUseStrictStringPathComparer_NotSamePaths_False()
var path2 = new AbsolutePath(nonCanonicalPath);

// Act
var equals = path1.Equals(path2, StrictStringPathComparer.Instance);
var equals = path1.Equals(path2, AbsolutePath.StrictStringComparer);

// Assert
Assert.False(equals);
Expand Down
16 changes: 13 additions & 3 deletions TruePath.Tests/LocalPathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT

using TruePath.Comparers;
using Xunit.Abstractions;

namespace TruePath.Tests;
Expand Down Expand Up @@ -143,6 +142,17 @@ public void ResolveToCurrentDirectoryTests()
}
}

[Fact]
public void PlatformDefaultPathComparerTest()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;

var path1 = new LocalPath(@"C:\Windows");
var path2 = new LocalPath(@"C:\WINDOWS");

Assert.True(path1.Equals(path2, LocalPath.PlatformDefaultComparer));
}

[Fact]
public void EqualsUseStrictStringPathComparer_SamePaths_True()
{
Expand All @@ -154,7 +164,7 @@ public void EqualsUseStrictStringPathComparer_SamePaths_True()
var path2 = new LocalPath(nonCanonicalPath);

// Act
var equals = path1.Equals(path2, StrictStringPathComparer.Instance);
var equals = path1.Equals(path2, LocalPath.StrictStringComparer);

// Assert
Assert.True(equals);
Expand All @@ -171,7 +181,7 @@ public void EqualsUseStrictStringPathComparer_NotSamePaths_False()
var path2 = new LocalPath(nonCanonicalPath);

// Act
var equals = path1.Equals(path2, StrictStringPathComparer.Instance);
var equals = path1.Equals(path2, LocalPath.StrictStringComparer);

// Assert
Assert.False(equals);
Expand Down
34 changes: 18 additions & 16 deletions TruePath/AbsolutePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ namespace TruePath;
/// <remarks>For a path that's not guaranteed to be absolute, use the <see cref="LocalPath"/> type.</remarks>
public readonly struct AbsolutePath : IEquatable<AbsolutePath>, IPath, IPath<AbsolutePath>
{
public static readonly IEqualityComparer<AbsolutePath> PlatformDefaultComparer =
new PlatformDefaultPathComparer<AbsolutePath>();

public static readonly IEqualityComparer<AbsolutePath> StrictStringComparer =
new StrictStringPathComparer<AbsolutePath>();

internal readonly LocalPath Underlying;

/// <summary>
Expand Down Expand Up @@ -102,24 +108,23 @@ public bool IsPrefixOf(AbsolutePath other)

/// <summary>Compares the path with another.</summary>
/// <remarks>Note that currently this comparison is case-sensitive.</remarks>
public bool Equals(AbsolutePath other)
{
var comparer = PlatformDefaultPathComparer.Instance;
return comparer.Compare(Underlying.Value, other.Underlying.Value) == 0;
}
public bool Equals(AbsolutePath other) => Equals(other, PlatformDefaultComparer);

/// <summary>
/// Determines whether the specified <see cref="AbsolutePath"/> is equal to the current <see cref="AbsolutePath"/> using the specified string comparer.
/// Determines whether the specified <see cref="AbsolutePath"/> is equal to the current <see cref="AbsolutePath"/>
/// using the specified comparer.
/// </summary>
/// <param name="other">The <see cref="AbsolutePath"/> to compare with the current <see cref="AbsolutePath"/>.</param>
/// <param name="comparer">The comparer to use for comparing the paths.</param>
/// <param name="comparer">
/// The comparer to use for comparing the paths. For example, pass <see cref="PlatformDefaultComparer"/> or
/// <see cref="StrictStringComparer"/>.
/// </param>
/// <returns>
/// <see langword="true"/> if the specified <see cref="AbsolutePath"/> is equal to the current <see cref="AbsolutePath"/> using the specified string comparer; otherwise, <see langword="false"/>.
/// <see langword="true"/> if the specified <see cref="AbsolutePath"/> is equal to the current
/// <see cref="AbsolutePath"/> using the specified comparer; otherwise, <see langword="false"/>.
/// </returns>
public bool Equals(AbsolutePath other, IComparer<string> comparer)
{
return comparer.Compare(Value, other.Value) == 0;
}
public bool Equals(AbsolutePath other, IEqualityComparer<AbsolutePath> comparer) =>
comparer.Equals(this, other);

/// <inheritdoc cref="Equals(AbsolutePath)"/>
public override bool Equals(object? obj)
Expand All @@ -128,10 +133,7 @@ public override bool Equals(object? obj)
}

/// <inheritdoc cref="Object.GetHashCode"/>
public override int GetHashCode()
{
return Underlying.GetHashCode();
}
public override int GetHashCode() => PlatformDefaultComparer.GetHashCode(this);

/// <inheritdoc cref="Equals(AbsolutePath)"/>
public static bool operator ==(AbsolutePath left, AbsolutePath right)
Expand Down
27 changes: 13 additions & 14 deletions TruePath/Comparers/PlatformDefaultPathComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace TruePath.Comparers;
/// <summary>
/// <para>Provides a default comparer for comparing file paths, aware of the current platform.</para>
/// <para>
/// On <b>Windows</b> and <b>macOS</b>, this will perform <b>case-insensitive</b> comparison, since the file
/// On <b>Windows</b> and <b>macOS</b>, this will perform <b>case-insensitive</b> string comparison, since the file
/// systems are case-insensitive on these operating systems by default.
/// </para>
/// <para>On <b>Linux</b>, the comparison will be <b>case-sensitive</b>.</para>
Expand All @@ -19,33 +19,32 @@ namespace TruePath.Comparers;
/// case-sensitiveness of either the whole file system or a part of it. This class does not take this into account,
/// having a benefit of no accessing the file system for any of the comparisons.
/// </remarks>
public class PlatformDefaultPathComparer : IComparer<string>
internal class PlatformDefaultPathComparer<TPath> : IEqualityComparer<TPath> where TPath : IPath
{
/// <summary>
/// Gets the singleton instance of the <see cref="PlatformDefaultPathComparer"/> class.
/// </summary>
public static readonly PlatformDefaultPathComparer Instance = new();

private readonly StringComparer _comparisonType;
private readonly StringComparer _stringComparer;

/// <summary>
/// Initializes a new instance of the <see cref="PlatformDefaultPathComparer"/> class.
/// </summary>
private PlatformDefaultPathComparer()
public PlatformDefaultPathComparer()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_comparisonType = StringComparer.OrdinalIgnoreCase;
_stringComparer = StringComparer.OrdinalIgnoreCase;
}
else
{
_comparisonType = StringComparer.Ordinal;
_stringComparer = StringComparer.Ordinal;
}
}

/// <inheritdoc cref="IComparer{T}.Compare"/>
public int Compare(string? x, string? y)
public bool Equals(TPath? x, TPath? y)
{
return _stringComparer.Equals(x?.Value, y?.Value);
}

public int GetHashCode(TPath obj)
{
return _comparisonType.Compare(x, y);
return _stringComparer.GetHashCode(obj.Value);
}
}
18 changes: 6 additions & 12 deletions TruePath/Comparers/StrictStringPathComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@

namespace TruePath.Comparers;

/// <summary>A strict comparer for comparing file paths using ordinal, case-sensitive comparison.</summary>
public class StrictStringPathComparer : IComparer<string>
/// <summary>
/// A strict comparer for comparing file paths using ordinal, case-sensitive comparison of the underlying path strings.
/// </summary>
internal class StrictStringPathComparer<TPath> : IEqualityComparer<TPath> where TPath : IPath
{
/// <summary>
/// Gets the singleton instance of the <see cref="StrictStringPathComparer"/> class.
/// </summary>
public static readonly StrictStringPathComparer Instance = new();

/// <inheritdoc cref="IComparer{T}.Compare"/>
public int Compare(string? x, string? y)
{
return StringComparer.Ordinal.Compare(x, y);
}
public bool Equals(TPath? x, TPath? y) => StringComparer.Ordinal.Equals(x?.Value, y?.Value);
public int GetHashCode(TPath obj) => StringComparer.Ordinal.GetHashCode(obj.Value);
}
22 changes: 9 additions & 13 deletions TruePath/LocalPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ namespace TruePath;
/// </summary>
public readonly struct LocalPath(string value) : IEquatable<LocalPath>, IPath, IPath<LocalPath>
{
public static readonly IEqualityComparer<LocalPath> PlatformDefaultComparer =
new PlatformDefaultPathComparer<LocalPath>();

public static readonly IEqualityComparer<LocalPath> StrictStringComparer =
new StrictStringPathComparer<LocalPath>();

private static char Separator => Path.DirectorySeparatorChar;

/// <inheritdoc cref="IPath.Value"/>
Expand Down Expand Up @@ -51,11 +57,7 @@ public LocalPath? Parent

/// <summary>Compares the path with another.</summary>
/// <remarks>Note that currently this comparison is case-sensitive.</remarks>
public bool Equals(LocalPath other)
{
var comparer = PlatformDefaultPathComparer.Instance;
return comparer.Compare(Value, other.Value) == 0;
}
public bool Equals(LocalPath other) => Equals(other, PlatformDefaultComparer);

/// <summary>
/// Determines whether the specified <see cref="LocalPath"/> is equal to the current <see cref="LocalPath"/> using the specified string comparer.
Expand All @@ -65,10 +67,7 @@ public bool Equals(LocalPath other)
/// <returns>
/// <see langword="true"/> if the specified <see cref="LocalPath"/> is equal to the current <see cref="LocalPath"/> using the specified string comparer; otherwise, <see langword="false"/>.
/// </returns>
public bool Equals(LocalPath other, IComparer<string> comparer)
{
return comparer.Compare(Value, other.Value) == 0;
}
public bool Equals(LocalPath other, IEqualityComparer<LocalPath> comparer) => comparer.Equals(this, other);

/// <inheritdoc cref="Equals(LocalPath)"/>
public override bool Equals(object? obj)
Expand All @@ -77,10 +76,7 @@ public override bool Equals(object? obj)
}

/// <inheritdoc cref="Object.GetHashCode"/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override int GetHashCode() => PlatformDefaultComparer.GetHashCode(this);

/// <inheritdoc cref="Equals(LocalPath)"/>
public static bool operator ==(LocalPath left, LocalPath right)
Expand Down

0 comments on commit 4e9a54e

Please sign in to comment.