forked from sinbad/UnityCsvUtil
-
Notifications
You must be signed in to change notification settings - Fork 0
/
CsvUtil.cs
290 lines (262 loc) · 12.7 KB
/
CsvUtil.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
284
285
286
287
288
289
290
using UnityEngine;
using System.Text.RegularExpressions;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.ComponentModel;
namespace Sinbad {
// This class uses Reflection and Linq so it's not the fastest thing in the
// world; however I only use it in development builds where we want to allow
// game data to be easily tweaked so this isn't an issue; I would recommend
// you do the same.
public static class CsvUtil {
// Quote semicolons too since some apps e.g. Numbers don't like them
static char[] quotedChars = new char[] { ',', ';'};
// Load a CSV into a list of struct/classes from a file where each line = 1 object
// First line of the CSV must be a header containing property names
// Can optionally include any other columns headed with #foo, which are ignored
// E.g. you can include a #Description column to provide notes which are ignored
// This method throws file exceptions if file is not found
// Field names are matched case-insensitive for convenience
// @param filename File to load
// @param strict If true, log errors if a line doesn't have enough
// fields as per the header. If false, ignores and just fills what it can
public static List<T> LoadObjects<T>(string filename, bool strict = true) where T: new() {
using (var stream = File.Open(filename, FileMode.Open)) {
using (var rdr = new StreamReader(stream)) {
return LoadObjects<T>(rdr, strict);
}
}
}
// Load a CSV into a list of struct/classes from a stream where each line = 1 object
// First line of the CSV must be a header containing property names
// Can optionally include any other columns headed with #foo, which are ignored
// E.g. you can include a #Description column to provide notes which are ignored
// Field names are matched case-insensitive for convenience
// @param rdr Input reader
// @param strict If true, log errors if a line doesn't have enough
// fields as per the header. If false, ignores and just fills what it can
public static List<T> LoadObjects<T>(TextReader rdr, bool strict = true) where T: new() {
var ret = new List<T>();
string header = rdr.ReadLine();
var fieldDefs = ParseHeader(header);
FieldInfo[] fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo[] pi = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
bool isValueType = typeof(T).IsValueType;
string line;
while((line = rdr.ReadLine()) != null) {
var obj = new T();
// box manually to avoid issues with structs
object boxed = obj;
if (ParseLineToObject(line, fieldDefs, fi, pi, boxed, strict)) {
// unbox value types
if (isValueType)
obj = (T)boxed;
ret.Add(obj);
}
}
return ret;
}
// Load a CSV file containing fields for a single object from a file
// No header is required, but it can be present with '#' prefix
// First column is property name, second is value
// You can optionally include other columns for descriptions etc, these are ignored
// If you want to include a header, make sure the first line starts with '#'
// then it will be ignored (as will any lines that start that way)
// This method throws file exceptions if file is not found
// Field names are matched case-insensitive for convenience
public static void LoadObject<T>(string filename, ref T destObject) {
using (var stream = File.Open(filename, FileMode.Open)) {
using (var rdr = new StreamReader(stream)) {
LoadObject<T>(rdr, ref destObject);
}
}
}
// Load a CSV file containing fields for a single object from a stream
// No header is required, but it can be present with '#' prefix
// First column is property name, second is value
// You can optionally include other columns for descriptions etc, these are ignored
// Field names are matched case-insensitive for convenience
public static void LoadObject<T>(TextReader rdr, ref T destObject) {
FieldInfo[] fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo[] pi = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// prevent auto-boxing causing problems with structs
object nonValueObject = destObject;
string line;
while((line = rdr.ReadLine()) != null) {
// Ignore optional header lines
if (line.StartsWith("#"))
continue;
string[] vals = EnumerateCsvLine(line).ToArray();
if (vals.Length >= 2) {
SetField(RemoveSpaces(vals[0].Trim()), vals[1], fi, pi, nonValueObject);
} else {
Debug.LogWarning(string.Format("CsvUtil: ignoring line '{0}': not enough fields", line));
}
}
if (typeof(T).IsValueType) {
// unbox
destObject = (T)nonValueObject;
}
}
// Save a single object to a CSV file
// Will write 1 line per field, first column is name, second is value
// This method throws exceptions if unable to write
public static void SaveObject<T>(T obj, string filename) {
using (var stream = File.Open(filename, FileMode.Create)) {
using (var wtr = new StreamWriter(stream)) {
SaveObject<T>(obj, wtr);
}
}
}
// Save a single object to a CSV stream
// Will write 1 line per field, first column is name, second is value
// This method throws exceptions if unable to write
public static void SaveObject<T>(T obj, TextWriter w) {
FieldInfo[] fi = typeof(T).GetFields();
bool firstLine = true;
foreach (FieldInfo f in fi) {
// Good CSV files don't have a trailing newline so only add here
if (firstLine)
firstLine = false;
else
w.Write(Environment.NewLine);
w.Write(f.Name);
w.Write(",");
string val = f.GetValue(obj).ToString();
// Quote if necessary
if (val.IndexOfAny(quotedChars) != -1) {
val = string.Format("\"{0}\"", val);
}
w.Write(val);
}
}
// Save multiple objects to a CSV file
// Writes a header line with field names, followed by one line per
// object with each field value in each column
// This method throws exceptions if unable to write
public static void SaveObjects<T>(IEnumerable<T> objs, string filename) {
using (var stream = File.Open(filename, FileMode.Create)) {
using (var wtr = new StreamWriter(stream)) {
SaveObjects<T>(objs, wtr);
}
}
}
// Save multiple objects to a CSV stream
// Writes a header line with field names, followed by one line per
// object with each field value in each column
// This method throws exceptions if unable to write
public static void SaveObjects<T>(IEnumerable<T> objs, TextWriter w) {
FieldInfo[] fi = typeof(T).GetFields();
WriteHeader<T>(fi, w);
bool firstLine = true;
foreach (T obj in objs) {
// Good CSV files don't have a trailing newline so only add here
if (firstLine)
firstLine = false;
else
w.Write(Environment.NewLine);
WriteObjectToLine(obj, fi, w);
}
}
private static void WriteHeader<T>(FieldInfo[] fi, TextWriter w) {
bool firstCol = true;
foreach (FieldInfo f in fi) {
// Good CSV files don't have a trailing comma so only add here
if (firstCol)
firstCol = false;
else
w.Write(",");
w.Write(f.Name);
}
w.Write(Environment.NewLine);
}
private static void WriteObjectToLine<T>(T obj, FieldInfo[] fi, TextWriter w) {
bool firstCol = true;
foreach (FieldInfo f in fi) {
// Good CSV files don't have a trailing comma so only add here
if (firstCol)
firstCol = false;
else
w.Write(",");
string val = f.GetValue(obj).ToString();
// Quote if necessary
if (val.IndexOfAny(quotedChars) != -1) {
val = string.Format("\"{0}\"", val);
}
w.Write(val);
}
}
// Parse the header line and return a mapping of field names to column
// indexes. Columns which have a '#' prefix are ignored.
private static Dictionary<string, int> ParseHeader(string header) {
var headers = new Dictionary<string, int>();
int n = 0;
foreach(string field in EnumerateCsvLine(header)) {
var trimmed = field.Trim();
if (!trimmed.StartsWith("#")) {
trimmed = RemoveSpaces(trimmed);
headers[trimmed] = n;
}
++n;
}
return headers;
}
// Parse an object line based on the header, return true if any fields matched
private static bool ParseLineToObject(string line, Dictionary<string, int> fieldDefs, FieldInfo[] fi, PropertyInfo[] pi, object destObject, bool strict) {
string[] values = EnumerateCsvLine(line).ToArray();
bool setAny = false;
foreach(string field in fieldDefs.Keys) {
int index = fieldDefs[field];
if (index < values.Length) {
string val = values[index];
setAny = SetField(field, val, fi, pi, destObject) || setAny;
} else if (strict) {
Debug.LogWarning(string.Format("CsvUtil: error parsing line '{0}': not enough fields", line));
}
}
return setAny;
}
private static bool SetField(string fieldName, string val, FieldInfo[] fi, PropertyInfo[] pi, object destObject) {
bool result = false;
foreach (PropertyInfo p in pi) {
// Case insensitive comparison
if (string.Compare(fieldName, p.Name, true) == 0) {
// Might need to parse the string into the property type
object typedVal = p.PropertyType == typeof(string) ? val : ParseString(val, p.PropertyType);
p.SetValue(destObject, typedVal, null);
result = true;
break;
}
}
foreach(FieldInfo f in fi) {
// Case insensitive comparison
if (string.Compare(fieldName, f.Name, true) == 0) {
// Might need to parse the string into the field type
object typedVal = f.FieldType == typeof(string) ? val : ParseString(val, f.FieldType);
f.SetValue(destObject, typedVal);
result = true;
break;
}
}
return result;
}
private static object ParseString(string strValue, Type t) {
var cv = TypeDescriptor.GetConverter(t);
return cv.ConvertFromInvariantString(strValue);
}
private static IEnumerable<string> EnumerateCsvLine(string line) {
// Regex taken from http://wiki.unity3d.com/index.php?title=CSVReader
foreach(Match m in Regex.Matches(line,
@"(((?<x>(?=[,\r\n]+))|""(?<x>([^""]|"""")+)""|(?<x>[^,\r\n]+)),?)",
RegexOptions.ExplicitCapture)) {
yield return m.Groups[1].Value;
}
}
private static string RemoveSpaces(string strValue) {
return Regex.Replace(strValue, @"\s", string.Empty);
}
}
}