Control recursive method depth - get all sub folders - c#

I'm traveling through some shares to get information/permissions .. etc
I'm using recursive to travel through all sub shares. it works fine however, the user should be able to limit the sub shares level to specific number which is a parameter in the application?
private static INodeCollection NodesLookUp(string path)
{
var shareCollectionNode = new ShareCollection(path);
// Do somethings
foreach (var directory in Directory.GetDirectories(shareCollectionNode.FullPath))
{
shareCollectionNode.AddNode(NodesLookUp(directory));
}
return shareCollectionNode;
}
this code will go all way to the lowest level, how can i stop it in specific level? for example get all shares until 2 levels only?
Thanks.

How about passing level variable and increasing it after each level of recursion call? This will allow you to control what are the current recursion level or how many levels left. Don't forget to check for null.
private const int maxDepth = 2;
private static INodeCollection NodesLookUp(string path, int level)
{
if(level >= maxDepth)
return null;
var shareCollectionNode = new ShareCollection(path);
// Do somethings
foreach (var directory in Directory.GetDirectories(shareCollectionNode.FullPath))
{
var nodes = NodesLookUp(directory, level + 1);
if(nodes != null)
shareCollectionNode.AddNode(nodes);
}
return shareCollectionNode;
}
Initial level can be zero-indexed, like
NodesLookUp("some path", 0);

Rather than using a global variable to control the level, pass the maxLevel and decrement with each recursive call.
private static INodeCollection NodesLookUp(string path, int maxLevel)
{
var shareCollectionNode = new ShareCollection(path);
if (maxLevel > 0)
{
foreach (var directory in Directory.GetDirectories(shareCollectionNode.FullPath))
{
shareCollectionNode.AddNode(NodesLookup(directory, maxLevel-1));
}
}
return shareCollectionNode;
}

What about this:
private static INodeCollection NodesLookUp(string path, Int32 currentLevel, Int32 maxLevel)
{
if (currentLevel > maxLevel)
{
return null;
}
var shareCollectionNode = new ShareCollection(path);
// Do somethings
foreach (var directory in Directory.GetDirectories(shareCollectionNode.FullPath))
{
INodeCollection foundCollection = NodesLookUp(directory, currentLevel + 1, maxLevel)
if(foundCollection != null)
{
shareCollectionNode.AddNode();
}
}
return shareCollectionNode;
}
In this case you don't have to worry about the state of your private fields being modified each time the method runs. And, as far as the rest of your code is thread-safe, it will be thread-safe.

Related

Make using statement usable for multiple disposable objects

