Skip to content

Instantly share code, notes, and snippets.

@sebastianknopf
Created January 30, 2026 20:54
Show Gist options
  • Select an option

  • Save sebastianknopf/c52bd9ebb5ec6d30fbe8cfcdab51e7ac to your computer and use it in GitHub Desktop.

Select an option

Save sebastianknopf/c52bd9ebb5ec6d30fbe8cfcdab51e7ac to your computer and use it in GitHub Desktop.
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