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.
315 lines
13 KiB
315 lines
13 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>
|
|
{
|
|
private const int charsPerHexBundle = 2;
|
|
private const int charsPerBinBundle = 8;
|
|
|
|
#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 ToString
|
|
/// <summary>
|
|
/// Converts the <see cref="MultiFormatString"/> to a string that only contains Unicode (UTF-16) characters.
|
|
/// </summary>
|
|
/// <returns>converted string</returns>
|
|
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
|
|
}
|
|
|