Compare commits

..

5 Commits

Author SHA1 Message Date
2b9bcd1a6d Fix unit name serialization
We just store the unit name as specified and serialize that (instead of using the name as given in UnitsNet)
2024-03-15 23:15:35 -07:00
78574d1872 Add basic implementation of physical units
Serde still isn't working, need to store the exact format of the unit as specified in the attribute
2024-03-15 18:58:07 -07:00
77b679bbdc Remove old ExtraTsv stuff 2024-03-10 23:26:42 -07:00
0807f0ffc2 Add iso8601 parsing 2024-03-10 22:43:21 -07:00
71af3653c4 Change column type management
The column types were tracked just as a Type. This changes them to be an instance
so they can track additional information (such as the specific units of a physical
units type). Becuase of this, the column type attributes need the type to be passed as strings.
(see CS0181).
2024-03-10 22:35:30 -07:00
9 changed files with 516 additions and 1056 deletions

View File

@ -1,125 +0,0 @@

using System.Globalization;
using System.Text.RegularExpressions;
namespace NathanMcRae;
public class ExtraTsv : SaneTsv
{
public class Iso8601Type : ColumnType { }
public class PhysicalUnitsType : ColumnType
{
public string Units { get; }
public PhysicalUnitsType(string Units) { }
}
public static readonly string[] ValidUnits =
{
"m",
"s",
"A",
"K",
"cd",
"mol",
"kg",
"Hz",
"rad",
"sr",
"N",
"Pa",
"J",
"W",
"C",
"V",
"F",
"Ω",
"S",
"Wb",
"T",
"H",
"°C",
"lm",
"lx",
"Bq",
"Gy",
"Sv",
"kat"
};
public static readonly int MajorVersion = 0;
public static readonly int MinorVersion = 0;
public static readonly int PatchVersion = 1;
public static Regex VersionRegex = new Regex(@"^ ExtraTSV V(\d+)\.(\d+)\.(\d+)");
public static ExtraTsv ParseExtraTsv(byte[] inputBuffer)
{
SaneTsv tsv = ParseCommentedTsv(inputBuffer);
if (tsv.FileComment == null) {
throw new Exception($"ExtraTSV expects the file to start with '# ExtraTSV Vx.y.z' where x.y.z is a version compatible with {MajorVersion}.{MinorVersion}.{PatchVersion}");
}
Match match = VersionRegex.Match(tsv.FileComment);
if (!match.Success)
{
throw new Exception($"ExtraTSV expects the file to start with '# ExtraTSV Vx.y.z' where x.y.z is a version compatible with {MajorVersion}.{MinorVersion}.{PatchVersion}");
}
int fileMajorVersion = int.Parse(match.Groups[1].Value);
if (fileMajorVersion != MajorVersion)
{
throw new Exception($"File has major version ({fileMajorVersion}) which is newer than this parser's version {MajorVersion}");
}
for (int i = 0; i < tsv.ColumnNames.Count(); i++)
{
string[] typeParts = tsv.ColumnNames[i].Split(":");
if (typeParts[typeParts.Length - 1] == "iso8601" && tsv.ColumnTypes[i] == typeof(StringType))
{
string columnName = tsv.ColumnNames[i].Substring(0, tsv.ColumnNames[i].Length - ":iso8601".Length);
tsv.ColumnNames[i] = columnName;
tsv.ColumnTypes[i] = typeof(Iso8601Type);
}
// TODO: ISO8601 time spans
// TODO: ISO8601 time durations
else if (typeParts[typeParts.Length - 1] == "units" && (tsv.ColumnTypes[i] == typeof(Float64Type) || tsv.ColumnTypes[i] == typeof(Float32Type)))
{
if (typeParts.Count() > 1 && ValidUnits.Contains(typeParts[typeParts.Length - 2]))
{
// TODO: How to store type information since the ColumnTypes is of type Type?
}
else
{
throw new Exception($"Invalid units type '{typeParts[typeParts.Length - 2]}' for column {i}");
}
}
}
CultureInfo provider = CultureInfo.InvariantCulture;
for (int i = 0; i < tsv.Records.Count; i++)
{
if (tsv.Records[i].Comment != null)
{
throw new Exception($"Line {tsv.Records[i].Line} has comment above it which is not allowed");
}
for (int j = 0; j < tsv.ColumnNames.Count(); j++)
{
if (tsv.ColumnTypes[j] == typeof(Iso8601Type))
{
if (!DateTime.TryParseExact((string)tsv.Records[i][j], "yyyy-MM-ddTHH:mm:ss.ffff", provider, DateTimeStyles.None, out DateTime parsed))
{
throw new Exception($"ISO 8601 timestamp format error on line {tsv.Records[i].Line}, field {j}");
}
tsv.Records[i].Fields[j] = parsed;
}
}
}
return (ExtraTsv)tsv;
}
}

View File

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>NathanMcRae</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SaneTsv.csproj" />
</ItemGroup>
</Project>

View File

@ -1,44 +0,0 @@
Extra TSV adds many convenience types to Sane TSV:
- Timestamps
Just this format for now: yyyy-MM-ddTHH:mm:ss.ffff
- Timespans
- Time durations
- Multiformats
- Multihashes
- Multiprotocols
- ...
- Physical units
To start with, just use SI base and derived units
- Base units
- m
- s
- A
- K
- cd
- mol
- kg
- Derived units
- Hz
- rad
- sr
- N
- Pa
- J
- W
- C
- V
- F
- Ω
- S
- Wb
- T
- H
- °C
- lm
- lx
- Bq
- Gy
- Sv
- kat
How to handle derived units?

View File

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ExtraTsv\ExtraTsv.csproj" />
</ItemGroup>
</Project>

View File

@ -1,20 +0,0 @@
using NathanMcRae;
using System.Text;
{
string testName = "Bool test";
string testString1 = "# ExtraTSV V0.0.1\n" +
"column1:ty\\#pe:boolean\tcolumn2:binary\tcolumnthree\\nyep:iso8601:string" +
"\nTRUE\tvalue\\\\t\0woo\t2024-02-15T18:03:30.0000" +
"\nFALSE\tnother\t2024-02-15T18:03:39.0001";
ExtraTsv parsed = ExtraTsv.ParseExtraTsv(Encoding.UTF8.GetBytes(testString1));
if (parsed.Records[0]["column1:ty#pe"] is bool result && result)
{
Console.WriteLine($"Passed {testName}");
}
else
{
Console.WriteLine($"Failed {testName}");
}
}

View File

@ -1,5 +1,9 @@
using System.Reflection;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnitsNet;
using UnitsNet.Units;
namespace NathanMcRae;
@ -32,6 +36,59 @@ public class SaneTsv
public class Int32Type : ColumnType { }
public class Int64Type : ColumnType { }
public class BinaryType : ColumnType { }
public class Iso8601Type : ColumnType { }
public class PhysicalUnitsType : ColumnType
{
public string UnitString { get; }
public UnitsNet.UnitInfo Units { get; }
public ColumnType BaseType { get; internal set; }
public PhysicalUnitsType(string unitString, UnitsNet.UnitInfo units, ColumnType baseType)
{
UnitString = unitString;
Units = units;
BaseType = baseType;
}
}
public static readonly string[] ValidUnits =
{
"m",
"s",
"A",
"K",
"cd",
"mol",
"kg",
"Hz",
"rad",
"sr",
"N",
"Pa",
"J",
"W",
"C",
"V",
"F",
"Ω",
"S",
"Wb",
"T",
"H",
"°C",
"lm",
"lx",
"Bq",
"Gy",
"Sv",
"kat"
};
public static readonly int MajorVersion = 0;
public static readonly int MinorVersion = 0;
public static readonly int PatchVersion = 1;
public static Regex VersionRegex = new Regex(@"^ ExtraTSV V(\d+)\.(\d+)\.(\d+)");
protected enum FormatType
{
@ -71,6 +128,31 @@ public class SaneTsv
return (CommentedTsv<T>)Parse<T>(inputBuffer, FormatType.COMMENTED_TSV);
}
public static CommentedTsv<T> ParseExtraTsv<T>(byte[] inputBuffer) where T : CommentedTsvRecord, new()
{
CommentedTsv<T> parsed = (CommentedTsv<T>)Parse<T>(inputBuffer, FormatType.COMMENTED_TSV);
if (parsed.FileComment == null)
{
throw new Exception($"ExtraTSV expects the file to start with '# ExtraTSV Vx.y.z' where x.y.z is a version compatible with {MajorVersion}.{MinorVersion}.{PatchVersion}");
}
Match match = VersionRegex.Match(parsed.FileComment);
if (!match.Success)
{
throw new Exception($"ExtraTSV expects the file to start with '# ExtraTSV Vx.y.z' where x.y.z is a version compatible with {MajorVersion}.{MinorVersion}.{PatchVersion}");
}
int fileMajorVersion = int.Parse(match.Groups[1].Value);
if (fileMajorVersion != MajorVersion)
{
throw new Exception($"File has major version ({fileMajorVersion}) which is newer than this parser's version {MajorVersion}");
}
return parsed;
}
protected static Tsv<T> Parse<T>(byte[] inputBuffer, FormatType format) where T : TsvRecord, new()
{
Tsv<T> parsed;
@ -84,7 +166,7 @@ public class SaneTsv
}
parsed.Records = new List<T>();
var columnTypes = new List<Type>();
var columnTypes = new List<ColumnType>();
var columnNames = new List<string>();
var columnPropertyInfos = new List<PropertyInfo>();
int columnCount = 0;
@ -169,7 +251,7 @@ public class SaneTsv
throw new Exception($"Header field {fields.Count} is not valid UTF-8", e);
}
string columnTypeString;
string[] columnTypeStrings;
string columnName;
if (columnString.Contains(':'))
{
@ -177,8 +259,8 @@ public class SaneTsv
{
throw new Exception($"Header field {j} contains ':', which is not allowed for column names");
}
columnTypeString = columnString.Split(":").Last();
columnName = columnString.Substring(0, columnString.Length - columnTypeString.Length - 1);
columnTypeStrings = columnString.Split(":");
columnName = string.Join(":", columnTypeStrings.Take(columnTypeStrings.Length - 1));
}
else
{
@ -186,53 +268,97 @@ public class SaneTsv
{
throw new Exception($"Header field {fields.Count} has no type");
}
columnTypeString = "";
columnTypeStrings = new string[] { "" };
columnName = columnString;
}
Type type;
ColumnType type;
switch (columnTypeString)
switch (columnTypeStrings.Last())
{
case "":
numTypesBlank++;
type = typeof(StringType);
type = new StringType();
break;
case "string":
type = typeof(StringType);
if (columnTypeStrings.Length > 2 && columnTypeStrings[columnTypeStrings.Length - 2] == "iso8601")
{
type = new Iso8601Type();
columnName = string.Join(":", columnTypeStrings.Take(columnTypeStrings.Length - 2));
}
// TODO: ISO8601 time spans
// TODO: ISO8601 time durations
else
{
type = new StringType();
}
break;
case "boolean":
type = typeof(BooleanType);
type = new BooleanType();
break;
case "float32":
type = typeof(Float32Type);
type = new Float32Type();
break;
case "float32-le":
type = typeof(Float32LEType);
type = new Float32LEType();
break;
case "float64":
type = typeof(Float64Type);
if (columnTypeStrings.Length > 3 && columnTypeStrings[columnTypeStrings.Length - 2] == UnitsTypeText)
{
string unitName = columnTypeStrings[columnTypeStrings.Length - 3];
if (UnitsNet.Quantity.TryFromUnitAbbreviation(1, unitName, out UnitsNet.IQuantity quantity))
{
type = new PhysicalUnitsType(unitName, UnitsNet.Quantity.GetUnitInfo(quantity.Unit), new Float64Type());
}
else
{
throw new Exception($"Invalid units: {unitName}");
}
columnName = string.Join(":", columnTypeStrings.Take(columnTypeStrings.Length - 3));
}
else
{
type = new Float64Type();
}
break;
case "float64-le":
type = typeof(Float64LEType);
if (columnTypeStrings.Length > 3 && columnTypeStrings[columnTypeStrings.Length - 2] == UnitsTypeText)
{
string unitName = columnTypeStrings[columnTypeStrings.Length - 3];
if (UnitsNet.Quantity.TryFromUnitAbbreviation(1, unitName, out UnitsNet.IQuantity quantity))
{
type = new PhysicalUnitsType(unitName, UnitsNet.Quantity.GetUnitInfo(quantity.Unit), new Float64LEType());
}
else
{
throw new Exception($"Invalid units: {unitName}");
}
columnName = string.Join(":", columnTypeStrings.Take(columnTypeStrings.Length - 3));
}
else
{
type = new Float64LEType();
}
break;
case "uint32":
type = typeof(UInt32Type);
type = new UInt32Type();
break;
case "uint64":
type = typeof(UInt64Type);
type = new UInt64Type();
break;
case "int32":
type = typeof(Int32Type);
type = new Int32Type();
break;
case "int64":
type = typeof(Int64Type);
type = new Int64Type();
break;
case "binary":
type = typeof(BinaryType);
type = new BinaryType();
break;
default:
throw new Exception($"Invalid type '{columnTypeString}' for column {j}");
throw new Exception($"Invalid type '{columnTypeStrings.Last()}' for column {j}");
}
// TODO: Allow lax parsing (only worry about parsing columns that are given in the specifying type
@ -242,7 +368,7 @@ public class SaneTsv
throw new Exception($"Column {j} has name {columnName}, but expected {columnNames[j]}");
}
if (columnTypes[j] != type)
if (columnTypes[j].GetType() != type.GetType())
{
throw new Exception($"Column {j} has type {type}, but expected {columnTypes[j]}");
}
@ -348,7 +474,7 @@ public class SaneTsv
// startIndex is in we'd have to go back to the start of the record's comment, and to know
// exactly where that comment started we'd have to go back to the start of the record before that
// (not including that other record's comment).
protected static T[] Parse<T>(byte[] inputBuffer, FormatType format, PropertyInfo[] columnPropertyInfos, Type[] columnTypes, int startIndex, int endIndex) where T : TsvRecord, new()
protected static T[] Parse<T>(byte[] inputBuffer, FormatType format, PropertyInfo[] columnPropertyInfos, ColumnType[] columnTypes, int startIndex, int endIndex) where T : TsvRecord, new()
{
var fieldBytes = new List<byte>();
var fields = new List<byte[]>();
@ -509,12 +635,12 @@ public class SaneTsv
return parsed.ToArray();
}
protected static T ParseCurrentCommentedRecord<T>(Type[] columnTypes, PropertyInfo[] properties, List<byte[]> fields, string comment, int line) where T : CommentedTsvRecord, new()
protected static T ParseCurrentCommentedRecord<T>(ColumnType[] columnTypes, PropertyInfo[] properties, List<byte[]> fields, string comment, int line) where T : CommentedTsvRecord, new()
{
return (T)ParseCurrentRecord<T>(columnTypes, properties, fields, comment, line);
}
protected static T ParseCurrentRecord<T>(Type[] columnTypes, PropertyInfo[] properties, List<byte[]> fields, string comment, int line) where T : TsvRecord, new()
protected static T ParseCurrentRecord<T>(ColumnType[] columnTypes, PropertyInfo[] properties, List<byte[]> fields, string comment, int line) where T : TsvRecord, new()
{
T record = new T();
@ -532,7 +658,7 @@ public class SaneTsv
for (int j = 0; j < fields.Count; j++)
{
// All other types require the content to be UTF-8. Binary fields can ignore that.
if (columnTypes[j] == typeof(BinaryType))
if (columnTypes[j].GetType() == typeof(BinaryType))
{
// TODO: Use faster method for property setting
// e.g. https://blog.marcgravell.com/2012/01/playing-with-your-member.html
@ -541,7 +667,7 @@ public class SaneTsv
properties[j].SetValue(record, fields[j]);
continue;
}
else if (columnTypes[j] == typeof(Float32LEType))
else if (columnTypes[j].GetType() == typeof(Float32LEType))
{
byte[] floatBytes;
if (!LittleEndian)
@ -560,7 +686,7 @@ public class SaneTsv
continue;
}
else if (columnTypes[j] == typeof(Float64LEType))
else if (columnTypes[j].GetType() == typeof(Float64LEType) || (columnTypes[j] is PhysicalUnitsType f64PhUnit && f64PhUnit.BaseType is Float64LEType))
{
byte[] floatBytes;
if (!LittleEndian)
@ -575,7 +701,15 @@ public class SaneTsv
{
floatBytes = fields[j];
}
properties[j].SetValue(record, BitConverter.ToDouble(floatBytes, 0));
double value = BitConverter.ToDouble(floatBytes, 0);
if (columnTypes[j] is PhysicalUnitsType unit)
{
properties[j].SetValue(record, UnitsNet.Quantity.From(value, unit.Units.Value));
}
else
{
properties[j].SetValue(record, value);
}
continue;
}
@ -592,11 +726,11 @@ public class SaneTsv
// TODO: Add checking for numeric types format
if (columnTypes[j] == typeof(StringType))
if (columnTypes[j].GetType() == typeof(StringType))
{
properties[j].SetValue(record, fieldString);
}
else if (columnTypes[j] == typeof(BooleanType))
else if (columnTypes[j].GetType() == typeof(BooleanType))
{
bool parsedBool;
if (fieldString == "TRUE")
@ -614,7 +748,7 @@ public class SaneTsv
properties[j].SetValue(record, parsedBool);
}
else if (columnTypes[j] == typeof(Float32Type))
else if (columnTypes[j].GetType() == typeof(Float32Type))
{
float parsedFloat;
if (!float.TryParse(fieldString, out parsedFloat))
@ -635,7 +769,7 @@ public class SaneTsv
properties[j].SetValue(record, parsedFloat);
}
else if (columnTypes[j] == typeof(Float64Type))
else if (columnTypes[j].GetType() == typeof(Float64Type) || (columnTypes[j] is PhysicalUnitsType f64PhUnit && f64PhUnit.BaseType is Float64Type))
{
double parsedDouble;
if (!double.TryParse(fieldString, out parsedDouble))
@ -654,9 +788,16 @@ public class SaneTsv
}
}
properties[j].SetValue(record, parsedDouble);
if (columnTypes[j] is PhysicalUnitsType unit)
{
properties[j].SetValue(record, UnitsNet.Quantity.From(parsedDouble, unit.Units.Value));
}
else
{
properties[j].SetValue(record, parsedDouble);
}
}
else if (columnTypes[j] == typeof(UInt32Type))
else if (columnTypes[j].GetType() == typeof(UInt32Type))
{
if (!UInt32.TryParse(fieldString, out UInt32 parsedUInt32))
{
@ -665,7 +806,7 @@ public class SaneTsv
properties[j].SetValue(record, parsedUInt32);
}
else if (columnTypes[j] == typeof(UInt64Type))
else if (columnTypes[j].GetType() == typeof(UInt64Type))
{
if (!UInt64.TryParse(fieldString, out UInt64 parsedUInt64))
{
@ -674,7 +815,7 @@ public class SaneTsv
properties[j].SetValue(record, parsedUInt64);
}
else if (columnTypes[j] == typeof(Int32Type))
else if (columnTypes[j].GetType() == typeof(Int32Type))
{
if (!Int32.TryParse(fieldString, out Int32 parsedInt32))
{
@ -683,7 +824,7 @@ public class SaneTsv
properties[j].SetValue(record, parsedInt32);
}
else if (columnTypes[j] == typeof(Int64Type))
else if (columnTypes[j].GetType() == typeof(Int64Type))
{
if (!Int64.TryParse(fieldString, out Int64 parsedInt64))
{
@ -692,6 +833,15 @@ public class SaneTsv
properties[j].SetValue(record, parsedInt64);
}
else if (columnTypes[j].GetType() == typeof(Iso8601Type))
{
if (!DateTime.TryParseExact(fieldString, "yyyy-MM-ddTHH:mm:ss.ffff", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed))
{
throw new Exception($"ISO 8601 timestamp format error on line {line}, field {j}");
}
properties[j].SetValue(record, parsed);
}
else
{
throw new Exception($"Unexpected type {columnTypes[j]}");
@ -1066,99 +1216,193 @@ public class SaneTsv
return records.ToArray();
}
public static Type GetColumnFromType(Type type)
public static string UnitsTypeText = "ph-unit";
public static Regex UnitsRegex = new Regex("([^:]+):" + UnitsTypeText + ":(float32|float32-le|float64|float64-le|uint32|uint64|int32|int64)");
public static ColumnType GetColumnFromString(string type)
{
if (type == typeof(string))
if (type == "string")
{
return typeof(StringType);
return new StringType();
}
else if (type == typeof(bool))
else if (type == "boolean")
{
return typeof(BooleanType);
return new BooleanType();
}
else if (type == typeof(float))
else if (type == "float32")
{
return typeof(Float32Type);
return new Float32Type();
}
else if (type == typeof(double))
else if (type == "float32-le")
{
return typeof(Float64Type);
return new Float32LEType();
}
else if (type == typeof(UInt32))
else if (type == "float64")
{
return typeof(UInt32Type);
return new Float64Type();
}
else if (type == typeof(UInt64))
else if (type == "float64-le")
{
return typeof(UInt64Type);
return new Float64LEType();
}
else if (type == typeof(Int32))
else if (type == "uint32")
{
return typeof(Int32Type);
return new UInt32Type();
}
else if (type == typeof(Int64))
else if (type == "uint64")
{
return typeof(Int64Type);
return new UInt64Type();
}
else if (type == typeof(byte[]))
else if (type == "int32")
{
return typeof(BinaryType);
return new Int32Type();
}
else if (type == "int64")
{
return new Int64Type();
}
else if (type == "binary")
{
return new BinaryType();
}
else if (type == "iso8601")
{
return new Iso8601Type();
}
else if (UnitsRegex.IsMatch(type))
{
Match match = UnitsRegex.Match(type);
string unitName = match.Groups[1].Value;
string baseType = match.Groups[2].Value;
return new PhysicalUnitsType(unitName, ParseUnit(unitName), GetColumnFromString(baseType));
//if (UnitsNet.Quantity.TryFromUnitAbbreviation(1, unitName, out UnitsNet.IQuantity quantity))
//{
// return new PhysicalUnitsType(UnitsNet.Quantity.GetUnitInfo(quantity.Unit), GetColumnFromString(baseType));
//}
//else
//{
// throw new Exception($"Invalid units: {unitName}");
//}
}
else
{
throw new Exception($"Invalid type: {type}");
throw new Exception($"Invalid type: {type.GetType()}");
}
}
public static string GetNameFromColumn(Type type)
public static ColumnType GetColumnFromType(Type type)
{
if (type == typeof(StringType))
if (type == typeof(string))
{
return "string";
return new StringType();
}
else if (type == typeof(BooleanType))
else if (type == typeof(bool))
{
return "boolean";
return new BooleanType();
}
else if (type == typeof(Float32Type))
else if (type == typeof(float))
{
return "float32";
return new Float32Type();
}
else if (type == typeof(Float32LEType))
else if (type == typeof(double))
{
return "float32-le";
return new Float64Type();
}
else if (type == typeof(Float64Type))
else if (type == typeof(UInt32))
{
return "float64";
return new UInt32Type();
}
else if (type == typeof(Float64LEType))
else if (type == typeof(UInt64))
{
return "float64-le";
return new UInt64Type();
}
else if (type == typeof(UInt32Type))
else if (type == typeof(Int32))
{
return "uint32";
return new Int32Type();
}
else if (type == typeof(UInt64Type))
else if (type == typeof(Int64))
{
return "uint64";
return new Int64Type();
}
else if (type == typeof(Int32Type))
else if (type == typeof(byte[]))
{
return "int32";
return new BinaryType();
}
else if (type == typeof(Int64Type))
else if (type == typeof(DateTime))
{
return "int64";
return new Iso8601Type();
}
else if (type == typeof(BinaryType))
else if (type == typeof(UnitsNet.Mass))
{
return "binary";
// TODO
//UnitsNet.UnitInfo a = new UnitsNet.UnitInfo([d])
var a = new UnitsNet.UnitInfo<UnitsNet.Units.MassUnit>(UnitsNet.Units.MassUnit.Kilogram, "kgs", new UnitsNet.BaseUnits(mass: UnitsNet.Units.MassUnit.Kilogram));
return new PhysicalUnitsType("kg", a, new Float64Type());
}
else
{
throw new Exception($"Invalid type: {type}");
throw new Exception($"Invalid type: {type.GetType()}");
}
}
public static string GetNameFromColumn(ColumnType type)
{
if (type.GetType() == typeof(StringType))
{
return "string";
}
else if (type.GetType() == typeof(BooleanType))
{
return "boolean";
}
else if (type.GetType() == typeof(Float32Type))
{
return "float32";
}
else if (type.GetType() == typeof(Float32LEType))
{
return "float32-le";
}
else if (type.GetType() == typeof(Float64Type))
{
return "float64";
}
else if (type.GetType() == typeof(Float64LEType))
{
return "float64-le";
}
else if (type.GetType() == typeof(UInt32Type))
{
return "uint32";
}
else if (type.GetType() == typeof(UInt64Type))
{
return "uint64";
}
else if (type.GetType() == typeof(Int32Type))
{
return "int32";
}
else if (type.GetType() == typeof(Int64Type))
{
return "int64";
}
else if (type.GetType() == typeof(BinaryType))
{
return "binary";
}
else if (type.GetType() == typeof(Iso8601Type))
{
return "iso8601:string";
}
else if (type is PhysicalUnitsType unit)
{
return $"{unit.UnitString}:{UnitsTypeText}:{GetNameFromColumn(unit.BaseType)}";
}
else
{
throw new Exception($"Invalid type: {type.GetType()}");
}
}
@ -1174,14 +1418,29 @@ public class SaneTsv
public static byte[] SerializeCommentedTsv<T>(IList<T> data, string fileComment) where T : CommentedTsvRecord
{
return SerializeTsv<T>(data, FormatType.COMMENTED_TSV);
return SerializeTsv<T>(data, FormatType.COMMENTED_TSV, fileComment);
}
protected static byte[] SerializeTsv<T>(IList<T> data, FormatType tsvFormat)
public static byte[] SerializeExtraTsv<T>(IList<T> data) where T : TsvRecord
{
return SerializeTsv<T>(data, FormatType.COMMENTED_TSV, $" ExtraTSV V{MajorVersion}.{MinorVersion}.{PatchVersion}");
}
protected static byte[] SerializeTsv<T>(IList<T> data, FormatType tsvFormat, string fileComment = null)
{
var bytes = new List<byte>();
var columnTypes = new List<Type>();
if (fileComment != null)
{
if (tsvFormat != FormatType.COMMENTED_TSV)
{
throw new Exception($"File comments are not valid for {tsvFormat}");
}
bytes.AddRange(Encoding.UTF8.GetBytes("#" + fileComment.Replace("\n", "\n#") + "\n"));
}
var columnTypes = new List<ColumnType>();
var columnNames = new List<string>();
var columnPropertyInfos = new List<PropertyInfo>();
int columnCount = 0;
@ -1197,8 +1456,8 @@ public class SaneTsv
string headerName = attribute.ColumnName ?? property.Name;
columnNames.Add(headerName);
Type headerType = attribute.ColumnType ?? GetColumnFromType(property.PropertyType);
if (tsvFormat == FormatType.SIMPLE_TSV && headerType != typeof(StringType))
ColumnType headerType = attribute.ColumnType ?? GetColumnFromType(property.PropertyType);
if (tsvFormat == FormatType.SIMPLE_TSV && headerType.GetType() != typeof(StringType))
{
throw new Exception($"Serializing Simple TSV requires all columns be of type string, but column '{headerName}' has type '{headerType}'");
}
@ -1278,7 +1537,7 @@ public class SaneTsv
return bytes.ToArray();
}
protected static void SerializeTsv<T>(IList<T> data, List<byte> bytes, PropertyInfo[] columnPropertyInfos, Type[] columnTypes, FormatType tsvFormat, int startIndex, int endIndex)
protected static void SerializeTsv<T>(IList<T> data, List<byte> bytes, PropertyInfo[] columnPropertyInfos, ColumnType[] columnTypes, FormatType tsvFormat, int startIndex, int endIndex)
{
// Serialize data
for (int i = 0; i < data.Count; i++)
@ -1293,16 +1552,16 @@ public class SaneTsv
// Some fields definitely don't need escaping, so we add them directly to bytes
bool skipEscaping = false;
if (columnTypes[j] == typeof(StringType))
if (columnTypes[j].GetType() == typeof(StringType))
{
fieldEncoded = Encoding.UTF8.GetBytes((string)datum);
}
else if (columnTypes[j] == typeof(BooleanType))
else if (columnTypes[j].GetType() == typeof(BooleanType))
{
bytes.AddRange((bool)datum ? TrueEncoded : FalseEncoded);
skipEscaping = true;
}
else if (columnTypes[j] == typeof(Float32Type))
else if (columnTypes[j].GetType() == typeof(Float32Type))
{
if (datum is float f)
{
@ -1326,7 +1585,7 @@ public class SaneTsv
}
skipEscaping = true;
}
else if (columnTypes[j] == typeof(Float32LEType))
else if (columnTypes[j].GetType() == typeof(Float32LEType))
{
if (LittleEndian)
{
@ -1342,39 +1601,63 @@ public class SaneTsv
}
}
}
else if (columnTypes[j] == typeof(Float64Type))
else if (columnTypes[j].GetType() == typeof(Float64Type) || (columnTypes[j] is PhysicalUnitsType f64PhUnit && f64PhUnit.BaseType is Float64Type))
{
double value;
if (datum is double d)
{
if (double.IsNegativeInfinity(d))
{
bytes.AddRange(Encoding.UTF8.GetBytes("-inf"));
}
else if (double.IsPositiveInfinity(d))
{
bytes.AddRange(Encoding.UTF8.GetBytes("+inf"));
}
else
{
// See https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#round-trip-format-specifier-r
bytes.AddRange(Encoding.UTF8.GetBytes((d).ToString("G17")));
}
value = d;
}
// TODO: check units match
else if (datum is UnitsNet.IQuantity quantity)
{
value = quantity.Value;
}
else
{
throw new InvalidCastException();
}
skipEscaping = true;
}
else if (columnTypes[j] == typeof(Float64LEType))
{
if (LittleEndian)
if (double.IsNegativeInfinity(value))
{
fieldEncoded = BitConverter.GetBytes((double)datum);
bytes.AddRange(Encoding.UTF8.GetBytes("-inf"));
}
else if (double.IsPositiveInfinity(value))
{
bytes.AddRange(Encoding.UTF8.GetBytes("+inf"));
}
else
{
byte[] doubleBytes = BitConverter.GetBytes((double)datum);
// See https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#round-trip-format-specifier-r
bytes.AddRange(Encoding.UTF8.GetBytes((value).ToString("G17")));
}
skipEscaping = true;
}
else if (columnTypes[j].GetType() == typeof(Float64LEType) || (columnTypes[j] is PhysicalUnitsType f64LEPhUnit && f64LEPhUnit.BaseType is Float64LEType))
{
double value;
if (datum is double d)
{
value = d;
}
// TODO: check units match
else if (datum is UnitsNet.IQuantity quantity)
{
value = quantity.Value;
}
else
{
throw new InvalidCastException();
}
if (LittleEndian)
{
fieldEncoded = BitConverter.GetBytes((double)value);
}
else
{
byte[] doubleBytes = BitConverter.GetBytes((double)value);
fieldEncoded = new byte[sizeof(double)];
for (int k = 0; k < sizeof(double); k++)
{
@ -1382,30 +1665,38 @@ public class SaneTsv
}
}
}
else if (columnTypes[j] == typeof(UInt32Type))
else if (columnTypes[j].GetType() == typeof(UInt32Type))
{
bytes.AddRange(Encoding.UTF8.GetBytes(((UInt32)datum).ToString()));
skipEscaping = true;
}
else if (columnTypes[j] == typeof(UInt64Type))
else if (columnTypes[j].GetType() == typeof(UInt64Type))
{
bytes.AddRange(Encoding.UTF8.GetBytes(((UInt64)datum).ToString()));
skipEscaping = true;
}
else if (columnTypes[j] == typeof(Int32Type))
else if (columnTypes[j].GetType() == typeof(Int32Type))
{
bytes.AddRange(Encoding.UTF8.GetBytes(((Int32)datum).ToString()));
skipEscaping = true;
}
else if (columnTypes[j] == typeof(Int64Type))
else if (columnTypes[j].GetType() == typeof(Int64Type))
{
bytes.AddRange(Encoding.UTF8.GetBytes(((Int64)datum).ToString()));
skipEscaping = true;
}
else if (columnTypes[j] == typeof(BinaryType))
else if (columnTypes[j].GetType() == typeof(BinaryType))
{
fieldEncoded = (byte[])datum;
}
else if (columnTypes[j].GetType() == typeof(Iso8601Type))
{
fieldEncoded = Encoding.UTF8.GetBytes(((DateTime)datum).ToString("yyyy-MM-ddTHH:mm:ss.ffff"));
}
else if (columnTypes[j] is PhysicalUnitsType phUnits)
{
throw new NotImplementedException($"Physical units types don't support {GetNameFromColumn(phUnits.BaseType)} as a base type");
}
else
{
throw new Exception($"Unexpected column type {columnTypes[j]} for column {j}");
@ -1512,15 +1803,15 @@ public class SaneTsv
public class TsvColumnAttribute : Attribute
{
public string ColumnName { get; }
public virtual Type ColumnType { get; }
public virtual ColumnType ColumnType { get; }
public TsvColumnAttribute()
{
ColumnType = typeof(StringType);
ColumnType = new StringType();
}
public TsvColumnAttribute(string columnName)
{
ColumnType = typeof(StringType);
ColumnType = new StringType();
ColumnName = columnName;
}
}
@ -1528,28 +1819,39 @@ public class SaneTsv
// TODO: Add column ordering
public class TypedTsvColumnAttribute : TsvColumnAttribute
{
public override Type ColumnType { get; }
public override ColumnType ColumnType { get; }
public TypedTsvColumnAttribute() { }
public TypedTsvColumnAttribute(string columnName) : base(columnName) { }
public TypedTsvColumnAttribute(string columnName, Type columnType) : base(columnName)
public TypedTsvColumnAttribute(string columnName, string columnType) : base(columnName)
{
if (columnType.BaseType != typeof(ColumnType))
{
throw new Exception("Column type must inherit from SaneTsv.ColumnType");
}
ColumnType = columnType;
ColumnType = GetColumnFromString(columnType);
}
public TypedTsvColumnAttribute(Type columnType)
public TypedTsvColumnAttribute(ColumnType columnType)
{
if (columnType.BaseType != typeof(ColumnType))
{
throw new Exception("Column type must inherit from SaneTsv.ColumnType");
}
ColumnType = columnType;
}
}
public static UnitInfo ParseUnit(string unitName)
{
// Find all unit enum types in the UnitsNet namespace
var unitEnumTypes = Assembly.GetAssembly(typeof(LengthUnit))
.GetTypes()
.Where(t => t.IsEnum && t.Namespace == typeof(LengthUnit).Namespace);
foreach (var unitEnumType in unitEnumTypes)
{
if (UnitParser.Default.TryParse(unitName, unitEnumType, out Enum unitEnum))
{
// Successfully parsed the abbreviation, retrieve UnitInfo
return Quantity.GetUnitInfo(unitEnum);
}
}
throw new ArgumentException($"Unable to parse unit abbreviation: {unitName}");
}
}

View File

@ -21,4 +21,8 @@
<None Remove="SaneTsvTest\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="UnitsNet" Version="6.0.0-pre004" />
</ItemGroup>
</Project>

View File

@ -7,10 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SaneTsv", "SaneTsv.csproj",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SaneTsvTest", "SaneTsvTest\SaneTsvTest.csproj", "{43B1B09C-19BD-4B45-B41B-7C00DB3F7E9C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExtraTsv", "ExtraTsv\ExtraTsv.csproj", "{D9F2E9C8-4F52-4BB7-9BBD-AE9A0C6168E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtraTsvTest", "ExtraTsvTest\ExtraTsvTest.csproj", "{A545B0DB-F799-43E2-9DFA-C18BDF3535F1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -25,14 +21,6 @@ Global
{43B1B09C-19BD-4B45-B41B-7C00DB3F7E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43B1B09C-19BD-4B45-B41B-7C00DB3F7E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43B1B09C-19BD-4B45-B41B-7C00DB3F7E9C}.Release|Any CPU.Build.0 = Release|Any CPU
{D9F2E9C8-4F52-4BB7-9BBD-AE9A0C6168E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9F2E9C8-4F52-4BB7-9BBD-AE9A0C6168E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9F2E9C8-4F52-4BB7-9BBD-AE9A0C6168E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9F2E9C8-4F52-4BB7-9BBD-AE9A0C6168E7}.Release|Any CPU.Build.0 = Release|Any CPU
{A545B0DB-F799-43E2-9DFA-C18BDF3535F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A545B0DB-F799-43E2-9DFA-C18BDF3535F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A545B0DB-F799-43E2-9DFA-C18BDF3535F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A545B0DB-F799-43E2-9DFA-C18BDF3535F1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,63 +1,9 @@
using NathanMcRae;
using System.Reflection;
using System.Text;
internal class Program : SaneTsv
{
public class TestRecord : SaneTsv.TsvRecord
{
[SaneTsv.TypedTsvColumn("string-test")]
public string StringTest { get; set; }
[SaneTsv.TypedTsvColumn("bool-test")]
public bool BoolTest { get; set; }
[SaneTsv.TypedTsvColumn("float32-test")]
public float Float32Test { get; set; }
[SaneTsv.TypedTsvColumn("float32-le-test", typeof(SaneTsv.Float32LEType))]
public float Float32LETest { get; set; }
[SaneTsv.TypedTsvColumn("float64-test")]
public double Float64Test { get; set; }
[SaneTsv.TypedTsvColumn("float64-le-test", typeof(SaneTsv.Float64LEType))]
public double Float64LETest { get; set; }
[SaneTsv.TypedTsvColumn("uint32-test")]
public UInt32 UInt32Test { get; set; }
[SaneTsv.TypedTsvColumn("uint64-test")]
public UInt64 UInt64Test { get; set; }
[SaneTsv.TypedTsvColumn("int32-test")]
public Int32 Int32Test { get; set; }
[SaneTsv.TypedTsvColumn("int64-test")]
public Int64 Int64Test { get; set; }
[SaneTsv.TypedTsvColumn("binary-test")]
public byte[] BinaryTest { get; set; }
public TestRecord(string stringTest, bool boolTest, float float32Test, float float32LETest, double float64Test, double float64LETest, UInt32 uInt32Test, UInt64 uInt64Test, Int32 int32Test, Int64 int64Test, byte[] binaryTest)
{
StringTest = stringTest;
BoolTest = boolTest;
Float32Test = float32Test;
Float32LETest = float32LETest;
Float64Test = float64Test;
Float64LETest = float64LETest;
UInt32Test = uInt32Test;
UInt64Test = uInt64Test;
Int32Test = int32Test;
Int64Test = int64Test;
BinaryTest = binaryTest;
}
public TestRecord() { }
}
public class BoolTestRecord : SaneTsv.CommentedTsvRecord
public class DateTest : SaneTsv.CommentedTsvRecord
{
[SaneTsv.TypedTsvColumn("column1:ty#pe")]
public bool Column1 { get; set; }
@ -66,75 +12,28 @@ internal class Program : SaneTsv
public byte[] column2 { get; set; }
[SaneTsv.TypedTsvColumn("columnthree\nyep")]
public string Column3 { get; set; }
public DateTime Column3 { get; set; }
}
public class BoolTestRecord2 : SaneTsv.CommentedTsvRecord
public class UnitTest : SaneTsv.CommentedTsvRecord
{
[SaneTsv.TypedTsvColumn("column1:type")]
public bool Column1 { get; set; }
[SaneTsv.TypedTsvColumn("id")]
public UInt32 Id { get; set; }
[SaneTsv.TypedTsvColumn]
public byte[] column2 { get; set; }
[SaneTsv.TypedTsvColumn("columnthree\nyep")]
public string Column3 { get; set; }
}
public class BoolTestRecord3 : SaneTsv.CommentedTsvRecord
{
[SaneTsv.TsvColumn("column1")]
public string Column1 { get; set; }
[SaneTsv.TsvColumn]
public string column2 { get; set; }
[SaneTsv.TsvColumn("columnthree\nyep")]
public string Column3 { get; set; }
}
public class SerdeTestRecord : SaneTsv.CommentedTsvRecord
{
[SaneTsv.TypedTsvColumn("column1")]
public bool Column1 { get; set; }
[SaneTsv.TypedTsvColumn]
public byte[] column2 { get; set; }
[SaneTsv.TypedTsvColumn("columnthree\nyep")]
public string Column3 { get; set; }
}
public class FloatTestRecord : SaneTsv.CommentedTsvRecord
{
[SaneTsv.TypedTsvColumn("somefloat")]
public double SomeFloat { get; set; }
[SaneTsv.TypedTsvColumn("binfloat", typeof(SaneTsv.Float64LEType))]
public double BinFloat { get; set; }
}
public class StringTestRecord : SaneTsv.TsvRecord
{
[SaneTsv.TypedTsvColumn("column1")]
public string Column1 { get; set; }
[SaneTsv.TypedTsvColumn]
public string column2 { get; set; }
[SaneTsv.TypedTsvColumn("columnthree\nyep")]
public string Column3 { get; set; }
[SaneTsv.TypedTsvColumn("value", "m/s:ph-unit:float64")]
public UnitsNet.Speed Value { get; set; }
}
private static void Main(string[] args)
{
{
string testName = "Bool test";
string testString1 = "column1:ty\\#pe:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
string testName = "Parse date";
string testString1 = "# ExtraTSV V0.0.1\n" +
"column1:ty\\#pe:boolean\tcolumn2:binary\tcolumnthree\\nyep:iso8601:string" +
"\nTRUE\tvalue\\\\t\0woo\t2024-02-15T18:03:30.0000" +
"\nFALSE\tnother\t2024-02-15T18:03:39.0001";
Tsv<BoolTestRecord> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord>(Encoding.UTF8.GetBytes(testString1));
CommentedTsv<DateTest> parsed = SaneTsv.ParseExtraTsv<DateTest>(Encoding.UTF8.GetBytes(testString1));
if (parsed.Records[0].Column1)
{
Console.WriteLine($"Passed {testName}");
@ -146,64 +45,33 @@ internal class Program : SaneTsv
}
{
string testName = "Bad bool test";
string testName = "Bad date column name";
string testString1 = "# ExtraTSV V0.0.1\n" +
"column1:ty\\#pe:boolean\tcolumn2:binary\tiso8601:string" +
"\nTRUE\tvalue\\\\t\0woo\t2024-02-15T18:03:30.0000" +
"\nFALSE\tnother\t2024-02-15T18:03:39.0001";
try
{
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
Tsv<BoolTestRecord> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord>(Encoding.UTF8.GetBytes(testString1));
CommentedTsv<DateTest> parsed = SaneTsv.ParseExtraTsv<DateTest>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName}");
}
catch (Exception)
catch (Exception e)
{
Console.WriteLine($"Passed {testName}");
}
}
{
string testName = "Comment test";
string testString1 = "#This is a file comment\n" +
"#One more file comment line\n" +
"column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
string testName = "Serde date";
string testString1 = "# ExtraTSV V0.0.1\n" +
"column1:ty\\#pe:boolean\tcolumn2:binary\tcolumnthree\\nyep:iso8601:string" +
"\nTRUE\tvalue\\\\t\0woo\t2024-02-15T18:03:30.0000" +
"\nFALSE\tnother\t2024-02-15T18:03:39.0001";
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
}
//{
// string testName = "Serde test";
// string testString1 = "column1\tcolumn2\tcolumnthree\\nyep" +
// "\nTRUE\tvalue\\\\twoo\tvaluetrhee" +
// "\nFALSE\tnother\tno\\ther";
// Tsv<SerdeTestRecord> parsed = SaneTsv.ParseSimpleTsv<SerdeTestRecord>(Encoding.UTF8.GetBytes(testString1));
// string serialized = Encoding.UTF8.GetString(SaneTsv.SerializeSimpleTsv(parsed.ColumnNames, parsed.Records.Select(r => r.Fields.Select(f => f.ToString()).ToArray()).ToArray()));
// if (testString1 == serialized)
// {
// Console.WriteLine($"Passed {testName}");
// }
// else
// {
// Console.WriteLine($"Failed {testName}");
// }
//}
{
string testName = "Float binary test";
var bytes = new List<byte>();
bytes.AddRange(Encoding.UTF8.GetBytes("somefloat:float64\tbinfloat:float64-le" +
"\n1.5\t")); bytes.AddRange(BitConverter.GetBytes(1.5));
bytes.AddRange(Encoding.UTF8.GetBytes("\n-8.0000005E-14\t")); bytes.AddRange(BitConverter.GetBytes(-8.0000005E-14));
Tsv<FloatTestRecord> parsed = SaneTsv.ParseTypedTsv<FloatTestRecord>(bytes.ToArray());
if (parsed.Records[0].BinFloat == parsed.Records[0].SomeFloat)
CommentedTsv<DateTest> parsed = SaneTsv.ParseExtraTsv<DateTest>(Encoding.UTF8.GetBytes(testString1));
string serialized = Encoding.UTF8.GetString(SaneTsv.SerializeExtraTsv<DateTest>(parsed.Records));
if (serialized == testString1)
{
Console.WriteLine($"Passed {testName}");
}
@ -214,22 +82,57 @@ internal class Program : SaneTsv
}
{
string testName = "Serde test";
string testName = "Parse unit";
string testString1 = "# ExtraTSV V0.0.1\n" +
"id:uint32\tvalue:m/s:ph-unit:float64\n" +
"0\t1.5\n" +
"1\t5.4e3";
TestRecord[] data =
CommentedTsv<UnitTest> parsed = SaneTsv.ParseExtraTsv<UnitTest>(Encoding.UTF8.GetBytes(testString1));
if (parsed.Records[0].Value.Value == 1.5)
{
new TestRecord("test", true, 44.5f, 44.5f, -88e-3, -88e-3, 7773, 88888888, -7773, -88888888, new byte[] { 0, 1, 2, 3 }),
new TestRecord("test2", false, 44.5000005f, 44.5000005f, -88e-30, -88e-30, 7773, 88888888, -7773, -88888888, new byte[] { 0, 1, 2, 3, 4 }),
new TestRecord("test2", false, float.NaN, float.NaN, double.NaN, double.NaN, 7773, 88888888, -7773, -88888888, new byte[] { 0, 1, 2, 3, 4 }),
new TestRecord("test2", false, float.NegativeInfinity, float.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, 7773, 88888888, -7773, -88888888, new byte[] { 0, 1, 2, 3, 4 }),
new TestRecord("test2", false, float.PositiveInfinity, float.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, 7773, 88888888, -7773, -88888888, new byte[] { 0, 1, 2, 3, 4 }),
Console.WriteLine($"Passed {testName}");
}
else
{
Console.WriteLine($"Failed {testName}");
}
}
{
string testName = "Serde unit";
UnitTest[] records = new UnitTest[]
{
new UnitTest
{
Id = 1,
Value = UnitsNet.Speed.FromMetersPerSecond(5.03),
},
new UnitTest
{
Id = 5,
Value = UnitsNet.Speed.FromMetersPerSecond(double.NaN),
},
new UnitTest
{
Id = 1000000,
Value = UnitsNet.Speed.FromMetersPerSecond(9859873.498),
},
};
byte[] serialized = SaneTsv.SerializeTypedTsv(data);
string serialized = Encoding.UTF8.GetString(SaneTsv.SerializeExtraTsv<UnitTest>(records));
CommentedTsv<UnitTest> parsed = SaneTsv.ParseExtraTsv<UnitTest>(Encoding.UTF8.GetBytes(serialized));
Tsv<TestRecord> parsed = SaneTsv.ParseTypedTsv<TestRecord>(serialized);
bool match = true;
for (int i = 0; i < records.Length; i++)
{
match = match && records[i].Id == parsed.Records[i].Id &&
(records[i].Value.Equals(parsed.Records[i].Value, UnitsNet.Speed.FromMetersPerSecond(1E-1))
|| (double.IsNaN(records[i].Value.Value) && double.IsNaN(parsed.Records[i].Value.Value)));
}
if ((float)parsed.Records[1].Float32Test == 44.5000005f)
if (match)
{
Console.WriteLine($"Passed {testName}");
}
@ -239,526 +142,6 @@ internal class Program : SaneTsv
}
}
{
string testName = "Trying to parse a not commented record as a Commented TSV test";
// These should not compile:
//byte[] serialized = SaneTsv.SerializeCommentedTsv(data);
// Gives this error: error CS7036: There is no argument given that corresponds to the required parameter 'fileComment' of 'SaneTsv.SerializeCommentedTsv<T>(IList<T>, string)'
//Tsv<TestRecord> parsed = SaneTsv.ParseCommentedTsv<TestRecord>(serialized);
// Gives this error: error CS0311: The type 'Program.TestRecord' cannot be used as type parameter 'T' in the generic type or method 'SaneTsv.ParseCommentedTsv<T>(byte[])'. There is no implicit reference conversion from 'Program.TestRecord' to 'NathanMcRae.SaneTsv.CommentedTsvRecord'.
}
{
string testName = "Try to parsed a Commented TSV as a Simple TSV";
string testString1 = "#This is a file comment\n" +
"#One more file comment line\n" +
"column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseSimpleTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName}");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName}");
}
}
{
string testName = "Try to parsed a Commented TSV as a Typed TSV";
string testString1 = "#This is a file comment\n" +
"#One more file comment line\n" +
"column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName}");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName}");
}
}
{
string testName = "Try to parsed a Typed TSV as a Simple TSV";
string testString1 =
"column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseSimpleTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName}");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName}");
}
}
{
string testName = "Timing comparison of simple parse methods and comparison of simple serialization methods";
int N = 1000000;
var records = new StringTestRecord[N];
var rand = new Random(1);
for (int i = 0; i < N; i++)
{
records[i] = new StringTestRecord()
{
Column1 = rand.Next().ToString(),
column2 = rand.Next().ToString(),
Column3 = rand.Next().ToString(),
};
}
string[][] recordStrings = records.Select(record => new string[] { record.Column1, record.column2, record.Column3 }).ToArray();
DateTime lastTime = DateTime.Now;
byte[] serialized1 = SaneTsv.SerializeSimpleTsv<StringTestRecord>(records);
TimeSpan speccedSerializationTime = DateTime.Now - lastTime;
Console.WriteLine($"Specced serialization time: {speccedSerializationTime}");
lastTime = DateTime.Now;
byte[] serialized2 = SaneTsv.SerializeSimpleTsv(new string[] { "column1", "column2", "columnthree\nyep" }, recordStrings);
TimeSpan unspeccedSerializationTime = DateTime.Now - lastTime;
Console.WriteLine($"Unspecced serialization time: {unspeccedSerializationTime}");
lastTime = DateTime.Now;
Tsv<StringTestRecord> parsed = SaneTsv.ParseSimpleTsv<StringTestRecord>(serialized1);
TimeSpan speccedParseTime = DateTime.Now - lastTime;
Console.WriteLine($"Specced parse time: {speccedParseTime}");
lastTime = DateTime.Now;
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(serialized2);
TimeSpan unspeccedParseTime = DateTime.Now - lastTime;
Console.WriteLine($"Unspecced parse time: {unspeccedParseTime}");
}
{
string testName = "With and without file comment";
string testString1 = "#This is a file comment\n" +
"#One more file comment line\n" +
"column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
string testString2 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
CommentedTsv<BoolTestRecord2> parsed2 = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString2));
if (parsed.FileComment == "This is a file comment\nOne more file comment line" && parsed2.FileComment == null)
{
Console.WriteLine($"Passed {testName}");
}
else
{
Console.WriteLine($"Failed {testName}");
}
}
{
string testName = "With and without types";
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\twoo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Passed {testName} 1A");
}
catch (Exception e)
{
Console.WriteLine($"Failed {testName} 1A");
}
try
{
Tsv<BoolTestRecord2> parsed2 = SaneTsv.ParseSimpleTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1B");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1C");
}
string testString2 = "column1\tcolumn2\tcolumnthree\\nyep" +
"\nTRUE\tvalue\\\\twoo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 2A");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 2A");
}
try
{
Tsv<BoolTestRecord2> parsed2 = SaneTsv.ParseSimpleTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 2B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 2B");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 2C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 2C");
}
}
{
string testName = "With and without line comment";
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\n#This is a comment" +
"\n#Another comment line" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther";
try
{
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Passed {testName} 1A");
}
catch (Exception e)
{
Console.WriteLine($"Failed {testName} 1A");
}
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1B");
}
try
{
Tsv<BoolTestRecord2> parsed2 = SaneTsv.ParseSimpleTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1C");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1D");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1D");
}
}
{
string testName = "End of file comment";
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\n# Hey, you're not supposed to have comments at the end of the tsv!";
try
{
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1A");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1A");
}
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1B");
}
string testString2 = "column1\tcolumn2\tcolumnthree\\nyep" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\n# Hey, you're not supposed to have comments at the end of the tsv!";
try
{
Tsv<BoolTestRecord3> parsed3 = SaneTsv.ParseSimpleTsv<BoolTestRecord3>(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1C");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1D");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1D");
}
}
{
string testName = "Partial parsing";
string line1 = "column1\tcolumn2\tcolumnthree\\nyep";
string line2 = "\nTRUE\tvalue\\\\t\0woo\tvaluetrhee";
string line3 = "\nFALSE\tnother\tno\\ther";
byte[] inputBuffer = Encoding.UTF8.GetBytes(line1 + line2 + line3);
var headerTypes = new List<Type>();
var headerNames = new List<string>();
var headerPropertyInfos = new List<PropertyInfo>();
int columnCount = 0;
foreach (PropertyInfo property in typeof(BoolTestRecord3).GetProperties())
{
TsvColumnAttribute attribute = (TsvColumnAttribute)Attribute.GetCustomAttribute(property, typeof(TsvColumnAttribute));
if (attribute == null)
{
continue;
}
headerNames.Add(attribute.ColumnName ?? property.Name);
headerTypes.Add(attribute.ColumnType ?? GetColumnFromType(property.PropertyType));
headerPropertyInfos.Add(property);
// TODO: Check that the property type and given column type are compatible
columnCount++;
}
BoolTestRecord3[] records = SaneTsv.Parse<BoolTestRecord3>(inputBuffer,
FormatType.SIMPLE_TSV,
headerPropertyInfos.ToArray(),
headerTypes.ToArray(),
line1.Length + line2.Length + 1,
inputBuffer.Length);
if (records.Length == 0 )
{
Console.WriteLine($"Passed {testName} 1");
}
else
{
Console.WriteLine($"Failed {testName} 1");
}
BoolTestRecord3[] records2 = SaneTsv.Parse<BoolTestRecord3>(inputBuffer,
FormatType.SIMPLE_TSV,
headerPropertyInfos.ToArray(),
headerTypes.ToArray(),
line1.Length,
line1.Length + 3);
if (records2[0].Column3 == "valuetrhee")
{
Console.WriteLine($"Passed {testName} 2");
}
else
{
Console.WriteLine($"Failed {testName} 2");
}
string[][] data = SaneTsv.ParseSimpleTsv(inputBuffer, 3, line1.Length + line2.Length + 1, inputBuffer.Length);
if (data[0][1] == "nother")
{
Console.WriteLine($"Passed {testName} 3");
}
else
{
Console.WriteLine($"Failed {testName} 3");
}
string[][] data2 = SaneTsv.ParseSimpleTsv(inputBuffer, 3, line1.Length, line1.Length + 3);
if (data2.Length == 0)
{
Console.WriteLine($"Passed {testName} 4");
}
else
{
Console.WriteLine($"Failed {testName} 4");
}
}
{
string testName = "End of file \\n";
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\n";
try
{
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1A");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1A");
}
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1B");
}
string testString2 = "column1\tcolumn2\tcolumnthree\\nyep" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\n";
try
{
Tsv<BoolTestRecord3> parsed3 = SaneTsv.ParseSimpleTsv<BoolTestRecord3>(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1C");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1D");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1D");
}
}
{
string testName = "End of file partial record";
string testString1 = "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\nTRUE\t";
try
{
CommentedTsv<BoolTestRecord2> parsed = SaneTsv.ParseCommentedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1A");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1A");
}
try
{
Tsv<BoolTestRecord2> parsed = SaneTsv.ParseTypedTsv<BoolTestRecord2>(Encoding.UTF8.GetBytes(testString1));
Console.WriteLine($"Failed {testName} 1B");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1B");
}
string testString2 = "column1\tcolumn2\tcolumnthree\\nyep" +
"\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" +
"\nFALSE\tnother\tno\\ther" +
"\nTRUE\t";
try
{
Tsv<BoolTestRecord3> parsed3 = SaneTsv.ParseSimpleTsv<BoolTestRecord3>(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1C");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1C");
}
try
{
(string[] columns, string[][] data) = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString2));
Console.WriteLine($"Failed {testName} 1D");
}
catch (Exception e)
{
Console.WriteLine($"Passed {testName} 1D");
}
}
Console.WriteLine("Done with tests");
}
}