7
\$\begingroup\$

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
\$\endgroup\$
4
  • 1
    \$\begingroup\$ They're C#'s "using alias directives" (basically type aliases) whose definition must come before namespace declarations. It's at the very top. \$\endgroup\$
    – Minh Tran
    Commented Oct 4, 2019 at 17:34
  • 1
    \$\begingroup\$ Oh boy, you have A LOT of gotos there o_O they are sometimes useful but they are pretty scarry here. \$\endgroup\$
    – t3chb0t
    Commented Oct 4, 2019 at 18:18
  • \$\begingroup\$ @t3chb0t It's a valid concern. The switch/goto pattern 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 1 case-statement runs initially to initialize a set of variables. Execution then jumps to case that does the actual work with those variables. \$\endgroup\$
    – Minh Tran
    Commented Oct 4, 2019 at 20:19
  • 2
    \$\begingroup\$ I rollbacked your latest edit. After an answer is made, you should not edit the question anymore. This to avoid answer invalidation. \$\endgroup\$
    – dfhwze
    Commented Oct 6, 2019 at 16:39

4 Answers 4

7
\$\begingroup\$
    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.

\$\endgroup\$
9
  • 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
    Commented 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
    Commented Oct 6, 2019 at 17:14
  • 1
    \$\begingroup\$ Some of your conversions could be made a little more intuitively: HexToString() Converting to byte[] and letting Encoding.UTF8.GetString do the conversion offers more flexibility, shortens code. Accepted! \$\endgroup\$
    – Minh Tran
    Commented 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 used Encoding.UTF8 API but will look into it. \$\endgroup\$
    – Minh Tran
    Commented 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 other PD0Format objects. \$\endgroup\$
    – Minh Tran
    Commented Oct 6, 2019 at 17:15
4
\$\begingroup\$

Implemented Henrik Hansen's suggested edits:

  • Removed goto statements in switch statements of indexer
  • HexToString() now uses Encoding.UTF.GetString() to convert hex to strings
  • StringToHex() now uses LINQ .SELECT(...) to convert bytes of byte[] to string
  • Added default constructor to initialize _hasciiData to 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
\$\endgroup\$
3
\$\begingroup\$

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
\$\endgroup\$
10
  • 1
    \$\begingroup\$ I'm afraid this solution, even if interesting, won't have many fans because it lacks explanation :-[ Could you ToString your thoughts? ;-P \$\endgroup\$
    – t3chb0t
    Commented 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\$ Commented 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
    Commented Oct 8, 2019 at 17:56
  • 1
    \$\begingroup\$ You get my unconditional upvote, but t3chb0t is right \$\endgroup\$
    – dfhwze
    Commented 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). PD0Format might 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
    Commented Oct 8, 2019 at 20:11
1
\$\begingroup\$

Implemented Henrik Hansen's suggested edits:

  • Define an abstract class, PD0Bytefield, to encapsulate how byte fields are specified (PD0Bytefield.StartHexDigitIndex and PD0Bytefield.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 PD0Format to defer implementation details to concrete implementations of PD0Bytefield (namely PD0IntBytefield, PD0FloatBytefield, PD0StringBytefield, PD0LongBytefield). This also makes castop obsolete — which is desirable since the current implementation requires matching keys in both castop and all ByteSpecification dictionaries, 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 of PD0Bytefield. Because the binary specification doesn't change between instantiations of PD0Format, this dictionary should be made static.

    Because PD0Format._hasciiData is not a static field, it can't be used in a static context (i.e., we can't pass PD0Format._hasciiData to concrete classes of PD0Bytefields when they're initialized by Categories's dictionary initializer). This forces us to pass PD0Format._hasciiData, an instance variable, to the abstract methods of PD0Bytefield by 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);
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.