I have a bunch of text files in a folder, and all of them should have identical headers. In other words the first 100 lines of all files should be identical. So I wrote a function to check this condition:
private static bool CheckHeaders(string folderPath, int headersCount)
{
var enumerators = Directory.EnumerateFiles(folderPath)
.Select(f => File.ReadLines(f).GetEnumerator())
.ToArray();
//using (enumerators)
//{
for (int i = 0; i < headersCount; i++)
{
foreach (var e in enumerators)
{
if (!e.MoveNext()) return false;
}
var values = enumerators.Select(e => e.Current);
if (values.Distinct().Count() > 1) return false;
}
return true;
//}
}
The reason I am using enumerators is memory efficiency. Instead of loading all file contents in memory I enumerate the files concurrently line-by-line until a mismatch is found, or all headers have been examined.
My problem is evident by the commented lines of code. I would like to utilize a using block to safely dispose all the enumerators, but unfortunately using (enumerators) doesn't compile. Apparently using can handle only a single disposable object. I know that I can dispose the enumerators manually, by wrapping the whole thing in a try-finally block, and running the disposing logic in a loop inside finally, but is seems awkward. Is there any mechanism I could employ to make the using statement a viable option in this case?
Update
I just realized that my function has a serious flaw. The construction of the enumerators is not robust. A locked file can cause an exception, while some enumerators have already been created. These enumerators will not be disposed. This is something I want to fix. I am thinking about something like this:
var enumerators = Directory.EnumerateFiles(folderPath)
.ToDisposables(f => File.ReadLines(f).GetEnumerator());
The extension method ToDisposables should ensure that in case of an exception no disposables are left undisposed.
You can create a disposable-wrapper over your enumerators:
class DisposableEnumerable : IDisposable
{
private IEnumerable<IDisposable> items;
public event UnhandledExceptionEventHandler DisposalFailed;
public DisposableEnumerable(IEnumerable<IDisposable> items) => this.items = items;
public void Dispose()
{
foreach (var item in items)
{
try
{
item.Dispose();
}
catch (Exception e)
{
var tmp = DisposalFailed;
tmp?.Invoke(this, new UnhandledExceptionEventArgs(e, false));
}
}
}
}
and use it with the lowest impact to your code:
private static bool CheckHeaders(string folderPath, int headersCount)
{
var enumerators = Directory.EnumerateFiles(folderPath)
.Select(f => File.ReadLines(f).GetEnumerator())
.ToArray();
using (var disposable = new DisposableEnumerable(enumerators))
{
for (int i = 0; i < headersCount; i++)
{
foreach (var e in enumerators)
{
if (!e.MoveNext()) return false;
}
var values = enumerators.Select(e => e.Current);
if (values.Distinct().Count() > 1) return false;
}
return true;
}
}
The thing is you have to dispose those objects separately one by one anyway. But it's up to you where to encapsulate that logic. And the code I've suggested has no manual try-finally,)
To the second part of the question. If I get you right this should be sufficient:
static class DisposableHelper
{
public static IEnumerable<TResult> ToDisposable<TSource, TResult>(this IEnumerable<TSource> source,
Func<TSource, TResult> selector) where TResult : IDisposable
{
var exceptions = new List<Exception>();
var result = new List<TResult>();
foreach (var i in source)
{
try { result.Add(selector(i)); }
catch (Exception e) { exceptions.Add(e); }
}
if (exceptions.Count == 0)
return result;
foreach (var i in result)
{
try { i.Dispose(); }
catch (Exception e) { exceptions.Add(e); }
}
throw new AggregateException(exceptions);
}
}
Usage:
private static bool CheckHeaders(string folderPath, int headersCount)
{
var enumerators = Directory.EnumerateFiles(folderPath)
.ToDisposable(f => File.ReadLines(f).GetEnumerator())
.ToArray();
using (new DisposableEnumerable(enumerators))
{
for (int i = 0; i < headersCount; i++)
{
foreach (var e in enumerators)
{
if (!e.MoveNext()) return false;
}
var values = enumerators.Select(e => e.Current);
if (values.Distinct().Count() > 1) return false;
}
return true;
}
}
and
try
{
CheckHeaders(folderPath, headersCount);
}
catch(AggregateException e)
{
// Prompt to fix errors and try again
}
I'm going to suggest an approach that uses recursive calls to Zip to allow parallel enumeration of a normal IEnumerable<string> without the need to resort to using IEnumerator<string>.
bool Zipper(IEnumerable<IEnumerable<string>> sources, int take)
{
IEnumerable<string> ZipperImpl(IEnumerable<IEnumerable<string>> ss)
=> (!ss.Skip(1).Any())
? ss.First().Take(take)
: ss.First().Take(take).Zip(
ZipperImpl(ss.Skip(1)),
(x, y) => (x == null || y == null || x != y) ? null : x);
var matching_lines = ZipperImpl(sources).TakeWhile(x => x != null).ToArray();
return matching_lines.Length == take;
}
Now build up your enumerables:
IEnumerable<string>[] enumerables =
Directory
.EnumerateFiles(folderPath)
.Select(f => File.ReadLines(f))
.ToArray();
Now it's simple to call:
bool headers_match = Zipper(enumerables, 100);
Here's a trace of running this code against three files with more than 4 lines:
Ben Petering at 5:28 PM ACST
Ben Petering at 5:28 PM ACST
Ben Petering at 5:28 PM ACST
From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin.
From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin.
From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin.
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
These storyishes should be considered when choosing the appropriate module to use.
These storyishes should be considered when choosing the appropriate module to use.X
These storyishes should be considered when choosing the appropriate module to use.
Note that the enumerations stop when they encountered a mismatch header in the 4th line on the second file. All enumerations then stopped.
Creating an IDisposable wrapper as #Alex suggested is correct. It needs just a logic to dispose already opened files if some of them is locked and probably some logic for error states. Maybe something like this (error state logic is very simple):
public class HeaderChecker : IDisposable
{
private readonly string _folderPath;
private readonly int _headersCount;
private string _lockedFile;
private readonly List<IEnumerator<string>> _files = new List<IEnumerator<string>>();
public HeaderChecker(string folderPath, int headersCount)
{
_folderPath = folderPath;
_headersCount = headersCount;
}
public string LockedFile => _lockedFile;
public bool CheckFiles()
{
_lockedFile = null;
if (!TryOpenFiles())
{
return false;
}
if (_files.Count == 0)
{
return true; // Not sure what to return here.
}
for (int i = 0; i < _headersCount; i++)
{
if (!_files[0].MoveNext()) return false;
string currentLine = _files[0].Current;
for (int fileIndex = 1; fileIndex < _files.Count; fileIndex++)
{
if (!_files[fileIndex].MoveNext()) return false;
if (_files[fileIndex].Current != currentLine) return false;
}
}
return true;
}
private bool TryOpenFiles()
{
bool result = true;
foreach (string file in Directory.EnumerateFiles(_folderPath))
{
try
{
_files.Add(File.ReadLines(file).GetEnumerator());
}
catch
{
_lockedFile = file;
result = false;
break;
}
}
if (!result)
{
DisposeCore(); // Close already opened files.
}
return result;
}
private void DisposeCore()
{
foreach (var item in _files)
{
try
{
item.Dispose();
}
catch
{
}
}
_files.Clear();
}
public void Dispose()
{
DisposeCore();
}
}
// Usage
using (var checker = new HeaderChecker(folderPath, headersCount))
{
if (!checker.CheckFiles())
{
if (checker.LockedFile is null)
{
// Error while opening files.
}
else
{
// Headers do not match.
}
}
}
I also removed .Select() and .Distinct() when checking the lines. The first just iterates over the enumerators array - the same as foreach above it, so you are enumerating this array twice. Then creates a new list of lines and .Distinct() enumerates over it.

