forked from Insprill/dv-multiplayer
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Now ignores blank/whitespace keys
- Loading branch information
Showing
1 changed file
with
111 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,128 +1,152 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.Collections.Specialized; | ||
using System.Linq; | ||
using System.Text; | ||
|
||
namespace Multiplayer.Utils; | ||
|
||
public static class Csv | ||
namespace Multiplayer.Utils | ||
{ | ||
/// <summary> | ||
/// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. | ||
/// </summary> | ||
public static ReadOnlyDictionary<string, Dictionary<string, string>> Parse(string data) | ||
public static class Csv | ||
{ | ||
string[] lines = data.Split('\n'); | ||
/// <summary> | ||
/// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. | ||
/// </summary> | ||
public static ReadOnlyDictionary<string, Dictionary<string, string>> Parse(string data) | ||
{ | ||
// Split the input data into lines | ||
string[] separators = new string[] { "\r\n" }; | ||
string[] lines = data.Split(separators, StringSplitOptions.None); | ||
|
||
// Dictionary<string, Dictionary<string, string>> | ||
OrderedDictionary columns = new(lines.Length - 1); | ||
// Use an OrderedDictionary to preserve the insertion order of keys | ||
var columns = new OrderedDictionary(); | ||
|
||
List<string> keys = ParseLine(lines[0]); | ||
foreach (string key in keys) | ||
columns.Add(key, new Dictionary<string, string>()); | ||
// Parse the header line to get the column keys | ||
List<string> keys = ParseLine(lines[0]); | ||
foreach (string key in keys) | ||
{ | ||
if (!string.IsNullOrWhiteSpace(key)) | ||
columns.Add(key, new Dictionary<string, string>()); | ||
} | ||
|
||
for (int i = 1; i < lines.Length; i++) | ||
{ | ||
string line = lines[i]; | ||
List<string> values = ParseLine(line); | ||
if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) | ||
continue; | ||
string key = values[0]; | ||
for (int j = 0; j < values.Count; j++) | ||
((Dictionary<string, string>)columns[j]).Add(key, values[j]); | ||
} | ||
// Iterate through the remaining lines (rows) | ||
for (int i = 1; i < lines.Length; i++) | ||
{ | ||
string line = lines[i]; | ||
List<string> values = ParseLine(line); | ||
if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) | ||
continue; | ||
|
||
return new ReadOnlyDictionary<string, Dictionary<string, string>>(columns.Cast<DictionaryEntry>() | ||
.ToDictionary(entry => (string)entry.Key, entry => (Dictionary<string, string>)entry.Value)); | ||
} | ||
string rowKey = values[0]; | ||
|
||
private static List<string> ParseLine(string line) | ||
{ | ||
bool inQuotes = false; | ||
bool wasBackslash = false; | ||
List<string> values = new(); | ||
StringBuilder builder = new(); | ||
// Add the row values to the appropriate column dictionaries | ||
for (int j = 0; j < values.Count && j < keys.Count; j++) | ||
{ | ||
string columnKey = keys[j]; | ||
if (!string.IsNullOrWhiteSpace(columnKey)) | ||
{ | ||
var columnDict = (Dictionary<string, string>)columns[columnKey]; | ||
columnDict[rowKey] = values[j]; | ||
} | ||
} | ||
} | ||
|
||
void FinishLine() | ||
{ | ||
values.Add(builder.ToString()); | ||
builder.Clear(); | ||
// Convert the OrderedDictionary to a ReadOnlyDictionary | ||
return new ReadOnlyDictionary<string, Dictionary<string, string>>( | ||
columns.Cast<DictionaryEntry>() | ||
.ToDictionary(entry => (string)entry.Key, entry => (Dictionary<string, string>)entry.Value) | ||
); | ||
} | ||
|
||
foreach (char c in line) | ||
private static List<string> ParseLine(string line) | ||
{ | ||
if (c == '\n' || (!inQuotes && c == ',')) | ||
{ | ||
FinishLine(); | ||
continue; | ||
} | ||
bool inQuotes = false; | ||
bool wasBackslash = false; | ||
List<string> values = new(); | ||
StringBuilder builder = new(); | ||
|
||
switch (c) | ||
void FinishValue() | ||
{ | ||
case '\r': | ||
Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); | ||
continue; | ||
case '"': | ||
inQuotes = !inQuotes; | ||
continue; | ||
case '\\': | ||
wasBackslash = true; | ||
continue; | ||
values.Add(builder.ToString()); | ||
builder.Clear(); | ||
} | ||
|
||
if (wasBackslash) | ||
foreach (char c in line) | ||
{ | ||
wasBackslash = false; | ||
if (c == 'n') | ||
if (c == ',' && !inQuotes) | ||
{ | ||
builder.Append('\n'); | ||
FinishValue(); | ||
continue; | ||
} | ||
|
||
// Not a special character, so just append the backslash | ||
builder.Append('\\'); | ||
} | ||
switch (c) | ||
{ | ||
case '\r': | ||
Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); | ||
continue; | ||
case '"': | ||
inQuotes = !inQuotes; | ||
continue; | ||
case '\\': | ||
wasBackslash = true; | ||
continue; | ||
} | ||
|
||
builder.Append(c); | ||
} | ||
if (wasBackslash) | ||
{ | ||
wasBackslash = false; | ||
if (c == 'n') | ||
{ | ||
builder.Append('\n'); | ||
continue; | ||
} | ||
|
||
// Not a special character, so just append the backslash | ||
builder.Append('\\'); | ||
} | ||
|
||
if (builder.Length > 0) | ||
FinishLine(); | ||
builder.Append(c); | ||
} | ||
|
||
return values; | ||
} | ||
if (builder.Length > 0) | ||
FinishValue(); | ||
|
||
public static string Dump(ReadOnlyDictionary<string, Dictionary<string, string>> data) | ||
{ | ||
StringBuilder result = new("\n"); | ||
return values; | ||
} | ||
|
||
foreach (KeyValuePair<string, Dictionary<string, string>> column in data) | ||
result.Append($"{column.Key},"); | ||
public static string Dump(ReadOnlyDictionary<string, Dictionary<string, string>> data) | ||
{ | ||
StringBuilder result = new("\n"); | ||
|
||
result.Remove(result.Length - 1, 1); | ||
result.Append('\n'); | ||
foreach (KeyValuePair<string, Dictionary<string, string>> column in data) | ||
result.Append($"{column.Key},"); | ||
|
||
result.Remove(result.Length - 1, 1); | ||
result.Append('\n'); | ||
|
||
int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; | ||
int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; | ||
|
||
for (int i = 0; i < rowCount; i++) | ||
{ | ||
foreach (KeyValuePair<string, Dictionary<string, string>> column in data) | ||
if (column.Value.Count > i) | ||
{ | ||
string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); | ||
result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); | ||
} | ||
else | ||
for (int i = 0; i < rowCount; i++) | ||
{ | ||
foreach (KeyValuePair<string, Dictionary<string, string>> column in data) | ||
{ | ||
result.Append(','); | ||
if (column.Value.Count > i) | ||
{ | ||
string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); | ||
result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); | ||
} | ||
else | ||
{ | ||
result.Append(','); | ||
} | ||
} | ||
|
||
result.Remove(result.Length - 1, 1); | ||
result.Append('\n'); | ||
} | ||
result.Remove(result.Length - 1, 1); | ||
result.Append('\n'); | ||
} | ||
|
||
return result.ToString(); | ||
return result.ToString(); | ||
} | ||
} | ||
} |