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 { /// /// 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) { if(item is FormattedCharacter formattedChar) { //check if valid, only insert then if(ValidateValue(formattedChar.Item1, formattedChar.Item2) == false) { return; } // check if spacing is required and insert if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index, formattedChar)) { base.InsertItem(index, new SpacingCharacter()); base.InsertItem(index + 1, item); return; } } base.InsertItem(index, item); } 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); } } /// /// Converts the to a string that only contains Unicode (UTF-16) characters. /// /// converted string public override string ToString() // TODO Implement Unit Tests! { StringBuilder stringBuilder = 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.Item1 != FormatType.Hexadecimal || hexConversionCharacters.Length == 4) { // finalize conversion stringBuilder.Append(FromHexString(hexConversionCharacters)); // 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.Item1 != FormatType.Binary || binaryConversionCharacters.Length == 16) { // finalize conversion stringBuilder.Append(FromBinaryString(binaryConversionCharacters)); // reset ongoing conversion binaryConversionCharacters = String.Empty; } } } // go through every character foreach (var character in this.Items) { // can only handle FormattedCharacters if(character is not FormattedCharacter formattedCharacter) { continue; } // finalize ongoing conversions if there are any finalizeHexConversion(formattedCharacter); finalizeBinaryConversion(formattedCharacter); switch (formattedCharacter.Item1) // switch by format { case FormatType.Character: stringBuilder.Append(formattedCharacter.Item2); break; case FormatType.Hexadecimal: hexConversionCharacters += formattedCharacter.Item2; break; case FormatType.Binary: binaryConversionCharacters += formattedCharacter.Item2; break; default: throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Item1}"); } } // fully finalize after the loop, finish all unfinished conversions finalizeHexConversion(null); finalizeBinaryConversion(null); return stringBuilder.ToString(); } /// /// 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: return true; case FormatType.Hexadecimal: if (character >= '0' && character <= '9' || character >= 'A' && character <= 'F' || character >= 'a' && character <= 'f') { return true; } else { return false; }; case FormatType.Binary: if (character == '0' || character == '1') { return true; } else { return false; }; default: throw new NotImplementedException($"'{nameof(ValidateValue)}()' does not implement validation for format {format}"); } } public static bool IsSpacingCharacterRequiredBeforeNewCharacter(IEnumerable collection, int indexOfInsertion, IFormattedCharacter newCharacter) { bool spacingCharRequired = false; const int charsPerHexBundle = 2; const int charsPerBinBundle = 8; // 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 formattedCharacter) { throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); } // switch spacing required by type of format spacingCharRequired = formattedCharacter.Item1 switch { FormatType.Character => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1), FormatType.Hexadecimal => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1) || CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerHexBundle, formattedCharacter.Item1), FormatType.Binary => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1) || CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerBinBundle, formattedCharacter.Item1), _ => throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for format {formattedCharacter.Item1}"), }; 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.Item1 != newCharFormat) { return true; } } catch (Exception) { // end of collection => break while break; } } } 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) { // 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.Item1 == newCharFormat); // elements of same Format are more (impossible) or equal to amount required for bundle if (sameFormatElements.Count() >= charsPerBundle) { return true; } } return false; } } // from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string // test with: returns: "Hello world" for "48656C6C6F20776F726C64" // see https://www.fileformat.info/info/charset/UTF-16/list.htm private static string FromHexString(string hexString) { // Added PadLeft so strings with one character do not get ignored string internalHexString = hexString.PadLeft(4, '0'); var bytes = new byte[internalHexString.Length / 2]; for (var i = 0; i < bytes.Length; i++) { bytes[i] = Convert.ToByte(internalHexString.Substring(i * 2, 2), 16); } return Encoding.BigEndianUnicode.GetString(bytes); } private static string FromBinaryString(string binaryString) { string internalBinaryString = binaryString.PadLeft(16, '0'); var bytes = new byte[internalBinaryString.Length / 8]; for (var i = 0; i < bytes.Length; i++) { bytes[i] = Convert.ToByte(internalBinaryString.Substring(i * 8, 8), 2); } return Encoding.BigEndianUnicode.GetString(bytes); } }