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 ToString /// /// 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.Format != FormatType.Hexadecimal || hexConversionCharacters.Length == charsPerHexBundle) { // 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.Format != FormatType.Binary || binaryConversionCharacters.Length == charsPerBinBundle) { // 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.Format) // switch by format { case FormatType.Character: stringBuilder.Append(formattedCharacter.Character); break; case FormatType.Hexadecimal: hexConversionCharacters += formattedCharacter.Character; break; case FormatType.Binary: binaryConversionCharacters += formattedCharacter.Character; break; default: throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Format}"); } } // fully finalize after the loop, finish all unfinished conversions finalizeHexConversion(null); finalizeBinaryConversion(null); return stringBuilder.ToString(); } // 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(charsPerHexBundle, '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.ASCII.GetString(bytes); } private static string FromBinaryString(string binaryString) { string internalBinaryString = binaryString.PadLeft(charsPerBinBundle, '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.ASCII.GetString(bytes); } #endregion }