Postupy: Použití LINQ k dotazování souborů a adresářů

Mnoho operací se systémem souborů jsou v podstatě dotazy a jsou proto vhodné pro přístup LINQ. Tyto dotazy nejsou strukturivní. Nemění obsah původních souborů ani složek. Dotazy by neměly způsobovat žádné vedlejší účinky. Obecně platí, že jakýkoli kód (včetně dotazů, které provádějí operace vytvoření, aktualizace nebo odstranění), které upraví zdrojová data, by se měly uchovávat odděleně od kódu, který se na data dotazuje.

Při vytváření zdroje dat, který přesně představuje obsah systému souborů, a řádně zpracovává výjimky, je velmi složité. Příklady v této části vytvoří kolekci FileInfo snímků objektů, které představují všechny soubory v zadané kořenové složce a všechny její podsložky. Skutečný stav každého z nich FileInfo se může v čase mezi zahájením a ukončením provádění dotazu změnit. Můžete například vytvořit seznam FileInfo objektů, které se mají použít jako zdroj dat. Pokud se pokusíte získat přístup Length k vlastnosti v dotazu, FileInfo objekt se pokusí o přístup k systému souborů, aby aktualizoval hodnotu Length. Pokud soubor již neexistuje, získáte FileNotFoundException v dotazu dotaz, i když dotazujete přímo systém souborů.

Jak vytvořit dotaz na soubory s konkrétním atributem či názvem

Tento příklad ukazuje, jak najít všechny soubory, které mají zadanou příponu názvu souboru (například ".txt") v zadaném adresářovém stromu. Také ukazuje, jak vrátit nejnovější nebo nejstarší soubor ve stromu na základě času vytvoření. Možná budete muset upravit první řádek mnoha ukázek bez ohledu na to, jestli používáte tento kód ve Windows, Macu nebo linuxovém systému.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);
var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                where file.Extension == ".txt"
                orderby file.Name
                select file;

// Uncomment this block to see the full query
// foreach (FileInfo fi in fileQuery)
// {
//    Console.WriteLine(fi.FullName);
// }

var newestFile = (from file in fileQuery
                  orderby file.CreationTime
                  select new { file.FullName, file.CreationTime })
                  .Last();

Console.WriteLine($"\r\nThe newest .txt file is {newestFile.FullName}. Creation time: {newestFile.CreationTime}");

Seskupení souborů podle přípony

Tento příklad ukazuje, jak lze LINQ použít k provádění pokročilých operací seskupování a řazení v seznamech souborů nebo složek. Také ukazuje, jak stránkovat výstup v okně konzoly pomocí metod Skip a Take metod.

Následující dotaz ukazuje, jak seskupit obsah zadaného stromu adresáře podle přípony názvu souboru.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

int trimLength = startFolder.Length;

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var queryGroupByExt = from file in fileList
                      group file by file.Extension.ToLower() into fileGroup
                      orderby fileGroup.Count(), fileGroup.Key
                      select fileGroup;

// Iterate through the outer collection of groups.
foreach (var filegroup in queryGroupByExt.Take(5))
{
    Console.WriteLine($"Extension: {filegroup.Key}");
    var resultPage = filegroup.Take(20);

    //Execute the resultPage query
    foreach (var f in resultPage)
    {
        Console.WriteLine($"\t{f.FullName.Substring(trimLength)}");
    }
    Console.WriteLine();
}

Výstup z tohoto programu může být dlouhý v závislosti na podrobnostech místního systému souborů a na tom, co startFolder je nastavené. Pokud chcete povolit zobrazení všech výsledků, ukazuje tento příklad, jak procházet výsledky. Je vyžadována vnořená foreach smyčka, protože každá skupina je vyčíslována samostatně.

Jak zadat dotaz na celkový počet bajtů v sadě složek

Tento příklad ukazuje, jak načíst celkový počet bajtů používaných všemi soubory v zadané složce a všechny její podsložky. Metoda Sum přidá hodnoty všech položek vybraných v klauzuli select . Tento dotaz můžete upravit tak, aby načítal největší nebo nejmenší soubor v zadaném adresářovém stromu voláním Min nebo Max metodou místo Sum.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

