-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCsvManager.cs
284 lines (246 loc) · 10 KB
/
CsvManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// ============================================================================
//
// CSV ファイルを管理するクラス
// Copyright (C) 2015-2021 by SHINTA
//
// ============================================================================
// ----------------------------------------------------------------------------
//
// ----------------------------------------------------------------------------
// ============================================================================
// Ver. | 更新日 | 更新内容
// ----------------------------------------------------------------------------
// -.-- | 2015/12/19 (Sat) | 作成開始。
// 1.00 | 2015/12/19 (Sat) | オリジナルバージョン。
// 1.10 | 2017/11/18 (Sat) | タイトル行を読み飛ばせるようにした。
// (1.11) | 2017/11/18 (Sat) | \" を読み飛ばせるようにした。
// 1.20 | 2017/12/09 (Sat) | 行番号を付与できるようにした。
// 1.30 | 2018/01/08 (Mon) | 書き込みできるようにした。
// (1.31) | 2018/04/22 (Sun) | 書き込み時にカンマを含む列をエスケープしていなかった不具合を修正。
// (1.32) | 2018/05/20 (Sun) | 書き込み時に " をエスケープしていなかった不具合を修正。
// (1.33) | 2019/12/07 (Sat) | null 許容参照型を有効化した。
// (1.34) | 2019/12/22 (Sun) | null 許容参照型を無効化できるようにした。
// (1.35) | 2021/05/27 (Thu) | null 許容参照型を必須にした。
// ============================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Shinta
{
public class CsvManager
{
// ====================================================================
// public メンバー関数
// ====================================================================
// --------------------------------------------------------------------
// 読み込み済みの CSV ファイル内容文字列を解析し、行列に分割
// 区切りカンマ前後のホワイトスペースは取り除く
// フィールドがダブルクォートで括られている場合、途中の改行・連続するダブルクォート・\" は文字扱い
// 行番号を付ける場合、0 ベース。CSV が空行でも行番号列を作成する。
// <例外> Exception
// --------------------------------------------------------------------
public static List<List<String>> CsvStringToList(String contents, Boolean skipTitleLine = false, Boolean addLineIndex = false)
{
List<List<String>> records = new();
List<String> fields = new();
// 改行コードを LF に統一
contents = contents.Replace("\r\n", "\n");
contents = contents.Replace("\r", "\n");
Int32 beginPos = 0;
Int32 endPos;
if (skipTitleLine)
{
beginPos = SkipTitleLine(contents);
}
if (addLineIndex)
{
fields.Add(records.Count.ToString());
}
while (beginPos < contents.Length)
{
// 先頭のホワイトスペース読み飛ばし
SkipWhiteSpace(contents, ref beginPos);
if (beginPos < contents.Length && contents[beginPos] == '"')
{
// ダブルクォートで囲まれている場合
endPos = FindPairQuotePos(contents, records.Count, beginPos);
// ダブルクォートの中身を抽出
String field = contents.Substring(beginPos + 1, endPos - beginPos - 1);
// 連続するダブルクォート・\" を 1 つに戻してからフィールド追加
fields.Add(field.Replace("\"\"", "\"").Replace("\\\"", "\""));
// 末尾のホワイトスペース読み飛ばし
endPos++;
SkipWhiteSpace(contents, ref endPos);
if (endPos < contents.Length && contents[endPos] != ',' && contents[endPos] != '\n')
{
throw new Exception((records.Count + 1).ToString() + " レコード目のダブルクォートに文字列が続いています。");
}
}
else
{
// ダブルクォートで囲まれていない場合
endPos = beginPos;
// 区切り文字を検索
while (endPos < contents.Length && contents[endPos] != ',' && contents[endPos] != '\n')
{
endPos++;
}
// 区切り文字までの文字を抽出(末尾のホワイトスペース除く)してフィールド追加
//fields.Add(contents.Substring(beginPos, endPos - beginPos).TrimEnd());
fields.Add(contents[beginPos..endPos].TrimEnd());
}
// 行末ならレコード追加
if (endPos >= contents.Length || contents[endPos] == '\n')
{
records.Add(fields);
// これまでの最大フィールド数+αのメモリを確保しつつリストを初期化
fields = new List<String>(fields.Capacity);
if (addLineIndex)
{
fields.Add(records.Count.ToString());
}
}
beginPos = endPos + 1;
}
return records;
}
// --------------------------------------------------------------------
// 行列を CSV 文字列に統合
// removeLineIndex に関わらず、title には行番号列は無いものとする
// --------------------------------------------------------------------
public static String ListToCsvString(List<List<String>> records, String crCode, List<String>? title = null, Boolean removeLineIndex = false)
{
StringBuilder stringBuilder = new();
// タイトル行
if (title != null)
{
AddRecord(stringBuilder, title, 0, crCode);
}
// 一般行
Int32 aStartColumnIndex = removeLineIndex ? 1 : 0;
for (Int32 i = 0; i < records.Count; i++)
{
AddRecord(stringBuilder, records[i], aStartColumnIndex, crCode);
}
return stringBuilder.ToString();
}
// --------------------------------------------------------------------
// ファイルから CSV を読み込む
// <例外> Exception
// --------------------------------------------------------------------
public static List<List<String>> LoadCsv(String oPath, Encoding oEncoding, Boolean oSkipTitleLine = false, Boolean oAddLineIndex = false)
{
return CsvStringToList(File.ReadAllText(oPath, oEncoding), oSkipTitleLine, oAddLineIndex);
}
// --------------------------------------------------------------------
// ファイルに CSV を書き込む
// <例外> Exception
// --------------------------------------------------------------------
public static void SaveCsv(String path, List<List<String>> records, String crCode, Encoding encoding, List<String>? title = null, Boolean removeLineIndex = false)
{
File.WriteAllText(path, ListToCsvString(records, crCode, title, removeLineIndex), encoding);
}
// ====================================================================
// private メンバー関数
// ====================================================================
// --------------------------------------------------------------------
// CSV 1 行分を文字列に変換
// --------------------------------------------------------------------
private static void AddRecord(StringBuilder oSB, List<String> oRecord, Int32 oStartColumnIndex, String oCrCode)
{
// 右側以外の列を追加
for (Int32 i = oStartColumnIndex; i < oRecord.Count - 1; i++)
{
oSB.Append(Escape(oRecord[i]) + ",");
}
// 右側の列を追加
if (oRecord.Count - 1 >= oStartColumnIndex)
{
//oSB.Append(Escape(oRecord[oRecord.Count - 1]) + oCrCode);
oSB.Append(Escape(oRecord[^1]) + oCrCode);
}
}
// --------------------------------------------------------------------
// 改行・ダブルクオート・\・カンマ が含まれる場合はダブルクオートで括る
// --------------------------------------------------------------------
private static String Escape(String oString)
{
if (String.IsNullOrEmpty(oString))
{
return String.Empty;
}
oString = oString.Replace("\"", "\\\"");
if (oString.IndexOfAny(new Char[] { '\r', '\n', '\"', '\\', ',' }) >= 0)
{
return "\"" + oString + "\"";
}
return oString;
}
// --------------------------------------------------------------------
// 対になるダブルクオートの位置を返す
// <例外> Exception
// --------------------------------------------------------------------
private static Int32 FindPairQuotePos(String oContents, Int32 oRecordIndex, Int32 oQuotePos)
{
for (; ; )
{
// 対になるダブルクォートを探す
oQuotePos = oContents.IndexOf('"', oQuotePos + 1);
if (oQuotePos < 0)
{
throw new Exception((oRecordIndex + 1).ToString() + " レコード目のダブルクォートと対になるダブルクォートがありません。");
}
if (oQuotePos > 0 && oContents[oQuotePos - 1] == '\\')
{
// 「\」「"」と連続しているので、内容の一部である
}
else if (oQuotePos + 1 < oContents.Length && oContents[oQuotePos + 1] == '"')
{
// ダブルクォートが連続しているので、内容の一部である
oQuotePos++;
}
else
{
// ダブルクォートが連続していないので、対になるダブルクォートである
break;
}
}
return oQuotePos;
}
// --------------------------------------------------------------------
// タイトル行を読み飛ばす
// '\n' の次の位置を返す
// --------------------------------------------------------------------
private static Int32 SkipTitleLine(String oContents)
{
Int32 aPos = 0;
while (aPos < oContents.Length)
{
if (oContents[aPos] == '\n')
{
return aPos + 1;
}
if (oContents[aPos] == '\"')
{
aPos = FindPairQuotePos(oContents, 0, aPos) + 1;
}
else
{
aPos++;
}
}
return aPos;
}
// --------------------------------------------------------------------
// 空白を読み飛ばす
// --------------------------------------------------------------------
private static void SkipWhiteSpace(String contents, ref Int32 pos)
{
while (pos < contents.Length && (contents[pos] == ' ' || contents[pos] == '\t'))
{
pos++;
}
}
}
}