@ -9,6 +9,10 @@ namespace MultiTerm.Core.Model;
/// </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>
@ -16,25 +20,35 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
/// <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 . Item1 , formattedChar . Item2 ) = = false )
if ( ValidateValue ( formattedChar . Format , formattedChar . Character ) = = false )
{
return ;
}
// check if spacing is required and insert
if ( IsSpacingCharacterRequiredBeforeNewCharacter ( this , index , formattedChar ) )
if ( IsSpacingCharacterRequiredBeforeNewCharacter ( this , index + offset , formattedChar ) )
{
base . InsertItem ( index , new SpacingCharacter ( ) ) ;
base . InsertItem ( index + 1 , item ) ;
offset + + ; // insert item with offset of 1
}
return ;
if ( IsSpacingCharacterRequiredAfterNewCharacter ( this , index + offset , formattedChar ) )
{
insertSpacingAfter = true ;
}
}
base . InsertItem ( index , item ) ;
base . InsertItem ( index + offset , item ) ;
if ( insertSpacingAfter )
{
base . InsertItem ( index + offset + 1 , new SpacingCharacter ( ) ) ;
}
}
protected override void RemoveItem ( int index )
@ -49,6 +63,141 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
}
}
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>
@ -67,8 +216,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
// ending conversion or reached limit of characters?
if ( formattedChar = = null | |
formattedChar . Item1 ! = FormatType . Hexadecimal | |
hexConversionCharacters . Length = = 4 )
formattedChar . Format ! = FormatType . Hexadecimal | |
hexConversionCharacters . Length = = charsPerHexBundle )
{
// finalize conversion
stringBuilder . Append ( FromHexString ( hexConversionCharacters ) ) ;
@ -87,8 +236,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
// ending conversion or reached limit of characters?
if ( formattedChar = = null | |
formattedChar . Item1 ! = FormatType . Binary | |
binaryConversionCharacters . Length = = 1 6 )
formattedChar . Format ! = FormatType . Binary | |
binaryConversionCharacters . Length = = charsPerBinBundle )
{
// finalize conversion
stringBuilder . Append ( FromBinaryString ( binaryConversionCharacters ) ) ;
@ -102,28 +251,28 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
foreach ( var character in this . Items )
{
// can only handle FormattedCharacters
if ( character is not FormattedCharacter formattedCharacter ) { continue ; }
if ( character is not FormattedCharacter formattedCharacter ) { continue ; }
// finalize ongoing conversions if there are any
finalizeHexConversion ( formattedCharacter ) ;
finalizeBinaryConversion ( formattedCharacter ) ;
switch ( formattedCharacter . Item1 ) // switch by format
switch ( formattedCharacter . Format ) // switch by format
{
case FormatType . Character :
stringBuilder . Append ( formattedCharacter . Item2 ) ;
stringBuilder . Append ( formattedCharacter . Character ) ;
break ;
case FormatType . Hexadecimal :
hexConversionCharacters + = formattedCharacter . Item2 ;
hexConversionCharacters + = formattedCharacter . Character ;
break ;
case FormatType . Binary :
binaryConversionCharacters + = formattedCharacter . Item2 ;
binaryConversionCharacters + = formattedCharacter . Character ;
break ;
default :
throw new NotImplementedException ( $"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Item1 }" ) ;
throw new NotImplementedException ( $"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Format }" ) ;
}
}
@ -134,143 +283,33 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
return stringBuilder . ToString ( ) ;
}
/// <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 :
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 < IFormattedCharacter > 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 < 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 . Item1 ! = newCharFormat )
{
return true ;
}
}
catch ( Exception )
{
// end of collection => break while
break ;
}
}
}
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 )
{
// 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' ) ;
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 ) , 1 6 ) ;
}
return Encoding . BigEndianUnicode . GetString ( bytes ) ;
return Encoding . ASCII . GetString ( bytes ) ;
}
private static string FromBinaryString ( string binaryString )
{
string internalBinaryString = binaryString . PadLeft ( 1 6 , '0' ) ;
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 . BigEndianUnicode . GetString ( bytes ) ;
return Encoding . ASCII . GetString ( bytes ) ;
}
# endregion
}