var fileList = Directory.GetFiles(startFolder, "*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                let fileLen = new FileInfo(file).Length
                where fileLen > 0
                select fileLen;

// Cache the results to avoid multiple trips to the file system.
long[] fileLengths = fileQuery.ToArray();

// Return the size of the largest file
long largestFile = fileLengths.Max();

// Return the total number of bytes in all the files under the specified folder.
long totalBytes = fileLengths.Sum();

Console.WriteLine($"There are {totalBytes} bytes in {fileList.Count()} files under {startFolder}");
Console.WriteLine($"The largest file is {largestFile} bytes.");

Tento příklad rozšiřuje předchozí příklad o následující:

  • Jak načíst velikost v bajtech největšího souboru
  • Jak načíst velikost v bajtech nejmenšího souboru.
  • Jak načíst FileInfo objekt největší nebo nejmenší soubor z jedné nebo více složek v zadané kořenové složce.
  • Jak načíst sekvenci, například 10 největších souborů.
  • Jak uspořádat soubory do skupin podle jejich velikosti v bajtech a ignorovat soubory, které jsou menší než zadaná velikost.

Následující příklad obsahuje pět samostatných dotazů, které ukazují, jak dotazovat a seskupit soubory v závislosti na jejich velikosti v bajtech. Tyto příklady můžete upravit tak, aby dotaz založil na některé jiné vlastnosti objektu FileInfo .

// Return the FileInfo object for the largest file
// by sorting and selecting from beginning of list
FileInfo longestFile = (from file in fileList
                        let fileInfo = new FileInfo(file)
                        where fileInfo.Length > 0
                        orderby fileInfo.Length descending
                        select fileInfo
                        ).First();

Console.WriteLine($"The largest file under {startFolder} is {longestFile.FullName} with a length of {longestFile.Length} bytes");

//Return the FileInfo of the smallest file
FileInfo smallestFile = (from file in fileList
                         let fileInfo = new FileInfo(file)
                         where fileInfo.Length > 0
                         orderby fileInfo.Length ascending
                         select fileInfo
                        ).First();

Console.WriteLine($"The smallest file under {startFolder} is {smallestFile.FullName} with a length of {smallestFile.Length} bytes");

//Return the FileInfos for the 10 largest files
var queryTenLargest = (from file in fileList
                       let fileInfo = new FileInfo(file)
                       let len = fileInfo.Length
                       orderby len descending
                       select fileInfo
                      ).Take(10);

Console.WriteLine($"The 10 largest files under {startFolder} are:");

foreach (var v in queryTenLargest)
{
    Console.WriteLine($"{v.FullName}: {v.Length} bytes");
}

// Group the files according to their size, leaving out
// files that are less than 200000 bytes.
var querySizeGroups = from file in fileList
                      let fileInfo = new FileInfo(file)
                      let len = fileInfo.Length
                      where len > 0
                      group fileInfo by (len / 100000) into fileGroup
                      where fileGroup.Key >= 2
                      orderby fileGroup.Key descending
                      select fileGroup;

foreach (var filegroup in querySizeGroups)
{
    Console.WriteLine($"{filegroup.Key}00000");
    foreach (var item in filegroup)
    {
        Console.WriteLine($"\t{item.Name}: {item.Length}");
    }
}

Pokud chcete vrátit jeden nebo více úplných FileInfo objektů, musí dotaz nejprve zkontrolovat každý z nich ve zdroji dat a pak je seřadit podle hodnoty vlastnosti Délka. Pak může vrátit jednu nebo sekvenci s největší délkou. Slouží First k vrácení prvního prvku v seznamu. Slouží Take k vrácení prvního n počtu prvků. Zadejte sestupné pořadí řazení pro umístění nejmenších prvků na začátek seznamu.

Jak zadat dotaz na duplicitní soubory ve stromu adresáře

Někdy mohou být soubory se stejným názvem umístěny ve více než jedné složce. Tento příklad ukazuje, jak se dotazovat na takové duplicitní názvy souborů v zadané kořenové složce. Druhý příklad ukazuje, jak se dotazovat na soubory, jejichž velikost a časy LastWrite se také shodují.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

// used in WriteLine to keep the lines shorter
int charsToSkip = startFolder.Length;

// var can be used for convenience with groups.
var queryDupNames = from file in fileList
                    group file.FullName.Substring(charsToSkip) by file.Name into fileGroup
                    where fileGroup.Count() > 1
                    select fileGroup;

foreach (var queryDup in queryDupNames.Take(20))
{
    Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

    foreach (var fileName in queryDup.Take(10))
    {
        Console.WriteLine($"\t{fileName}");
    }   
}

První dotaz používá klíč k určení shody. Najde soubory, které mají stejný název, ale jejichž obsah se může lišit. Druhý dotaz používá složený klíč ke shodě se třemi vlastnostmi objektu FileInfo . Tento dotaz je mnohem pravděpodobnější, že najde soubory se stejným názvem a podobným nebo identickým obsahem.

    string startFolder = """C:\Program Files\dotnet\sdk""";
    // Or
    // string startFolder = "/usr/local/share/dotnet/sdk";

    // Make the lines shorter for the console display
    int charsToSkip = startFolder.Length;

    // Take a snapshot of the file system.
    DirectoryInfo dir = new DirectoryInfo(startFolder);
    IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

    // Note the use of a compound key. Files that match
    // all three properties belong to the same group.
    // A named type is used to enable the query to be
    // passed to another method. Anonymous types can also be used
    // for composite keys but cannot be passed across method boundaries
    //
    var queryDupFiles = from file in fileList
                        group file.FullName.Substring(charsToSkip) by
                        (Name: file.Name, LastWriteTime: file.LastWriteTime, Length: file.Length )
                        into fileGroup
                        where fileGroup.Count() > 1
                        select fileGroup;

    foreach (var queryDup in queryDupFiles.Take(20))
    {
        Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

        foreach (var fileName in queryDup)
        {
            Console.WriteLine($"\t{fileName}");
        }
    }
}

