I am looking for a better/safer/more elegant generic method that could give me an n depth long directory from a given path.
I've created something that works but it based on string parsing so I hope you could find a better solution. The method could use a directory info for passed/returned values as well.
public static string GetDirectoryNDepth(string root, string target, int depth)
{
string[] splittedRoot = root.Split('\\');
string[] splittedTarget = target.Split('\\');
StringBuilder sb = new StringBuilder();
for (int i = 0; i < splittedTarget.Length; i++)
if (i < splittedRoot.Count() + depth)
sb.Append(String.Format("{0}\\", splittedTarget[i]));
else
break;
return sb.ToString();
}
Sample values:
//For 3 depth long parametr it should return expected value
//First case filepath
string root = #"C\Desktop\temp\MSC\IH";
string target = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel\ThirdLevel\dsf - Copy (2).xml";
string expected = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel\ThirdLevel";
//Second case target shorter then depth
string root = #"C\Desktop\temp\MSC\IH";
string target = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel";
string expected = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel";
From your comment, instead of using \ as the separator, you can use Path.DirectorySeparatorChar instead like this
public string GetDirectoryNDepth(string root, string target, int depth)
{
string[] splittedRoot = root.Split(Path.DirectorySeparatorChar);
string[] splittedTarget = target.Split(Path.DirectorySeparatorChar);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < splittedTarget.Length; i++)
if (i < splittedRoot.Length + depth)
sb.Append(String.Format("{0}{1}", splittedTarget[i], Path.DirectorySeparatorChar));
else
break;
return sb.ToString();
}
From the Path.DirectorySeparatorChar doc:
Provides a platform-specific character used to separate directory
levels in a path string that reflects a hierarchical file system
organization...
...The value of this field is a slash ("/") on UNIX, and a backslash
("\") on the Windows and Macintosh operating systems.
I found input set very particular that root doesn't start with C: it is C\
don't know whether it is purposefully like that
string root = #"C\Desktop\temp\MSC\IH";
string target = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel\ThirdLevel\dsf - Copy (2).xml";
string expected = #"C:\Desktop\temp\MSC\IH\FirstLevel\SecondLevel\ThirdLevel";
Based upon input sample & code this would be efficient code
public static string GetDirectoryNDepth(string root, string target, int depth)
{
var separatorChar = Path.DirectorySeparatorChar;
int rootSlashCount = root.Split(separatorChar).Length;
int totalSlashCount = rootSlashCount + depth;
var iCnt = root.Length;
while(iCnt < target.Length && rootSlashCount <= totalSlashCount)
{
if (target[iCnt] == separatorChar)
rootSlashCount++;
iCnt++;
}
var retVal = target.Substring(0, iCnt);
if (retVal.EndsWith(separatorChar+"") == false)
retVal += separatorChar;
return retVal;
}
Related
I need to parse reactjs file in main.451e57c9.js to retrieve version number with C#.
This file contains mixed data, here is little part of it:
.....inally{if(s)throw i}}return a}}(e,t)||xe(e,t)||we()}var Se=
JSON.parse('{"shortVersion":"v3.1.56"}')
,Ne="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA
AASAAAAAqCAYAAAATb4ZSAAAACXBIWXMAAAsTAAALEw.....
I need to extract json data of {"shortVersion":"v3.1.56"}
The last time I tried to simply find the string shortVersion and return a certain number of characters after, but it seems like I'm trying to create the bicycle from scratch. Is there proper way to identify and extract json from the mixed text?
public static void findVersion()
{
var partialName = "main.*.js";
string[] filesInDir = Directory.GetFiles(#pathToFile, partialName);
var lines = File.ReadLines(filesInDir[0]);
foreach (var line in File.ReadLines(filesInDir[0]))
{
string keyword = "shortVersion";
int indx = line.IndexOf(keyword);
if (indx != -1)
{
string code = line.Substring(indx + keyword.Length);
Console.WriteLine(code);
}
}
}
RESULT
":"v3.1.56"}'),Ne="data:image/png;base64,iVBORw0KGgoAA.....
string findJson(string input, string keyword) {
int startIndex = input.IndexOf(keyword) - 2; //Find the starting point of shortversion then subtract 2 to start at the { bracket
input = input.Substring(startIndex); //Grab everything after the start index
int endIndex = 0;
for (int i = 0; i < input.Length; i++) {
char letter = input[i];
if (letter == '}') {
endIndex = i; //Capture the first instance of the closing bracket in the new trimmed input string.
break;
}
}
return input.Remove(endIndex+1);
}
Console.WriteLine(findJson("fwekjfwkejwe{'shortVersion':'v3.1.56'}wekjrlklkj23klj23jkl234kjlk", "shortVersion"));
You will recieve {'shortVersion':'v3.1.56'} as output
Note you may have to use line.Replace('"', "'");
Try below method -
public static object ExtractJsonFromText(string mixedStrng)
{
for (var i = mixedStrng.IndexOf('{'); i > -1; i = mixedStrng.IndexOf('{', i + 1))
{
for (var j = mixedStrng.LastIndexOf('}'); j > -1; j = mixedStrng.LastIndexOf("}", j -1))
{
var jsonProbe = mixedStrng.Substring(i, j - i + 1);
try
{
return JsonConvert.DeserializeObject(jsonProbe);
}
catch
{
}
}
}
return null;
}
Fiddle
https://dotnetfiddle.net/N1jiWH
You should not use GetFiles() since you only need one and that returns all before you can do anything. This should give your something you can work with here and it should be as fast as it likely can be with big files and/or lots of files in a folder (to be fair I have not tested this on such a large file system or file)
using System;
using System.IO;
using System.Linq;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello World");
var path = $#"c:\SomePath";
var jsonString = GetFileVersion(path);
if (!string.IsNullOrWhiteSpace(jsonString))
{
// do something with string; deserialize or whatever.
var result=JsonConvert.DeserializeObject<List<Version>>(jsonString);
var vers = result.shortVersion;
}
}
private static string GetFileVersion(string path)
{
var partialName = "main.*.js";
// JSON string fragment to find: doubled up braces and quotes for the $# string
string matchString = $#"{{""shortVersion"":";
string matchEndString = $#" ""}}'";
// we can later stop on the first match
DirectoryInfo dir = new DirectoryInfo(path);
if (!dir.Exists)
{
throw new DirectoryNotFoundException("The directory does not exist.");
}
// Call the GetFileSystemInfos method and grab the first one
FileSystemInfo info = dir.GetFileSystemInfos(partialName).FirstOrDefault();
if (info.Exists)
{
// walk the file contents looking for a match (assumptions made here there IS a match and it has that string noted)
var line = File.ReadLines(info.FullName).SkipWhile(line => !line.Contains(matchString)).Take(1).First();
var indexStart = line.IndexOf(matchString);
var indexEnd = line.IndexOf(matchEndString, indexStart);
var jsonString = line.Substring(indexStart, indexEnd + matchEndString.Length);
return jsonString;
}
return string.Empty;
}
public class Version
{
public string shortVersion { get; set; }
}
}
Use this this should be faster - https://dotnetfiddle.net/sYFvYj
public static object ExtractJsonFromText(string mixedStrng)
{
string pattern = #"\(\'\{.*}\'\)";
string str = null;
foreach (Match match in Regex.Matches(mixedStrng, pattern, RegexOptions.Multiline))
{
if (match.Success)
{
str = str + Environment.NewLine + match;
}
}
return str;
}
I'm facing a problem while developing an application.
Basically,
I have a fixed string, let's say "IHaveADream"
I now want to user to insert another string, for my purpose of a fixed length, and then concatenate every character of the fixed string with every character of the string inserted by the user.
e.g.
The user inserts "ByeBye"
then the output would be:
"IBHyaevBeyAeDream".
How to accomplish this?
I have tried with String.Concat and String.Join, inside a for statement, with no luck.
One memory-efficient option is to use a string builder, since both the original string and the user input could potentially be rather large. As mentioned by Kris, you can initialize your StringBuilder capacity to the combined length of both strings.
void Main()
{
var start = "IHaveADream";
var input = "ByeBye";
var sb = new StringBuilder(start.Length + input.Length);
for (int i = 0; i < start.Length; i++)
{
sb.Append(start[i]);
if (input.Length >= i + 1)
sb.Append(input[i]);
}
sb.ToString().Dump();
}
This only safely accounts for the input string being shorter or equal in length to the starting string. If you had a longer input string, you'd want to take the longer length as the end point for your for loop iteration and check that each array index is not out of bounds.
void Main()
{
var start = "IHaveADream";
var input = "ByeByeByeByeBye";
var sb = new StringBuilder(start.Length + input.Length);
var length = start.Length >= input.Length ? start.Length : input.Length;
for (int i = 0; i < length; i++)
{
if (start.Length >= i + 1)
sb.Append(start[i]);
if (input.Length >= i + 1)
sb.Append(input[i]);
}
sb.ToString().Dump();
}
You can create an array of characters and then re-combine them in the order you want.
char[] chars1 = "IHaveADream".ToCharArray();
char[] chars2 = "ByeBye".ToCharArray();
// you can create a custom algorithm to handle
// different size strings.
char[] c = new char[17];
c[0] = chars1[0];
c[1] = chars2[0];
...
c[13] = chars1[10];
string s = new string(c);
var firstString = "Ihaveadream";
var secondString = "ByeBye";
var stringBuilder = new StringBuilder();
for (int i = 0; i< firstString.Length; i++) {
stringBuilder .Append(str[i]);
if (i < secondString.Length) {
stringBuilder .Append(secondStr[i]);
}
}
var result = stringBuilder.ToString();
If you don't care much about memory usage or perfomance you can just use:
public static string concatStrings(string value, string value2)
{
string result = "";
int i = 0;
for (i = 0; i < Math.Max(value.Length, value2.Length) ; i++)
{
if (i < value.Length) result += value[i].ToString();
if (i < value2.Length) result += value2[i].ToString();
}
return result;
}
Usage:
string conststr = "IHaveADream";
string input = "ByeBye";
var result = ConcatStrings(conststr, input);
Console.WriteLine(result);
Output: IBHyaevBeyAeDream
P.S.
Just checked perfomance of both methods (with strBuilder and simple cancatenation) and it appears to be that both of this methods take same time to execute (if you have just one operation). The main reason for it is that string builder take considerable time to initialize while with use of concatenation we don't need that.
But in case if you have to process something like 1500 strings then it's different story and string builder is more of an option.
For 100 000 method executions it showed 85 (str buld) vs 22 (concat) ms respectively.
My Code
I am dynamically editing a regex for matching text in a pdf, which can contain hyphenation at the end of some lines.
Example:
Source string:
"consecuti?vely"
Replace rules:
.Replace("cuti?",#"cuti?(-\s+)?")
.Replace("con",#"con(-\s+)?")
.Replace("consecu",#"consecu(-\s+)?")
Desired output:
"con(-\s+)?secu(-\s+)?ti?(-\s+)?vely"
The replace rules are built dynamically, this is just an example which causes problems.
Whats the best solution to perform such a multiple replace, which will produce the desired output?
So far I thought about using Regex.Replace and zipping the word to replace with optional (-\s+)? between each character, but that would not work, because the word to replace already contains special-meaning characters in regex context.
EDIT: My current code, doesnt work when replace rules overlap like in example above
private string ModifyRegexToAcceptHyphensOfCurrentPage(string regex, int searchedPage)
{
var originalTextOfThePage = mPagesNotModified[searchedPage];
var hyphenatedParts = Regex.Matches(originalTextOfThePage, #"\w+\-\s");
for (int i = 0; i < hyphenatedParts.Count; i++)
{
var partBeforeHyphen = String.Concat(hyphenatedParts[i].Value.TakeWhile(c => c != '-'));
regex = regex.Replace(partBeforeHyphen, partBeforeHyphen + #"(-\s+)?");
}
return regex;
}
the output of this program is "con(-\s+)?secu(-\s+)?ti?(-\s+)?vely";
and as I understand your problem, my code can completely solve your problem.
class Program
{
class somefields
{
public string first;
public string secound;
public string Add;
public int index;
public somefields(string F, string S)
{
first = F;
secound = S;
}
}
static void Main(string[] args)
{
//declaring output
string input = "consecuti?vely";
List<somefields> rules=new List<somefields>();
//declaring rules
rules.Add(new somefields("cuti?",#"cuti?(-\s+)?"));
rules.Add(new somefields("con",#"con(-\s+)?"));
rules.Add(new somefields("consecu",#"consecu(-\s+)?"));
// finding the string which must be added to output string and index of that
foreach (var rul in rules)
{
var index=input.IndexOf(rul.first);
if (index != -1)
{
var add = rul.secound.Remove(0,rul.first.Count());
rul.Add = add;
rul.index = index+rul.first.Count();
}
}
// sort rules by index
for (int i = 0; i < rules.Count(); i++)
{
for (int j = i + 1; j < rules.Count(); j++)
{
if (rules[i].index > rules[j].index)
{
somefields temp;
temp = rules[i];
rules[i] = rules[j];
rules[j] = temp;
}
}
}
string output = input.ToString();
int k=0;
foreach(var rul in rules)
{
if (rul.index != -1)
{
output = output.Insert(k + rul.index, rul.Add);
k += rul.Add.Length;
}
}
System.Console.WriteLine(output);
System.Console.ReadLine();
}
}
You should probably write your own parser, it's probably easier to maintain :).
Maybe you could add "special characters" around pattern in order to protect them like "##" if the strings not contains it.
Try this one:
var final = Regex.Replace(originalTextOfThePage, #"(\w+)(?:\-[\s\r\n]*)?", "$1");
I had to give up an easy solution and did the editing of the regex myself. As a side effect, the new approach goes only twice trough the string.
private string ModifyRegexToAcceptHyphensOfCurrentPage(string regex, int searchedPage)
{
var indexesToInsertPossibleHyphenation = GetPossibleHyphenPositions(regex, searchedPage);
var hyphenationToken = #"(-\s+)?";
return InsertStringTokenInAllPositions(regex, indexesToInsertPossibleHyphenation, hyphenationToken);
}
private static string InsertStringTokenInAllPositions(string sourceString, List<int> insertionIndexes, string insertionToken)
{
if (insertionIndexes == null || string.IsNullOrEmpty(insertionToken)) return sourceString;
var sb = new StringBuilder(sourceString.Length + insertionIndexes.Count * insertionToken.Length);
var linkedInsertionPositions = new LinkedList<int>(insertionIndexes.Distinct().OrderBy(x => x));
for (int i = 0; i < sourceString.Length; i++)
{
if (!linkedInsertionPositions.Any())
{
sb.Append(sourceString.Substring(i));
break;
}
if (i == linkedInsertionPositions.First.Value)
{
sb.Append(insertionToken);
}
if (i >= linkedInsertionPositions.First.Value)
{
linkedInsertionPositions.RemoveFirst();
}
sb.Append(sourceString[i]);
}
return sb.ToString();
}
private List<int> GetPossibleHyphenPositions(string regex, int searchedPage)
{
var originalTextOfThePage = mPagesNotModified[searchedPage];
var hyphenatedParts = Regex.Matches(originalTextOfThePage, #"\w+\-\s");
var indexesToInsertPossibleHyphenation = new List<int>();
//....
// Aho-Corasick to find all occurences of all
//strings in "hyphenatedParts" in the "regex" string
// ....
return indexesToInsertPossibleHyphenation;
}
Is there any function in c# to shink a file path ?
Input: "c:\users\Windows\Downloaded Program Files\Folder\Inside\example\file.txt"
Output: "c:\users\...\example\file.txt"
Nasreddine answer was nearly correct.
Just specify StringBuilder size, in your case:
[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathCompactPathEx(
[Out] StringBuilder pszOut,
string szPath,
int cchMax,
int dwFlags);
static string PathShortener(string path, int length)
{
StringBuilder sb = new StringBuilder(length + 1);
PathCompactPathEx(sb, path, length, 0);
return sb.ToString();
}
Jeff Atwood posted a solution to this on his blog and here it is :
[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathCompactPathEx([Out] StringBuilder pszOut, string szPath, int cchMax, int dwFlags);
static string PathShortener(string path, int length)
{
StringBuilder sb = new StringBuilder();
PathCompactPathEx(sb, path, length, 0);
return sb.ToString();
}
It uses the unmanaged function PathCompactPathEx to achieve what you want.
That looks less human readable to me. Anyway, I don't think there is such a function. split it on the \ character and just keep the first two slots and the last two slots and you have it.
Something like this, although that code is not very elegant
string[] splits = path.Split('\\');
Console.WriteLine( splits[0] + "\\" + splits[1] + "\\...\\" + splits[splits.Length - 2] + "\\" + splits[splits.Length - 1]);
If you want, do insert ellipsis dependent on the length of the path string, then use this code:
TextRenderer.MeasureText(path, Font,
new System.Drawing.Size(Width, 0),
TextFormatFlags.PathEllipsis | TextFormatFlags.ModifyString);
It will modify path in-place.
EDIT: Be careful with this method. It breaks the rule, saying that strings in .NET are immutable. In fact, the first parameter of the MeasureText method is not a ref parameter, which means that no new string can be returned. Instead, the existing string is altered. It would be careful to work on a copy created with
string temp = String.Copy(path);
You could use something like:
public string ShrinkPath(string path, int maxLength)
{
List<string> parts = new List<string>(path.Split('\\'));
string start = parts[0] + #"\" + parts[1];
parts.RemoveAt(1);
parts.RemoveAt(0);
string end = parts[parts.Count-1];
parts.RemoveAt(parts.Count-1);
parts.Insert(0, "...");
while(parts.Count > 1 &&
start.Length + end.Length + parts.Sum(p=>p.Length) + parts.Count > maxLength)
parts.RemoveAt(parts.Count-1);
string mid = "";
parts.ForEach(p => mid += p + #"\");
return start+mid+end;
}
Or just use Olivers solution, which is much easier ;-).
private string ShrinkPath(string path, int maxLength)
{
var parts = path.Split('\\');
var output = String.Join("\\", parts, 0, parts.Length);
var endIndex = (parts.Length - 1);
var startIndex = endIndex / 2;
var index = startIndex;
var step = 0;
while (output.Length >= maxLength && index != 0 && index != endIndex)
{
parts[index] = "...";
output = String.Join("\\", parts, 0, parts.Length);
if (step >= 0) step++;
step = (step * -1);
index = startIndex + step;
}
return output;
}
I was just faced with this issue as long paths were becoming a complete eye sore. Here is what I tossed together real quick (mind the sloppiness) but it gets the job done.
private string ShortenPath(string path, int maxLength)
{
int pathLength = path.Length;
string[] parts;
parts = label1.Text.Split('\\');
int startIndex = (parts.Length - 1) / 2;
int index = startIndex;
string output = "";
output = String.Join("\\", parts, 0, parts.Length);
decimal step = 0;
int lean = 1;
do
{
parts[index] = "...";
output = String.Join("\\", parts, 0, parts.Length);
step = step + 0.5M;
lean = lean * -1;
index = startIndex + ((int)step * lean);
}
while (output.Length >= maxLength && index != -1);
return output;
}
EDIT
Below is an update with Merlin2001's corrections.
private string ShortenPath(string path, int maxLength)
{
int pathLength = path.Length;
string[] parts;
parts = path.Split('\\');
int startIndex = (parts.Length - 1) / 2;
int index = startIndex;
String output = "";
output = String.Join("\\", parts, 0, parts.Length);
decimal step = 0;
int lean = 1;
while (output.Length >= maxLength && index != 0 && index != -1)
{
parts[index] = "...";
output = String.Join("\\", parts, 0, parts.Length);
step = step + 0.5M;
lean = lean * -1;
index = startIndex + ((int)step * lean);
}
// result can be longer than maxLength
return output.Substring(0, Math.Min(maxLength, output.Length));
}
If you want to write you own solution to this problem, use build in classes like: FileInfo, Directory, etc... which makes it less error prone.
The following code produces "VS style" shortened path like: "C:\...\Folder\File.ext".
public static class PathFormatter
{
public static string ShrinkPath(string absolutePath, int limit, string spacer = "…")
{
if (string.IsNullOrWhiteSpace(absolutePath))
{
return string.Empty;
}
if (absolutePath.Length <= limit)
{
return absolutePath;
}
var parts = new List<string>();
var fi = new FileInfo(absolutePath);
string drive = Path.GetPathRoot(fi.FullName);
parts.Add(drive.TrimEnd('\\'));
parts.Add(spacer);
parts.Add(fi.Name);
var ret = string.Join("\\", parts);
var dir = fi.Directory;
while (ret.Length < limit && dir != null)
{
if (ret.Length + dir.Name.Length > limit)
{
break;
}
parts.Insert(2, dir.Name);
dir = dir.Parent;
ret = string.Join("\\", parts);
}
return ret;
}
}
Nearly all answers here shorten the path string by counting characters.
But this approach ignores the width of each character.
These are 30 'W' characters:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
These are 30 'i' characters:
iiiiiiiiiiiiiiiiiiiiiiiiiiiiii
As you see, counting characters is not really useful.
And there is no need to write your own code because the Windows API has this functionaly since Windows 95.
The name of this functionality is "Path Ellipsis".
The Windows API DrawTextW() has a flag DT_PATH_ELLIPSIS which does exactly this.
In the .NET framwork this is available (without the need to use PInvoke) in the TextRenderer class.
There are 2 ways how this can be used:
1.) Drawing the path directly into a Label:
public class PathLabel : Label
{
protected override void OnPaint(PaintEventArgs e)
{
if (AutoSize)
throw new Exception("You must set "+Name+".AutoSize = false in VS "
+ "Designer and assign a fix width to the PathLabel.");
Color c_Fore = Enabled ? ForeColor : SystemColors.GrayText;
TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, c_Fore,
BackColor, TextFormatFlags.PathEllipsis);
}
}
This label requires you to turn AutoEllipsis off in Visual Studio Designer and assign a fix width to the Label (the maximum width that your path should occupy).
You even see the truncated path in Visual Studio Designer.
I entered a long path which does not fit into the label:
C:\WINDOWS\Installer{40BF1E83-20EB-11D8-97C5-0009C5020658}\ARPPRODUCTICON.exe
Even in Visual Studio Designer it is displayed like this:
2.) Shorten the path without drawing it on the screen:
public static String ShortenPath(String s_Path, Font i_Font, int s32_Width)
{
TextRenderer.MeasureText(s_Path, i_Font, new Size(s32_Width, 100),
TextFormatFlags.PathEllipsis | TextFormatFlags.ModifyString);
// Windows inserts a '\0' character into the string instead of shortening the string
int s32_Nul = s_Path.IndexOf((Char)0);
if (s32_Nul > 0)
s_Path = s_Path.Substring(0, s32_Nul);
return s_Path;
}
The flag TextFormatFlags.ModifyString inserts a '\0' character into the string. It is very unusual that a string is modified in C#.
Normally strings are unmutable.
This is because the underlying API DrawTextW() works this way.
But as the string is only shortened and never will become longer there is no risk of a buffer overflow.
The following code
String s_Text = #"C:\WINDOWS\Installer{40BF1E83-20EB-11D8-97C5-0009C5020658}\ARPPRODUCTICON.exe";
s_Text = ShortenPath(s_Text, new Font("Arial", 12), 500);
will result in "C:\WINDOWS\Installer{40BF1E83-20EB-1...\ARPPRODUCTICON.exe"
This question already has answers here:
How to get relative path from absolute path
(24 answers)
Closed 7 years ago.
I'm writing a console utility to do some processing on files specified on the commandline, but I've run into a problem I can't solve through Google/Stack Overflow. If a full path, including drive letter, is specified, how do I reformat that path to be relative to the current working directory?
There must be something similar to the VirtualPathUtility.MakeRelative function, but if there is, it eludes me.
If you don't mind the slashes being switched, you could [ab]use Uri:
Uri file = new Uri(#"c:\foo\bar\blop\blap.txt");
// Must end in a slash to indicate folder
Uri folder = new Uri(#"c:\foo\bar\");
string relativePath =
Uri.UnescapeDataString(
folder.MakeRelativeUri(file)
.ToString()
.Replace('/', Path.DirectorySeparatorChar)
);
As a function/method:
string GetRelativePath(string filespec, string folder)
{
Uri pathUri = new Uri(filespec);
// Folders must end in a slash
if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
folder += Path.DirectorySeparatorChar;
}
Uri folderUri = new Uri(folder);
return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar));
}
You can use Environment.CurrentDirectory to get the current directory, and FileSystemInfo.FullPath to get the full path to any location. So, fully qualify both the current directory and the file in question, and then check whether the full file name starts with the directory name - if it does, just take the appropriate substring based on the directory name's length.
Here's some sample code:
using System;
using System.IO;
class Program
{
public static void Main(string[] args)
{
string currentDir = Environment.CurrentDirectory;
DirectoryInfo directory = new DirectoryInfo(currentDir);
FileInfo file = new FileInfo(args[0]);
string fullDirectory = directory.FullName;
string fullFile = file.FullName;
if (!fullFile.StartsWith(fullDirectory))
{
Console.WriteLine("Unable to make relative path");
}
else
{
// The +1 is to avoid the directory separator
Console.WriteLine("Relative path: {0}",
fullFile.Substring(fullDirectory.Length+1));
}
}
}
I'm not saying it's the most robust thing in the world (symlinks could probably confuse it) but it's probably okay if this is just a tool you'll be using occasionally.
public string MakeRelativePath(string workingDirectory, string fullPath)
{
string result = string.Empty;
int offset;
// this is the easy case. The file is inside of the working directory.
if( fullPath.StartsWith(workingDirectory) )
{
return fullPath.Substring(workingDirectory.Length + 1);
}
// the hard case has to back out of the working directory
string[] baseDirs = workingDirectory.Split(new char[] { ':', '\\', '/' });
string[] fileDirs = fullPath.Split(new char[] { ':', '\\', '/' });
// if we failed to split (empty strings?) or the drive letter does not match
if( baseDirs.Length <= 0 || fileDirs.Length <= 0 || baseDirs[0] != fileDirs[0] )
{
// can't create a relative path between separate harddrives/partitions.
return fullPath;
}
// skip all leading directories that match
for (offset = 1; offset < baseDirs.Length; offset++)
{
if (baseDirs[offset] != fileDirs[offset])
break;
}
// back out of the working directory
for (int i = 0; i < (baseDirs.Length - offset); i++)
{
result += "..\\";
}
// step into the file path
for (int i = offset; i < fileDirs.Length-1; i++)
{
result += fileDirs[i] + "\\";
}
// append the file
result += fileDirs[fileDirs.Length - 1];
return result;
}
This code is probably not bullet-proof but this is what I came up with. It's a little more robust. It takes two paths and returns path B as relative to path A.
example:
MakeRelativePath("c:\\dev\\foo\\bar", "c:\\dev\\junk\\readme.txt")
//returns: "..\\..\\junk\\readme.txt"
MakeRelativePath("c:\\dev\\foo\\bar", "c:\\dev\\foo\\bar\\docs\\readme.txt")
//returns: "docs\\readme.txt"
Thanks to the other answers here and after some experimentation I've created some very useful extension methods:
public static string GetRelativePathFrom(this FileSystemInfo to, FileSystemInfo from)
{
return from.GetRelativePathTo(to);
}
public static string GetRelativePathTo(this FileSystemInfo from, FileSystemInfo to)
{
Func<FileSystemInfo, string> getPath = fsi =>
{
var d = fsi as DirectoryInfo;
return d == null ? fsi.FullName : d.FullName.TrimEnd('\\') + "\\";
};
var fromPath = getPath(from);
var toPath = getPath(to);
var fromUri = new Uri(fromPath);
var toUri = new Uri(toPath);
var relativeUri = fromUri.MakeRelativeUri(toUri);
var relativePath = Uri.UnescapeDataString(relativeUri.ToString());
return relativePath.Replace('/', Path.DirectorySeparatorChar);
}
Important points:
Use FileInfo and DirectoryInfo as method parameters so there is no ambiguity as to what is being worked with. Uri.MakeRelativeUri expects directories to end with a trailing slash.
DirectoryInfo.FullName doesn't normalize the trailing slash. It outputs whatever path was used in the constructor. This extension method takes care of that for you.
There is also a way to do this with some restrictions. This is the code from the article:
public string RelativePath(string absPath, string relTo)
{
string[] absDirs = absPath.Split('\\');
string[] relDirs = relTo.Split('\\');
// Get the shortest of the two paths
int len = absDirs.Length < relDirs.Length ? absDirs.Length : relDirs.Length;
// Use to determine where in the loop we exited
int lastCommonRoot = -1; int index;
// Find common root
for (index = 0; index < len; index++)
{
if (absDirs[index] == relDirs[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 < absDirs.Length; index++)
{
if (absDirs[index].Length > 0) relativePath.Append("..\\");
}
// Add on the folders
for (index = lastCommonRoot + 1; index < relDirs.Length - 1; index++)
{
relativePath.Append(relDirs[index] + "\\");
}
relativePath.Append(relDirs[relDirs.Length - 1]);
return relativePath.ToString();
}
When executing this piece of code:
string path1 = #"C:\Inetpub\wwwroot\Project1\Master\Dev\SubDir1";
string path2 = #"C:\Inetpub\wwwroot\Project1\Master\Dev\SubDir2\SubDirIWant";
System.Console.WriteLine (RelativePath(path1, path2));
System.Console.WriteLine (RelativePath(path2, path1));
it prints out:
..\SubDir2\SubDirIWant
..\..\SubDir1