How to compare sequential elements in a foreach loop in C#

In a foreach loop I want to compare an element with the previous element that was read. How can I do that? What is the syntax for addressing a previous element in a foreach loop?
You don't have that option built in with a foreach loop.
You can either switch to a for loop or use a variable.
Suppose you iterate through a list of objects, these are your options:
object prev = null;
foreach(var current in myListOfObjects)
{
if(current == prev)
{
// do stuff
}
// don't forget the next row!
prev = current;
}
or
for(var i = 1; i < myListOfObjects.count, i++) // Note: starting from 1 to avoid another condition inside the loop.
{
if(myListOfObjects[i] == myListOfObjects[i-1])
{
// do stuff
}
}
Everything is better with Bluetooth extension methods:
public static class EnumerableExtensions
{
public struct CurrentAndPrevious<T>
{
public T Current { get; private set; }
public T Previous { get; private set; }
public CurrentAndPrevious(T current, T previous) : this()
{
Previous = previous;
Current = current;
}
}
public static IEnumerable<CurrentAndPrevious<T>> WithPrevious<T>(this IEnumerable<T> enumerable)
{
var previous = default(T);
using(var enumerator = enumerable.GetEnumerator())
{
while(enumerator.MoveNext())
{
yield return new CurrentAndPrevious<T>(enumerator.Current, previous);
previous = enumerator.Current;
}
}
}
}
var items = new[] { 1, 2, 3, 4, 5 };
foreach(var item in items.WithPrevious())
{
Console.WriteLine(item.Previous + " " + item.Current);
}
You might need to tweak this depending on how you want first and last elements handled.
You can loop over a bit modified source instead of initial, say ListOfMyObjects:
MyObject prior = default(MyObject);
var source = ListOfMyObjects
.Select(item => {
var result = new {
Current = item,
Prior = prior,
};
prior = item; // side effect, not a good practice
return result;
});
So you can loop
foreach(var item in source) {
if (item.Prior == item.Current) {
...
}
}
A foreach itself has no syntax 'for addressing a previous element'. There are two options, depending on the characteristics of the collection and also the notion of a 'previous' element in respect of the first one. The following the examples are a little bit simplistic, but you should be able to choose the right path and fine-tune the details.
Option 1: Use a temporary variable
Works well if there's no cheap (performance-wise) way to index elements in the sequence, and you are OK with 'pretending' there's an empty (null, or default(T)) item before the very first item.
T previous = default(T); // corresponds to null for reference types
foreach (T item in sequence)
{
… work with previous and item here…
// the current 'item' is the new 'previous' for the next iteration
previous = item;
}
Note that if T is a value type, your would be actually copying the values themselves.
Option 2: Use a for loop and indexing
Works well if there is a cheap (performance-wise) way to index individual elements directly. List<T> and arrays are good examples here.
// indexing from 1, i.e. from the second item in the sequence
for (int i = 1; i < sequence.Count; i++)
{
var previous = sequence[i-1]; // this is obviously the previous item
var current = sequence[i]; // this is obviously the current item
}
Similar to using a temp variable, however this solution moves the scope of the temp variable inside the loop
var collection = new List<int>() { 1, 2, 3, 4, 5 };
foreach (var item in collection)
{
var currentIndex = collection.IndexOf(item);
if (currentIndex > 0 && currentIndex < collection.Count)
{
var previousItem = collection[currentIndex - 1];
}
}
As mentioned by Pham X, one easy way to do this would be a temp variable.
ObjectType temp_object = null;
foreach(var entry in ListOfObjects)
{
if(temp_object==null)
{
//this is the first time through...
temp_object=entry;
}
else
{
//it's anything after the first loop
if(entry==temp_object) Console.WriteLine("There is a match between two entries.");
else temp_object=entry;
}
}

