Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 16 additions & 1 deletion Pathy.ApiVerificationTests/ApprovedApi/pathy.net47.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Pathy
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
public readonly struct ChainablePath : System.IComparable, System.IComparable<Pathy.ChainablePath>, System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand All @@ -26,6 +26,8 @@ namespace Pathy
public static Pathy.ChainablePath New { get; }
public static Pathy.ChainablePath Null { get; }
public static Pathy.ChainablePath Temp { get; }
public int CompareTo(Pathy.ChainablePath other) { }
public int CompareTo(object? obj) { }
public Pathy.ChainablePath FindParentWithFileMatching(params string[] wildcards) { }
public bool HasExtension(string extension) { }
public bool HasName(string name) { }
Expand All @@ -46,6 +48,10 @@ namespace Pathy
public static Pathy.ChainablePath operator +(Pathy.ChainablePath leftPath, string additionalPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, string subPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath? leftPath, string subPath) { }
public static bool operator <(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator <=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
}
public static class ChainablePathExtensions
{
Expand All @@ -70,6 +76,15 @@ namespace Pathy
public override object? ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) { }
public override object? ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) { }
}
public sealed class PathComparer : System.Collections.Generic.IComparer<Pathy.ChainablePath>, System.Collections.Generic.IEqualityComparer<Pathy.ChainablePath>
{
public static Pathy.PathComparer Default { get; }
public static Pathy.PathComparer Ordinal { get; }
public static Pathy.PathComparer OrdinalIgnoreCase { get; }
public int Compare(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public bool Equals(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public int GetHashCode(Pathy.ChainablePath obj) { }
}
public static class StringExtensions
{
public static Pathy.ChainablePath ToPath(this string path) { }
Expand Down
17 changes: 16 additions & 1 deletion Pathy.ApiVerificationTests/ApprovedApi/pathy.net8.0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Pathy
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable, System.ISpanFormattable
public readonly struct ChainablePath : System.IComparable, System.IComparable<Pathy.ChainablePath>, System.IEquatable<Pathy.ChainablePath>, System.IFormattable, System.ISpanFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand All @@ -27,6 +27,8 @@ namespace Pathy
public static Pathy.ChainablePath Null { get; }
public static Pathy.ChainablePath Temp { get; }
public Pathy.ChainablePath AsRelativeTo(Pathy.ChainablePath basePath) { }
public int CompareTo(Pathy.ChainablePath other) { }
public int CompareTo(object? obj) { }
public Pathy.ChainablePath FindParentWithFileMatching(params string[] wildcards) { }
public bool HasExtension(string extension) { }
public bool HasName(string name) { }
Expand All @@ -49,6 +51,10 @@ namespace Pathy
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, System.Range range) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, string subPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath? leftPath, string subPath) { }
public static bool operator <(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator <=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
}
public static class ChainablePathExtensions
{
Expand All @@ -73,6 +79,15 @@ namespace Pathy
public override object? ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) { }
public override object? ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) { }
}
public sealed class PathComparer : System.Collections.Generic.IComparer<Pathy.ChainablePath>, System.Collections.Generic.IEqualityComparer<Pathy.ChainablePath>
{
public static Pathy.PathComparer Default { get; }
public static Pathy.PathComparer Ordinal { get; }
public static Pathy.PathComparer OrdinalIgnoreCase { get; }
public int Compare(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public bool Equals(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public int GetHashCode(Pathy.ChainablePath obj) { }
}
public static class StringExtensions
{
public static Pathy.ChainablePath ToPath(this string path) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Pathy
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
public readonly struct ChainablePath : System.IComparable, System.IComparable<Pathy.ChainablePath>, System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand All @@ -26,6 +26,8 @@ namespace Pathy
public static Pathy.ChainablePath New { get; }
public static Pathy.ChainablePath Null { get; }
public static Pathy.ChainablePath Temp { get; }
public int CompareTo(Pathy.ChainablePath other) { }
public int CompareTo(object? obj) { }
public Pathy.ChainablePath FindParentWithFileMatching(params string[] wildcards) { }
public bool HasExtension(string extension) { }
public bool HasName(string name) { }
Expand All @@ -46,6 +48,10 @@ namespace Pathy
public static Pathy.ChainablePath operator +(Pathy.ChainablePath leftPath, string additionalPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, string subPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath? leftPath, string subPath) { }
public static bool operator <(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator <=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
}
public static class ChainablePathExtensions
{
Expand All @@ -70,6 +76,15 @@ namespace Pathy
public override object? ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) { }
public override object? ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) { }
}
public sealed class PathComparer : System.Collections.Generic.IComparer<Pathy.ChainablePath>, System.Collections.Generic.IEqualityComparer<Pathy.ChainablePath>
{
public static Pathy.PathComparer Default { get; }
public static Pathy.PathComparer Ordinal { get; }
public static Pathy.PathComparer OrdinalIgnoreCase { get; }
public int Compare(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public bool Equals(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public int GetHashCode(Pathy.ChainablePath obj) { }
}
public static class StringExtensions
{
public static Pathy.ChainablePath ToPath(this string path) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Pathy
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
public readonly struct ChainablePath : System.IComparable, System.IComparable<Pathy.ChainablePath>, System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand All @@ -27,6 +27,8 @@ namespace Pathy
public static Pathy.ChainablePath Null { get; }
public static Pathy.ChainablePath Temp { get; }
public Pathy.ChainablePath AsRelativeTo(Pathy.ChainablePath basePath) { }
public int CompareTo(Pathy.ChainablePath other) { }
public int CompareTo(object? obj) { }
public Pathy.ChainablePath FindParentWithFileMatching(params string[] wildcards) { }
public bool HasExtension(string extension) { }
public bool HasName(string name) { }
Expand All @@ -48,6 +50,10 @@ namespace Pathy
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, System.Range range) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath leftPath, string subPath) { }
public static Pathy.ChainablePath operator /(Pathy.ChainablePath? leftPath, string subPath) { }
public static bool operator <(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator <=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
public static bool operator >=(Pathy.ChainablePath left, Pathy.ChainablePath right) { }
}
public static class ChainablePathExtensions
{
Expand All @@ -72,6 +78,15 @@ namespace Pathy
public override object? ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) { }
public override object? ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) { }
}
public sealed class PathComparer : System.Collections.Generic.IComparer<Pathy.ChainablePath>, System.Collections.Generic.IEqualityComparer<Pathy.ChainablePath>
{
public static Pathy.PathComparer Default { get; }
public static Pathy.PathComparer Ordinal { get; }
public static Pathy.PathComparer OrdinalIgnoreCase { get; }
public int Compare(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public bool Equals(Pathy.ChainablePath x, Pathy.ChainablePath y) { }
public int GetHashCode(Pathy.ChainablePath obj) { }
}
public static class StringExtensions
{
public static Pathy.ChainablePath ToPath(this string path) { }
Expand Down
29 changes: 29 additions & 0 deletions Pathy.Specs/ChainablePathSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -33,6 +35,33 @@ public void Can_build_from_an_absolute_path()
path.IsRooted.Should().BeTrue();
}

[Fact]
public void Can_sort_paths_by_segments()
{
var paths = new[]
{
ChainablePath.From("a/b/c"),
ChainablePath.From("a-b"),
ChainablePath.From("a/b")
};

paths.OrderBy(path => path, PathComparer.Ordinal).Should().Equal(
ChainablePath.From("a/b"),
ChainablePath.From("a/b/c"),
ChainablePath.From("a-b"));
}

[Fact]
public void Can_compare_paths_with_an_explicit_case_policy()
{
var upper = ChainablePath.From("folder/File.txt");
var lower = ChainablePath.From("folder/file.txt");

PathComparer.Ordinal.Equals(upper, lower).Should().BeFalse();
PathComparer.OrdinalIgnoreCase.Equals(upper, lower).Should().BeTrue();
new SortedSet<ChainablePath>(new[] { upper, lower }, PathComparer.OrdinalIgnoreCase).Should().ContainSingle();
}

[Fact]
public void Can_build_from_a_string()
{
Expand Down
54 changes: 50 additions & 4 deletions Pathy/ChainablePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Pathy
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
[global::System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
public readonly record struct ChainablePath : IFormattable, ISpanFormattable
public readonly record struct ChainablePath : IFormattable, ISpanFormattable, IComparable<ChainablePath>, IComparable
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
Expand All @@ -36,7 +36,7 @@ namespace Pathy
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
[global::System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
internal readonly record struct ChainablePath : IFormattable, ISpanFormattable
internal readonly record struct ChainablePath : IFormattable, ISpanFormattable, IComparable<ChainablePath>, IComparable
#endif
#else
#if PATHY_PUBLIC
Expand All @@ -45,7 +45,7 @@ namespace Pathy
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
[global::System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
public readonly record struct ChainablePath : IFormattable
public readonly record struct ChainablePath : IFormattable, IComparable<ChainablePath>, IComparable
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
Expand All @@ -54,7 +54,7 @@ namespace Pathy
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
[global::System.Diagnostics.DebuggerDisplay("{DebuggerDisplayValue,nq}")]
internal readonly record struct ChainablePath : IFormattable
internal readonly record struct ChainablePath : IFormattable, IComparable<ChainablePath>, IComparable
#endif
#endif
{
Expand Down Expand Up @@ -357,6 +357,52 @@ private static bool EndsInDirectorySeparator(string path)
return From(leftPath.ToString() + additionalPath);
}

/// <summary>
/// Compares two paths using <see cref="PathComparer.Default"/>.
/// </summary>
public int CompareTo(ChainablePath other)
{
return PathComparer.Default.Compare(this, other);
}

/// <summary>
/// Compares this path with another object using <see cref="PathComparer.Default"/>.
/// </summary>
public int CompareTo(object? obj)
{
if (obj is null)
{
return 1;
}

if (obj is ChainablePath other)
{
return CompareTo(other);
}

throw new ArgumentException($"Object must be of type {nameof(ChainablePath)}", nameof(obj));
}

/// <summary>
/// Determines whether one path sorts before another using <see cref="PathComparer.Default"/>.
/// </summary>
public static bool operator <(ChainablePath left, ChainablePath right) => left.CompareTo(right) < 0;

/// <summary>
/// Determines whether one path sorts after another using <see cref="PathComparer.Default"/>.
/// </summary>
public static bool operator >(ChainablePath left, ChainablePath right) => left.CompareTo(right) > 0;

/// <summary>
/// Determines whether one path sorts before or equal to another using <see cref="PathComparer.Default"/>.
/// </summary>
public static bool operator <=(ChainablePath left, ChainablePath right) => left.CompareTo(right) <= 0;

/// <summary>
/// Determines whether one path sorts after or equal to another using <see cref="PathComparer.Default"/>.
/// </summary>
public static bool operator >=(ChainablePath left, ChainablePath right) => left.CompareTo(right) >= 0;

/// <summary>
/// Gets a value indicating whether the current <see cref="ChainablePath"/> instance is equal to <see cref="ChainablePath.Null"/>.
/// </summary>
Expand Down
74 changes: 74 additions & 0 deletions Pathy/PathComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;

namespace Pathy;

/// <summary>
/// Compares paths by their individual segments.
/// </summary>
public sealed class PathComparer : IEqualityComparer<ChainablePath>, IComparer<ChainablePath>
{
private readonly StringComparer segmentComparer;

private PathComparer(StringComparer segmentComparer)
{
this.segmentComparer = segmentComparer;
}

/// <summary>
/// Gets an ordinal, case-sensitive path comparer.
/// </summary>
public static PathComparer Ordinal { get; } = new(StringComparer.Ordinal);

/// <summary>
/// Gets an ordinal, case-insensitive path comparer.
/// </summary>
public static PathComparer OrdinalIgnoreCase { get; } = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets the platform default path comparer.
/// </summary>
public static PathComparer Default { get; } =
#if NET5_0_OR_GREATER
OperatingSystem.IsWindows() ? OrdinalIgnoreCase : Ordinal;
#else
Environment.OSVersion.Platform == PlatformID.Win32NT ? OrdinalIgnoreCase : Ordinal;
#endif

/// <inheritdoc />
public int Compare(ChainablePath x, ChainablePath y)
{
string[] leftSegments = GetSegments(x);
string[] rightSegments = GetSegments(y);
int length = Math.Min(leftSegments.Length, rightSegments.Length);

for (int index = 0; index < length; index++)
{
int comparison = segmentComparer.Compare(leftSegments[index], rightSegments[index]);
if (comparison != 0)
{
return comparison;
}
}

return leftSegments.Length.CompareTo(rightSegments.Length);
}

/// <inheritdoc />
public bool Equals(ChainablePath x, ChainablePath y) => Compare(x, y) == 0;

/// <inheritdoc />
public int GetHashCode(ChainablePath obj)
{
int hash = 17;
foreach (string segment in GetSegments(obj))
{
hash = unchecked((hash * 31) + segmentComparer.GetHashCode(segment));
}

return hash;
}

private static string[] GetSegments(ChainablePath path) =>
path.ToString().Split(new[] { '/', '\\' }, StringSplitOptions.None);
}