Linq to get distinct subdirectories from list of paths - c#

I am trying to make a File Directory browser in C# for a project.
I start with the current path (for this example it will be '/').
From the list of paths that I have
Example: /a/b, /a/bb, /a/bbb, /b/a, /b/aa, /b/aaa, /c/d, /d/e
I would like to return a list of distinct subdirectories
Expected return: /a/, /b/, /c/, /d/
How would one go about using LINQ to accomplish this?

I think this just about covers it. Sample console app:
public static void Main()
{
string[] paths = new[] { "/a/b", "/a/bb", "/a/bbb", "/b/a", "/b/aa", "/b/aaa", "/c/d", "/d/e" };
string root = "/";
Console.WriteLine(string.Join(", ", paths.Select(s => GetSubdirectory(root, s)).Where(s => s != null).Distinct()));
}
static string GetSubdirectory(string root, string path)
{
string subDirectory = null;
int index = path.IndexOf(root);
Console.WriteLine(index);
if (root != path && index == 0)
{
subDirectory = path.Substring(root.Length, path.Length - root.Length).Trim('/').Split('/')[0];
}
return subDirectory;
}
See fiddle: http://dotnetfiddle.net/SXAqxY
Sample input: "/"
Sample output: a, b, c, d
Sample input: "/a"
Sample output: b, bb, bbb

I may missing the point, but wouldn't something like this be what you are looking for?
var startingPath = #"c:\";
var directoryInfo = new DirectoryInfo(startingPath);
var result = directoryInfo.GetDirectories().Select(x => x.FullName).ToArray();
The result would be an array of paths to the various immediate sub directories (example):
"c:\Boot"
"c:\Temp"
etc.