Jak dotazovat obsah textových souborů ve složce

Tento příklad ukazuje, jak dotazovat všechny soubory v zadaném adresářovém stromu, otevřít každý soubor a zkontrolovat jeho obsah. Tento typ techniky lze použít k vytvoření indexů nebo obrácení indexů obsahu stromu adresáře. V tomto příkladu se provádí jednoduché vyhledávání řetězců. Složitější typy porovnávání vzorů je však možné provádět s regulárním výrazem.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

string searchTerm = "change";

var queryMatchingFiles = from file in fileList
                         where file.Extension == ".txt"
                         let fileText = File.ReadAllText(file.FullName)
                         where fileText.Contains(searchTerm)
                         select file.FullName;

// Execute the query.
Console.WriteLine($"""The term "{searchTerm}" was found in:""");
foreach (string filename in queryMatchingFiles)
{
    Console.WriteLine(filename);
}

Jak porovnat obsah dvou složek

Tento příklad ukazuje tři způsoby porovnání dvou výpisů souborů:

  • Dotazováním na logickou hodnotu, která určuje, jestli jsou dva seznamy souborů identické.
  • Dotazováním průniku načtěte soubory, které jsou v obou složkách.
  • Dotazováním rozdílu sady načtěte soubory, které jsou v jedné složce, ale ne v druhé.

Zde uvedené techniky lze přizpůsobit tak, aby porovnávaly sekvence objektů libovolného typu.

Zde FileComparer uvedená třída ukazuje, jak použít vlastní porovnávací třídu společně s operátory standardních dotazů. Třída není určená k použití ve scénářích z reálného světa. Pouze používá název a délku v bajtech každého souboru k určení, jestli je obsah každé složky identický nebo ne. V reálném scénáři byste měli tento porovnávač upravit tak, aby prováděl přísnější kontrolu rovnosti.

// This implementation defines a very simple comparison
// between two FileInfo objects. It only compares the name
// of the files being compared and their length in bytes.
class FileCompare : IEqualityComparer<FileInfo>
{
    public bool Equals(FileInfo? f1, FileInfo? f2)
    {
        return (f1?.Name == f2?.Name &&
                f1?.Length == f2?.Length);
    }

    // Return a hash that reflects the comparison criteria. According to the
    // rules for IEqualityComparer<T>, if Equals is true, then the hash codes must
    // also be equal. Because equality as defined here is a simple value equality, not
    // reference identity, it is possible that two or more objects will produce the same
    // hash code.
    public int GetHashCode(FileInfo fi)
    {
        string s = $"{fi.Name}{fi.Length}";
        return s.GetHashCode();
    }
}

