aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Tango.CSV
diff options
context:
space:
mode:
authorRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-12 17:39:45 +0300
committerRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-12 17:39:45 +0300
commit7eb361c1201381c6ad88efa0ebed2c6595b45d13 (patch)
tree005c5e210d9352d3b26cbb8ab1f80139279b1898 /Software/Visual_Studio/Tango.CSV
parent8e15f292e2950cac71282923adc357f2abf8b306 (diff)
downloadTango-7eb361c1201381c6ad88efa0ebed2c6595b45d13.tar.gz
Tango-7eb361c1201381c6ad88efa0ebed2c6595b45d13.zip
Fixed FSE Gateway service with production slot cookie.
Implemented FSE dynamic csv job upload. extra inks. Implemented PPC dynamic csv job read. extra inks.
Diffstat (limited to 'Software/Visual_Studio/Tango.CSV')
-rw-r--r--Software/Visual_Studio/Tango.CSV/CsvDynamicReader.cs310
-rw-r--r--Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs20
-rw-r--r--Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj1
3 files changed, 331 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Tango.CSV/CsvDynamicReader.cs b/Software/Visual_Studio/Tango.CSV/CsvDynamicReader.cs
new file mode 100644
index 000000000..33ba859eb
--- /dev/null
+++ b/Software/Visual_Studio/Tango.CSV/CsvDynamicReader.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace Tango.CSV
+{
+ /// <summary>
+ /// CSV reader that supports unknown schemas and typed access with defaults.
+ /// - Case-insensitive column lookup
+ /// - Robust CSV parsing (RFC-4180 style: quotes, commas, escaped quotes)
+ /// - Typed Read with defaults: string, int, double, bool, and enums
+ /// </summary>
+ public sealed class CsvDynamicReader
+ {
+ private readonly Dictionary<string, int> _colIndex;
+ private readonly List<Row> _rows;
+
+ public IReadOnlyList<Row> Rows => _rows;
+
+ public char Delimiter { get; }
+
+ public CsvDynamicReader(string path, char delimiter = ',')
+ {
+ if (path == null) throw new ArgumentNullException(nameof(path));
+ if (!File.Exists(path)) throw new FileNotFoundException("CSV file not found.", path);
+
+ Delimiter = delimiter;
+ _colIndex = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
+ _rows = new List<Row>();
+
+ using (var sr = new StreamReader(path))
+ {
+ // Read to first non-empty line for headers
+ string headerLine;
+ do
+ {
+ headerLine = sr.ReadLine();
+ if (headerLine == null)
+ throw new InvalidDataException("CSV file has no header row.");
+ } while (string.IsNullOrWhiteSpace(headerLine));
+
+ var headers = ParseCsvLine(headerLine, Delimiter);
+ for (int i = 0; i < headers.Count; i++)
+ {
+ var clean = CleanHeader(headers[i]);
+ if (!_colIndex.ContainsKey(clean))
+ _colIndex.Add(clean, i);
+ // If duplicate header name appears, first one wins.
+ }
+
+ // Read all rows
+ string line;
+ while ((line = sr.ReadLine()) != null)
+ {
+ if (line.Length == 0) continue; // skip empty
+ var fields = ParseCsvLine(line, Delimiter).ToArray();
+ _rows.Add(new Row(this, fields));
+ }
+ }
+ }
+
+ internal bool TryGetIndex(string columnName, out int index)
+ {
+ if (columnName == null) { index = -1; return false; }
+ return _colIndex.TryGetValue(columnName.Trim(), out index);
+ }
+
+ private static string CleanHeader(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return string.Empty;
+ // Trim quotes if header was quoted
+ var t = s.Trim();
+ if (t.Length >= 2 && t[0] == '"' && t[t.Length - 1] == '"')
+ {
+ t = t.Substring(1, t.Length - 2).Replace("\"\"", "\"");
+ }
+ // Remove BOM if present
+ t = t.Trim('\uFEFF').Trim();
+ return t;
+ }
+
+ /// <summary>
+ /// RFC-4180-ish CSV line parser: supports quoted fields, commas, escaped quotes ("").
+ /// </summary>
+ private static List<string> ParseCsvLine(string line, char delimiter)
+ {
+ var result = new List<string>();
+ if (line == null)
+ {
+ result.Add(string.Empty);
+ return result;
+ }
+
+ var sb = new System.Text.StringBuilder(line.Length);
+ bool inQuotes = false;
+
+ for (int i = 0; i < line.Length; i++)
+ {
+ var c = line[i];
+
+ if (inQuotes)
+ {
+ if (c == '"')
+ {
+ // Escaped quote?
+ if (i + 1 < line.Length && line[i + 1] == '"')
+ {
+ sb.Append('"');
+ i++; // skip next
+ }
+ else
+ {
+ inQuotes = false;
+ }
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ else
+ {
+ if (c == '"')
+ {
+ inQuotes = true;
+ }
+ else if (c == delimiter)
+ {
+ result.Add(sb.ToString());
+ sb.Clear();
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ }
+
+ result.Add(sb.ToString());
+ return result;
+ }
+
+ // ------- Row --------
+
+ public sealed class Row
+ {
+ private readonly CsvDynamicReader _reader;
+ private readonly string[] _values;
+
+ internal Row(CsvDynamicReader reader, string[] values)
+ {
+ _reader = reader;
+ _values = values ?? new string[0];
+ }
+
+ public bool Exists(string columnName)
+ {
+ int idx;
+ return _reader.TryGetIndex(columnName, out idx);
+ }
+
+ /// <summary>
+ /// Typed read with a default fallback. Works for string, int, double, bool, and enums.
+ /// Usage: var v = row.Read("Col", 0); var s = row.Read("Col","def"); var b = row.Read("Flag", false);
+ /// </summary>
+ public T Read<T>(string columnName, T defaultValue)
+ {
+ int idx;
+ if (!_reader.TryGetIndex(columnName, out idx))
+ return defaultValue;
+
+ var raw = (idx >= 0 && idx < _values.Length) ? _values[idx] : null;
+ return ConvertValue(raw, defaultValue);
+ }
+
+ // -------- Conversion helpers --------
+
+ private static T ConvertValue<T>(string raw, T defaultValue)
+ {
+ if (typeof(T) == typeof(string))
+ {
+ // For strings return raw as-is (trim optional)
+ object s = raw ?? (object)defaultValue ?? string.Empty;
+ return (T)s;
+ }
+
+ if (string.IsNullOrWhiteSpace(raw))
+ return defaultValue;
+
+ var trimmed = raw.Trim();
+
+ // int
+ if (typeof(T) == typeof(int))
+ {
+ int v;
+ if (int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out v))
+ return (T)(object)v;
+ return defaultValue;
+ }
+
+ // double
+ if (typeof(T) == typeof(double))
+ {
+ double v;
+ if (double.TryParse(trimmed, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out v))
+ return (T)(object)v;
+ return defaultValue;
+ }
+
+ // bool (accepts true/false, 1/0, yes/no, y/n)
+ if (typeof(T) == typeof(bool))
+ {
+ bool bv;
+ if (TryParseBool(trimmed, out bv))
+ return (T)(object)bv;
+ return defaultValue;
+ }
+
+ // Enums (case-insensitive; also accept underlying numeric)
+ var t = typeof(T);
+ if (t.IsEnum)
+ {
+ try
+ {
+ // Numeric?
+ var underlying = Enum.GetUnderlyingType(t);
+ object numericObj;
+ if (TryParseNumeric(trimmed, underlying, out numericObj))
+ {
+ var boxed = Enum.ToObject(t, numericObj);
+ return (T)boxed;
+ }
+
+ T parsed;
+ if (TryParseEnum(trimmed, out parsed))
+ return parsed;
+ }
+ catch { /* fall through to default */ }
+
+ return defaultValue;
+ }
+
+ // Fallback: try ChangeType
+ try
+ {
+ object any = System.Convert.ChangeType(trimmed, typeof(T), CultureInfo.InvariantCulture);
+ return (T)any;
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ private static bool TryParseBool(string s, out bool value)
+ {
+ // Standard
+ if (bool.TryParse(s, out value)) return true;
+
+ // Common variants
+ switch (s.Trim().ToLowerInvariant())
+ {
+ case "1":
+ case "yes":
+ case "y":
+ case "true":
+ case "t":
+ value = true; return true;
+ case "0":
+ case "no":
+ case "n":
+ case "false":
+ case "f":
+ value = false; return true;
+ }
+ value = false; return false;
+ }
+
+ private static bool TryParseNumeric(string s, Type numericType, out object boxed)
+ {
+ if (numericType == typeof(byte)) { byte v; if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(sbyte)) { sbyte v; if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(short)) { short v; if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(ushort)) { ushort v; if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(int)) { int v; if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(uint)) { uint v; if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(long)) { long v; if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+ if (numericType == typeof(ulong)) { ulong v; if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
+
+ boxed = null;
+ return false;
+ }
+
+ private static bool TryParseEnum<T>(string s, out T value)
+ {
+ try
+ {
+ value = (T)Enum.Parse(typeof(T), s, ignoreCase: true);
+ return true;
+ }
+ catch
+ {
+ value = default(T);
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs b/Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs
index 1c460d5e3..70024954e 100644
--- a/Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs
+++ b/Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs
@@ -24,6 +24,26 @@ namespace Tango.CSV
private readonly List<Dictionary<string, string>> _rows = new List<Dictionary<string, string>>();
private Dictionary<string, string> _currentRow = new Dictionary<string, string>();
+ public void Write(int value, string columnName, int defaultValue, int index)
+ {
+ Write(value.ToString(), columnName, defaultValue.ToString(), index);
+ }
+
+ public void Write(double value, string columnName, double defaultValue, int index)
+ {
+ Write(value.ToString(), columnName, defaultValue.ToString(), index);
+ }
+
+ public void Write(bool value, string columnName, bool defaultValue, int index)
+ {
+ Write(value.ToString(), columnName, defaultValue.ToString(), index);
+ }
+
+ public void Write(Enum value, string columnName, Enum defaultValue, int index)
+ {
+ Write(value.ToString(), columnName, defaultValue.ToString(), index);
+ }
+
/// <summary>
/// Writes a value to the specified column in the current row.
/// If the column does not exist, it is created with the given default value and index.
diff --git a/Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj b/Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj
index 0fc1948a4..5674c1c56 100644
--- a/Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj
+++ b/Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj
@@ -90,6 +90,7 @@
<Compile Include="CsvSource.cs" />
<Compile Include="DynamicCsvFile.cs" />
<Compile Include="DynamicCsvFileColumn.cs" />
+ <Compile Include="CsvDynamicReader.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />