Created
January 30, 2026 20:54
-
-
Save sebastianknopf/c52bd9ebb5ec6d30fbe8cfcdab51e7ac to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| using System.Globalization; | |
| using System.Text; | |
| using System.Text.RegularExpressions; | |
| namespace Vdv | |
| { | |
| public class X10File | |
| { | |
| #region Static Helper Methods | |
| public static X10File ReadX10File(string filename, string nullValue = "NULL", string encoding = "utf-8", Dictionary<string, object?>? filter = null) | |
| { | |
| var x10File = new X10File(filename) | |
| { | |
| NullValue = nullValue, | |
| EncodingName = encoding | |
| }; | |
| x10File.Read(filter); | |
| return x10File; | |
| } | |
| public static X10File OpenX10File(Stream stream, string nullValue = "NULL", string encoding = "utf-8") | |
| { | |
| var x10File = new X10File(stream) | |
| { | |
| NullValue = nullValue, | |
| EncodingName = encoding | |
| }; | |
| return x10File; | |
| } | |
| public static X10File CreateX10File(string filename) | |
| { | |
| return new X10File(filename); | |
| } | |
| #endregion | |
| #region Properties | |
| public string NullValue { get; set; } = string.Empty; | |
| public string EncodingName { get; set; } = "utf-8"; | |
| public bool Strict { get; set; } = false; | |
| public string? DateFormat { get; set; } | |
| public string? TimeFormat { get; set; } | |
| public string? Representation { get; set; } | |
| public string? CreatorName { get; set; } | |
| public string? CreationDate { get; set; } | |
| public string? CreationTime { get; set; } | |
| public string? Charset { get; set; } | |
| public string? FileVersion { get; set; } | |
| public string? InterfaceVersion { get; set; } | |
| public string? DataVersion { get; set; } | |
| public string? FileFormat { get; set; } | |
| public string? TableName { get; set; } | |
| public List<string> Attributes { get; private set; } = new(); | |
| public List<Dictionary<string, string?>> Datatypes { get; private set; } = new(); | |
| public List<Dictionary<string, object?>> Records { get; private set; } = new(); | |
| private string? FileName = null; | |
| private Stream? FileStream = null; | |
| #endregion | |
| public X10File(string? filename = null) | |
| { | |
| this.InternalInit(); | |
| this.FileName = filename; | |
| } | |
| public X10File(Stream stream) | |
| { | |
| this.InternalInit(); | |
| this.FileStream = stream; | |
| } | |
| #region Private Methods | |
| private void InternalInit() | |
| { | |
| this.DateFormat = null; | |
| this.TimeFormat = null; | |
| this.Representation = null; | |
| this.CreatorName = null; | |
| this.CreationDate = null; | |
| this.CreationTime = null; | |
| this.Charset = null; | |
| this.FileVersion = null; | |
| this.InterfaceVersion = null; | |
| this.DataVersion = null; | |
| this.FileFormat = null; | |
| this.TableName = null; | |
| this.Attributes = new List<string>(); | |
| this.Datatypes = new List<Dictionary<string, string?>>(); | |
| this.Records = new List<Dictionary<string, object?>>(); | |
| } | |
| #endregion | |
| #region Helper Methods | |
| private string EscapeValue(object? value, Type? dtype = null) | |
| { | |
| if (dtype == typeof(string)) | |
| { | |
| return $"\"{value}\""; | |
| } | |
| else | |
| { | |
| return $"{value}"; | |
| } | |
| } | |
| private Type DTypeOfFStr(string fstr, string? fsize) | |
| { | |
| if (fstr == "char") | |
| { | |
| return typeof(string); | |
| } | |
| else if (fstr == "boolean") | |
| { | |
| return typeof(bool); | |
| } | |
| else if (fstr == "num" && fsize != null && fsize.Contains(".")) | |
| { | |
| int decimals = int.Parse(fsize.Split('.')[1]); | |
| if (decimals > 0) | |
| { | |
| return typeof(double); | |
| } | |
| else | |
| { | |
| return typeof(int); | |
| } | |
| } | |
| else if (fstr == "num") | |
| { | |
| return typeof(int); | |
| } | |
| else | |
| { | |
| return typeof(int); | |
| } | |
| } | |
| private string FStrOfDType(Type dtype) | |
| { | |
| if (dtype == typeof(string)) | |
| { | |
| return "char"; | |
| } | |
| else if (dtype == typeof(bool)) | |
| { | |
| return "boolean"; | |
| } | |
| else | |
| { | |
| return "num"; | |
| } | |
| } | |
| private Dictionary<string, object?> CreateCompareRecord(Dictionary<string, object?> record, IEnumerable<string>? primaryKey) | |
| { | |
| if (primaryKey != null) | |
| { | |
| var compareRecord = new Dictionary<string, object?>(); | |
| var pkSet = new HashSet<string>(primaryKey); | |
| foreach (var kv in record) | |
| { | |
| if (pkSet.Contains(kv.Key)) | |
| { | |
| compareRecord[kv.Key] = kv.Value; | |
| } | |
| } | |
| return compareRecord; | |
| } | |
| else | |
| { | |
| return record; | |
| } | |
| } | |
| private static string Trim(string s) | |
| { | |
| return s.Trim().Trim('"'); | |
| } | |
| private static List<string> ParseCsvLine(string line) | |
| { | |
| return line.Split(';').ToList(); | |
| } | |
| private static bool DictionaryEquals(Dictionary<string, object?> a, Dictionary<string, object?> b) | |
| { | |
| if (a.Count != b.Count) | |
| { | |
| return false; | |
| } | |
| foreach (var kv in a) | |
| { | |
| if (!b.TryGetValue(kv.Key, out var val)) | |
| { | |
| return false; | |
| } | |
| if (!Equals(kv.Value, val)) | |
| { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| #endregion | |
| #region Reading Methods | |
| public IEnumerable<Dictionary<string, object?>> Stream() | |
| { | |
| if (this.FileName == null && this.FileStream == null) | |
| { | |
| yield break; | |
| } | |
| if (this.FileStream == null) | |
| { | |
| this.FileStream = File.OpenRead(this.FileName!); | |
| } | |
| var encoding = Encoding.GetEncoding(this.EncodingName); | |
| using (var reader = new StreamReader(this.FileStream, encoding)) | |
| { | |
| string? line; | |
| while ((line = reader.ReadLine()) != null) | |
| { | |
| var row = ParseCsvLine(line); | |
| if (row.Count == 0) | |
| { | |
| continue; | |
| } | |
| switch (row[0]) | |
| { | |
| case "mod": | |
| this.DateFormat = Trim(row[1]); | |
| this.TimeFormat = Trim(row[2]); | |
| this.Representation = Trim(row[3]); | |
| break; | |
| case "src": | |
| this.CreatorName = Trim(row[1]); | |
| this.CreationDate = Trim(row[2]); | |
| this.CreationTime = Trim(row[3]); | |
| break; | |
| case "chs": | |
| this.Charset = Trim(row[1]); | |
| break; | |
| case "ver": | |
| this.FileVersion = Trim(row[1]); | |
| break; | |
| case "ifv": | |
| this.InterfaceVersion = Trim(row[1]); | |
| break; | |
| case "dve": | |
| this.DataVersion = Trim(row[1]); | |
| break; | |
| case "fft": | |
| this.FileFormat = Trim(row[1]); | |
| break; | |
| case "tbl": | |
| this.TableName = Trim(row[1]); | |
| break; | |
| case "atr": | |
| this.Attributes = row.Skip(1).Select(Trim).ToList(); | |
| break; | |
| case "frm": | |
| this.Datatypes.Clear(); | |
| foreach (var val in row.Skip(1)) | |
| { | |
| var parts = Regex.Split(Trim(val), @"[\[\]]"); | |
| if (parts.Length > 1) | |
| { | |
| this.Datatypes.Add(new Dictionary<string, string?> | |
| { | |
| ["type"] = parts[0], | |
| ["size"] = parts[1] | |
| }); | |
| } | |
| else | |
| { | |
| this.Datatypes.Add(new Dictionary<string, string?> | |
| { | |
| ["type"] = parts[0], | |
| ["size"] = null | |
| }); | |
| } | |
| } | |
| break; | |
| case "rec": | |
| var record = new Dictionary<string, object?>(); | |
| for (int i = 0; i < row.Count - 1; i++) | |
| { | |
| var raw = Trim(row[i + 1]); | |
| var dtype = this.DTypeOfFStr(this.Datatypes[i]["type"]!, this.Datatypes[i]["size"]); | |
| if (dtype == typeof(string)) | |
| { | |
| record[this.Attributes[i]] = raw; | |
| } | |
| else if (dtype == typeof(int)) | |
| { | |
| if (!raw.Equals(this.NullValue) && !raw.Equals(string.Empty)) | |
| { | |
| record[this.Attributes[i]] = int.Parse(raw); | |
| } | |
| else | |
| { | |
| record[this.Attributes[i]] = null; | |
| } | |
| } | |
| else if (dtype == typeof(double) && !raw.Equals(this.NullValue)) | |
| { | |
| if (!raw.Equals(this.NullValue) && !raw.Equals(string.Empty)) | |
| { | |
| record[this.Attributes[i]] = double.Parse(raw, CultureInfo.InvariantCulture); | |
| } | |
| else | |
| { | |
| record[this.Attributes[i]] = null; | |
| } | |
| } | |
| else | |
| { | |
| record[this.Attributes[i]] = raw; | |
| } | |
| } | |
| yield return record; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| public void Read(Dictionary<string, object?>? filter = null) | |
| { | |
| foreach (var record in this.Stream()) | |
| { | |
| if (filter != null) | |
| { | |
| var filtered = this.CreateCompareRecord(record, filter.Keys); | |
| if (DictionaryEquals(filtered, filter)) | |
| { | |
| this.Records.Add(record); | |
| } | |
| } | |
| else | |
| { | |
| this.Records.Add(record); | |
| } | |
| } | |
| } | |
| #endregion | |
| #region Writing Methods | |
| public void Write(string? filename = null) | |
| { | |
| if (filename == null) | |
| { | |
| filename = this.FileName; | |
| } | |
| if (filename == null) | |
| { | |
| throw new InvalidOperationException("Filename must be set."); | |
| } | |
| var encoding = Encoding.GetEncoding(this.EncodingName); | |
| using (var writer = new StreamWriter(filename, false, encoding)) | |
| { | |
| void WriteLine(params object[] parts) | |
| { | |
| writer.WriteLine(string.Join(";", parts)); | |
| } | |
| // mod | |
| WriteLine("mod", this.EscapeValue(this.DateFormat), this.EscapeValue(this.TimeFormat), this.EscapeValue(this.Representation)); | |
| // src | |
| WriteLine("src", this.EscapeValue(this.CreatorName), this.EscapeValue(this.CreationDate), this.EscapeValue(this.CreationTime)); | |
| // other headers | |
| WriteLine("chs", this.EscapeValue(this.Charset)); | |
| WriteLine("ver", this.EscapeValue(this.FileVersion)); | |
| WriteLine("ifv", this.EscapeValue(this.InterfaceVersion)); | |
| WriteLine("dve", this.EscapeValue(this.DataVersion)); | |
| WriteLine("fft", this.EscapeValue(this.FileFormat)); | |
| // tbl | |
| WriteLine(); | |
| WriteLine("tbl", this.EscapeValue(this.TableName)); | |
| // attributes | |
| var attrRow = new List<object?> { "atr" }; | |
| foreach (var attr in this.Attributes) | |
| { | |
| attrRow.Add(this.EscapeValue(attr)); | |
| } | |
| WriteLine(attrRow.ToArray()); | |
| // datatypes | |
| var dtypeRow = new List<object?> { "frm" }; | |
| foreach (var dtype in this.Datatypes) | |
| { | |
| string dtypeValue = dtype["size"] != null ? $"{dtype["type"]}[{dtype["size"]}]" : dtype["type"]!; | |
| dtypeRow.Add(this.EscapeValue(dtypeValue)); | |
| } | |
| WriteLine(dtypeRow.ToArray()); | |
| // records | |
| foreach (var record in this.Records) | |
| { | |
| var recRow = new List<object?> { "rec" }; | |
| foreach (var key in this.Attributes) | |
| { | |
| int index = this.Attributes.IndexOf(key); | |
| var dtype = this.DTypeOfFStr(this.Datatypes[index]["type"]!, this.Datatypes[index]["size"]); | |
| recRow.Add(this.EscapeValue(record[key], dtype)); | |
| } | |
| WriteLine(recRow.ToArray()); | |
| } | |
| // end | |
| WriteLine("end", this.EscapeValue(this.Records.Count, typeof(int))); | |
| WriteLine("eof", this.EscapeValue(1, typeof(int))); | |
| } | |
| } | |
| #endregion | |
| #region Modification Methods | |
| public void AddColumn(string cname, Type dtype, int dsize, object defaultValue = null!) | |
| { | |
| this.Attributes.Add(cname); | |
| string fstr = this.FStrOfDType(dtype); | |
| string? fsize = dtype == typeof(string) ? dsize.ToString() : $"{dsize}.0"; | |
| this.Datatypes.Add(new Dictionary<string, string?> { ["type"] = fstr, ["size"] = fsize }); | |
| foreach (var record in this.Records) | |
| { | |
| record[cname] = defaultValue; | |
| } | |
| } | |
| public void RemoveColumn(string cname) | |
| { | |
| int index = this.Attributes.IndexOf(cname); | |
| this.Attributes.RemoveAt(index); | |
| this.Datatypes.RemoveAt(index); | |
| foreach (var record in this.Records) | |
| { | |
| record.Remove(cname); | |
| } | |
| } | |
| public void AddRecord(Dictionary<string, object?> rdata, IEnumerable<string>? primaryKey = null) | |
| { | |
| bool recordExists = false; | |
| var pkRecord = this.CreateCompareRecord(rdata, primaryKey); | |
| for (int i = 0; i < this.Records.Count; i++) | |
| { | |
| var compare = this.CreateCompareRecord(this.Records[i], primaryKey); | |
| if (DictionaryEquals(pkRecord, compare)) | |
| { | |
| recordExists = true; | |
| break; | |
| } | |
| } | |
| if (!recordExists) | |
| { | |
| this.Records.Add(rdata); | |
| } | |
| } | |
| public void RemoveRecords(Dictionary<string, object?> rdata, IEnumerable<string>? primaryKey = null) | |
| { | |
| var newRecords = new List<Dictionary<string, object?>>(); | |
| var target = this.CreateCompareRecord(rdata, primaryKey); | |
| foreach (var rec in this.Records) | |
| { | |
| var compare = this.CreateCompareRecord(rec, primaryKey); | |
| if (!DictionaryEquals(target, compare)) | |
| { | |
| newRecords.Add(rec); | |
| } | |
| } | |
| this.Records = newRecords; | |
| } | |
| public List<Dictionary<string, object?>> FindRecords(Dictionary<string, object?> rdata, IEnumerable<string>? primaryKey = null) | |
| { | |
| var result = new List<Dictionary<string, object?>>(); | |
| var target = this.CreateCompareRecord(rdata, primaryKey); | |
| foreach (var rec in this.Records) | |
| { | |
| var compare = this.CreateCompareRecord(rec, primaryKey); | |
| if (DictionaryEquals(target, compare)) | |
| { | |
| result.Add(rec); | |
| } | |
| } | |
| return result; | |
| } | |
| public Dictionary<string, object?>? FindRecord(Dictionary<string, object?> rdata, IEnumerable<string>? primaryKey = null) | |
| { | |
| var target = this.CreateCompareRecord(rdata, primaryKey); | |
| foreach (var rec in this.Records) | |
| { | |
| var compare = this.CreateCompareRecord(rec, primaryKey); | |
| if (DictionaryEquals(target, compare)) | |
| { | |
| return rec; | |
| } | |
| } | |
| return null; | |
| } | |
| public void ReplaceForeignKeys(List<string> foreignKeyColumns, Dictionary<string, object?> replMap) | |
| { | |
| for (int i = 0; i < this.Records.Count; i++) | |
| { | |
| var rec = this.Records[i]; | |
| var newRec = new Dictionary<string, object?>(rec); | |
| bool updated = false; | |
| foreach (var fk in foreignKeyColumns) | |
| { | |
| if (rec.ContainsKey(fk) && replMap.ContainsKey(rec[fk]?.ToString())) | |
| { | |
| newRec[fk] = replMap[rec[fk]?.ToString()]; | |
| updated = true; | |
| } | |
| } | |
| if (updated) | |
| { | |
| this.Records[i] = newRec; | |
| } | |
| } | |
| } | |
| #endregion | |
| #region Other Methods | |
| public void Close() | |
| { | |
| this.Records.Clear(); | |
| this.InternalInit(); | |
| } | |
| #endregion | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment