Multiprocotol Terminalprogram (BAT)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MultiTerm/MultiTerm.Core/Model/MultiFormatString.cs

360 lines
16 KiB

using MultiTerm.Core.Types;
using System.Collections.ObjectModel;
using System.Text;
namespace MultiTerm.Core.Model;
/// <summary>
/// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts.
/// </summary>
public class MultiFormatString: ObservableCollection<IFormattedCharacter>, IHistoryElement, ICloneable
{
private const int charsPerHexBundle = 2;
private const int charsPerBinBundle = 8;
#region IHistoryElement implementation
public string DisplayText => this.ToAsciiEncodedString();
#endregion
public MultiFormatString() { }
public MultiFormatString(IEnumerable<IFormattedCharacter> collection) : base(collection) { }
#region Insertion / Removal of Characters
/// <summary>
/// Inserts item at given index after checking if its valid using <see cref="ValidateValue(FormatType, string)"/>.
/// </summary>
/// <param name="index">index where to add item, usually items.Count when using Add method.
/// <param name="item">item to insert</param>
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<IFormattedCharacter> 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<IFormattedCharacter> 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<IFormattedCharacter> 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<IFormattedCharacter> 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
/// <summary>
/// Validates wether a certain <paramref name="value"/> is valid for the given <paramref name="format"/>.
/// </summary>
/// <returns>true if valid</returns>
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
/// <summary>
/// Converts this <see cref="MultiFormatString"/> to a byte array. Associated bundles of <see cref="FormattedCharacter"/> are converted to bytes.
/// Characters of Format <see cref="FormatType.Character"/> are are ASCII Encoded.
/// </summary>
/// <returns>byte array of this <see cref="MultiFormatString"/></returns>
public byte[] GetBytes()
{
List<byte> 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!
/// <summary>
/// Converts the <see cref="MultiFormatString"/> to a string that only contains ASCII characters.
/// Therefore this method converts all parts of other formats (e.g. <see cref="FormatType.Hexadecimal"/> or <see cref="FormatType.Binary"/>) to ASCII characters.
/// </summary>
/// <returns>converted string</returns>
public string ToAsciiEncodedString()
{
return Encoding.ASCII.GetString(this.GetBytes());
}
// TODO Unit Tests
/// <summary>
/// Converts an input string to a bytes array with the given base (<paramref name="fromBase"/>) and bundle size (<paramref name="byteWidth"/>).
/// Input string gets padded with zeroes left.
/// Idea from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string
/// </summary>
/// <param name="str">input string, without separators between individual bytes! </param>
/// <param name="fromBase">number base of the input string (e.g. 2 for binary)</param>
/// <param name="byteWidth">amount of characters required for a complete byte in the <paramref name="fromBase"/> format (e.g. 8 for a binary byte)</param>
/// <returns>byte array of the given input string</returns>
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;
}
/// <summary>
/// Returns ASCII encoded string from a hex string of <see cref="charsPerHexBundle"/> amount of hexadecimal characters.
/// </summary>
/// <param name="hexString">hex string with maximum amount of hex characters according to <see cref="charsPerHexBundle"/>. hex characters must be without any separator (no : or - inbetween bytes)!</param>
/// <returns>ASCII Encoded string of the <paramref name="hexString"/></returns>
private static string GetAsciiEncodedStringFromHexString(string hexString)
{
var bytes = GetBytesFromString(hexString, 16, charsPerHexBundle);
return Encoding.ASCII.GetString(bytes);
}
/// <summary>
/// Returns ASCII encoded string from a binary string of <see cref="charsPerBinBundle"/> amount of binary characters.
/// </summary>
/// <param name="binaryString">hex string with maximum amount of binaryString characters according to <see cref="charsPerBinBundle"/>. binary characters must be without any separator (no space or - inbetween bytes)!</param>
/// <returns>ASCII Encoded string of the <paramref name="binaryString"/></returns>
private static string GetAsciiEncodedStringFromBinaryString(string binaryString)
{
var bytes = GetBytesFromString(binaryString, 2, charsPerBinBundle);
return Encoding.ASCII.GetString(bytes);
}
#endregion
#region ICloneable implementation
public object Clone()
{
return new MultiFormatString(this);
}
#endregion
}