void Main()
{
string [] paths = { #"/a/b", #"/a/bb", #"/a/bbb", #"/b/a", #"/b/aa", #"/b/aaa", #"/c/d", #"/d/e" };
var result = paths.Select(x => x.Split('/')[1]).Distinct();
result.Dump();
}
If you don't know if you have a leading / then use this:
var result = paths.Select(x =>x.Split(new string [] {"/"},
StringSplitOptions.RemoveEmptyEntries)[0])
.Distinct();

You can use Path.GetPathRoot
var rootList = new List <string>();
foreach (var fullPath in myPaths)
{
rootList.Add(Path.GetPathRoot(fullPath))
}
return rootList.Distinct();
Or:
myPaths.Select(x => Path.GetPathRoot(x)).Distinct();
Or use Directory.GetDirectoryRoot:
myPaths.Select(x => Directory.GetDirectoryRoot(x)).Distinct();
Edit
If you want the N+1 path you could do:
string dir = #"C:\Level1\Level2;
string root = Path.GetPathRoot(dir);
string pathWithoutRoot = dir.Substring(root.Length);
string firstDir = pathWithoutRoot.Split(Path.DirectorySeparatorChar).First();

Assuming you have a list of paths named paths, you can do something like this:
string currentDirectory = "/";
var distinctDirectories = paths.Where(p => p.StartsWith(currentDirectory)
.Select(p => GetFirstSubDir(p, currentDirectory)).Distinct();
...
string GetFirstSubDir(string path, string currentDirectory)
{
int index = path.IndexOf('/', currentDirectory.Length);
if (index >= 0)
return path.SubString(currentDirectory.Length - 1, index + 1 - currentDirectory.Length);
return path.SubString(currentDirectory.Length - 1);
}

Related

Is there a better way (fastest) to get the longest common folder path?

I have a folder list,
want to get the longest common substring as output.
Here's my code, seems not very good,
How to improve these?
These files all from My RecursiveSearch function, It may be D:\123, E:\456, F:\a\e\eff, I save to xml.
Later read back from xml, want to get previous decision.
Files could be ten-thousands.
I do care about the speed.
var li = new List<string>()
{
#"C:\Users\jared\Desktop\fld1\eumaps\4.jfif",
#"C:\Users\jared\Desktop\fld2\eumaps - (2)\4.jfif",
#"C:\Users\jared\Desktop\fld4\ade\4.jfif",
#"C:\Users\jared\Desktop\fld4\abc\S.png",
#"C:\Users\jared\Desktop\fld1\file\Snipaste_2021-07-07_03-03-45.png",
};
//var shortest1 = li.OrderBy(name => name.Length).FirstOrDefault();
string shortest = li.Aggregate((a1, a2) => a1.Length < a2.Length ? a1 : a2);
string longest = li.Aggregate((a1, a2) => a1.Length > a2.Length ? a1 : a2);
string common = string.Concat(shortest.TakeWhile((c, i) => c == longest[i]));
// C:\Users\jared\Desktop\fld NOT MY WANT CAUSE THERE IS NO SUCH FOLDER.
if (!Directory.Exists(common))
{
common = common.Replace(common.Split("\\").Last(), "");
}
Your approach would fail as you are just comparing the shortest and the longest, if there is any other path with different length and have different path in middle.
As per my understanding you want the output to be till the matched folder (not the substring of the folder or filename) i.e. C:\Users\jared\Desktop\
Here is my solution which runs in O(Nk) solution, Where N is number of paths and k is the shortest available length.
Working Sample Code here.
This can also be done using the Trie Data structure in more optimized way (O(N + k)) but it would take extra space to build the Trie
var folders = new List<string>()
{
#"C:\Users\jared\Desktop\fld1\eumaps\4.jfif",
#"C:\Users\jared\Desktop\fld2\eumaps - (2)\4.jfif",
#"C:\Users\jared\Desktop\fld4\ade\4.jfif",
#"C:\Users\jared\Desktop\fld4\abc\S.png",
#"C:\Users\jared\Desktop\fld1\file\Snipaste_2021-07-07_03-03-45.png",
};
var minPathLength = folders.Min(x => x.Length);
var maxCommonPath = new StringBuilder();
var currentCommonPath = new StringBuilder();
for (int i = 0; i < minPathLength; i++)
{
var boolAllSame = true;
var c = folders[0][i];
boolAllSame = folders.All(x => x[i] == c);
if (boolAllSame)
{
currentCommonPath.Append(c);
if (c == '\\')
{
maxCommonPath.Append(currentCommonPath.ToString());
currentCommonPath = new StringBuilder();
}
}
else
break;
}
var result = maxCommonPath.ToString();
Console.WriteLine(result);
Here is a succinct, but not particularly performant implementation that uses Linq. It also has a dependency on the MoreLinq package, for the Transpose operator:
var paths = new List<string>()
{
#"C:\Users\jared\Desktop\fld1\eumaps\4.jfif",
#"C:\Users\jared\Desktop\fld2\eumaps - (2)\4.jfif",
#"C:\Users\jared\Desktop\fld4\ade\4.jfif",
#"C:\Users\jared\Desktop\fld4\abc\S.png",
#"C:\Users\jared\Desktop\fld1\file\Snipaste_2021-07-07_03-03-45.png",
};
string[] longestCommonPathComponents = paths
.Select(path => path.Split(Path.DirectorySeparatorChar))
.Transpose()
.Select(parts => parts.Distinct(StringComparer.OrdinalIgnoreCase))
.TakeWhile(distinct => distinct.Count() == 1)
.Select(distinct => distinct.First())
.ToArray();
string longestCommonPath = Path.Combine(longestCommonPathComponents);
Console.WriteLine($"Longest common path: {longestCommonPath}");
Output:
Longest common path: C:/Users/jared/Desktop
Try it on fiddle.
The signature of the Transpose operator:
// Transposes a sequence of rows into a sequence of columns.
public static IEnumerable<IEnumerable<T>> Transpose<T>(
this IEnumerable<IEnumerable<T>> source);
var li = new List<string>()
{
#"C:\Users\jared\Desktop\fld1\eumaps\4.jfif",
#"C:\Users\jared\Desktop\fld2\eumaps - (2)\4.jfif",
#"C:\Users\jared\Desktop\fld4\ade\4.jfif",
#"C:\Users\jared\Desktop\fld4\abc\S.png",
#"C:\Users\jared\Desktop\fld1\file\Snipaste_2021-07-07_03-03-45.png",
};
string first = li.First();
int n = 0;
while (li.All(x => x.Length > n && x[n] == first[n]))
n++;
string longestCommon = first.Substring(0, n); // C:\Users\jared\Desktop\fld
You say C:\Users\jared\Desktop\fld is not what you want, but what do you want? The most nested valid folder? Just pull to the last slash, or use Path methods to get the bit you want.
suggestion:
use this code for your image paths
var li = new List<string>()
{
Application.StartupPath + "/Images/1.png",
Application.StartupPath + "/Images/2.png",
Application.StartupPath + "/Images/3.png",
}

C#: How to split a string with a changing prefix

Hello I looked at several post about this topics but no answer could help me.
I extract data about various machines which look like this:
"time, M1.A, M1.B, M1.C, M2.A, M2.B, M2.C, M3.A, M3.B, M3.C"
M1 is the prefix which specifies which machine. A,B,C are attributes of this machine like temperature, pressure, etc.
The output should then look like this:
{{"time", "M1.A", "M1.B", "M1.C"}, {"time", "M2.A",....}}
I know that I could possibly split at "," and then create the list but I was wondering if there is another way to detect if the prefix changed.
Regex.Matches(myList, #"M(?<digit>\d+)\..") //find all M1.A etc
.Cast<Match>() //convert the resulting list to an enumerable of Match
.GroupBy(m => m.Groups["digit"].Value) //find the groups with the same digits
.Select(g => new[] { "time" }.Union(g.Select(m => m.Value)).ToArray());
//combine the groups into arrays beginning with "time"
You mention "the output should then look like this...", but then you mention a list, so I'm going to assume that you mean to make the original string into a list of lists of strings.
List<string> split = new List<string>(s.Split(','));
string first = split[0];
split.RemoveAt(0);
List<List<string>> result = new List<List<string>>();
foreach (var dist in split.Select(o => o.Split('.')[0]).Distinct())
{
List<string> temp = new List<string> {first};
temp.AddRange(split.Where(o => o.StartsWith(dist)));
result.Add(temp);
}
This does the original split, removes the first value (you didn't really specify that, I assumed), then loops around each machine. The machines are created by splitting each value further by '.' and making a distinct list. It then selects all values in the list that start with the machine and adds them with the first value to the resulting list.
Using Regex I created a dictionary :
string input = "time, M1.A, M1.B, M1.C, M2.A, M2.B, M2.C, M3.A, M3.B, M3.C";
string pattern1 = #"^(?'name'[^,]*),(?'machines'.*)";
Match match1 = Regex.Match(input, pattern1);
string name = match1.Groups["name"].Value;
string machines = match1.Groups["machines"].Value.Trim();
string pattern2 = #"\s*(?'machine'[^.]*).(?'attribute'\w+)(,|$)";
MatchCollection matches = Regex.Matches(machines, pattern2);
Dictionary<string, List<string>> dict = matches.Cast<Match>()
.GroupBy(x => x.Groups["machine"].Value, y => y.Groups["attribute"].Value)
.ToDictionary(x => x.Key, y => y.ToList());
Some quick example for you. I think is better to parse it by you own way and have string structure of your Machine-Attribute pair.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp4 {
class Program {
static void Main(string[] args) {
string inputString = "time, M1.A, M1.B, M1.C, M2.A, M2.B, M2.C, M3.A, M3.B, M3.C";
string[] attrList = inputString.Split(',');
// 1. Get all machines with attributes
List<MachineAttribute> MachineAttributeList = new List<MachineAttribute>();
for (int i = 1; i < attrList.Length; i++) {
MachineAttributeList.Add(new MachineAttribute(attrList[i]));
}
// 2. For each machine create
foreach (var machine in MachineAttributeList.Select(x=>x.Machine).Distinct()) {
Console.Write(attrList[0]);
foreach (var attribute in MachineAttributeList.Where(x=>x.Machine == machine)) {
Console.Write(attribute + ",");
}
Console.WriteLine();
}
Console.ReadLine();
}
}
public class MachineAttribute {
public string Machine { get; }
public string Attribute { get; }
public MachineAttribute(string inputData) {
var array = inputData.Split('.');
if (array.Length > 0) Machine = array[0];
if (array.Length > 1) Attribute = array[1];
}
public override string ToString() {
return Machine + "." + Attribute;
}
}
}

Make group from list string

I have one List as below:
var paths = new List<string> {
#"rootuploaded\samplefolder\1232_234234_1.jpg",
#"rootuploaded\samplefolder\1232_2342.jpg",
#"rootuploaded\samplefolder\subfolder\1232_234234_1.jpg",
#"rootuploaded\samplefolder\subfolder\1232_2342.jpg",
#"rootuploaded\file-­5.txt",
#"rootuploaded\file­-67.txt",
#"rootuploaded\file­-a.txt",
#"rootuploaded\file1.txt",
#"rootuploaded\file5.txt",
#"rootuploaded\filea.txt",
#"rootuploaded\text.txt",
#"rootuploaded\file_sample_a.txt",
#"rootuploaded\file2.txt",
#"rootuploaded\file_sample.txt",
#"rootuploaded\samplefolder\1232_234234_2.bmp",
};
How to print output like this:
○ Group 1
rootuploaded\samplefolder\1232_234234_1.jpg,
rootuploaded\samplefolder\1232_234234_2.bmp
○ Group 2
rootuploaded\file1.txt
rootuploaded\file2.txt
rootuploaded\file5.txt
○ Group 3
rootuploaded\file-5.txt
rootuploaded\file-67.txt
○ Group 4
rootuploaded\file_sample.txt
rootuploaded\file_sample_a.txt
○ Cannot grouped
rootuploaded\samplefolder\1232_2342.jpg
rootuploaded\file-a.txt
rootuploaded\filea.txt
rootuploaded\text.txt
Grouping files based on 6 naming conventions (with top¬down priority):
FileName.ext, FileName_anything.ext, FileName_anythingelse.ext, ...
FileName.ext, FileName-anything.ext, FileName-anythingelse.ext, ...
FileName_1.ext, FileName_2.ext, ..., FileName_N.ext (maybe not continuous)
FileName-1.ext, FileName-2.ext, ..., FileName-N.ext (maybe not continuous)
FileName 1.ext, FileName 2.ext, ..., FileName N.ext (maybe not continuous)
FileName1.ext, FileName2.ext, ..., FileNameN.ext (maybe not continuous)
I used Linq to separate:
var groups1 = paths.GroupBy(GetFileName, (key, g) => new
{
key = key,
count = g.Count(),
path = g.ToList()
}).Where(x => x.count < 5 && x.count >= 2).ToList();
public string GetFileName(string fileName)
{
var index = 0;
if (fileName.Contains("_"))
index = fileName.IndexOf("_", StringComparison.Ordinal);
else if (fileName.Contains("-"))
index = fileName.IndexOf("-", StringComparison.Ordinal);
var result = fileName.Substring(0, index);
return result;
}
Try doing this:
var groups = new []
{
new { regex = #"rootuploaded\\samplefolder\\1232_234234_\d\..{3}", grp = 1 },
new { regex = #"rootuploaded\\file\d\.txt", grp = 2 },
new { regex = #"rootuploaded\\file-\d+\.txt", grp = 3 },
new { regex = #"rootuploaded\\file_sample.*\.txt", grp = 4 },
};
var results =
from path in paths
group path by
groups
.Where(x => Regex.IsMatch(path, x.regex))
.Select(x => x.grp)
.DefaultIfEmpty(99)
.First()
into gpaths
orderby gpaths.Key
select new
{
Group = gpaths.Key,
Files = gpaths.ToArray(),
};
That gives you this:
You would just have to jig around with the regex until you get exactly what you want.
Sadly, 1. and 2. group turn this solution difficult. Cause both contain 'FileName.ext', so it has to check whole list together :(
I try to separate groupping 1. 2. and 3 - 6:
First step:
Find and remove Group 1 and 2 candidates.
It orders the list base on file path:
var orderedFilenames = pathsDistinct().OrderBy(p => p).ToList();
Than find Group 1 and 2 candidates:
var groupped = orderedFilenames.GroupBy(s => GetStarterFileName(s, orderedFilenames));
private static string GetStarterFileName(string fileNameMatcher, List<string> orderedFilenames)
{
string fileNameMatcherWOExt = Path.GetFileNameWithoutExtension(fileNameMatcher);
return orderedFilenames.FirstOrDefault(p =>
{
if (p == fileNameMatcher) return true;
string p_directory = Path.GetDirectoryName(p);
string directory = Path.GetDirectoryName(fileNameMatcher);
if (p_directory != directory) return false;
string pure = Path.GetFileNameWithoutExtension(p);
if (!fileNameMatcherWOExt.StartsWith(pure)) return false;
if (fileNameMatcherWOExt.Length <= pure.Length) return false;
char separator = fileNameMatcherWOExt[pure.Length];
if (separator != '_' && separator != '-') return false;
return true;
});
}
Step two:
After first step, you got Group 1 and 2 candidates, but all others are separated into different groups.
Collect remaining path and separete group 1 and 2:
var mergedGroupps = groupped.Where(grp => grp.Count() == 1).SelectMany(grp => grp);
var starterFileNameGroups = groupped.Where(grp => grp.Count() > 1);
Step three
Now you could find 3-6 based on regex validation:
var endWithNumbersGroups = mergedGroupps.GroupBy(s => GetEndWithNumber(s));
private static string GetEndWithNumber(string fileNameMatcher)
{
string fileNameWithoutExtesion = Path.Combine(Path.GetDirectoryName(fileNameMatcher), Path.GetFileNameWithoutExtension(fileNameMatcher));
string filename = null;
filename = CheckWithRegex(#"_(\d+)$", fileNameWithoutExtesion, 1);
if (filename != null) return filename;
filename = CheckWithRegex(#"-(\d+)$", fileNameWithoutExtesion, 1);
if (filename != null) return filename;
filename = CheckWithRegex(#" (\d+)$", fileNameWithoutExtesion, 1);
if (filename != null) return filename;
filename = CheckWithRegex(#"(\d+)$", fileNameWithoutExtesion);
if (filename != null) return filename;
return fileNameWithoutExtesion;
}
private static string CheckWithRegex(string p, string filename, int additionalCharLength = 0)
{
Regex regex = new Regex(p, RegexOptions.Compiled | RegexOptions.CultureInvariant);
Match match = regex.Match(filename);
if (match.Success)
return filename.Substring(0, filename.Length - (match.Groups[0].Length - additionalCharLength));
return null;
}
Final Step:
Collect non groupped items and merge Group 1-2 and 3-6 candidates
var nonGroupped = endWithNumbersGroups.Where(grp => grp.Count() == 1).SelectMany(grp => grp);
endWithNumbersGroups = endWithNumbersGroups.Where(grp => grp.Count() > 1);
var result = starterFileNameGroups.Concat(endWithNumbersGroups);
You could try to solve both step in one shot, but as you see groupping mechanism are different. My solution is not so beautiful, but I think it's clear... maybe :)

Finding filenames containing version numbers in C#

I have file names with version numbers embedded, similar to NuGet's naming scheme. Examples:
A.B.C.1.2.3.4.zip
A.B.C.1.2.3.5.zip
A.B.C.3.4.5.dll
A.B.C.1.2.3.6.zip
A.B.C.1.2.3.dll
X.Y.Z.7.8.9.0.zip
X.Y.Z.7.8.9.1.zip
Given a pattern "A.B.C.1.2.3", how do I find all those files and directories that match, regardless of version number? I support both major.minor.build.revision and major.minor.build schemes.
That is, given "A.B.C.1.2.3", return the following list:
A.B.C.1.2.3.4.zip
A.B.C.1.2.3.5.zip
A.B.C.1.2.3.6.zip
A.B.C.1.2.3.dll
A.B.C.3.4.5.dll
Bonus points for determining which file name has the highest version.
If you know the filenames end with the version, you could Split the filename string on .. Then iterate backwards from the end (skipping the extension) and stop on the first non-numeric string. (TryParse is probably good for this.) Then you can string.Join the remaining parts and you have the package name.
Do this for the search term to find the package name, then each file in the directory, and you can compare just the package names.
Credits to jdwweng for his answer as well as 31eee384 for his thoughts. This answer basically combines both ideas.
First, you can create a custom class like so:
class CustomFile
{
public string FileName { get; private set; }
public Version FileVersion { get; private set; }
public CustomFile(string file)
{
var split = file.Split(".".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
int versionIndex;
int temp;
for (int i = split.Length - 2; i >= 0; i--)
{
if (!Int32.TryParse(split[i], out temp))
{
versionIndex = i+1;
break;
}
}
FileName = string.Join(".", split, 0, versionIndex);
FileVersion = Version.Parse(string.Join(".", split, versionIndex, split.Length - versionIndex - 1));
}
}
Using it to parse the filename, you can then filter based on it.
string[] input = new string[] {
"A.B.C.D.1.2.3.4.zip",
"A.B.C.1.2.3.5.zip",
"A.B.C.3.4.5.dll",
"A.B.C.1.2.3.6.zip",
"A.B.C.1.2.3.dll",
"X.Y.Z.7.8.9.0.zip",
"X.Y.Z.7.8.9.1.zip"
};
var parsed = input.Select(x => new CustomFile(x));
var results = parsed
.Where(cf => cf.FileName == "A.B.C")
.OrderByDescending(cf=>cf.FileVersion)
.ToList();
In this example, the first element would have the highest version.
Try this
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
string[] input = new string[] {
"A.B.C.1.2.3.4.zip",
"A.B.C.1.2.3.5.zip",
"A.B.C.3.4.5.dll",
"A.B.C.1.2.3.6.zip",
"A.B.C.1.2.3.dll",
"X.Y.Z.7.8.9.0.zip",
"X.Y.Z.7.8.9.1.zip"
};
var parsed = input.Select(x => x.Split(new char[] { '.' }))
.Select(y => new
{
name = string.Join(".", new string[] { y[0], y[1], y[2] }),
ext = y[y.Count() - 1],
major = int.Parse(y[3]),
minor = int.Parse(y[4]),
build = int.Parse(y[5]),
revision = y.Count() == 7 ? (int?)null : int.Parse(y[6])
}).ToList();
var results = parsed.Where(x => (x.major >= 1) && (x.major <= 3)).ToList();
var dict = parsed.GroupBy(x => x.name, y => y)
.ToDictionary(x => x.Key, y => y.ToList());
var abc = dict["A.B.C"];
}
}
}
​
​
you can use new Version() to compare versions like this:
List<string> fileNames = new List<string>();
fileNnames.AddRange(new[] {
"A.B.C.1.2.3.4.zip",
"A.B.C.1.2.3.5.zip",
"A.B.C.3.4.5.dll",
"A.B.C.1.2.3.6.zip",
"A.B.C.1.2.3.dll",
"X.Y.Z.7.8.9.0.zip",
"X.Y.Z.7.8.9.1.zip" });
string filter = "a.b.c";
var files = fileNames
//Filter the filenames that start with your filter
.Where(f => f
.StartsWith(filter, StringComparison.InvariantCultureIgnoreCase)
)
//retrieve the version number and create a new version element to order by
.OrderBy(f =>
new Version(
f.Substring(filter.Length + 1, f.Length - filter.Length - 5)
)
);
Try to use regular expression like in example below
var firstPart = Console.ReadLine();
var names = new List<string>
{
"A.B.C.1.2.3.4.zip",
"A.B.C.1.2.3.5.zip",
"A.B.C.1.2.3.6.zip",
"A.B.C.1.2.3.dll",
"X.Y.Z.7.8.9.0.zip",
"X.Y.Z.7.8.9.1.zip"
};
var versionRegexp = new Regex("^" + firstPart + "\\.([\\d]+\\.){1}([\\d]+\\.){1}([\\d]+\\.){1}([\\d]+\\.)?[\\w\\d]+$");
foreach (var name in names)
{
if (versionRegexp.IsMatch(name))
{
Console.WriteLine(name);
foreach (Group group in versionRegexp.Match(name).Groups)
{
Console.WriteLine("Index {0}: {1}", group.Index, group.Value);
}
}
}
Console.ReadKey();
This works using only LINQ, assuming the file name itself doesn't end with a digit:
List<string> names = new List<string> { "A.B.C.1.2.3.4.zip",
"A.B.C.1.2.3.5.zip",
"A.B.C.3.4.5.dll",
"A.B.C.1.2.3.6.zip" ,
"A.B.C.1.2.3.dll",
"X.Y.Z.7.8.9.0.zip",
"X.Y.Z.7.8.9.1.zip" };
var groupedFileNames = names.GroupBy(file => new string(Path.GetFileNameWithoutExtension(file)
.Reverse()
.SkipWhile(c => Char.IsDigit(c) || c == '.')
.Reverse().ToArray()));
foreach (var g in groupedFileNames)
{
Console.WriteLine(g.Key);
foreach (var file in g)
Console.WriteLine(" " + file);
}
First of all I think you can use Version class for comparison.
I believe function below can get you versions starting with certain name.
It matches the starting name then performs a non greedy search until a dot and digit followed by 2 or 3 dot digit pair and any character after.
public static List<Version> GetLibraryVersions(List<string> files, string Name)
{
string regexPattern = String.Format(#"\A{0}(?:.*?)(?:\.)(\d+(?:\.\d+){{2,3}})(?:\.)(?:.*)\Z", Regex.Escape(Name));
Regex regex = new Regex(regexPattern);
return files.Where(f => regex.Match(f).Success).
Select(f => new Version(regex.Match(f).Groups[1].Value)).ToList();
}

Implementing method in a functional style

I have the following method:
private List<string> CreateSegments(string virtualPath)
{
List<string> segments = new List<string>();
int i = virtualPath.IndexOf('/', 1);
while (i >= 0 && i < virtualPath.Length)
{
var segment = virtualPath.Substring(0, i);
if (!string.IsNullOrWhiteSpace(segment))
{
segments.Add(segment);
segments.Add(VirtualPathUtility.Combine(segment, "default"));
}
i = virtualPath.IndexOf('/', i + 1);
}
segments.Add(virtualPath);
segments.Add(VirtualPathUtility.Combine(virtualPath, "default"));
return segments;
}
Basically, it creates path segments which I will use to check if a file exists in any of those segments. Like this:
string[] extensions = GetRegisteredExtensions();
HttpServerUtilityBase server = HttpContext.Current.Server;
List<string> segments = CreateSegments(virtualPath);
// check if a file exists with any of the registered extensions
var match = extensions.SelectMany(x => segments.Select(s => string.Format("{0}.{1}", s, x)))
.FirstOrDefault(p => System.IO.File.Exists(server.MapPath(p)));
All the above code looks like it could use some clean up and optimization, but I'm looking for a way to use LINQ if possible to generate the segments.
Something like: var segments = virtualPath.Split('/').SelectMany(...) and get a result similar to the following:
/path
/path/default
/path/to
/path/to/default
/path/to/file
/path/to/file/default
Where virtualPath would contain the value "/path/to/file"
EDIT: Changed string.Format("{0}/{1}", ...) to VirtualPathUtility.Combine(..., ...)
Any ideas?
One way would be to incrementally select the path segments, then "join" it with an empty string and "/default" to get the two variations:
string path = #"/path/to/file";
string temp = "";
var query = path.Split('/')
.Where(s => !string.IsNullOrEmpty(s))
.Select((p) => {temp += ("/" + p); return temp;} )
.SelectMany(s => new[]{"","/default"}.Select (d => s + d) );
If you first define an extension method like this:
public static IEnumerable<int> SplitIndexes(this string subject, char search)
{
for(var i = 1; i < subject.Length; i++)
{
if(subject[i] == search)
{
yield return i;
}
}
yield return subject.Length;
}
Then you could do this:
var endings = new string[] { string.Empty, "/default" };
var virtualPath = "/path/to/file";
var results =
from i in virtualPath.SplitIndexes('/')
from e in endings
select virtualPath.Substring(0, i) + e;
Or if you prefer query syntax:
var endings = new string[] { string.Empty, "/default" };
var virtualPath = "/path/to/file";
var results = virtualPath.SplitIndexes('/')
.SelectMany(i => endings.Select(e => virtualPath.Substring(0, i) + e));
The result will be:
/path
/path/default
/path/to
/path/to/default
/path/to/file
/path/to/file/default
As others have suggested, you can this in a more platform independent way by using Path.Combine, like this:
var endings = new string[] { string.Empty, "default" }; // Note: no / before default
var results =
from i in virtualPath.SplitIndexes(Path.DirectorySeparatorChar)
from e in endings
select Path.Combine(virtualPath.Substring(0, i), e);
This might do the trick. It is not the most succinct code but it seems very readable to me. It uses string concatenation because for short strings, like paths or URLs, it is faster than any of the alternatives.
Edit: fixed and tested.
var query = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries)
.Aggregate(new List<string>(), (memo, segment) => {
memo.Add(memo.DefaultIfEmpty("").Last() + "/" + segment);
return memo;
}).Aggregate(new List<string>(), (memo, p) => {
memo.Add(p);
memo.Add(p + "/default");
return memo;
});
The higher-order function you're looking for is called scan. There is no such function in normal LINQ, but you can find it in MoreLinq. Using that, your code could look like this:
private List<string> CreateSegments(string virtualPath)
{
return virtualPath.Split('/')
.Scan((s1, s2) => s1 + '/' + s2)
.Skip(1)
.SelectMany(p => new[] { p, p + "/default" })
.ToList();
}
This assumes your path will be always an absolute path starting with a /. For relative paths, you will need to remove the .Skip(1) part.
If you don't want to get MoreLinq just for this one method, you can just copy its source into your project.
The provided answers so far are more succinct, but this is what I came up with:
public static IEnumerable<string> PossiblePaths(string basePath)
{
return PossiblePaths(basePath.Split(new[] { "/" },
StringSplitOptions.RemoveEmptyEntries));
}
private static IEnumerable<string> PossiblePaths(IEnumerable<string> segments,
string current = "/")
{
if (segments.Count() == 0)
{
return new string[0];
}
else
{
string next = current + segments.First();
return new[] { next, next + "/default" }
.Concat(PossiblePaths(segments.Skip(1), next + "/"));
}
}
Something like this:
public static IEnumerable<string> EnumerateSegments( this IEnumerable<string> segments )
{
StringBuilder sb = new StringBuilder() ;
foreach ( string segment in segements )
{
sb.Append( Path.DirectorySeparatorChar ).Append( segment ) ;
yield return sb.ToString() ;
int n = sb.Length ;
sb.Append( Path.DirectorySeparatorChar ).Append("default") ;
yield return sb.ToString() ;
sb.Length = n ;
}
}
ought to do you.

Categories

Resources