There is no arguments validations. this is important, as you need to validate the arguments before processing them. In your case, ArgumentNullException
would be suitable. to check if the passed arguments are null or empty strings (whitespace perhaps).
you can use char.IsLetterOrDigit(char c)
instead of
if ('a' <= cc && cc <= 'z' || 'A' <= cc && cc <= 'Z' || '0' <= cc && cc <= '9')
using Convert.ToXX
extensions is fine in safe-senarios (where the values are always valid), however, in most cases you should consider using TryParse
as a safe and less error prone parsing approach. (e.g. char.TryParse
, int.TryParse
..etc.).
str.ToCharArray()
is unnecessary as you can access to the string chars directly using indexing.
In Decode
your List<byte>
is not needed, as you can store the two-characters in a temporary string, and then convert it from hexadecimal to its original form, and append the results to the StringBuilder
. (or you can store the start index, and use it for the conversion.
Decode
method assumes that there is an open and close brackets, how do you think it would behave if there is missing close brackets or duplicated brackets or a bracket that is not intent to be a part of the conversion process ?. For simple processing, you can use IndexOf
to check the next bracket index, and get the hexadecimal value between them, hence, Substring
would be useful here (also Range
would be much modern approach). Although, if the hexdecimal value will always have fixed length (in your code you only use one-hexdecimal that would take two characters, so the length is fixed to 2-chars). You can define that in your class, and decode it based on the fixed length (in this case, you would use Substring
or Range
directly, and you only check for the current index plus the next index (which is index + fixed length)).
(untested) see the Update Below
Example of applying some of the above notes :
public static string Encode(string str)
{
// no need to do anything, just return the source.
if (string.IsNullOrWhiteSpace(str)) return str;
StringBuilder builder = new StringBuilder();
for(int x = 0; x < str.Length; x++)
{
var character = str[x];
if(!char.IsLetterOrDigit(character))
{
builder.Append($"[{((byte)character):X2}]");
}
else
{
builder.Append(character);
}
}
return builder.ToString();
}
public static string Decode(string str)
{
// no need to do anything, just return the source.
if (string.IsNullOrWhiteSpace(str)) return str;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < str.Length; i++)
{
char character = str[i];
if (character == '[')
{
var nextIndex = str.IndexOf(']', i) - 1;
if(nextIndex > 0)
{
var temp = str.Substring(i + 1, nextIndex - i);
builder.Append(Encoding.UTF8.GetString(new[] { Convert.ToByte(temp, 16) }));
i = nextIndex + 1;
}
}
else
{
builder.Append(character);
}
}
return builder.ToString();
}
In the Encode
method, I didn't use LINQ
; because it would be more readable and easy to follow if I just use a simple loop. As I prefer readability over LINQ approach, and also it gives more harmony to the code between both Decode
and Encode
methods.
If you want another way of Decoding
you could use Regex.Replace
this would give you the same results, but with shorter code:
public static string Decode(string str)
{
// no need to do anything, just return the source.
if (string.IsNullOrWhiteSpace(str)) return str;
return Regex.Replace(str, "\\[([^]]+)\\]", new MatchEvaluator((match) =>
{
return Encoding.UTF8.GetString(new[] { Convert.ToByte(match.Groups[1].Value, 16) });
}), RegexOptions.IgnoreCase);
}
UPDATE
I've done some testing on the above example (thanks to @pm100 for mention it), I just changed it to a better one. The following would only serves the same logic as the original, however, it can be expanded to adopt any new requirements. You can override GetEncodeCharacterBytes
to manipulate how the character would be encoded. (the default would returns a hexadecimal bytes).
public class DefaultNonAlphanumericEncoder : NonAlphanumericEncoder
{
// default configurations
public DefaultNonAlphanumericEncoder() : base('[', ']', Encoding.UTF8) { }
}
public abstract class NonAlphanumericEncoder
{
private readonly byte _openTag;
private readonly byte _closeTag;
private Encoding _encoding;
/// <summary>
/// An open constructor to define the open/close tags along with an encoding.
/// </summary>
/// <param name="openTag"></param>
/// <param name="closeTag"></param>
/// <param name="encoding"></param>
/// <exception cref="ArgumentNullException"></exception>
public NonAlphanumericEncoder(char openTag, char closeTag, Encoding encoding)
{
_openTag = (byte)openTag;
_closeTag = (byte)closeTag;
_encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
}
protected virtual IEnumerable<byte> GetEncodeCharacterBytes(byte character) => _encoding.GetBytes($"[{character:X2}]");
private IEnumerable<byte> InternalEncode(byte[] bytes)
{
if (bytes?.Length == 0) yield break;
for (int i = 0; i < bytes.Length; i++)
{
byte c = bytes[i];
if (IsNonAlphanumeric(c))
{
var encodedBytes = GetEncodeCharacterBytes(c);
foreach (var encoded in encodedBytes)
yield return encoded;
}
else
{
yield return c;
}
}
}
private IEnumerable<byte> InternalDecode(byte[] bytes)
{
byte[] hexBytes = new byte[2];
for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
// covers case 4
if ((i + 1) < bytes.Length && bytes[i] == _closeTag && bytes[i + 1] == _openTag) { continue; }
if (b != _openTag)
{
yield return b;
}
else
{
hexBytes[0] = bytes[++i];
hexBytes[1] = bytes[++i];
yield return Convert.ToByte(_encoding.GetString(hexBytes), 16);
// we assumed that the next index would hold the close tag
// for this, we increament by 1 to skip it.
i++;
}
}
}
public string Encode(string str)
{
// no need to do anything, just return the source.
if (string.IsNullOrWhiteSpace(str)) return str;
var sourceBytes = _encoding.GetBytes(str);
var encodedBytes = InternalEncode(sourceBytes).ToArray();
return _encoding.GetString(encodedBytes);
}
public string Decode(string str)
{
// no need to do anything, just return the source.
if (string.IsNullOrWhiteSpace(str)) return str;
var sourceBytes = _encoding.GetBytes(str);
var decodedBytes = InternalDecode(sourceBytes).ToArray();
return _encoding.GetString(decodedBytes);
}
// helpers
private bool IsNumber(byte character) => character >= '0' && character <= '9';
private bool IsLetter(byte character) => (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z');
private bool IsNonAlphanumeric(byte character) => !IsLetter(character) && !IsNumber(character);
}
usage :
// Default encoder uses UTF-8 with [] tags
var encoder = new DefaultNonAlphanumericEncoder();
var encoded = encoder.Encode("abc©def");
var decoded = encoder.Decode(encoded);