Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Многие операции файловой системы по сути являются запросами и поэтому хорошо подходят для подхода LINQ. Эти запросы являются недеструктивными. Они не изменяют содержимое исходных файлов или папок. Запросы не должны вызывать побочные эффекты. Как правило, любой код (включая запросы, выполняющие операции создания и обновления и удаления), которые изменяют исходные данные, должны храниться отдельно от кода, который просто запрашивает данные.
Существует некоторая сложность создания источника данных, который точно представляет содержимое файловой системы и обрабатывает исключения корректно. В примерах этого раздела создается коллекция моментальных снимков объектов FileInfo, представляющая все файлы в указанной корневой папке и все ее вложенные папки. Фактическое состояние каждого FileInfo может измениться в период между началом и завершением выполнения запроса. Например, можно создать список FileInfo объектов для использования в качестве источника данных. Если вы пытаетесь получить доступ к Length
свойству в запросе, FileInfo объект пытается получить доступ к файловой системе, чтобы обновить значение Length
. Если файл больше не существует, в вашем запросе появляется FileNotFoundException, даже если вы не обращаетесь к файловой системе напрямую.
Как запрашивать файлы с указанным атрибутом или именем
В этом примере показано, как найти все файлы с указанным расширением имени файла (например,.txt") в указанном дереве каталогов. В нем также показано, как вернуть новый или старый файл в дереве на основе времени создания. Возможно, вам потребуется изменить первую строку во многих примерах, если вы запускаете этот код на Windows, Mac или Linux системе.
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}");
Группирование файлов по расширению
В этом примере показано, как LINQ можно использовать для выполнения расширенных операций группировки и сортировки в списках файлов или папок. В нём также показано, как странично выводить данные в окне консоли с помощью методов Skip и Take.
В следующем запросе показано, как сгруппировать содержимое указанного дерева каталогов по расширению имени файла.
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();
}
Выходные данные из этой программы могут быть длинными в зависимости от сведений о локальной файловой системе и заданном параметре startFolder
. Чтобы включить просмотр всех результатов, в этом примере показано, как просмотреть результаты. Вложенный foreach
цикл является обязательным, так как каждая группа перечисляется отдельно.
Как запросить общее количество байтов в наборе папок
В этом примере показано, как получить общее количество байтов, используемых всеми файлами в указанной папке и всеми вложенными папками. Метод Sum добавляет значения всех элементов, выбранных в предложении select
. Этот запрос можно изменить, чтобы получить самый большой или наименьший файл в указанном дереве каталогов, вызвав метод Min или Max вместо 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.");
Этот пример расширяет предыдущий пример, чтобы сделать следующее:
- Как получить размер в байтах крупнейшего файла.
- Как получить размер в байтах наименьшего файла.
- Как получить самый большой или наименьший FileInfo файл объекта из одной или нескольких папок в указанной корневой папке.
- Как получить последовательность, например 10 крупнейших файлов.
- Как упорядочить файлы в группы на основе их размера в байтах, игнорируя файлы, которые меньше указанного размера.
В следующем примере содержатся пять отдельных запросов, которые показывают, как запрашивать и группировать файлы в зависимости от размера файла в байтах. Эти примеры можно изменить, чтобы создать запрос на другое свойство 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}");
}
}
Чтобы вернуть один или несколько полных FileInfo объектов, запрос сначала должен проверить каждый из них в источнике данных, а затем отсортировать их по значению свойства Length. Затем он может вернуть одну или последовательность с наибольшей длиной. Используется First для возврата первого элемента в списке. Используется Take для возврата первого n числа элементов. Укажите порядок сортировки по убыванию, чтобы поместить наименьшие элементы в начало списка.
Как выполнить поиск повторяющихся файлов в иерархии каталогов
Иногда файлы с одинаковым именем могут находиться в нескольких папках. В этом примере показано, как запрашивать такие повторяющиеся имена файлов в указанной корневой папке. Во втором примере показано, как запрашивать файлы, размер которых и время LastWrite также совпадают.
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}");
}
}
Первый запрос использует ключ для определения соответствия. Он находит файлы с одинаковым именем, но содержимое которого может отличаться. Второй запрос использует составной ключ для сопоставления с тремя свойствами FileInfo объекта. Этот запрос гораздо чаще находит файлы с одинаковым именем и аналогичным или идентичным содержимым.
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}");
}
}
}
Запрос содержимого текстовых файлов в папке
В этом примере показано, как запрашивать все файлы в указанном дереве каталогов, открывать каждый файл и проверять его содержимое. Этот тип метода можно использовать для создания индексов или обратных индексов содержимого дерева каталогов. В этом примере выполняется простой поиск строк. Однако более сложные типы сопоставления шаблонов можно выполнять с регулярным выражением.
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);
}
Сравнение содержимого двух папок
В этом примере показано три способа сравнения двух списков файлов:
- Запрашивая логическое значение, указывающее, идентичны ли два списка файлов.
- Запросив пересечение папок для получения файлов, которые находятся в обеих папках.
- Запросив разность множеств, чтобы получить файлы, которые находятся в одной папке, но не в другой.
Приведенные здесь методы можно адаптировать для сравнения последовательностей объектов любого типа.
В классе FileComparer
, показанном здесь, демонстрируется, как использовать настраиваемый класс сравнения вместе со стандартными операторами запросов. Класс не предназначен для использования в реальных сценариях. Он просто использует имя и длину в байтах каждого файла для определения, совпадает ли содержимое папок. В реальном сценарии необходимо изменить это средство сравнения, чтобы выполнить более строгую проверку равенства.
// 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);
}
}
Как изменить порядок полей файла с разделителями
Файл значений, разделённых запятыми (CSV) — это текстовый файл, который часто используется для хранения данных электронной таблицы или других табличных данных, представленных строками и столбцами. Используя Split метод для разделения полей, можно легко запрашивать и управлять CSV-файлами с помощью LINQ. На самом деле, один и тот же метод можно использовать для переупорядочения частей любой структурированной строки текста; Он не ограничен CSV-файлами.
В следующем примере предполагается, что три столбца представляют "имя семьи", "имя" и "идентификатор". Поля находятся в алфавитном порядке на основе имен семей учащихся. Запрос создает новую последовательность, в которой отображается столбец идентификатора, а затем второй столбец, который объединяет имя учащегося и имя семьи. Строки переупорядочены в соответствии с полем идентификатора. Результаты сохраняются в новом файле и исходные данные не изменяются. В следующем тексте показано содержимое файла spreadsheet1.csv , используемого в следующем примере:
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
Следующий код считывает исходный файл и изменяет порядок столбцов в CSV-файле, чтобы изменить порядок столбцов:
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
*/
Разделение файла на множество файлов с помощью групп
В этом примере показано, как объединить содержимое двух файлов, а затем создать набор новых файлов, которые упорядочивают данные новым способом. Запрос использует содержимое двух файлов. В следующем тексте показано содержимое первого файла ,names1.txt:
Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
Второй файл ,names2.txt, содержит другой набор имен, некоторые из которых являются общими для первого набора:
Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi
Следующий код запрашивает оба файла, принимает объединение обоих файлов, а затем записывает новый файл для каждой группы, определенный первой буквой имени семейства:
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
*/
Присоединение содержимого из разнородных файлов
В этом примере показано, как объединить данные из двух файлов с разделителями-запятыми, которые используют общее значение, используемое в качестве соответствующего ключа. Этот метод может быть полезен, если необходимо объединить данные из двух электронных таблиц или из электронной таблицы и из файла с другим форматом в новый файл. Пример можно изменить для работы с любым структурированным текстом.
В следующем тексте показано содержимое scores.csv. Файл представляет данные электронной таблицы. Столбец 1 — это идентификатор учащегося, а столбцы 2–5 — тестовые оценки.
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
В следующем тексте показано содержимое names.csv. Файл представляет электронную таблицу, содержащую имя семьи учащегося, имя и идентификатор учащегося.
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
Присоединяйте содержимое из разнородных файлов, содержащих связанные сведения. Файлnames.csv содержит имя учащегося и номер идентификатора. Файлscores.csv содержит идентификатор и набор четырех показателей тестирования. Следующий запрос объединяет оценки с именами учащихся с помощью идентификатора в качестве соответствующего ключа. Код показан в следующем примере:
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($"{scoreQuery.Count()} total names in list");
/* 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
*/
Вычисление значений столбцов в текстовом файле CSV
В этом примере показано, как выполнять статистические вычисления, такие как Sum, Average, Min и Max в столбцах файла .csv. Примеры принципов, показанных здесь, можно применить к другим типам структурированного текста.
В следующем тексте показано содержимое scores.csv. Предположим, что первый столбец представляет идентификатор учащегося, а последующие столбцы представляют оценки из четырех экзаменов.
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
В следующем тексте показано, как использовать Split метод для преобразования каждой строки текста в массив. Каждый элемент массива представляет столбец. Наконец, текст в каждом столбце преобразуется в его числовое представление.
public static class SumColumns
{
public static void ProcessColumns(string filePath, string seperator)
{
// Divide each exam into a group
var exams = from line in MatrixFrom(filePath, seperator)
from score in line
// Identify the column number
let colNumber = Array.FindIndex(line, t => ReferenceEquals(score, t))
// The first column is the student ID, not the exam score
// so it needs to be excluded
where colNumber > 0
// Convert the score from string to int
// Group by column number, i.e. one group per exam
group double.Parse(score) by colNumber into g
select new
{
Title = $"Exam#{g.Key}",
Min = g.Min(),
Max = g.Max(),
Avg = Math.Round(g.Average(), 2),
Total = g.Sum()
};
foreach (var exam in exams)
{
Console.WriteLine($"{exam.Title}\t"
+ $"Average:{exam.Avg,6}\t"
+ $"High Score:{exam.Max,3}\t"
+ $"Low Score:{exam.Min,3}\t"
+ $"Total:{exam.Total,5}");
}
}
// Transform the file content to an IEnumerable of string arrays
// like a matrix
private static IEnumerable<string[]> MatrixFrom(string filePath, string seperator)
{
using StreamReader reader = File.OpenText(filePath);
for (string? line = reader.ReadLine(); line is not null; line = reader.ReadLine())
{
yield return line.Split(seperator, StringSplitOptions.TrimEntries);
}
}
}
// Output:
// Exam#1 Average: 86.08 High Score: 99 Low Score: 35 Total: 1033
// Exam#2 Average: 86.42 High Score: 94 Low Score: 72 Total: 1037
// Exam#3 Average: 84.75 High Score: 91 Low Score: 65 Total: 1017
// Exam#4 Average: 76.92 High Score: 94 Low Score: 39 Total: 923
Если ваш файл разделён табуляцией, просто обновите аргумент в методе SumColumns.ProcessColumns
на \t
.