How to LINQ-ify the following loop?

Viewed 57

I have a method in a C# program. It enumerates all the .cs files in a certain folder and then runs through the list. For each file, I read all the lines using File.ReadAllLines on it. I only want to process a file if it contains a class, whether conventional, static, or abstract, whose name begins with a certain phrase and does not end with the word Tests. Moreover, I wish to find the line index in the line of lines containing the declaration of the class --- i.e., the public static class Foo part.

Given that I take the result of File.ReadAllLines and call ToList() on it to create a List<string>, I wish to use the FindIndex method to find the index of the line matching my criteria (if it exists) using a Predicate.

My question is: What is a good way to write such a predicate?

I realize I could probably use more sophisticated methods, but I am just putting this code into a quick-and-dirty LINQPad script. So, I don't have to get super fancy.

Let me show you what I have so far (assume that the outermost namespace and class are already suitably declared):

void Main()
{
    var files = Directory
        .EnumerateDirectories(
            Path.Combine(
                Environment.GetFolderPath(
                    Environment.SpecialFolder.UserProfile
                ), @"source\repos\astrohart\MFR"
            ), "*", SearchOption.TopDirectoryOnly
        ).SelectMany(
            x => Directory.EnumerateFiles(
                x, "FileSystemEntry*.cs", SearchOption.AllDirectories
            )
        )
        .Where(x => !"FileSystemEntry.cs".Equals(Path.GetFileName(x)))
        .ToList();
    if (files == null || !files.Any()) return;

    foreach (var file in files)
    {
        var contents = string.Empty;

        try
        {
            contents = File.ReadAllText(file);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"ERROR: {ex.Message}");

            contents = string.Empty;
        }

        if (string.IsNullOrWhiteSpace(contents)) continue;
        if (contents.Contains("[TestFixture]")) continue;
        if (contents.Contains("[Log(AttributeExclude = true)]")) continue;

        file.Dump();

        var lines = new List<string>();
        lines.TrimExcess();

        try
        {
            lines = File.ReadAllLines(file).ToList();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"ERROR: {ex.Message}");

            lines = new List<string>();
            lines.TrimExcess();
        }

        if (lines == null || !lines.Any()) continue;

        var index = -1;

        for (var i = 0; i < lines.Count; i++)
        {
            var currentLine = lines[i].Trim();
            if (currentLine.EndsWith("Tests")) continue;

            if (currentLine.StartsWith("public static class FileSystemEntry"))
            {
                index = i;
                break;
            }
            if (currentLine.StartsWith("public class FileSystemEntry"))
            {
                index = i;
                break;
            }
            if (currentLine.StartsWith("public abstract class FileSystemEntry"))
            {
                index = i;
                break;
            }
        }

        if (index < 0) continue;
     
        /*...*/
    }
}

How do I translate the for loop in:

var index = -1;

for (var i = 0; i < lines.Count; i++)
{
    var currentLine = lines[i].Trim();
    if (currentLine.EndsWith("Tests")) continue;

    if (currentLine.StartsWith("public static class FileSystemEntry"))
    {
        index = i;
        break;
    }
    if (currentLine.StartsWith("public class FileSystemEntry"))
    {
        index = i;
        break;
    }
    if (currentLine.StartsWith("public abstract class FileSystemEntry"))
    {
        index = i;
        break;
    }
}

if (index < 0) continue;

into a call thus:

var index = lines.FindIndex(currentLine => /*...*/);

I need help with how to derive the proper body of the lambda expression that matches what the for loop does.

Thanks in advance!

EDIT 1

I squinted my eyes at my loop just a little more. I am looking for a predicate to use specifically with the FindIndex method. I thought a little harder and I figured out maybe I can get away with:

var index = lines.FindIndex(currentLine => !currentLine.Trim.EndsWith("Tests") && currentLine.Trim().StartsWith("public static class FileSystemEntry") || currentLine.Trim().StartsWith("public class FileSystemEntry") || currentLine.Trim().StartsWith("public abstract class FileSystemEntry"));

Perhaps I can implement an extension method

public static bool StartsWithAnyOf(this string value, params string[] testStrings)
{
    var result = false;

    try
    {
        if (string.IsNullOrWhiteSpace(value.Trim())) return result;
        if (testStrings == null || !testStrings.Any()) return result;

        foreach(var element in testStrings)
            if (value.Trim().StartsWith(element))
            {
                result = true;
                break;
            }
    }
    catch
    {
        result = false;
    }

    return result;
}

Then I'd declare another method:

public static bool KeepLine(string currentLine)
{
     if (string.IsNullOrWhiteSpace(currentLine.Trim())) return false;
     if (currentLine.Trim().EndsWith("Tests")) return false;
     
     return currentLine.StartsWithAnyOf(
         "public static class FileSystemEntry",
         "public class FileSystemEntry",
         "public abstract FileSystemEntry"
     );
}

Then use it thus:

var index = lines.FindIndex(KeepLine);

Would that work?

1 Answers

I havent tested this thoroughly but it seems to pass basic sanity if I compare to original code supplied above. Note that this is not best when it comes to measuring performance. The 'foreach' loop with anonymous function has flaw that you cannot break away from the anonymous function. The only way to come out of a foreach is to run all foreach statements. In order to preserve first index where criteria matches against line contents, I am using index in else if() comparison statement. This means the foreach loop will run for all lines despite of having the first occurrence of matching lines found.

lines.ForEach((l) =>
{
    if (l.EndsWith("Tests")) ;
    else if (index ==0 && (l.StartsWith("public static class FileSystemEntry") ||
    l.StartsWith("public class FileSystemEntry") ||
    l.StartsWith("public abstract class FileSystemEntry")))
    {
        index = lines.IndexOf(l);
    }
});
Related