diff options
| author | Roy Ben Shabat <roy.mail.net@gmail.com> | 2025-09-12 17:39:45 +0300 |
|---|---|---|
| committer | Roy Ben Shabat <roy.mail.net@gmail.com> | 2025-09-12 17:39:45 +0300 |
| commit | 7eb361c1201381c6ad88efa0ebed2c6595b45d13 (patch) | |
| tree | 005c5e210d9352d3b26cbb8ab1f80139279b1898 /Software/Visual_Studio/Tango.CSV | |
| parent | 8e15f292e2950cac71282923adc357f2abf8b306 (diff) | |
| download | Tango-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.cs | 310 | ||||
| -rw-r--r-- | Software/Visual_Studio/Tango.CSV/CsvDynamicWriter.cs | 20 | ||||
| -rw-r--r-- | Software/Visual_Studio/Tango.CSV/Tango.CSV.csproj | 1 |
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" /> |