Algorithm for detecting cycles finds far to few cycles

I have the following algorithm:
class CycleData : List<IntPoint>{
public IntPoint startPoint;
public Boolean ended=false;
public CycleData(IntPoint startpoint) { startPoint = startpoint; base.Add(startpoint); }
}
class GeoDataGraphPoint
{
private IntPoint point;
private List<GeoDataGraphPoint> connected = new List<GeoDataGraphPoint>();
private int generation=-9999;
public void AddConnection(GeoDataGraphPoint c)
{
connected.Add(c);
c.connected.Add(this);
}
public GeoDataGraphPoint(IntPoint point)
{
this.point = point;
}
public List<CycleData> GetCycles(int gen)
{
if (generation != -9999)
{
var r = new CycleData(point);
return new List<CycleData> { r };
}
generation = gen;
List<CycleData> res = new List<CycleData>();
foreach (GeoDataGraphPoint p in connected)
{
if (p.generation != gen-1)
{
res.AddRange(p.GetCycles(gen + 1));
}
}
foreach (CycleData list in res)
{
if (list.ended == false)
{
list.Add(point);
if (list.startPoint == this.point)
{
list.ended = false;
}
}
}
gen = -9999;
return res;
}
}
Now in principle this should return every cycle in the graph (for polygon detection). However it seems to fail to return anything in some occasions, I suspect that there is some kind of memory problem as removing parts of the graph sometimes causes new cycles to be found.
Here is a part of the input where it fails:
connection:(2282,3) to (2282,-192)
connection:(2282,3) to (2085,3)
connection:(2282,-192) to (2282,3)
connection:(2282,-192) to (2466,-192)
connection:(2466,-192) to (2282,-192)
connection:(2466,-192) to (2466,581)
connection:(2466,581) to (2466,-192)
connection:(2466,581) to (1494,581)
connection:(1494,581) to (2466,581)
connection:(1494,581) to (1494,397)
connection:(1494,397) to (1494,581)
connection:(1494,397) to (2282,397)
connection:(2282,397) to (1494,397)
connection:(2282,397) to (2282,187)
connection:(2282,187) to (2282,397)
connection:(2282,187) to (2085,187)
connection:(2085,187) to (2282,187)
connection:(2085,187) to (2085,3)
connection:(2085,3) to (2085,187)
connection:(2085,3) to (2282,3)
connection:(2085,3) to (2085,187)
connection:(2085,3) to (2282,3)
connection:(2085,187) to (2282,187)
connection:(2085,187) to (2085,3)
connection:(2282,187) to (2282,397)
connection:(2282,187) to (2085,187)
connection:(2282,397) to (1494,397)
The above code is for two triangles arranged to form a square (in coordinates) where both sides touch one another so like this:
Where I can the function as following:
class GeoDataGraph : Dictionary<IntPoint, GeoDataGraphPoint>
{
public void resetGens()
{
foreach(var v in base.Values)
{
v.generation = -9999;
}
}
public static Island GetHolesInIsland(Island input)
{
GeoDataGraph graph = new GeoDataGraph();
for (int i = 0; i < input.area.Count-1; i = i + 2)
{
var p1 = new IntPoint(input.area[i].X, input.area[i].Y);
var p2 = new IntPoint(input.area[i + 1].X, input.area[i + 1].Y);
if (!graph.ContainsKey(p1)) graph.Add(p1, new GeoDataGraphPoint(p1));
if (!graph.ContainsKey(p2)) graph.Add(p2, new GeoDataGraphPoint(p2));
graph[p1].AddConnection(graph[p2]);
}
IntPoint min = new IntPoint(int.MaxValue, int.MaxValue);
List<IntPoint> minCycle = null;
List<List<IntPoint>> cycles = new List<List<IntPoint>>();
while (graph.Count != 0)
{
var first = graph.First();
var NewCycles = first.Value.GetCycles(1);
graph.resetGens();
if (NewCycles.Count == 0)
{
graph.Remove(first.Key);
Console.WriteLine("point" + first.Key + "is uncycled");
}
cycles.AddRange(NewCycles);
foreach (var cycle in NewCycles)
{
foreach (var cycleNode in cycle)
{
graph.Remove(cycleNode);
if (min.X > cycleNode.X || min.Y > cycleNode.Y)
{
minCycle = cycle;
min = cycleNode;
}
}
}
}
cycles.Remove(minCycle);
if (minCycle == null) { minCycle = new List<IntPoint>();
foreach(IntPoint a in input.area) {
Console.Write(a);
} }
input.holes = cycles;
input.area = minCycle;
return input;
}
}
}
where Island contains.area contains a list of points ordered by connected pairs.
The basic algorithm is simple: take a node recursively visit every node connected to it until you detect a cycle, then return that node and append any node on the way out until you find the start of the cycle again. Once you have found every node connected to the start node remove the cycles (they since we checked every node connected to the cycle we shouldn't be deleting the once we already did) and start again on the next node, if a node contains no cycles remove it. I suspect something might be going wrong at this step but am unsure.
Any idea what I'm doing wrong that causes a weird interdependence that causes seemingly unrelated polygons to go wrong?
I think that one problem is the way you ignore connected nodes using p.generation != gen-1. As you use depth first search you will tag all the nodes until the most depth and when backtracking I think it can miss some nodes or explore nodes twice.
As a general advise I can say: Don't reinvent the wheel yourself but use a known algorithm.
First ask yourself what you want to do. The number of cycles can be exponential. So the question is if you really need all the cycles.
If the answer is yes and you want to find all the cycles in a undirected graph you can get more information here.
If you don't really need all the cyles maybe what your are looking for is to find strongly connected components.

c# find a FOLDER when path is unknown then list files and return

I am learning c# and need to find a folder when the complete path is unknown. An example would be you know the the album name but not the artist in the music folder. Finding an album name is NOT the final usage for this code but the best example for this question. I am doing this with recursion and limiting the depth of the search. Everything works good except when I find the folder and list the files I want it to stop and return but it does not, it just keeps the recursion going even after I have the the folder. I have also struggled with exception handling like how to skip a folder if permissions are not valid.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace listFoldersTest
{
class Program
{
static void Main(string[] args)
{
Console.SetWindowSize(100, 50);
DirectoryInfo dir = new DirectoryInfo(#"C:\Users\username\Music");
getDirsFiles(dir, 0, 2);
Console.ReadKey();
Console.WriteLine("done");
}
public static void getDirsFiles(DirectoryInfo d, int currentDepth, int maxDepth)
{
String folderToFindName = ("albumName");
bool foundIt = false;
if (currentDepth < maxDepth)
{
DirectoryInfo[] dirs = d.GetDirectories("*.*");
foreach (DirectoryInfo dir in dirs)
{
String pathName = (dir.FullName);
Console.WriteLine("\r{0} ", dir.Name);
if (currentDepth == (maxDepth - 1))
{
if (pathName.IndexOf(folderToFindName) != -1)
{
foundIt = true;
FileInfo[] files = dir.GetFiles("*.*");
foreach (FileInfo file in files)
{
Console.WriteLine("-------------------->> {0} ", file.Name);
} //end foreach files
} // end if pathName
} // end if of get files current depth
if (foundIt == true)
{
return;
}
getDirsFiles(dir, currentDepth + 1, maxDepth);
} //end if foreach directories
} //end if directories current depth
} // end getDirsFiles function
}
}
using System;
using System.IO;
namespace listFoldersTest
{
class Program
{
private static bool foundIt;
static void Main(string[] args)
{
Console.SetWindowSize(100, 50);
try
{
DirectoryInfo dir = new DirectoryInfo(args[0]);
getDirsFiles(dir, 0, 2);
}
catch
{
}
Console.ReadKey();
Console.WriteLine("done");
}
public static void getDirsFiles(DirectoryInfo d, int currentDepth, int maxDepth)
{
if(d == null || foundIt) return;
String folderToFindName = ("albumName");
if (currentDepth < maxDepth)
{
DirectoryInfo[] dirs = d.GetDirectories("*.*");
foreach (DirectoryInfo dir in dirs)
{
String pathName = (dir.FullName);
Console.WriteLine("\r{0} ", dir.Name);
if (currentDepth == (maxDepth - 1))
{
if (pathName.IndexOf(folderToFindName) != -1)
{
foundIt = true;
FileInfo[] files = dir.GetFiles("*.*");
foreach (FileInfo file in files)
{
Console.WriteLine("-------------------->> {0} ", file.Name);
} //end foreach files
return;
} // end if pathName
} // end if of get files current depth
getDirsFiles(dir, currentDepth + 1, maxDepth);
} //end if foreach directories
} //end if directories current depth
} // end getDirsFiles function
}
}
Create a boolean at a global scope.
Default it to false.
When the folder is found, set it to true.
In your recursive function, if the value is true, return and exit from the function without doing anything.
In your case, the foundIt variable is declared and initialized within the function. Declare it at the global level, and check it first thing in the function.
For the exception handling, simply use a try/catch and exit the function if it fails.
You can use the solution from below which doesn't use a global variable.
public static string FindFolder(DirectoryInfo rootDirectory, string folderToFind, int currentDepth, int maxDepth)
{
if(currentDepth == maxDepth)
{
return null;
}
foreach(var directory in rootDirectory.GetDirectories())
{
Console.WriteLine(directory.FullName);
if(directory.Name.Equals(folderToFind,StringComparison.OrdinalIgnoreCase))
{
return directory.FullName;
}
string tempFindResult;
if((tempFindResult = FindFolder(directory,folderToFind,++currentDepth,maxDepth)) != null)
{
return tempFindResult;
}
}
return null;
}
What I think you want to do is break out of the foreach:
if (foundIt == true)
{
break; // instead of return
}
edit
...right, of course, you need to make foundIt a class member. The current scope applied to each iteration of the method.

C#: How to make this method non-recursive

I have this recursive method which deletes empty folders:
private void DeleteEmpty(DirectoryInfo directory)
{
foreach (var d in directory.GetDirectories())
{
DeleteEmpty(d);
}
if (directory.GetFileSystemInfos().Length == 0)
{
try
{
directory.Delete();
}
catch (Exception)
{
// Already gone, no permission, not empty, et cetera
}
}
}
How can I refactor this method so that it is not recursive?
The standard refactoring is to store the data you would otherwise be passing to the function in a LIFO (i.e. a stack) or FIFO queue. Note that this doesn't change asymptotic space usage; you're using your own data structure rather than the call stack.
If you can define a "next sibling" function, you can visit the nodes with constant additional space. This is because the graph of directories (sans files) is essentially undirected due to parent pointers. Pseudocode:
nextBranchingSibling(sibling):
while sibling exists
if sibling has children
return sibling
sibling = nextSibling(sibling)
return null
nextBranch(node):
if node is marked
unmark node
else
if nextBranchingSibling(firstChild(node)) exists
return nextBranchingSibling(firstChild(node))
if nextBranchingSibling(nextSibling(node)) exists
return nextBranchingSibling(nextSibling(node))
mark parent(node)
return parent(node)
prune(node):
while node exists:
tmpNode = node
node = nextBranch(node)
if count of tmpNode's children is 0
delete tmpNode
Note that you're not actually using O(1) space total, since the directory structure is itself O(n). Methods like DirectoryInfo.GetDirectories can remove the need for loops in nextBranchingSibling.
private static Queue<DirectoryInfo> directoryQueue = new Queue<DirectoryInfo>();
private void DeleteEmpty(DirectoryInfo directory)
{
directoryQueue.Enqueue(directory);
while (directoryQueue.Count > 0)
{
var current = directoryQueue.Dequeue();
foreach (var d in current.GetDirectories())
{
directoryQueue.Enqueue(d);
}
if (directory.GetFileSystemInfos().Length == 0)
{
try
{
directory.Delete();
}
catch (Exception)
{
// Already gone, no permission, not empty, et cetera
}
}
}
}
Try this:
private void DeleteEmpty(string path)
{
string[] directories = Directory.GetDirectories(
path, "*", SearchOption.AllDirectories);
// you should delete deeper directories first
// .OrderByDescending(
// dir => dir.Split(Path.DirectorySeparatorChar).Length)
// .ToArray();
foreach (string directory in directories)
{
DirectoryInfo info = new DirectoryInfo(directory);
if (info.GetFileSystemInfos().Length == 0)
{
info.Delete();
}
}
// If you wanna a LINQ-ish version
// directories.Where(dir =>
// new DirectoryInfo(dir).GetFileSystemInfos().Length == 0)
// .ToList().ForEach(dir => Directory.Delete(dir));
}
Another performance step could be: if you tried to remove a directory and it contains files, all parent levels should be skipped since they WILL fail too.
You could use a local Stack and loop while the stack is not empty.
public void DeleteDirectories(DirectoryInfo directoryInfo, bool deleteFiles)
{
Stack<DirectoryInfo> directories = new Stack<DirectoryInfo>();
directories.Push(directoryInfo);
while (directories.Count > 0)
{
var current = directories.Peek();
foreach (var d in current.GetDirectories())
directories.Push(d);
if (current != directories.Peek())
continue;
if (deleteFiles)
foreach (var f in current.GetFiles())
{
f.Delete();
}
if (current.GetFiles().Length > 0 || current.GetDirectories().Length > 0)
throw new InvalidOperationException("The directory " + current.FullName + " was not empty and could not be deleted.");
current.Delete();
directories.Pop();
}
}
I had the same problem and I created a nice (imho) solution: beggining in a root directory, I "recursively" get the children directories and I store them in an ArrayList object. In this way, I create a list contaning first the higher level dirs, and at the end the deeper nested directories. This array is ideally divided in sub-arrays using the indexes stored in the levels ArrayList object.
Doing this, I can first check the deeper directories and delete them if they're empty, and then go back to the root level by level.
For example:
private void directoryCleanup(string root)
{
try
{
// Create directory "tree"
ArrayList dirs = new ArrayList();
// Beginning and ending indexes for each level
ArrayList levels = new ArrayList();
int start = 0;
dirs.Add(root);
while (start < dirs.Count)
{
ArrayList temp = new ArrayList();
for (int i = start; i < dirs.Count; i++)
{
DirectoryInfo dinfo = new DirectoryInfo((string)dirs[i]);
DirectoryInfo[] children = dinfo.GetDirectories();
for (int j = 0; j < children.Length; j++)
{
temp.Add(children[j].FullName);
}
Array.Clear(children, 0, children.Length);
children = null;
dinfo = null;
}
start = dirs.Count;
levels.Add(dirs.Count);
dirs.AddRange(temp);
temp.Clear();
temp = null;
}
levels.Reverse();
// Navigate the directory tree level by level, starting with the deepest one
for (int i = 0; i < levels.Count - 1; i++)
{
int end = (int)levels[i] - 1;
int begin = (int)levels[i + 1];
for (int j = end; j >= begin; j--)
{
string path = (string)dirs[j];
if (Directory.GetFileSystemEntries(path).Length == 0)
{
Directory.Delete(path);
}
}
}
levels.Clear();
levels = null;
dirs.Clear();
dirs = null;
}
catch (IOException ioex)
{
// Manage exception
return;
}
catch (Exception e)
{
// Manage exception
return;
}
}
Create a queue that has all the directories in the starting directory, then while it's not empty, take the next item, check if the directory's empty, if it is delete it, if not add all the subdirectories to the queue.
I don't know C#, but if there isn't a standard queue type, a linked list or mutable array type thing would work just as well.
Pseudocode;
directories = empty queue
until directories is not empty
next = directories.shift
if next is an empty folder
delete it
or else
add all the subdiretories to the queue

Categories