多くのファイル システム操作は基本的にクエリであるため、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.");
この例では、前の例を拡張して次の操作を行います。
- 最大ファイルのサイズ (バイト単位) を取得する方法。
- 最小ファイルのサイズをバイト単位で取得する方法。
- 指定したルート フォルダーの下にある 1 つ以上のフォルダーから、 FileInfo オブジェクトの最大または最小のファイルを取得する方法。
- 最大 10 個のファイルなどのシーケンスを取得する方法。
- 指定したサイズ未満のファイルを無視して、ファイル サイズ (バイト単位) に基づいてファイルをグループに並べ替える方法。
次の例には、ファイル サイズに応じてファイルのクエリとグループ化を行う方法を示す 5 つのクエリが含まれています (バイト単位)。 これらの例を変更して、 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}");
}
}
1 つ以上の完全な FileInfo オブジェクトを返すには、まずクエリでデータ ソース内の各オブジェクトを調べ、Length プロパティの値で並べ替える必要があります。 その後、最も長い単独のものか一連のシーケンスを返すことができます。 Firstを使用して、リスト内の最初の要素を返します。 Takeを使用して、最初の n 個の要素を返します。 リストの先頭に最小の要素を配置する降順の並べ替え順序を指定します。
ディレクトリ ツリー内の重複するファイルを照会する方法
同じ名前のファイルを複数のフォルダーに配置できる場合があります。 この例では、指定したルート フォルダーの下で、このような重複するファイル名を照会する方法を示します。 2 番目の例では、サイズと 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}");
}
}
最初のクエリでは、キーを使用して一致を判断します。 同じ名前を持ち、内容が異なる可能性があるファイルが検索されます。 2 番目のクエリでは、複合キーを使用して、 FileInfo オブジェクトの 3 つのプロパティと照合します。 このクエリは、同じ名前で、同じ内容または同じコンテンツを持つファイルを検索する可能性がはるかに高くなります。
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);
}
2 つのフォルダーの内容を比較する方法
この例では、2 つのファイル一覧を比較する 3 つの方法を示します。
- 2 つのファイル リストが同じかどうかを指定するブール値をクエリします。
- 交差部分のクエリを実行して、両方のフォルダーにあるファイルを取得します。
- セットの違いを照会して、一方のフォルダーにあるが他方のフォルダーにないファイルを取得します。
ここで示す手法は、任意の型のオブジェクトのシーケンスを比較するように調整できます。
ここで示す 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メソッドを使用してフィールドを区切ることで、LINQ を使用して CSV ファイルのクエリと操作を簡単に行うことができます。 実際には、同じ手法を使用して、構造化されたテキスト行の部分を並べ替えることができます。CSV ファイルに限定されるわけではありません。
次の例では、3 つの列が学生の "家族名"、"名"、"ID" を表しているとします。フィールドは、学生の家族名に基づいてアルファベット順に並べられています。 このクエリでは、ID 列が最初に表示され、次に学生の名とファミリ名を組み合わせた 2 番目の列が表示される新しいシーケンスが生成されます。 行は ID フィールドに従って並べ替えされます。 結果は新しいファイルに保存され、元のデータは変更されません。 次のテキストは、次の例で使用される 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
*/
グループを使用してファイルを多数のファイルに分割する方法
この例では、2 つのファイルの内容をマージし、新しい方法でデータを整理する一連の新しいファイルを作成する方法の 1 つを示します。 このクエリでは、2 つのファイルの内容が使用されます。 次のテキスト は、最初 のファイルnames1.txtの内容を示しています。
Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
2 番目のファイル (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
*/
異種ファイルからコンテンツを結合する方法
この例では、一致するキーとして使用される共通の値を共有する 2 つのコンマ区切りファイルのデータを結合する方法を示します。 この手法は、2 つのスプレッドシートまたはスプレッドシートのデータと、別の形式のファイルのデータを新しいファイルに結合する必要がある場合に役立ちます。 この例は、任意の種類の構造化テキストを操作するように変更できます。
次のテキストは、 scores.csvの内容を示しています。 ファイルはスプレッドシート データを表します。 列 1 は学生の ID で、列 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の内容を示しています。 このファイルは、学生の家族名、名、学生 ID を含むスプレッドシートを表します。
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 には、学生の名前と ID 番号が含まれています。 ファイル scores.csv には、ID と 4 つのテスト スコアのセットが含まれています。 次のクエリでは、ID を一致するキーとして使用して、スコアを学生名に結合します。 コードを次の例に示します。
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 テキスト ファイルで列の値を計算する方法
この例では、.csv ファイルの列に対して Sum、Average、Min、Max などの集計計算を実行する方法を示します。 ここで示す原則の例は、他の種類の構造化テキストに適用できます。
次のテキストは、 scores.csvの内容を示しています。 最初の列が学生 ID を表し、後続の列が 4 つの試験のスコアを表しているとします。
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
に更新するだけです。
.NET