When listing some file paths for display in a grid, I found that I needed to shorten them, since they can quickly become to large to fit in the relatively small space I had allocated for them. Instead of simply cropping of the beginning or end I wanted to remove some of the middel directories, just like all the standard Windows apps does in e.g. the "Recent files" overview.
Let's try to turn this:
c:\websites\acmeinc\myproject\www_myproj\App_Data\somefile.txt
into this:
c:\websites\acmeinc\...\App_Data\somefile.txt
So I went googling, but found no exact matches. This post on codinghorror.com points to a trimming function which operates directly on the Win-forms canvas (so you never get hold of the shortened string object) and to a function in the Windows API (which means you have to include a reference to a DLL that is not native .NET).
This is my take on a function for this purpose. It is not as thouroughly tested or performant as an official MS-API-method would be, but I think it should suffice for most purposes:
/// <summary>
/// Shortens a file path to the specified length
/// </summary>
/// <param name="path">The file path to shorten</param>
/// <param name="maxLength">The max length of the output path (including the ellipsis if inserted)</param>
/// <returns>The path with some of the middle directory paths replaced with an ellipsis (or the entire path if it is already shorter than maxLength)</returns>
/// <remarks>
/// Shortens the path by removing some of the "middle directories" in the path and inserting an ellipsis. If the filename and root path (drive letter or UNC server name) in itself exceeds the maxLength, the filename will be cut to fit.
/// UNC-paths and relative paths are also supported.
/// The inserted ellipsis is not a true ellipsis char, but a string of three dots.
/// </remarks>
/// <example>
/// ShortenPath(@"c:\websites\myproject\www_myproj\App_Data\themegafile.txt", 50)
/// Result: "c:\websites\myproject\...\App_Data\themegafile.txt"
///
/// ShortenPath(@"c:\websites\myproject\www_myproj\App_Data\theextremelylongfilename_morelength.txt", 30)
/// Result: "c:\...gfilename_morelength.txt"
///
/// ShortenPath(@"\\myserver\theshare\myproject\www_myproj\App_Data\theextremelylongfilename_morelength.txt", 30)
/// Result: "\\myserver\...e_morelength.txt"
///
/// ShortenPath(@"\\myserver\theshare\myproject\www_myproj\App_Data\themegafile.txt", 50)
/// Result: "\\myserver\theshare\...\App_Data\themegafile.txt"
///
/// ShortenPath(@"\\192.168.1.178\theshare\myproject\www_myproj\App_Data\themegafile.txt", 50)
/// Result: "\\192.168.1.178\theshare\...\themegafile.txt"
///
/// ShortenPath(@"\theshare\myproject\www_myproj\App_Data\", 30)
/// Result: "\theshare\...\App_Data\"
///
/// ShortenPath(@"\theshare\myproject\www_myproj\App_Data\themegafile.txt", 35)
/// Result: "\theshare\...\themegafile.txt"
/// </example>
public static string ShortenPath(string path, int maxLength)
{
string ellipsisChars = "...";
char dirSeperatorChar = Path.DirectorySeparatorChar;
string directorySeperator = dirSeperatorChar.ToString();
//simple guards
if (path.Length <= maxLength)
{
return path;
}
int ellipsisLength = ellipsisChars.Length;
if (maxLength <= ellipsisLength)
{
return ellipsisChars;
}
//alternate between taking a section from the start (firstPart) or the path and the end (lastPart)
bool isFirstPartsTurn = true; //drive letter has first priority, so start with that and see what else there is room for
//vars for accumulating the first and last parts of the final shortened path
string firstPart = "";
string lastPart = "";
//keeping track of how many first/last parts have already been added to the shortened path
int firstPartsUsed = 0;
int lastPartsUsed = 0;
string[] pathParts = path.Split(dirSeperatorChar);
for (int i = 0; i < pathParts.Length; i++)
{
if (isFirstPartsTurn)
{
string partToAdd = pathParts[firstPartsUsed] + directorySeperator;
if ((firstPart.Length + lastPart.Length + partToAdd.Length + ellipsisLength) > maxLength)
{
break;
}
firstPart = firstPart + partToAdd;
if (partToAdd == directorySeperator)
{
//this is most likely the first part of and UNC or relative path
//do not switch to lastpart, as these are not "true" directory seperators
//otherwise "\\myserver\theshare\outproject\www_project\file.txt" becomes "\\...\www_project\file.txt" instead of the intended "\\myserver\...\file.txt")
}
else
{
isFirstPartsTurn = false;
}
firstPartsUsed++;
}
else
{
int index = pathParts.Length - lastPartsUsed - 1; //-1 because of length vs. zero-based indexing
string partToAdd = directorySeperator + pathParts[index];
if ((firstPart.Length + lastPart.Length + partToAdd.Length + ellipsisLength) > maxLength)
{
break;
}
lastPart = partToAdd + lastPart;
if (partToAdd == directorySeperator)
{
//this is most likely the last part of a relative path (e.g. "\websites\myproject\www_myproj\App_Data\")
//do not proceed to processing firstPart yet
}
else
{
isFirstPartsTurn = true;
}
lastPartsUsed++;
}
}
if (lastPart == "")
{
//the filename (and root path) in itself was longer than maxLength, shorten it
lastPart = pathParts[pathParts.Length - 1];//"pathParts[pathParts.Length -1]" is the equivalent of "Path.GetFileName(pathToShorten)"
lastPart = lastPart.Substring(lastPart.Length + ellipsisLength + firstPart.Length - maxLength, maxLength - ellipsisLength - firstPart.Length);
}
return firstPart + ellipsisChars + lastPart;
}