Problem Statement
Given a hexadecimal string (hascii) and its binary specification (a list of byte field names, their offsets, and byte lengths), design classes to parse all byte-fields, cast bytes into suitable types for storage, and allow the user to write to a byte-field. For this purpose, I've created a simple binary specification:
string hasciiData = "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000";
With the following binary specification:
string _year = "07E3"; // 2019 16-bit int, 0x07E3
string _month = "0A"; // 10 8-bit int, 0x0A
string _day = "02"; // 2 8-bit int, 0x02
string _pi = "40490FD8"; // IEEE-754 Single Precision Float, 0x4049 0FD8
string _eulers = "402DF854"; // IEEE-754 Single Precision Float, 0x402D F854
string _secretValue = "80000000"; // 32-bit int, 0x80000000 (Decimal -2147483648)
string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!", 0x48656C6C6F576F726C6421
string _bigInt = "8000000000000000"; // 64-bit int, 0x8000 0000 0000 0000 (Decimal -9223372036854775808)
Class Design
The design separates byte-level specification from the code that reads bytes from the string. The "specification" encapsulates the logical structure of the hascii data (storing details like the names of byte fields, byte offsets and field lengths) so that it can be used to parse (read) and write to the hascii string.
Manipulation of hascii characters (hascii bytes are pairs of hascii characters) are handled by the HasciiParser class. The PD0Format class stores the specification, implemented using multiple dictionaries that map each byte field name (string) to a tuple (int, int) where Tuple.Item1 is the base-1 byte position of hascii bytes (i.e., the index value, which starts at 0 and corresponds to byte position #1, increments by 2 since there are 2 hex characters per byte) and Tuple.Item2 is the length of the byte field (in units of 'bytes' as in the "Year" bytefield begins at byte position #1 and contains 2 bytes).
protected ByteSpecification dateSpec = new ByteSpecification()
{
{"Year", (1, 2)},
{"Month", (3, 1)},
{"Day", (4, 1)},
};
The specification also includes a dictionary mapping byte field names to functions that cast bytes to appropriate types (determined by the binary specification).
Hascii character manipulation is decoupled from the binary specification so that if you were to say, rearrange the order of bytefields or add additional byte fields, you need only change the Specification dictionaries.
Code
using System;
using System.Collections.Generic;
using System.Text;
using ByteSpecification = System.Collections.Generic.Dictionary<string, (int, int)>;
using CastOperation = System.Collections.Generic.Dictionary<string, System.Func<string, object>>;
namespace ByteParsing
{
public class HasciiParser
{
public static string HexToString(string hascii)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hascii.Length; i += 2)
{
string hexByte = hascii.Substring(i, 2);
sb.Append(Convert.ToChar(Convert.ToUInt32(hexByte, 16)));
}
return sb.ToString();
}
public static string StringToHex(string str, int maxWidth, bool padLeft=true, char padChar='0')
{
StringBuilder sb = new StringBuilder();
foreach (char c in str)
{
sb.AppendFormat("{0:X}", Convert.ToInt32(c));
}
if (padLeft)
{
return sb.ToString().PadLeft(maxWidth, padChar);
}
return sb.ToString().PadRight(maxWidth, padChar);
}
public DateTime HexToDatetime(string hascii)
{
int year = Convert.ToInt32(hascii.Substring(0, 4), 16);
int month = Convert.ToInt32(hascii.Substring(4, 2), 16);
int day = Convert.ToInt32(hascii.Substring(6, 2), 16);
return new DateTime(year, month, day);
}
public static float HexToFloat(string hascii)
{
// https://stackoverflow.com/a/7903300/3396951
uint num = uint.Parse(hascii, System.Globalization.NumberStyles.AllowHexSpecifier);
byte[] floatVals = BitConverter.GetBytes(num);
return BitConverter.ToSingle(floatVals, 0);
}
}
public class PD0Format
{
public string _hasciiData; // Normally private
public PD0Format(string hasciiData)
{
_hasciiData = hasciiData;
}
protected CastOperation castop = new CastOperation()
{
{"Year", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Month", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Day", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Pi", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
{"EulersNumber", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
{"SecretValue", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"SecretMessage", (hascii_str) => HasciiParser.HexToString(hascii_str)},
{"BigInt", (hascii_str) => Convert.ToInt64(hascii_str, 16)}
};
protected ByteSpecification dateSpec = new ByteSpecification()
{
{"Year", (1, 2)},
{"Month", (3, 1)},
{"Day", (4, 1)},
};
protected ByteSpecification mathConstantsSpec = new ByteSpecification()
{
{"Pi", (5, 4)},
{"EulersNumber", (9, 4)}
};
protected ByteSpecification secretsSpec = new ByteSpecification()
{
{"SecretValue", (13, 4)},
{"SecretMessage", (17, 11)},
{"BigInt", (28, 8)}
};
private string GetHasciiBytes(ByteSpecification byteSpec, string fieldName)
{
// ByteSpecification assumes base-1 indexing. Substring requires base-0 indexing so we must subtract 2 (reason below).
// Because there are two hex characters to a byte, we have to multiply the startIndex of string.Substring
// by 2. To get to the startIndex of a byte, we must substract by multiples of 2.
// Item2 of ByteSpecification's dictionary value represents the number of bytes. Since two hex characters represent
// a byte, the number of characters to extract using string.Substring is Item2 * 2
return _hasciiData.Substring(byteSpec[fieldName].Item1 * 2 - 2, byteSpec[fieldName].Item2 * 2);
}
public dynamic this[string category, string fieldName]
{
get
{
ByteSpecification spec = null;
switch (category.ToLower())
{
case "date":
spec = dateSpec;
goto case "cast";
case "constants":
spec = mathConstantsSpec;
goto case "cast";
case "secrets":
spec = secretsSpec;
goto case "cast";
case "cast":
string hascii_bytes = GetHasciiBytes(spec, fieldName); // Retrieve bytes from underlying string
return castop[fieldName](hascii_bytes); // Cast to appropriate type, according to mapping defined in CastOperation
}
return new ArgumentException();
}
set
{
switch (category.ToLower())
{
case "secrets":
int insertLocation = secretsSpec[fieldName].Item1 * 2 - 2;
int maxCharFieldWidth = secretsSpec[fieldName].Item2 * 2; // Used for padding when the number of hex chars isn't even
string val = null;
switch (fieldName)
{
case "SecretValue":
val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0'); // Convert value to hascii representation
goto case "EmplaceString";
case "SecretMessage":
val = HasciiParser.StringToHex(value, maxCharFieldWidth);
goto case "EmplaceString";
case "BigInt":
val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0');
goto case "EmplaceString";
case "EmplaceString":
_hasciiData = _hasciiData.Remove(insertLocation, maxCharFieldWidth); // Remove the characters currently present
_hasciiData = _hasciiData.Insert(insertLocation, val ?? throw new InvalidOperationException());
Console.WriteLine(_hasciiData);
break;
}
break;
case "date":
throw new NotImplementedException();
case "constants":
throw new NotImplementedException();
}
}
}
}
class Program
{
static void Main(string[] args)
{
/*
string _year = "07E3"; // 2019 16-bit int, 0x07E3
string _month = "0A"; // 10 8-bit int, 0x0A
string _day = "02"; // 2 8-bit int, 0x02
string _pi = "40490FD8"; // IEEE-754 Single Precision Float, 0x4049 0FD8
string _eulers = "402DF854"; // IEEE-754 Single Precision Float, 0x402D F854
string _secretValue = "80000000"; // 32-bit int, 0x80000000 (Decimal -2147483648)
string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!", 0x48656C6C6F576F726C6421
string _bigInt = "8000000000000000"; // 64-bit int, 0x8000 0000 0000 0000 (Decimal -9223372036854775808)
*/
string hasciiData = "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000";
PD0Format ensemble = new PD0Format(hasciiData);
int recordYear = ensemble["date", "Year"];
int recordMonth = ensemble["date", "Month"];
int recordDay = ensemble["date", "Day"];
Console.WriteLine(new DateTime(recordYear, recordMonth, recordDay));
float Pi = ensemble["constants", "Pi"];
float exp1 = ensemble["Constants", "EulersNumber"];
Console.WriteLine($"Pi: {Pi}\nEuler's Number: {exp1}");
int secretValue = ensemble["secrets", "SecretValue"];
string secretMsg = ensemble["secrets", "SecretMessage"];
long bigInt = ensemble["secrets", "BigInt"];
Console.WriteLine($"Secret Value: {secretValue}\nSecret Msg: {secretMsg}\nbigInt: {bigInt}");
// Usage: Writing
string defaultData = "0000000000000000000000000000000000000000000000000000000000000000000000";
PD0Format defaultRecord = new PD0Format(defaultData);
// 35791394 corresponds to 0x02222222 written as "02222222" in hascii
defaultRecord["secrets", "SecretValue"] = 35791394;
// "FooBarBaz" corresponds to 0x00 0046 6F6F 4261 7242 617A written as "0000466F6F42617242617A" in hascii
defaultRecord["secrets", "SecretMessage"] = "FooBarBaz";
// 1229782938247303441 corresponds to 0x1111 1111 1111 1111 written as "1111111111111111" in hascii
defaultRecord["secrets", "BigInt"] = 1229782938247303441;
// Original defaultData: "0000000000000000000000000000000000000000000000000000000000000000000000"
// Modified defaultData: "000000000000000000000000022222220000466F6F42617242617A1111111111111111"
Console.WriteLine(defaultRecord._hasciiData);
Console.ReadLine(); // Prevent console from closing
}
}
}
Program Output
10/2/2019 00:00:00
Pi: 3.141592
Euler's Number: 2.718282
Secret Value: -2147483648
Secret Msg: HelloWorld!
bigInt: -9223372036854775808
0000000000000000000000000222222200000000000000000000000000000000000000
000000000000000000000000022222220000466F6F42617242617A0000000000000000
000000000000000000000000022222220000466F6F42617242617A1111111111111111
000000000000000000000000022222220000466F6F42617242617A1111111111111111
4 Answers 4
ByteSpecification spec = null; switch (category.ToLower()) { case "date": spec = dateSpec; goto case "cast"; case "constants": spec = mathConstantsSpec; goto case "cast"; case "secrets": spec = secretsSpec; goto case "cast"; case "cast": string hascii_bytes = GetHasciiBytes(spec, fieldName); // Retrieve bytes from underlying string return castop[fieldName](hascii_bytes); // Cast to appropriate type, according to mapping defined in CastOperation } return new ArgumentException();
As t3chb0t addresses in his comment the above concept is a rather unconventional use of the switcth-case statement. switch statements are ment to select between discrete values of the same type, but here you use the last case to effectively make a local function. For a small context as this, it is maybe readable for now, but in maintenance situations (in five years by another programmer) it may be cumbersome to figure out why this kind of design is chosen. In general: I would avoid to "bend" statements just to be "smart".
Instead I would use a method to extract the value or you could just make the "cast" after the switch:
get
{
ByteSpecification spec = null;
switch (category.ToLower())
{
case "date":
spec = dateSpec;
break;
case "constants":
spec = mathConstantsSpec;
break;
case "secrets":
spec = secretsSpec;
break;
default:
throw new ArgumentException();
}
string hascii_bytes = GetHasciiBytes(spec, fieldName); // Retrieve bytes from underlying string
return castop[fieldName](hascii_bytes); // Cast to appropriate type, according to mapping defined in CastOperation
}
By the way: you probably mean throw new ArgumentException() rather than returning it?
ByteSpecification assumes base-1 indexing
Why do you choose an one based indexing? I don't see it justified in the context. It's just used internally in the PD0Format class. IMO you make things more complicated than they have to be.
Some of your conversions could be made a little more intuitively:
public static string HexToString(string hascii) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < hascii.Length; i += 2) { string hexByte = hascii.Substring(i, 2); sb.Append(Convert.ToChar(Convert.ToUInt32(hexByte, 16))); } return sb.ToString(); }
=>
public string HexToString(string hascii)
{
byte[] bytes = new byte[hascii.Length / 2];
for (int i = 0; i < slice.Length; i += 2)
{
bytes[i / 2] = byte.Parse(hascii.Substring(i, 2), NumberStyles.HexNumber);
}
return Encoding.UTF8.GetString(bytes); // Or use Encoding.Default
}
public static string StringToHex(string str, int maxWidth, bool padLeft = true, char padChar = '0') { StringBuilder sb = new StringBuilder(); foreach (char c in str) { sb.AppendFormat("{0:X}", Convert.ToInt32(c)); } if (padLeft) { return sb.ToString().PadLeft(maxWidth, padChar); } return sb.ToString().PadRight(maxWidth, padChar); }
=>
public string StringToHex(string value)
{
byte[] bytes = Encoding.UTF8.GetBytes((string)value);
string slice = string.Join("", bytes.Select(b => b.ToString("X2"))).PadLeft(Length, '0');
return SetSlice(hasciiFormat, slice);
}
// Usage: Writing string defaultData = "0000000000000000000000000000000000000000000000000000000000000000000000"; PD0Format defaultRecord = new PD0Format(defaultData);
It is very error prone to let the client provide a "zero-string" this way. Why not provide a default constructor:
public PD0Format()
{
_hasciiData = "0000000000000000000000000000000000000000000000000000000000000000000000";
}
that sets the neutral _hasciiData value.
Regarding the overall design of PD0Format, I don't like the ByteSpecification and CastOperation fields, because you have to address them via the switch statement.
Instead I would make a dictionary, so it would be possible to write the indexer as:
public dynamic this[string category, string fieldName]
{
get
{
// TODO: check the input values for null and existence if default behavior of Dictionary<> isn't enough.
return categories[category.ToLower()][fieldName.ToLower()].Extract(_hasciiData);
}
set
{
categories[category.ToLower()][fieldName.ToLower()].Insert(ref _hasciiData, value);
}
}
categories is then defined as:
static Dictionary<string, Dictionary<string, PD0Entry>> categories;
static PD0Format()
{
categories = new Dictionary<string, Dictionary<string, PD0Entry>>
{
{ "date", new Dictionary<string, PD0Entry>
{
{ "year", new PD0IntEntry(0, 2) },
{ "month", new PD0IntEntry(2, 1) },
{ "day", new PD0IntEntry(3, 1) },
}
},
{ "constants", new Dictionary<string, PD0Entry>
{
{ "pi", new PD0FloatEntry(4, 4) },
{ "eulersnumber", new PD0FloatEntry(8, 4) },
}
},
{ "secrets", new Dictionary<string, PD0Entry>
{
{ "secretvalue", new PD0IntEntry(12, 4) },
{ "secretmessage", new PD0StringEntry(16, 11) },
{ "bigint", new PD0LongEntry(27, 8) },
}
},
};
}
where PD0Entry is an abstract base class for classes that can handle sections of int, float, long and string:
abstract class PD0Entry
{
public PD0Entry(int start, int length)
{
Start = start * 2;
Length = length * 2;
}
public int Start { get; }
public int Length { get; }
protected string GetSlice(string hasciiFormat) => hasciiFormat.Substring(Start, Length);
protected string SetSlice(string hasciiFormat, string slice)
{
string prefix = hasciiFormat.Substring(0, Start);
string postfix = hasciiFormat.Substring(Start + Length);
return $"{prefix}{slice}{postfix}";
}
public abstract void Insert(ref string hasciiFormat, object value);
public abstract object Extract(string hasciiFormat);
}
The definition of this could be different - for instance after thinking about it, I don't like the definition of Insert(...) because of the ref string.. argument...
PD0IntEntry as an example could then be defined as:
class PD0IntEntry : PD0Entry
{
public PD0IntEntry(int start, int length) : base(start, length)
{
}
public override object Extract(string hasciiFormat)
{
string slice = GetSlice(hasciiFormat);
return Convert.ToInt32(slice, 16);
}
public override void Insert(ref string hasciiFormat, object value)
{
hasciiFormat = SetSlice(hasciiFormat, ((int)value).ToString($"X{Length}"));
}
}
In this way it is easy to add/remove sections/entries to/from the categories because you'll only have to change the categories dictionary.
Besides that, the responsibility is clear and separated.
-
1\$\begingroup\$
... you could just make the "cast" after the switch.Great suggestion - Implemented!I would use method to extract the value ...the call stack increases if we use method extraction.get{}would call a method that switch on a category name and do what is done here. There isn't anything else that needs to be done except return the byte field value so I chose to keep the call stack flat.you probably mean throw new ArgumentException() rather than returning it?Yes! Thanks for catching that. \$\endgroup\$Minh Tran– Minh Tran2019年10月06日 17:14:37 +00:00Commented Oct 6, 2019 at 17:14 -
1\$\begingroup\$
Why do you choose a one based indexing?The binary specification I'm working with described byte field offsets using this convention and so I did this to maintain consistency. \$\endgroup\$Minh Tran– Minh Tran2019年10月06日 17:14:58 +00:00Commented Oct 6, 2019 at 17:14 -
1\$\begingroup\$
Some of your conversions could be made a little more intuitively: HexToString()Converting to byte[] and lettingEncoding.UTF8.GetStringdo the conversion offers more flexibility, shortens code. Accepted! \$\endgroup\$Minh Tran– Minh Tran2019年10月06日 17:15:17 +00:00Commented Oct 6, 2019 at 17:15 -
1\$\begingroup\$
Some of your conversions could be made a little more intuitively: StringToHex()I like it. I've never usedEncoding.UTF8API but will look into it. \$\endgroup\$Minh Tran– Minh Tran2019年10月06日 17:15:31 +00:00Commented Oct 6, 2019 at 17:15 -
1\$\begingroup\$
It is very error prone to let the client provide a "zero-string" this way. Why not provide a default constructor.Great suggestion. Some features of the constructor need to be added here to allow the user to initialize byte fields from otherPD0Formatobjects. \$\endgroup\$Minh Tran– Minh Tran2019年10月06日 17:15:48 +00:00Commented Oct 6, 2019 at 17:15
Implemented Henrik Hansen's suggested edits:
- Removed
gotostatements inswitchstatements of indexer HexToString()now usesEncoding.UTF.GetString()to convert hex to stringsStringToHex()now uses LINQ.SELECT(...)to convert bytes ofbyte[]to string- Added default constructor to initialize
_hasciiDatato string of 70 empty zeros
Structural changes to come.
Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using ByteSpecification = System.Collections.Generic.Dictionary<string, (int, int)>;
using CastOperation = System.Collections.Generic.Dictionary<string, System.Func<string, object>>;
namespace ByteParsing
{
public class HasciiParser
{
public static string HexToString(string hascii)
{
byte[] bytes = new byte[hascii.Length / 2];
for (int i = 0; i < hascii.Length; i += 2)
{
bytes[i / 2] = byte.Parse(hascii.Substring(i, 2), NumberStyles.HexNumber);
}
return Encoding.UTF8.GetString(bytes); // Or use Encoding.Default
}
public static string StringToHex(string str, int maxWidth)
{
byte[] bytes = Encoding.UTF8.GetBytes(str);
return string.Join("", bytes.Select(b => b.ToString("X2"))).PadLeft(maxWidth, '0');
}
public DateTime HexToDatetime(string hascii)
{
int year = Convert.ToInt32(hascii.Substring(0, 4), 16);
int month = Convert.ToInt32(hascii.Substring(4, 2), 16);
int day = Convert.ToInt32(hascii.Substring(6, 2), 16);
return new DateTime(year, month, day);
}
public static float HexToFloat(string hascii)
{
// https://stackoverflow.com/a/7903300/3396951
uint num = uint.Parse(hascii, System.Globalization.NumberStyles.AllowHexSpecifier);
byte[] floatVals = BitConverter.GetBytes(num);
return BitConverter.ToSingle(floatVals, 0);
}
}
public class PD0Format
{
public string _hasciiData; // Normally private
public PD0Format()
{
_hasciiData = "0000000000000000000000000000000000000000000000000000000000000000000000";
}
public PD0Format(string hasciiData)
{
_hasciiData = hasciiData;
}
protected CastOperation castop = new CastOperation()
{
{"Year", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Month", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Day", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"Pi", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
{"EulersNumber", (hascii_str) => HasciiParser.HexToFloat(hascii_str)},
{"SecretValue", (hascii_str) => Convert.ToInt32(hascii_str, 16)},
{"SecretMessage", (hascii_str) => HasciiParser.HexToString(hascii_str)},
{"BigInt", (hascii_str) => Convert.ToInt64(hascii_str, 16)}
};
protected ByteSpecification dateSpec = new ByteSpecification()
{
{"Year", (1, 2)},
{"Month", (3, 1)},
{"Day", (4, 1)},
};
protected ByteSpecification mathConstantsSpec = new ByteSpecification()
{
{"Pi", (5, 4)},
{"EulersNumber", (9, 4)}
};
protected ByteSpecification secretsSpec = new ByteSpecification()
{
{"SecretValue", (13, 4)},
{"SecretMessage", (17, 11)},
{"BigInt", (28, 8)}
};
private string GetHasciiBytes(ByteSpecification byteSpec, string fieldName)
{
// ByteSpecification assumes base-1 indexing. Substring requires base-0 indexing so we must subtract 2 (reason below).
// Because there are two hex characters to a byte, we have to multiply the startIndex of string.Substring
// by 2. To get to the startIndex of a byte, we must substract by multiples of 2.
// Item2 of ByteSpecification's dictionary value represents the number of bytes. Since two hex characters represent
// a byte, the number of characters to extract using string.Substring is Item2 * 2
return _hasciiData.Substring(byteSpec[fieldName].Item1 * 2 - 2, byteSpec[fieldName].Item2 * 2);
}
public dynamic this[string category, string fieldName]
{
get
{
ByteSpecification spec = null;
switch (category.ToLower())
{
case "date":
spec = dateSpec;
break;
case "constants":
spec = mathConstantsSpec;
break;
case "secrets":
spec = secretsSpec;
break;
default:
throw new ArgumentException($"Unimplemented specification category `{category}`");
}
string hasciiBytes = GetHasciiBytes(spec, fieldName); // Retrieve bytes from underlying string
return castop[fieldName](hasciiBytes); // Cast to appropriate type, according to mapping defined in CastOperation
}
set
{
switch (category.ToLower())
{
case "secrets":
int insertLocation = secretsSpec[fieldName].Item1 * 2 - 2;
int maxCharFieldWidth = secretsSpec[fieldName].Item2 * 2; // Used for padding when the number of hex chars isn't even
string val = null;
switch (fieldName)
{
case "SecretValue":
val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0'); // Convert value to hascii representation
break;
case "SecretMessage":
val = HasciiParser.StringToHex(value, maxCharFieldWidth);
break;
case "BigInt":
val = String.Format("{0:X}", value).PadLeft(maxCharFieldWidth, '0');
break;
}
_hasciiData = _hasciiData.Remove(insertLocation, maxCharFieldWidth); // Remove the characters currently present
_hasciiData = _hasciiData.Insert(insertLocation, val ?? throw new InvalidOperationException());
Debug.WriteLine(_hasciiData);
break;
case "date":
throw new NotImplementedException();
case "constants":
throw new NotImplementedException();
}
}
}
}
class Program
{
static void Main(string[] args)
{
/*
string _year = "07E3"; // 2019 16-bit int, 0x07E3
string _month = "0A"; // 10 8-bit int, 0x0A
string _day = "02"; // 2 8-bit int, 0x02
string _pi = "40490FD8"; // IEEE-754 Single Precision Float, 0x4049 0FD8
string _eulers = "402DF854"; // IEEE-754 Single Precision Float, 0x402D F854
string _secretValue = "80000000"; // 32-bit int, 0x80000000 (Decimal -2147483648)
string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!", 0x48656C6C6F576F726C6421
string _bigInt = "8000000000000000"; // 64-bit int, 0x8000 0000 0000 0000 (Decimal -9223372036854775808)
*/
string hasciiData = "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000";
PD0Format ensemble = new PD0Format(hasciiData);
int recordYear = ensemble["date", "Year"];
int recordMonth = ensemble["date", "Month"];
int recordDay = ensemble["date", "Day"];
Debug.WriteLine(new DateTime(recordYear, recordMonth, recordDay));
float Pi = ensemble["constants", "Pi"];
float exp1 = ensemble["Constants", "EulersNumber"];
Debug.WriteLine($"Pi: {Pi}\nEuler's Number: {exp1}");
int secretValue = ensemble["secrets", "SecretValue"];
string secretMsg = ensemble["secrets", "SecretMessage"];
long bigInt = ensemble["secrets", "BigInt"];
Debug.WriteLine($"Secret Value: {secretValue}\nSecret Msg: {secretMsg}\nbigInt: {bigInt}");
// Usage: Writing
PD0Format defaultRecord = new PD0Format();
// 35791394 corresponds to 0x02222222 written as "02222222" in hascii
defaultRecord["secrets", "SecretValue"] = 35791394;
// "FooBarBaz" corresponds to 0x00 0046 6F6F 4261 7242 617A written as "0000466F6F42617242617A" in hascii
defaultRecord["secrets", "SecretMessage"] = "FooBarBaz";
// 1229782938247303441 corresponds to 0x1111 1111 1111 1111 written as "1111111111111111" in hascii
defaultRecord["secrets", "BigInt"] = 1229782938247303441;
// Original defaultData: "0000000000000000000000000000000000000000000000000000000000000000000000"
// Modified defaultData: "000000000000000000000000022222220000466F6F42617242617A1111111111111111"
Debug.WriteLine(defaultRecord._hasciiData);
Console.ReadLine(); // Prevent console from closing
}
}
}
Output
10/2/2019 00:00:00
Pi: 3.141592
Euler's Number: 2.718282
Secret Value: -2147483648
Secret Msg: HelloWorld!
bigInt: -9223372036854775808
0000000000000000000000000222222200000000000000000000000000000000000000
000000000000000000000000022222220000466F6F42617242617A0000000000000000
000000000000000000000000022222220000466F6F42617242617A1111111111111111
000000000000000000000000022222220000466F6F42617242617A1111111111111111
What about a simpler approach?
public class PD0Format
{
public static PD0Format Empty => new PD0Format(new byte[35]);
public static PD0Format Parse(string text) =>
new PD0Format(Enumerable
.Range(0, text.Length / 2)
.Select(i => Convert.ToByte(text.Substring(i * 2, 2), 16))
.ToArray());
PD0Format(byte[] array)
{
Stream = new MemoryStream(array);
Reader = new BinaryReader(Stream);
Writer = new BinaryWriter(Stream);
}
MemoryStream Stream { get; }
BinaryReader Reader { get; }
BinaryWriter Writer { get; }
public override string ToString() =>
BitConverter.ToString(Stream.ToArray()).Replace("-", "");
public DateTime Date
{
get
{
Stream.Position = 0;
return new DateTime(
Reader.ReadInt32(),
Reader.ReadInt32(),
Reader.ReadInt32());
}
set
{
Stream.Position = 0;
Writer.Write(value.Year);
Writer.Write(value.Month);
Writer.Write(value.Day);
}
}
public float Pi
{
get
{
Stream.Position = 4;
return Reader.ReadSingle();
}
set
{
Stream.Position = 4;
Writer.Write(value);
}
}
// etc...
}
So you could extract Pi like this:
var pd0 = PD0Format.Parse("00000000D80F4940000000000000000000000000000000000000000000000000000000");
Console.WriteLine(pd0.Pi); // 3.141592
or create a new packet in a way like this:
var pd0 = PD0Format.Empty;
pd0.Pi = 3.141592;
Console.WriteLine(pd0); // 00000000D80F4940000000000000000000000000000000000000000000000000000000
-
1\$\begingroup\$ I'm afraid this solution, even if interesting, won't have many fans because it lacks explanation :-[ Could you
ToStringyour thoughts? ;-P \$\endgroup\$t3chb0t– t3chb0t2019年10月08日 17:31:41 +00:00Commented Oct 8, 2019 at 17:31 -
1\$\begingroup\$ @t3chb0t I do believe that code is the best way to document design, and only bad code or bug requires text description. Here is the usage sample though, not sure if this is what you were looking for :) \$\endgroup\$Dmitry Nogin– Dmitry Nogin2019年10月08日 17:52:28 +00:00Commented Oct 8, 2019 at 17:52
-
2\$\begingroup\$ In general I share your opinion, however, Code Review has slightly different rules concerning documentation and reviews. I mean that a casual reader might not see what we do and the reasoning is also not always obvious ;-] \$\endgroup\$t3chb0t– t3chb0t2019年10月08日 17:56:50 +00:00Commented Oct 8, 2019 at 17:56
-
1\$\begingroup\$ You get my unconditional upvote, but t3chb0t is right \$\endgroup\$dfhwze– dfhwze2019年10月08日 18:09:52 +00:00Commented Oct 8, 2019 at 18:09
-
\$\begingroup\$ I might need to associate each byte field name with a description of what the stored value represents (the binary specification is for sensor data output).
PD0Formatmight be used by a GUI to display the byte field description in a tool-tip. The description itself will be a string. \$\endgroup\$Minh Tran– Minh Tran2019年10月08日 20:11:35 +00:00Commented Oct 8, 2019 at 20:11
Implemented Henrik Hansen's suggested edits:
Define an abstract class,
PD0Bytefield, to encapsulate how byte fields are specified (PD0Bytefield.StartHexDigitIndexandPD0Bytefield.HexDigitLength) with abstract methods to specify how they should be casted when read from (PD0Bytefield.Extract()) and how they should be written to the underlying string (PD0Bytefield.Insert()).This allows the indexer of
PD0Formatto defer implementation details to concrete implementations ofPD0Bytefield(namelyPD0IntBytefield,PD0FloatBytefield,PD0StringBytefield,PD0LongBytefield). This also makescastopobsolete — which is desirable since the current implementation requires matching keys in bothcastopand allByteSpecificationdictionaries, which is hard to maintain as the number of keys increases.Define a single dictionary,
PD0Format.Categories, to map byte field names to concrete implementations ofPD0Bytefield. Because the binary specification doesn't change between instantiations ofPD0Format, this dictionary should be made static .Because
PD0Format._hasciiDatais not a static field, it can't be used in a static context (i.e., we can't passPD0Format._hasciiDatato concrete classes ofPD0Bytefieldswhen they're initialized byCategories's dictionary initializer). This forces us to passPD0Format._hasciiData, an instance variable, to the abstract methods ofPD0Bytefieldby reference.
Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using ByteSpecification = System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<string, ByteParsing.Bytefield>>;
namespace ByteParsing
{
public class HasciiParser
{
public static int GetByteSize(string hascii)
{
return (int)Math.Ceiling(hascii.Length / 2.0);
}
public static string HexToString(string hascii)
{
byte[] bytes = new byte[hascii.Length / 2];
for (int i = 0; i < hascii.Length; i += 2)
{
bytes[i / 2] = byte.Parse(hascii.Substring(i, 2), NumberStyles.HexNumber);
}
return Encoding.UTF8.GetString(bytes); // Or use Encoding.Default
}
public static string StringToHex(string str, int maxWidth)
{
byte[] bytes = Encoding.UTF8.GetBytes(str);
return string.Join("", bytes.Select(b => b.ToString("X2"))).PadLeft(maxWidth, '0');
}
public DateTime HexToDatetime(string hascii)
{
int year = Convert.ToInt32(hascii.Substring(0, 4), 16);
int month = Convert.ToInt32(hascii.Substring(4, 2), 16);
int day = Convert.ToInt32(hascii.Substring(6, 2), 16);
return new DateTime(year, month, day);
}
public static float HexToFloat(string hascii)
{
// https://stackoverflow.com/a/7903300/3396951
uint num = uint.Parse(hascii, System.Globalization.NumberStyles.AllowHexSpecifier);
byte[] floatVals = BitConverter.GetBytes(num);
return BitConverter.ToSingle(floatVals, 0);
}
}
public abstract class Bytefield
{
protected int StartHexDigitIndex { get; }
protected int HexDigitLength { get; }
public string BytefieldDescription { get; set; }
public int ByteStart => (StartHexDigitIndex / 2) + 1;
public int ByteLength => HexDigitLength / 2;
protected Bytefield(int byteStartIndex, int byteLength)
{
// User provides startIndex, which follows base-1 indexing (to be consistent with convention used in binary specification)
// but this constructor converts index to base-0. Indexing, here, refers to the indexing of hex digits (there are 2 hex-digits to a byte)
// and NOT the indexing of bytes.
StartHexDigitIndex = (byteStartIndex - 1) * 2;
HexDigitLength = byteLength * 2;
}
protected Bytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: this(byteStartIndex, byteLength)
{
BytefieldDescription = bytefieldDescription;
}
protected string GetBytes(ref string bytefieldData) => bytefieldData.Substring(StartHexDigitIndex, HexDigitLength);
protected void SetBytes(ref string bytefieldData, string hexval)
{
// If the number of bytes occupied by the assigned value is greater than the byte length specfication of the bytefield,
// ArgumentException will be thrown.
if (HasciiParser.GetByteSize(hexval) != ByteLength)
throw new ArgumentException($"Bytefield length is {ByteLength} bytes. Tried to assign {HasciiParser.GetByteSize(hexval)} bytes.");
string prefix = bytefieldData.Substring(0, StartHexDigitIndex);
string postfix = bytefieldData.Substring(StartHexDigitIndex + HexDigitLength);
bytefieldData = $"{prefix}{hexval}{postfix}"; // Potentially inefficient for large number of SetBytes()
}
public abstract void Insert(ref string bytefieldData, object value);
public abstract object Extract(ref string bytefieldData);
}
public class UInt16Bytefield : Bytefield
{
public UInt16Bytefield(int byteStartIndex, int byteLength) : base(byteStartIndex, byteLength)
{
}
public UInt16Bytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: base(byteStartIndex, byteLength, bytefieldDescription)
{
}
public override void Insert(ref string bytefieldData, object value) =>
SetBytes(ref bytefieldData, ((UInt16) value).ToString($"X{HexDigitLength}"));
public override object Extract(ref string bytefieldData) => Convert.ToUInt16(GetBytes(ref bytefieldData), 16);
}
public class Int32Bytefield : Bytefield
{
public Int32Bytefield(int byteStartIndex, int byteLength) : base(byteStartIndex, byteLength)
{
}
public Int32Bytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: base(byteStartIndex, byteLength, bytefieldDescription)
{
}
public override void Insert(ref string bytefieldData, object value) =>
SetBytes(ref bytefieldData, ((Int32)(value)).ToString($"X{HexDigitLength}"));
public override object Extract(ref string bytefieldData) =>
Convert.ToInt32(GetBytes(ref bytefieldData), 16);
}
public class FloatBytefield : Bytefield
{
public FloatBytefield(int byteStartIndex, int byteLength) : base(byteStartIndex, byteLength)
{
}
public FloatBytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: base(byteStartIndex, byteLength, bytefieldDescription)
{
}
public override void Insert(ref string bytefieldData, object value) => SetBytes(ref bytefieldData, ((Single) value).ToString($"X{HexDigitLength}"));
public override object Extract(ref string bytefieldData) => HasciiParser.HexToFloat(GetBytes(ref bytefieldData));
}
public class StringBytefield : Bytefield
{
public StringBytefield(int byteStartIndex, int byteLength) : base(byteStartIndex, byteLength)
{
}
public StringBytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: base(byteStartIndex, byteLength, bytefieldDescription)
{
}
public override void Insert(ref string bytefieldData, object value) => SetBytes(ref bytefieldData, HasciiParser.StringToHex((string) value, HexDigitLength));
public override object Extract(ref string bytefieldData) => HasciiParser.HexToString(GetBytes(ref bytefieldData));
}
public class ULongBytefield : Bytefield
{
public ULongBytefield(int byteStartIndex, int byteLength) : base(byteStartIndex, byteLength)
{
}
public ULongBytefield(int byteStartIndex, int byteLength, string bytefieldDescription)
: base(byteStartIndex, byteLength, bytefieldDescription)
{
}
public override void Insert(ref string bytefieldData, object value) => SetBytes(ref bytefieldData, ((UInt64) value).ToString($"X{HexDigitLength}"));
public override object Extract(ref string bytefieldData) => Convert.ToUInt64(GetBytes(ref bytefieldData), 16);
}
public class PD0Format
{
private string _hasciiData;
public string HasciiData
{
get => _hasciiData;
set => _hasciiData = value;
}
public ByteSpecification ByteSpec { get; set; }
public PD0Format()
{
HasciiData = new String('0', 70); // binary specification contains 280 bits/35 bytes, which is 70 hex digits
ByteSpec = InitializeByteSpecification();
}
public PD0Format(string hasciiData)
{
_hasciiData = hasciiData;
ByteSpec = InitializeByteSpecification();
}
protected ByteSpecification InitializeByteSpecification()
{
return new ByteSpecification()
{
{
"date", new Dictionary<string, Bytefield>()
{
{"year", new UInt16Bytefield(1, 2, "Today's four digit-year.")},
{"month", new UInt16Bytefield(3, 1, "Today's four digit-month.")},
{"day", new UInt16Bytefield(4, 1, "Today's four digit-day.")},
}
},
{
"constants", new Dictionary<string, Bytefield>()
{
{"mathconstant", new FloatBytefield(5, 4, "")},
{"physicsconstant", new FloatBytefield(9, 4, null)},
}
},
{
"secrets", new Dictionary<string, Bytefield>()
{
{"secretvalue", new Int32Bytefield(13, 4, "Keep this value a secret.")},
{"secretmessage", new StringBytefield(17, 11, "Keep this message a secret")},
{"bigint", new ULongBytefield(28, 8, HasciiData)},
}
}
};
}
public dynamic this[string category, string fieldname]
{
get => ByteSpec[category.ToLower()][fieldname.ToLower()].Extract(ref _hasciiData);
set => ByteSpec[category.ToLower()][fieldname.ToLower()].Insert(ref _hasciiData, value);
}
}
class Program
{
static void Main(string[] args)
{ }
}
}
Unit Test
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ByteParsing;
using TestHexData = System.Collections.Generic.Dictionary<string, string>;
namespace ByteParsingTests
{
[TestClass]
public class ByteParsingTests
{
public static readonly TestHexData hexdata = new TestHexData()
{
// string _year = "07E3"; // 2019, 16-bit int
// string _month = "0A"; // 10, 8-bit int
// string _day = "02"; // 2, 8-bit int
// string _pi = "40490FD8"; // 3.141592, IEEE-754 Single Precision Float
// string _eulers = "402DF854"; // 2.71828175, IEEE-754 Single Precision Float
// string _secretValue = "80000000"; // Decimal -2147483648, 32-bit int
// string _secretMsg = "48656C6C6F576F726C6421"; // ASCII string "HelloWorld!"
// string _bigInt = "8000000000000000"; // Decimal 9223372036854775808, 64-bit long (uint64)
{"PiEuler", "07E30A0240490FD8402df8548000000048656C6C6F576F726C64218000000000000000"},
// string _year = "07E3"; // 2019, 16-bit int
// string _month = "0A"; // 10, 8-bit int
// string _day = "09"; // 9, 8-bit int
// string _tau = "40C90E56"; // 6.282999, IEEE-754 Single Precision Float
// string _speedOfLight = "4F32D05E"; // 3E9, IEEE-754 Single Precision Float
// string _secretValue = "11111111"; // 286331153, 32-bit int
// string _secretMsg = "42697A7A7942757A7A2121"; // ASCII string "BizzyBuzz!!"
// string _bigInt = "2222222222222222"; // 2459565876494606882, 64-bit long (uint64)
{"TauSpeedOfLight", "07E30A0940C90E564F32D05E1111111142697A7A7942757A7A21212222222222222222"},
// string _year = "07E3"; // 2019, 16-bit int
// string _month = "0A"; // 10, 8-bit int
// string _day = "0A"; // 10, 8-bit int
// string _root2 = "3FB504D5"; // 1.4142099, IEEE-754 Single Precision Float
// string _electronCharge = "203D217B"; // 1.602E-19, IEEE-754 Single Precision Float
// string _secretValue = "33333333"; // 858993459, 32-bit int
// string _secretMsg = "4F6365616E466C6F6F7221"; // ASCII string "OceanFloor!"
// string _bigInt = "4444444444444444"; // 4919131752989213764, 64-bit long (uint64)
{"root2Charge", "07E30A0A3FB504D5203D217B333333334F6365616E466C6F6F72214444444444444444" }
};
public static bool NearlyEqual(float a, float b, float epsilon)
{
return NearlyEqual((double) a, (double) b, (double) epsilon);
}
public static bool NearlyEqual(double a, double b, double epsilon)
{
// Michael Borgwardt, https://stackoverflow.com/a/3875619/3396951
const double MinNormal = 2.2250738585072014E-308d;
double absA = Math.Abs(a);
double absB = Math.Abs(b);
double diff = Math.Abs(a - b);
if (a.Equals(b))
{ // shortcut, handles infinities
return true;
}
else if (a == 0 || b == 0 || absA + absB < MinNormal)
{
// a or b is zero or both are extremely close to it
// relative error is less meaningful here
return diff < (epsilon * MinNormal);
}
else
{ // use relative error
return diff / (absA + absB) < epsilon;
}
}
[TestMethod, TestCategory("ExceptionalCases")]
[ExpectedException(typeof(InvalidCastException))]
public void IndexerAssignment_Int16Value_ThrowsInvalidCastException()
{
PD0Format record = new PD0Format(hexdata["PiEuler"]);
Assert.AreEqual((UInt16)2019, record["date", "year"]);
record["date", "year"] = (Int16)2020; // Should be casted to (UInt16)
}
[TestMethod, TestCategory("ExceptionalCases")]
[ExpectedException(typeof(ArgumentException))]
public void IndexerAssignment_UInt16Value_ThrowsInvalidCastException()
{
PD0Format record = new PD0Format(hexdata["PiEuler"]);
// This should be ok. 255 isn't a valid month but what determines whether a value can be
// assigned is the range of values the underlying data type can store (0 - 2^16-1, in this case)
// and the byte specification. If the number of bytes occupied by the assigned value is greater
// than the byte length specfication of the bytefield, ArgumentException should be thrown.
record["date", "month"] = (UInt16) 0xFF;
Assert.AreEqual((UInt16)255, record["date", "month"]);
// Values greater than 255 (0xFF) should throw ArgumentException. This is because the "month"
// specification is defined to be one byte long. The underlying data type (UInt16, see PD0Format.ByteSpec)
// can hold unsigned values that fit within two bytes but the assigned value occupies more bytes than
// defined by the specification.
record["date", "month"] = (UInt16) 0x0100;
}
[TestMethod, TestCategory("TypicalUseCase")]
public void IndexerAssignment_UInt16MaxValue_SetsCorrectHexbytes()
{
PD0Format record = new PD0Format(hexdata["PiEuler"]);
record["date", "year"] = (UInt16) 65535; // 0xFFFF
Assert.AreEqual((UInt16)65535, record["date", "year"]);
Assert.AreEqual("FFFF", record["date", "year"].ToString("X4"));
}
[TestMethod, TestCategory("TypicalUseCase")]
[DataRow("PiEuler", (UInt16)2019, (UInt16)10, (UInt16)2, 3.141592F, 2.71828175F, (Int32)(-2147483648), "HelloWorld!", (UInt64)9223372036854775808)]
[DataRow("TauSpeedOfLight", (UInt16)2019, (UInt16)10, (UInt16)9, 6.282999F, 3000000000F, (Int32)(286331153), "BizzyBuzz!!", (UInt64)2459565876494606882)]
[DataRow("root2Charge", (UInt16)2019, (UInt16)10, (UInt16)10, 1.4142099F, 1.602000046096E-19F, (Int32)(858993459), "OceanFloor!", (UInt64)4919131752989213764)]
public void IndexerAccess_ReturnsCorrectTypesAndValues(string hexdataField, UInt16 year, UInt16 month, UInt16 day,
float mathConstant, float physicsConstants, Int32 secretValue, string secretMessage, ulong bigint)
{
var record = new PD0Format(hexdata[hexdataField]);
Assert.AreEqual(year, record["date", "year"]);
Assert.AreEqual(month, record["date", "month"]);
Assert.AreEqual(day, record["date", "day"]);
Assert.IsTrue(NearlyEqual(mathConstant, record["constants","mathconstant"], .0000001));
Assert.IsTrue(NearlyEqual(physicsConstants, record["constants", "physicsconstant"], .0000001));
Assert.AreEqual(secretValue, record["secrets","secretvalue"]);
Assert.AreEqual(secretMessage, record["secrets", "secretmessage"]);
Assert.AreEqual(bigint, record["secrets", "bigint"]);
}
[TestMethod, TestCategory("TypicalUseCase")]
public void IndexerAssignment_secretsCategory_SetsCorrectHexbytes()
{
PD0Format defaultRecord = new PD0Format();
Assert.AreEqual("0000000000000000000000000000000000000000000000000000000000000000000000", defaultRecord.HasciiData);
// 35791394 = 0x02222222 = "02222222"
// "FooBarBaz" = 0x0000466F6F42617242617A = "0000466F6F42617242617A"
// 1229782938247303441 =わ 0x1111111111111111 = "1111111111111111"
defaultRecord["secrets", "secretvalue"] = (Int32) 35791394;
defaultRecord["secrets", "secretmessage"] = "FooBarBaz";
defaultRecord["secrets", "bigint"] = (UInt64)1229782938247303441;
Assert.AreEqual("000000000000000000000000022222220000466F6F42617242617A1111111111111111", defaultRecord.HasciiData);
}
}
}
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
gotos there o_O they are sometimes useful but they are pretty scarry here. \$\endgroup\$switch/gotopattern used here is a step towards cutting down repetition and improving readability. Ultimately, it was used as a substitute for "method extraction" because the number of cases is small. If the binary specification called for hundreds of fields, I'd convert it to a function. I hope the flow of execution is self-evident: exactly 1case-statement runs initially to initialize a set of variables. Execution then jumps tocasethat does the actual work with those variables. \$\endgroup\$