There\'s a part in my apps that displays the file path loaded by the user through OpenFileDialog. It\'s taking up too much space to display the whole path, but I don\'t want
It's a long way around, but System.Uri class has a method named MakeRelativeUri. Maybe you could use that. It's a shame really that System.IO.Path doesn't have this.
here's mine:
public static string RelativePathTo(this System.IO.DirectoryInfo @this, string to)
{
var rgFrom = @this.FullName.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
var rgTo = to.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
var cSame = rgFrom.TakeWhile((p, i) => i < rgTo.Length && string.Equals(p, rgTo[i])).Count();
return Path.Combine(
Enumerable.Range(0, rgFrom.Length - cSame)
.Select(_ => "..")
.Concat(rgTo.Skip(cSame))
.ToArray()
);
}
As Alex Brault points out, especially on Windows, the absolute path (with drive letter and all) is unambiguous and often better.
Shouldn't your OpenFileDialog use a regular tree-browser structure?
To get some nomenclature in place, the RefDir is the directory relative to which you want to specify the path; the AbsName is the absolute path name that you want to map; and the RelPath is the resulting relative path.
Take the first of these options that matches:
To illustrate the last rule (which is, of course, by far the most complex), start with:
RefDir = D:\Abc\Def\Ghi
AbsName = D:\Abc\Default\Karma\Crucible
Then
LCP = D:\Abc
(RefDir - LCP) = Def\Ghi
(Absname - LCP) = Default\Karma\Crucible
RelPath = ..\..\Default\Karma\Crucible
While I was typing, DavidK produced an answer which suggests that you are not the first to need this feature and that there is a standard function to do this job. Use it. But there's no harm in being able to think your way through from first principles, either.
Except that Unix systems do not support drive letters (so everything is always located under the same root directory, and the first bullet therefore is irrelevant), the same technique could be used on Unix.
As pointed above .NET Core 2.x has implementation of Path.GetRelativePath
.
Code below is adapted from sources and works fine with .NET 4.7.1 Framework.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//Adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Path.cs#L697
// by Anton Krouglov
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Text;
using Xunit;
namespace System.IO {
// Provides methods for processing file system strings in a cross-platform manner.
// Most of the methods don't do a complete parsing (such as examining a UNC hostname),
// but they will handle most string operations.
public static class PathNetCore {
/// <summary>
/// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
/// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
/// </summary>
/// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
/// <param name="path">The destination path.</param>
/// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
public static string GetRelativePath(string relativeTo, string path) {
return GetRelativePath(relativeTo, path, StringComparison);
}
private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) {
if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
Debug.Assert(comparisonType == StringComparison.Ordinal ||
comparisonType == StringComparison.OrdinalIgnoreCase);
relativeTo = Path.GetFullPath(relativeTo);
path = Path.GetFullPath(path);
// Need to check if the roots are different- if they are we need to return the "to" path.
if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType))
return path;
int commonLength = PathInternalNetCore.GetCommonPathLength(relativeTo, path,
ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
// If there is nothing in common they can't share the same root, return the "to" path as is.
if (commonLength == 0)
return path;
// Trailing separators aren't significant for comparison
int relativeToLength = relativeTo.Length;
if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo))
relativeToLength--;
bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path);
int pathLength = path.Length;
if (pathEndsInSeparator)
pathLength--;
// If we have effectively the same path, return "."
if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";
// We have the same root, we need to calculate the difference now using the
// common Length and Segment count past the length.
//
// Some examples:
//
// C:\Foo C:\Bar L3, S1 -> ..\Bar
// C:\Foo C:\Foo\Bar L6, S0 -> Bar
// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
StringBuilder
sb = new StringBuilder(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));
// Add parent segments for segments past the common on the "from" path
if (commonLength < relativeToLength) {
sb.Append("..");
for (int i = commonLength + 1; i < relativeToLength; i++) {
if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) {
sb.Append(DirectorySeparatorChar);
sb.Append("..");
}
}
}
else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) {
// No parent segments and we need to eat the initial separator
// (C:\Foo C:\Foo\Bar case)
commonLength++;
}
// Now add the rest of the "to" path, adding back the trailing separator
int differenceLength = pathLength - commonLength;
if (pathEndsInSeparator)
differenceLength++;
if (differenceLength > 0) {
if (sb.Length > 0) {
sb.Append(DirectorySeparatorChar);
}
sb.Append(path, commonLength, differenceLength);
}
return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb);
}
// Public static readonly variant of the separators. The Path implementation itself is using
// internal const variant of the separators for better performance.
public static readonly char DirectorySeparatorChar = PathInternalNetCore.DirectorySeparatorChar;
public static readonly char AltDirectorySeparatorChar = PathInternalNetCore.AltDirectorySeparatorChar;
public static readonly char VolumeSeparatorChar = PathInternalNetCore.VolumeSeparatorChar;
public static readonly char PathSeparator = PathInternalNetCore.PathSeparator;
/// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
internal static StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
}
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
internal static class PathInternalNetCore {
internal const char DirectorySeparatorChar = '\\';
internal const char AltDirectorySeparatorChar = '/';
internal const char VolumeSeparatorChar = ':';
internal const char PathSeparator = ';';
internal const string ExtendedDevicePathPrefix = @"\\?\";
internal const string UncPathPrefix = @"\\";
internal const string UncDevicePrefixToInsert = @"?\UNC\";
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
internal const string DevicePathPrefix = @"\\.\";
//internal const int MaxShortPath = 260;
// \\?\, \\.\, \??\
internal const int DevicePrefixLength = 4;
/// <summary>
/// Returns true if the two paths have the same root
/// </summary>
internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) {
int firstRootLength = GetRootLength(first);
int secondRootLength = GetRootLength(second);
return firstRootLength == secondRootLength
&& string.Compare(
strA: first,
indexA: 0,
strB: second,
indexB: 0,
length: firstRootLength,
comparisonType: comparisonType) == 0;
}
/// <summary>
/// Gets the length of the root of the path (drive, share, etc.).
/// </summary>
internal static int GetRootLength(string path) {
int i = 0;
int volumeSeparatorLength = 2; // Length to the colon "C:"
int uncRootLength = 2; // Length to the start of the server name "\\"
bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix);
bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix);
if (extendedSyntax) {
// Shift the position we look for the root from to account for the extended prefix
if (extendedUncSyntax) {
// "\\" -> "\\?\UNC\"
uncRootLength = UncExtendedPathPrefix.Length;
}
else {
// "C:" -> "\\?\C:"
volumeSeparatorLength += ExtendedDevicePathPrefix.Length;
}
}
if ((!extendedSyntax || extendedUncSyntax) && path.Length > 0 && IsDirectorySeparator(path[0])) {
// UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
i = 1; // Drive rooted (\foo) is one character
if (extendedUncSyntax || (path.Length > 1 && IsDirectorySeparator(path[1]))) {
// UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
// (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
i = uncRootLength;
int n = 2; // Maximum separators to skip
while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
}
}
else if (path.Length >= volumeSeparatorLength &&
path[volumeSeparatorLength - 1] == PathNetCore.VolumeSeparatorChar) {
// Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
// If the colon is followed by a directory separator, move past it
i = volumeSeparatorLength;
if (path.Length >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
}
return i;
}
/// <summary>
/// True if the given character is a directory separator.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsDirectorySeparator(char c) {
return c == PathNetCore.DirectorySeparatorChar || c == PathNetCore.AltDirectorySeparatorChar;
}
/// <summary>
/// Get the common path length from the start of the string.
/// </summary>
internal static int GetCommonPathLength(string first, string second, bool ignoreCase) {
int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);
// If nothing matches
if (commonChars == 0)
return commonChars;
// Or we're a full string and equal length or match to a separator
if (commonChars == first.Length
&& (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
return commonChars;
if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
return commonChars;
// It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
commonChars--;
return commonChars;
}
/// <summary>
/// Gets the count of common characters from the left optionally ignoring case
/// </summary>
internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) {
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;
int commonChars = 0;
fixed (char* f = first)
fixed (char* s = second) {
char* l = f;
char* r = s;
char* leftEnd = l + first.Length;
char* rightEnd = r + second.Length;
while (l != leftEnd && r != rightEnd
&& (*l == *r || (ignoreCase &&
char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) {
commonChars++;
l++;
r++;
}
}
return commonChars;
}
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(string path)
=> path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
}
/// <summary> Tests for PathNetCore.GetRelativePath </summary>
public static class GetRelativePathTests {
[Theory]
[InlineData(@"C:\", @"C:\", @".")]
[InlineData(@"C:\a", @"C:\a\", @".")]
[InlineData(@"C:\A", @"C:\a\", @".")]
[InlineData(@"C:\a\", @"C:\a", @".")]
[InlineData(@"C:\", @"C:\b", @"b")]
[InlineData(@"C:\a", @"C:\b", @"..\b")]
[InlineData(@"C:\a", @"C:\b\", @"..\b\")]
[InlineData(@"C:\a\b", @"C:\a", @"..")]
[InlineData(@"C:\a\b", @"C:\a\", @"..")]
[InlineData(@"C:\a\b\", @"C:\a", @"..")]
[InlineData(@"C:\a\b\", @"C:\a\", @"..")]
[InlineData(@"C:\a\b\c", @"C:\a\b", @"..")]
[InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")]
[InlineData(@"C:\a\b\c", @"C:\a", @"..\..")]
[InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")]
[InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")]
[InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")]
[InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")]
[InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")]
[InlineData(@"C:\a\", @"C:\b", @"..\b")]
[InlineData(@"C:\a", @"C:\a\b", @"b")]
[InlineData(@"C:\a", @"C:\A\b", @"b")]
[InlineData(@"C:\a", @"C:\b\c", @"..\b\c")]
[InlineData(@"C:\a\", @"C:\a\b", @"b")]
[InlineData(@"C:\", @"D:\", @"D:\")]
[InlineData(@"C:\", @"D:\b", @"D:\b")]
[InlineData(@"C:\", @"D:\b\", @"D:\b\")]
[InlineData(@"C:\a", @"D:\b", @"D:\b")]
[InlineData(@"C:\a\", @"D:\b", @"D:\b")]
[InlineData(@"C:\ab", @"C:\a", @"..\a")]
[InlineData(@"C:\a", @"C:\ab", @"..\ab")]
[InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")]
[InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")]
//[PlatformSpecific(TestPlatforms.Windows)] // Tests Windows-specific paths
public static void GetRelativePath_Windows(string relativeTo, string path, string expected) {
string result = PathNetCore.GetRelativePath(relativeTo, path);
Assert.Equal(expected, result);
// Check that we get the equivalent path when the result is combined with the sources
Assert.Equal(
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar),
Path.GetFullPath(Path.Combine(Path.GetFullPath(relativeTo), result))
.TrimEnd(Path.DirectorySeparatorChar),
ignoreCase: true,
ignoreLineEndingDifferences: false,
ignoreWhiteSpaceDifferences: false);
}
}
}
If you know that toPath is contained by fromPath then you can keep it simple. I'll leave out the asserts for brevity.
public static string MakeRelativePath(string fromPath, string toPath)
{
// use Path.GetFullPath to canonicalise the paths (deal with multiple directory seperators, etc)
return Path.GetFullPath(toPath).Substring(Path.GetFullPath(fromPath).Length + 1);
}
If you are using .NET Core 2.0, Path.GetRelativePath() is available providing this specific functionality:
var relativeTo = @"C:\Program Files\Dummy Folder\MyProgram";
var path = @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat";
string relativePath = System.IO.Path.GetRelativePath(relativeTo, path);
System.Console.WriteLine(relativePath);
// output --> Data\datafile1.dat
Otherwise, for .NET full framework (as of v4.7) recommend using one of the other suggested answers.