using MultiTerm.Core.Types; using System.Collections.ObjectModel; using System.Text; namespace MultiTerm.Core.Model; /// /// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts. /// public class MultiFormatString: ObservableCollection { private const int charsPerHexBundle = 2; private const int charsPerBinBundle = 8; #region Insertion / Removal of Characters /// /// Inserts item at given index after checking if its valid using . /// /// index where to add item, usually items.Count when using Add method. /// item to insert protected override void InsertItem(int index, IFormattedCharacter item) { int offset = 0; bool insertSpacingAfter = false; if(item is FormattedCharacter formattedChar) { //check if valid, only insert then if(ValidateValue(formattedChar.Format, formattedChar.Character) == false) { return; } // check if spacing is required and insert if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index + offset, formattedChar)) { base.InsertItem(index, new SpacingCharacter()); offset++; // insert item with offset of 1 } if(IsSpacingCharacterRequiredAfterNewCharacter(this, index + offset, formattedChar)) { insertSpacingAfter = true; } } base.InsertItem(index + offset, item); if (insertSpacingAfter) { base.InsertItem(index + offset + 1, new SpacingCharacter()); } } protected override void RemoveItem(int index) { // if formatted character => just remove base.RemoveItem(index); // if previous item is a spacing character => remove it too if(this.ElementAt(index-1) is SpacingCharacter) { base.RemoveAt(index - 1); } } private static bool IsSpacingCharacterRequiredBeforeNewCharacter(IEnumerable collection, int indexOfInsertion, IFormattedCharacter newCharacter) { bool spacingCharRequired = false; // no spacing required if new character is a spacing character if (newCharacter is SpacingCharacter) { return false; } // no handling implemented for items that are not a FormattedCharacter if (newCharacter is not FormattedCharacter newFormattedCharacter) { throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); } // switch spacing required by type of format spacingCharRequired = newFormattedCharacter.Format switch { FormatType.Character => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format), FormatType.Hexadecimal => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) || CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerHexBundle, newFormattedCharacter.Format), FormatType.Binary => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) || CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerBinBundle, newFormattedCharacter.Format), _ => throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for format {newFormattedCharacter.Format}"), }; return spacingCharRequired; static bool CheckSpacingRequiredBecauseChangedFormat(IEnumerable collection, int indexOfInsertion, FormatType newCharFormat) { // check if previous item is of other type if (collection.Any()) { int count = 1; FormattedCharacter? prevChar = null; while (prevChar is null) { try { prevChar = collection.ElementAt(indexOfInsertion - count++) as FormattedCharacter; // format of previous is not equal to new => spacer required if (prevChar != null && prevChar.Format != newCharFormat) { return true; } } catch { break; } // end of collection => break while } } return false; } static bool CheckSpacingRequiredBecauseBundleMaxCountReached(IEnumerable collection, int indexOfInsertion, int charsPerBundle, FormatType newCharFormat) { // spacing required if previous x characters are of same format if (collection.Count() >= charsPerBundle) { try { // get last x elements of collection var lastXElements = collection.Take(new Range(new Index(indexOfInsertion - charsPerBundle), new Index(indexOfInsertion))); // get elements of last x elements where the same format is used var sameFormatElements = lastXElements.Where(x => x is FormattedCharacter formChar && formChar.Format == newCharFormat); // elements of same Format are more (impossible) or equal to amount required for bundle if (sameFormatElements.Count() >= charsPerBundle) { return true; } } catch { } // catch any exception, ignore it and return default } return false; } } private static bool IsSpacingCharacterRequiredAfterNewCharacter(IEnumerable collection, int indexOfInsertion, IFormattedCharacter newCharacter) { bool spacingCharRequired = false; // no spacing required if new character is a spacing character if (newCharacter is SpacingCharacter) { return false; } // no handling implemented for items that are not a FormattedCharacter if (newCharacter is not FormattedCharacter newFormattedCharacter) { throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredAfterNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); } // if there is anything in the collection if (collection.Any()) { // check if next character in collection is a FormattedCharacter try { var nextChar = collection.ElementAt(indexOfInsertion) as FormattedCharacter; // format of next is not equal to new => spacer required if (nextChar != null && nextChar.Format != newFormattedCharacter.Format) { return true; } } catch { } // catch any exception and return default result } return spacingCharRequired; } #endregion /// /// Validates wether a certain is valid for the given . /// /// true if valid public static bool ValidateValue(FormatType format, string value) { // invalid if more than one character if(value.Length > 1) { return false; } // extract character var character = value.First(); switch (format) { case FormatType.Character: // accept only ascii, not extended ascii or unicode return char.IsAscii(character); case FormatType.Hexadecimal: return (character >= '0' && character <= '9' || character >= 'A' && character <= 'F' || character >= 'a' && character <= 'f'); case FormatType.Binary: return (character == '0' || character == '1'); default: throw new NotImplementedException($"'{nameof(ValidateValue)}()' does not implement validation for format {format}"); } } #region GetBytes, Conversions /// /// Converts this to a byte array. Associated bundles of are converted to bytes. /// Characters of Format are are ASCII Encoded. /// /// byte array of this public byte[] GetBytes() { List bytes = new(); string hexConversionCharacters = String.Empty, binaryConversionCharacters = String.Empty; // Internal function to check and finalize a Hexadecimal Conversion // parameter == null will force ending void finalizeHexConversion(FormattedCharacter? formattedChar) { // hex conversion ongoing? if (String.IsNullOrEmpty(hexConversionCharacters) == false) { // ending conversion or reached limit of characters? if (formattedChar == null || formattedChar.Format != FormatType.Hexadecimal || hexConversionCharacters.Length == charsPerHexBundle) { // finalize conversion, expects one byte only => therefore use first bytes.Add(GetBytesFromString(hexConversionCharacters, 16, charsPerHexBundle).First()); // reset ongoing conversion hexConversionCharacters = String.Empty; } } } // Internal function to check and finalize a Binary Conversion // parameter == null will force ending void finalizeBinaryConversion(FormattedCharacter? formattedChar) { // binary conversion ongoing? if (String.IsNullOrEmpty(binaryConversionCharacters) == false) { // ending conversion or reached limit of characters? if (formattedChar == null || formattedChar.Format != FormatType.Binary || binaryConversionCharacters.Length == charsPerBinBundle) { // finalize conversion, expects one byte only => therefore use first bytes.Add(GetBytesFromString(hexConversionCharacters, 2, charsPerBinBundle).First()); // reset ongoing conversion binaryConversionCharacters = String.Empty; } } } foreach (IFormattedCharacter character in this.Items) { // skip all characters that are not of type FormattedCharacter if(character is not FormattedCharacter formattedCharacter) { continue; } // finalize ongoing conversions if there are any finalizeHexConversion(formattedCharacter); finalizeBinaryConversion(formattedCharacter); switch (formattedCharacter.Format) // switch by format { case FormatType.Character: bytes.Add(Encoding.ASCII.GetBytes(formattedCharacter.Character).First()); break; case FormatType.Hexadecimal: hexConversionCharacters += formattedCharacter.Character; break; case FormatType.Binary: binaryConversionCharacters += formattedCharacter.Character; break; default: throw new NotImplementedException($"'{nameof(GetBytes)}()' does not implement conversion for format {formattedCharacter.Format}"); } } // fully finalize after the loop, finish all unfinished conversions finalizeHexConversion(null); finalizeBinaryConversion(null); return bytes.ToArray(); } // TODO Implement Unit Tests! /// /// Converts the to a string that only contains ASCII characters. /// Therefore this method converts all parts of other formats (e.g. or ) to ASCII characters. /// /// converted string public string ToAsciiEncodedString() { return Encoding.ASCII.GetString(this.GetBytes()); } // TODO Unit Tests /// /// Converts an input string to a bytes array with the given base () and bundle size (). /// Input string gets padded with zeroes left. /// Idea from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string /// /// input string, without separators between individual bytes! /// number base of the input string (e.g. 2 for binary) /// amount of characters required for a complete byte in the format (e.g. 8 for a binary byte) /// byte array of the given input string private static byte[] GetBytesFromString(string str, int fromBase, int byteWidth) { // Added left padded zeroes (total width + spill) so the block size matches the number base string paddedString = str.PadLeft((str.Length + (str.Length % byteWidth)), '0'); // creates bytes array of correct size var bytes = new byte[paddedString.Length / byteWidth]; // convert every part of the string to for (var i = 0; i < bytes.Length; i++) { bytes[i] = Convert.ToByte(paddedString.Substring(i * byteWidth, byteWidth), fromBase); } return bytes; } /// /// Returns ASCII encoded string from a hex string of amount of hexadecimal characters. /// /// hex string with maximum amount of hex characters according to . hex characters must be without any separator (no : or - inbetween bytes)! /// ASCII Encoded string of the private static string GetAsciiEncodedStringFromHexString(string hexString) { var bytes = GetBytesFromString(hexString, 16, charsPerHexBundle); return Encoding.ASCII.GetString(bytes); } /// /// Returns ASCII encoded string from a binary string of amount of binary characters. /// /// hex string with maximum amount of binaryString characters according to . binary characters must be without any separator (no space or - inbetween bytes)! /// ASCII Encoded string of the private static string GetAsciiEncodedStringFromBinaryString(string binaryString) { var bytes = GetBytesFromString(binaryString, 2, charsPerBinBundle); return Encoding.ASCII.GetString(bytes); } #endregion }