public static void CompareDirectories()
{
    string pathA = """C:\Program Files\dotnet\sdk\8.0.104""";
    string pathB = """C:\Program Files\dotnet\sdk\8.0.204""";

    DirectoryInfo dir1 = new DirectoryInfo(pathA);
    DirectoryInfo dir2 = new DirectoryInfo(pathB);

    IEnumerable<FileInfo> list1 = dir1.GetFiles("*.*", SearchOption.AllDirectories);
    IEnumerable<FileInfo> list2 = dir2.GetFiles("*.*", SearchOption.AllDirectories);

    //A custom file comparer defined below
    FileCompare myFileCompare = new FileCompare();

    // This query determines whether the two folders contain
    // identical file lists, based on the custom file comparer
    // that is defined in the FileCompare class.
    // The query executes immediately because it returns a bool.
    bool areIdentical = list1.SequenceEqual(list2, myFileCompare);

    if (areIdentical == true)
    {
        Console.WriteLine("the two folders are the same");
    }
    else
    {
        Console.WriteLine("The two folders are not the same");
    }

    // Find the common files. It produces a sequence and doesn't
    // execute until the foreach statement.
    var queryCommonFiles = list1.Intersect(list2, myFileCompare);

    if (queryCommonFiles.Any())
    {
        Console.WriteLine($"The following files are in both folders (total number = {queryCommonFiles.Count()}):");
        foreach (var v in queryCommonFiles.Take(10))
        {
            Console.WriteLine(v.Name); //shows which items end up in result list
        }
    }
    else
    {
        Console.WriteLine("There are no common files in the two folders.");
    }

    // Find the set difference between the two folders.
    var queryList1Only = (from file in list1
                          select file)
                          .Except(list2, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list1 but not list2 (total number = {queryList1Only.Count()}):");
    foreach (var v in queryList1Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }

    var queryList2Only = (from file in list2
                          select file)
                          .Except(list1, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list2 but not list1 (total number = {queryList2Only.Count()}:");
    foreach (var v in queryList2Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }
}

Změna pořadí polí souboru s oddělovači

Soubor hodnot oddělených čárkami (CSV) je textový soubor, který se často používá k ukládání tabulkových dat nebo jiných tabulkových dat reprezentovaných řádky a sloupci. Použití Split metody k oddělení polí je snadné dotazovat a manipulovat se soubory CSV pomocí LINQ. Ve skutečnosti lze stejnou techniku použít ke změna pořadí částí libovolného strukturovaného řádku textu; není omezena na soubory CSV.

V následujícím příkladu předpokládejme, že tři sloupce představují "jméno rodiny", "jméno" a "ID". Pole jsou v abecedním pořadí podle jmen členů rodiny studentů. Dotaz vytvoří novou sekvenci, ve které se sloupec ID zobrazí jako první, následovaný druhým sloupcem, který kombinuje jméno studenta a jméno rodiny. Řádky se změní podle pole ID. Výsledky se uloží do nového souboru a původní data se nezmění. Následující text ukazuje obsah souboru spreadsheet1.csv použitého v následujícím příkladu:

Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121

Následující kód přečte zdrojový soubor a přeuspořádá každý sloupec v souboru CSV a přeuspořádá pořadí sloupců:

string[] lines = File.ReadAllLines("spreadsheet1.csv");

// Create the query. Put field 2 first, then
// reverse and combine fields 0 and 1 from the old field
IEnumerable<string> query = from line in lines
                            let fields = line.Split(',')
                            orderby fields[2]
                            select $"{fields[2]}, {fields[1]} {fields[0]}";

File.WriteAllLines("spreadsheet2.csv", query.ToArray());

/* Output to spreadsheet2.csv:
111, Svetlana Omelchenko
112, Claire O'Donnell
113, Sven Mortensen
114, Cesar Garcia
115, Debra Garcia
116, Fadi Fakhouri
117, Hanying Feng
118, Hugo Garcia
119, Lance Tucker
120, Terry Adams
121, Eugene Zabokritski
122, Michael Tucker
*/

Rozdělení souboru do mnoha souborů pomocí skupin

Tento příklad ukazuje jeden ze způsobů, jak sloučit obsah dvou souborů a pak vytvořit sadu nových souborů, které data uspořádají novým způsobem. Dotaz používá obsah dvou souborů. Následující text ukazuje obsah prvního souboru names1.txt:

Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra

Druhý soubor, names2.txt, obsahuje jinou sadu názvů, z nichž některé jsou společné s první sadou:

Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi

Následující kód se dotazuje na oba soubory, vezme sjednocení obou souborů a pak zapíše nový soubor pro každou skupinu definovanou prvním písmenem rodinného jména:

string[] fileA = File.ReadAllLines("names1.txt");
string[] fileB = File.ReadAllLines("names2.txt");

// Concatenate and remove duplicate names
var mergeQuery = fileA.Union(fileB);

// Group the names by the first letter in the last name.
var groupQuery = from name in mergeQuery
                 let n = name.Split(',')[0]
                 group name by n[0] into g
                 orderby g.Key
                 select g;

foreach (var g in groupQuery)
{
    string fileName = $"testFile_{g.Key}.txt";

    Console.WriteLine(g.Key);

    using StreamWriter sw = new StreamWriter(fileName);
    foreach (var item in g)
    {
        sw.WriteLine(item);
        // Output to console for example purposes.
        Console.WriteLine($"   {item}");
    }
}
/* Output:
    A
       Aw, Kam Foo
    B
       Bankov, Peter
       Beebe, Ann
    E
       El Yassir, Mehdi
    G
       Garcia, Hugo
       Guy, Wey Yuan
       Garcia, Debra
       Gilchrist, Beth
       Giakoumakis, Leo
    H
       Holm, Michael
    L
       Liu, Jinghao
    M
       Myrcha, Jacek
       McLin, Nkenge
    N
       Noriega, Fabricio
    P
       Potra, Cristina
    T
       Toyoshima, Tim
 */

Jak spojit obsah z nepodobných souborů

Tento příklad ukazuje, jak spojit data ze dvou souborů oddělených čárkami, které sdílejí společnou hodnotu, která se používá jako odpovídající klíč. Tato technika může být užitečná, pokud potřebujete zkombinovat data ze dvou tabulek nebo z tabulky a ze souboru, který má jiný formát, do nového souboru. Příklad můžete upravit tak, aby fungoval s jakýmkoli druhem strukturovaného textu.

Následující text zobrazuje obsah scores.csv. Soubor představuje data tabulky. Sloupec 1 je ID studenta a sloupce 2 až 5 jsou skóre testů.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Následující text zobrazuje obsah names.csv. Soubor představuje tabulku, která obsahuje rodinné jméno studenta, jméno a ID studenta.

Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122

Spojte obsah z různorodých souborů, které obsahují související informace. Soubor names.csv obsahuje jméno studenta a číslo ID. Soubor scores.csv obsahuje ID a sadu čtyř skóre testu. Následující dotaz spojí skóre se jmény studentů pomocí ID jako odpovídajícího klíče. Kód je zobrazený v následujícím příkladu:

string[] names = File.ReadAllLines(@"names.csv");
string[] scores = File.ReadAllLines(@"scores.csv");

var scoreQuery = from name in names
                  let nameFields = name.Split(',')
                  from id in scores
                  let scoreFields = id.Split(',')
                  where Convert.ToInt32(nameFields[2]) == Convert.ToInt32(scoreFields[0])
                  select $"{nameFields[0]},{scoreFields[1]},{scoreFields[2]},{scoreFields[3]},{scoreFields[4]}";

Console.WriteLine("\r\nMerge two spreadsheets:");
foreach (string item in scoreQuery)
{
    Console.WriteLine(item);
}
Console.WriteLine("{0} total names in list", scoreQuery.Count());
/* Output:
Merge two spreadsheets:
Omelchenko, 97, 92, 81, 60
O'Donnell, 75, 84, 91, 39
Mortensen, 88, 94, 65, 91
Garcia, 97, 89, 85, 82
Garcia, 35, 72, 91, 70
Fakhouri, 99, 86, 90, 94
Feng, 93, 92, 80, 87
Garcia, 92, 90, 83, 78
Tucker, 68, 79, 88, 92
Adams, 99, 82, 81, 79
Zabokritski, 96, 85, 91, 60
Tucker, 94, 92, 91, 91
12 total names in list
 */

Jak vypočítat hodnoty sloupců v textovém souboru CSV

Tento příklad ukazuje, jak provádět agregované výpočty, jako je Součet, Průměr, Minimum a Maximum ve sloupcích .csv souboru. Ukázkové principy, které jsou zde uvedeny, lze použít na jiné typy strukturovaného textu.

Následující text zobrazuje obsah scores.csv. Předpokládejme, že první sloupec představuje ID studenta a následující sloupce představují skóre ze čtyř zkoušek.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Následující text ukazuje, jak pomocí Split metody převést každý řádek textu na pole. Každý prvek pole představuje sloupec. Nakonec se text v každém sloupci převede na jeho číselnou reprezentaci.

public class SumColumns
{
    public static void SumCSVColumns(string fileName)
    {
        string[] lines = File.ReadAllLines(fileName);

        // Specifies the column to compute.
        int exam = 3;

        // Spreadsheet format:
        // Student ID    Exam#1  Exam#2  Exam#3  Exam#4
        // 111,          97,     92,     81,     60

        // Add one to exam to skip over the first column,
        // which holds the student ID.
        SingleColumn(lines, exam + 1);
        Console.WriteLine();
        MultiColumns(lines);
    }

    static void SingleColumn(IEnumerable<string> strs, int examNum)
    {
        Console.WriteLine("Single Column Query:");

        // Parameter examNum specifies the column to
        // run the calculations on. This value could be
        // passed in dynamically at run time.

        // Variable columnQuery is an IEnumerable<int>.
        // The following query performs two steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) convert the element at position examNum to an int
        //    and select it.
        var columnQuery = from line in strs
                          let elements = line.Split(',')
                          select Convert.ToInt32(elements[examNum]);

        // Execute the query and cache the results to improve
        // performance. This is helpful only with very large files.
        var results = columnQuery.ToList();

        // Perform aggregate calculations Average, Max, and
        // Min on the column specified by examNum.
        double average = results.Average();
        int max = results.Max();
        int min = results.Min();

        Console.WriteLine($"Exam #{examNum}: Average:{average:##.##} High Score:{max} Low Score:{min}");
    }

    static void MultiColumns(IEnumerable<string> strs)
    {
        Console.WriteLine("Multi Column Query:");

        // Create a query, multiColQuery. Explicit typing is used
        // to make clear that, when executed, multiColQuery produces
        // nested sequences. However, you get the same results by
        // using 'var'.

        // The multiColQuery query performs the following steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) use Skip to skip the "Student ID" column, and store the
        //    rest of the row in scores.
        // 3) convert each score in the current row from a string to
        //    an int, and select that entire sequence as one row
        //    in the results.
        var multiColQuery = from line in strs
                            let elements = line.Split(',')
                            let scores = elements.Skip(1)
                            select (from str in scores
                                    select Convert.ToInt32(str));

        // Execute the query and cache the results to improve
        // performance.
        // ToArray could be used instead of ToList.
        var results = multiColQuery.ToList();

        // Find out how many columns you have in results.
        int columnCount = results[0].Count();

        // Perform aggregate calculations Average, Max, and
        // Min on each column.
        // Perform one iteration of the loop for each column
        // of scores.
        // You can use a for loop instead of a foreach loop
        // because you already executed the multiColQuery
        // query by calling ToList.
        for (int column = 0; column < columnCount; column++)
        {
            var results2 = from row in results
                           select row.ElementAt(column);
            double average = results2.Average();
            int max = results2.Max();
            int min = results2.Min();

            // Add one to column because the first exam is Exam #1,
            // not Exam #0.
            Console.WriteLine($"Exam #{column + 1} Average: {average:##.##} High Score: {max} Low Score: {min}");
        }
    }
}
/* Output:
    Single Column Query:
    Exam #4: Average:76.92 High Score:94 Low Score:39

    Multi Column Query:
    Exam #1 Average: 86.08 High Score: 99 Low Score: 35
    Exam #2 Average: 86.42 High Score: 94 Low Score: 72
    Exam #3 Average: 84.75 High Score: 91 Low Score: 65
    Exam #4 Average: 76.92 High Score: 94 Low Score: 39
 */

Pokud je soubor oddělený tabulátorem, stačí aktualizovat argument v Split metodě na \t.