2007-05-16

Convert absolute path to relative path

Today I needed to convert an absolute path to a relative path based on a specified base path. E.g.

c:\a\b\c -> c:\a\b\c\d\file.txt = d\file.txt
c:\a\b\c -> c:\a\file.txt = ..\..\file.txt
c:\a\b\c -> c:\a\x\file.txt = ..\..\x\file.txt

I am surprised there is nothing in the .NET framework so I had a hunt around and converted the code from the following URL (http://www.vergentsoftware.com/blogs/ckinsman/default.aspx?date=2006-08-07) into C#....


private string RelativePath(string absolutePath, string relativeTo)
{
string[] absoluteDirectories = absolutePath.Split('\\');
string[] relativeDirectories = relativeTo.Split('\\');

//Get the shortest of the two paths
int length = absoluteDirectories.Length < relativeDirectories.Length ? absoluteDirectories.Length : relativeDirectories.Length;

//Use to determine where in the loop we exited
int lastCommonRoot = -1;
int index;

//Find common root
for (index = 0; index < length; index++)
if (absoluteDirectories[index] == relativeDirectories[index])
lastCommonRoot = index;
else
break;

//If we didn't find a common prefix then throw
if (lastCommonRoot == -1)
throw new ArgumentException("Paths do not have a common base");

//Build up the relative path
StringBuilder relativePath = new StringBuilder();

//Add on the ..
for (index = lastCommonRoot + 1; index < absoluteDirectories.Length; index++)
if (absoluteDirectories[index].Length > 0)
relativePath.Append("..\\");

//Add on the folders
for (index = lastCommonRoot + 1; index < relativeDirectories.Length - 1; index++)
relativePath.Append(relativeDirectories[index] + "\\");
relativePath.Append(relativeDirectories[relativeDirectories.Length - 1]);

return relativePath.ToString();
}

20 comments:

David Tombs said...

Your relative path and absolute path parameters are backwards, but other than that a great help. Thanks!

Patrick said...

Here is a C# open-source library that helps handling complex path scenario such as relative/absolute conversion, rebasing, and much more
http://www.codeplex.com/FileDirectoryPath

Anonymous said...

thanks for this code !

ciberado said...

Cool!!!! Here's the java version, thanks! ;-)

public static String convertToRelativePath(String absolutePath, String relativeTo) {
StringBuilder relativePath = null;

// Thanks to:
// http://mrpmorris.blogspot.com/2007/05/convert-absolute-path-to-relative-path.html
absolutePath = absolutePath.replaceAll("\\\\", "/");
relativeTo = relativeTo.replaceAll("\\\\", "/");

if (absolutePath.equals(relativeTo) == true) {

} else {
String[] absoluteDirectories = absolutePath.split("/");
String[] relativeDirectories = relativeTo.split("/");

//Get the shortest of the two paths
int length = absoluteDirectories.length < relativeDirectories.length ?
absoluteDirectories.length : relativeDirectories.length;

//Use to determine where in the loop we exited
int lastCommonRoot = -1;
int index;

//Find common root
for (index = 0; index < length; index++) {
if (absoluteDirectories[index].equals(relativeDirectories[index])) {
lastCommonRoot = index;
} else {
break;
//If we didn't find a common prefix then throw
}
}
if (lastCommonRoot != -1) {
//Build up the relative path
relativePath = new StringBuilder();
//Add on the ..
for (index = lastCommonRoot + 1; index < absoluteDirectories.length; index++) {
if (absoluteDirectories[index].length() > 0) {
relativePath.append("../");
}
}
for (index = lastCommonRoot + 1; index < relativeDirectories.length - 1; index++) {
relativePath.append(relativeDirectories[index] + "/");
}
relativePath.append(relativeDirectories[relativeDirectories.length - 1]);
}
}
return relativePath == null ? null : relativePath.toString();
}

EvilNando said...

Very useful thanks

Anonymous said...

Great! Thank you! Here is the Qt version:

QString RelativePath( QString absolutePath, QString relativeTo, bool bIsFile /*= false*/ )
{
QStringList absoluteDirectories = absolutePath.split( '/', QString::SkipEmptyParts );
QStringList relativeDirectories = relativeTo.split( '/', QString::SkipEmptyParts );

//Get the shortest of the two paths
int length = absoluteDirectories.count() < relativeDirectories.count() ? absoluteDirectories.count() : relativeDirectories.count();

//Use to determine where in the loop we exited
int lastCommonRoot = -1;
int index;

//Find common root
for (index = 0; index < length; index++)
if (absoluteDirectories[index] == relativeDirectories[index])
lastCommonRoot = index;
else
break;

//If we didn't find a common prefix then throw
if (lastCommonRoot == -1)
throw QString("Paths do not have a common base");

//Build up the relative path
QString relativePath;

//Add on the ..
for (index = lastCommonRoot + 1; index < absoluteDirectories.count() - (bIsFile?1:0); index++)
if (absoluteDirectories[index].length() > 0)
relativePath.append("../");

//Add on the folders
for (index = lastCommonRoot + 1; index < relativeDirectories.count() - 1; index++)
relativePath.append( relativeDirectories[index] ).append( "/" );
relativePath.append(relativeDirectories[relativeDirectories.count() - 1]);

return relativePath;
}

berndwilli said...

I changed the following lines and it works for me like expected:

//Add on the ..
for (index = lastCommonRoot + 1; index < relativeDirectories.Length; index++)
if (relativeDirectories[index].Length > 0)
relativePath.Append("..\\");

//Add on the folders
for (index = lastCommonRoot + 1; index < absoluteDirectories.Length - 1; index++)
relativePath.Append(absoluteDirectories[index] + "\\");

relativePath.Append(absoluteDirectories[absoluteDirectories.Length - 1]);

norgepaul said...

...and a Delphi version...

function GetRelativePath(RelativeTo, AbsolutePath: String): String;

procedure SplitDirectories(Directory: String; const Directories: TStringList);
var
i: Integer;
begin
for i := length(Directory) downto 1 do
begin
if (Directory[i] = '\') or (i = 1) then
begin
Directories.Insert(0, copy(Directory, i + 1, MaxInt));
Directory := copy(Directory, 1, i - 1);
end;
end;
end;

var
AbsoluteDirectories,
RelativeDirectories: TStringList;
Len, LastCommonRoot, i: Integer;
begin
Result := '';

AbsoluteDirectories := TStringList.Create;
RelativeDirectories := TStringList.Create;
try
SplitDirectories(AbsolutePath, AbsoluteDirectories);
SplitDirectories(RelativeTo, RelativeDirectories);

//Get the shortest of the two paths
Len := AbsoluteDirectories.Count;
if Len < RelativeDirectories.Count then
Len := RelativeDirectories.Count;

//Use to determine where in the loop we exited
LastCommonRoot := -1;

//Find common root
for i := 0 to Len do
begin
if AbsoluteDirectories[i] = RelativeDirectories[i] then
LastCommonRoot := i
else
break;
end;

//If we didn't find a common prefix then throw exception
if lastCommonRoot = -1 then
raise Exception.Create('Paths do not have a common base');

//Build up the relative path
//Add on the ..
for i := LastCommonRoot + 1 to pred(AbsoluteDirectories.Count) do
if length(AbsoluteDirectories[i]) > 0 then
Result := concat(Result, '..\');

//Add on the folders
for i := LastCommonRoot + 1 to pred(RelativeDirectories.Count) do
Result := concat(Result, RelativeDirectories[i], '\');
finally
// Free the stringlists
FreeAndNil(AbsoluteDirectories);
FreeAndNil(RelativeDirectories);
end;
end;

Neil said...

Thanks, this was very helpful, I was about to write my own when I found this, saves me trouble.

Kseda said...

Wow... Are you crazy inventing the wheels?

string relativePath = Path.Combine(pathToRelate, absolutePath)

Bram Smulders said...

Kseda, thanks, you made my day :) It's so funny to see people struggling to get their algorithms right when there is such a simple oneliner.

anonymous said...

Kseda, your one liner does NOT convert absolute paths to relative ones. If you use the example given, the result of the call to Path.combine is an absolute path that not even close to the what the correct relative path is.
Examples:
c:\a\b\c -> c:\a\b\c\d\file.txt = d\file.txt

Pseudo code:
using System.IO;
Console.Write(Path.Combine("c:\a\b\c\d\file.txt", "c:\a\b\c");

Result:
c:\a\b\c

This is not a relative path from c:\a\b\c to c:\a\b\c\d\file.txt which is d\file.txt.

Nurettin Onur said...

QDir("/home/bob").relativeFilePath("/home/mary/images/img.jpg");

will give you "../images/img.jpg"

decasteljau said...

use PathRelativePathTo via pinvoke

[DllImport("shlwapi.dll", CharSet=CharSet.Auto)]
static extern bool PathRelativePathTo(
[Out] StringBuilder pszPath,
[In] string pszFrom,
[In] FileAttributes dwAttrFrom,
[In] string pszTo,
[In] FileAttributes dwAttrTo
);

RenHoek said...

Here's my C++ version.
( note: contains Berndwilli fix )

bool StringEQ( const std::string& str0, const std::string& str1 )
{
if ( str0.size() != str1.size() )
return false;
return ( strcmp( str0.c_str(), str1.c_str() ) == 0 );
}

// 7.3: http://www.oopweb.com/CPP/Documents/CPPHOWTO/Volume/C++Programming-HOWTO-7.html
//
void Tokenize(const std::string& str, std::vector& tokens, const std::string& delimiters)
{
// Skip delimiters at beginning.
std::string::size_type lastPos = str.find_first_not_of(delimiters, 0);
// Find first "non-delimiter".
std::string::size_type pos = str.find_first_of(delimiters, lastPos);

while (std::string::npos != pos || std::string::npos != lastPos)
{
// Found a token, add it to the vector.
tokens.push_back(str.substr(lastPos, pos - lastPos));
// Skip delimiters. Note the "not_of"
lastPos = str.find_first_not_of(delimiters, pos);
// Find next "non-delimiter"
pos = str.find_first_of(delimiters, lastPos);
}
}

//
// "c:\a\b\c" + "c:\a\x\file.txt" => "..\..\x\file.txt"
// http://mrpmorris.blogspot.com/2007/05/convert-absolute-path-to-relative-path.html
//
std::string ConvertAbsoluteToRelative( const std::string& absolutePath, const std::string& relativeTo )
{
std::vector< std::string > absoluteDirectories;
std::vector< std::string > relativeDirectories;
Tokenize( absolutePath, absoluteDirectories, "/" );
Tokenize( relativeTo, relativeDirectories, "/" );

// Get the shortest of the two paths
int length = absoluteDirectories.size() < relativeDirectories.size() ? absoluteDirectories.size() : relativeDirectories.size();

// Use to determine where in the loop we exited
int lastCommonRoot = -1;
int index;

// Find common root
for (index = 0; index < length; index++)
if (StringEQ( absoluteDirectories[index], relativeDirectories[index]))
lastCommonRoot = index;
else
break;

// If we didn't find a common prefix then throw
if (lastCommonRoot == -1)
{
assert( 0 ); // "Paths do not have a common base"
return "";
}

std::string relativePath;

// Add on the ..
for (index = lastCommonRoot + 1; index < relativeDirectories.size(); index++)
if (relativeDirectories[index].size() > 0)
relativePath += "../";

// Add on the folders
for (index = lastCommonRoot + 1; index < absoluteDirectories.size() - 1; index++)
{
relativePath += absoluteDirectories[index];
relativePath += "/";
}

relativePath += absoluteDirectories[absoluteDirectories.size() - 1];

// :)
return relativePath;
}

Ian said...

Kseda is right, Path.Combine is the best way to go if you know the path you are resolving from.

If you just want a path relative to the current directory, use Path.GetFullPath(relativePath).

Path.GetFullPath(".") == Directory.GetCurrentDirectory()

Sonal Khunt said...

Nice, Very useful,
Need PHP Version, here it is

function RelativePath($absolutePath, $relativeTo)
{
$absoluteDirectories = explode('/',$absolutePath);
$relativeDirectories = explode('/',$relativeTo);

//Get the shortest of the two paths
$length = count($absoluteDirectories) < count($relativeDirectories) ? count($absoluteDirectories) : count($relativeDirectories);

//Use to determine where in the loop we exited
$lastCommonRoot = -1;
$index;

//Find common root
for ($index = 0; $index < $length; $index++)
if ($absoluteDirectories[$index] == $relativeDirectories[$index])
$lastCommonRoot = $index;
else
break;

//If we didn't find a common prefix then throw
if ($lastCommonRoot == -1)
return "Paths do not have a common base";

//Build up the relative path
//StringBuilder relativePath = new StringBuilder();
$relativePath = "";

//Add on the ..
for ($index = $lastCommonRoot + 1; $index < count($absoluteDirectories); $index++)
if (strlen($absoluteDirectories[$index]) > 0)
$relativePath .= "..\\";

//Add on the folders
for ($index = $lastCommonRoot + 1; $index < count($relativeDirectories) - 1; $index++)
$relativePath .= $relativeDirectories[$index]."\\";

$relativePath .= $relativeDirectories[count($relativeDirectories) - 1];

return $relativePath;
}

Niket said...

Hi can any body give me a Function signature or example of how to use function

EternaL said...

I found an error when comparing directories instead of files.

eg.: C:\a\b -> C:\a = ..\a

It should be ..\

To correct that i made a change:

int lastPiece = relativeDirectories.length - 1;

if (lastPiece > lastCommonRoot) {
relativePath.append(relativeDirectories[lastPiece]);
}

return relativePath.toString();

The code is Java, but is easy to convert as you can see.

EternaL said...

I found an error when comparing directories instead of files.

eg.: C:\a\b -> C:\a = ..\a

It should be ..\

To correct that i made a change:

int lastPiece = relativeDirectories.length - 1;

if (lastPiece > lastCommonRoot) {
relativePath.append(relativeDirectories[lastPiece]);
}

return relativePath.toString();

The code is Java, but is easy to convert as you can see.