using System;
using System.Runtime.CompilerServices;
using System.Text;
using static System.BitConverter;
public static class CompactGuid
{
private static readonly byte[] Alphabet = Encoding.ASCII.GetBytes("0123456789abcdefghjkmnpqrstvwxyz");
private static readonly string EmptyString = new string('0', Length);
private static readonly int[] AsciiMapping = GenerateAsciiMapping();
private const int Length = 26;
/// <summary>
/// Produces a 26-character, Base32-encoded representation of a <see cref="Guid"/>.
/// </summary>
/// <remarks>
/// This has a bunch of nice characteristics:
/// - Safe without encoding (uses only characters from ASCII)
/// - Avoids ambiguous characters (i/I/l/L/o/O/0)
/// - Easy for humans to read and pronounce
/// - Supports full UUID range (128 bits)
/// - Safe for URLs and file names
/// - Case-insensitive
/// - 30% smaller
/// </remarks>
/// <param name="value">The <see cref="Guid"/> to encode.</param>
/// <returns>A 26-character, Base32-encoded <see cref="string"/>.</returns>
public static string ToCompactString(this Guid value)
{
if (value == Guid.Empty) return EmptyString;
Span<byte> bytes = stackalloc byte[Length];
TryWriteUuid(bytes, value);
var hi = ToUInt64(bytes.Slice(0, sizeof(long)));
var lo = ToUInt64(bytes.Slice(sizeof(long)));
EncodeUInt64(bytes.Slice(0, Length / 2), hi);
EncodeUInt64(bytes.Slice(Length / 2), lo);
return Encoding.ASCII.GetString(bytes);
}
/// <summary>
/// Parses a 26-character, Base32-encoded representation of a <see cref="Guid"/>.
/// </summary>
/// <param name="chars">The characters to parse.</param>
/// <returns>The parsed <see cref="Guid"/>.</returns>
/// <exception cref="FormatException">If the characters represents an invalid compact <see cref="Guid"/> string.</exception>
public static Guid Parse(ReadOnlySpan<char> chars)
{
return TryParse(chars, out var result) ? result : throw new FormatException("Invalid compact GUID format.");
}
/// <summary>
/// Tries to parse a 26-character, Base32-encoded representation of a <see cref="Guid"/>.
/// </summary>
/// <param name="chars">The characters to parse.</param>
/// <param name="result">The parsed <see cref="Guid"/>.</param>
/// <returns>Returns <c>true</c> if the parsing succeeded, <c>false</c> otherwise.</returns>
public static bool TryParse(ReadOnlySpan<char> chars, out Guid result)
{
if (!IsValid(chars)) return false;
Span<byte> bytes = stackalloc byte[Length];
Encoding.ASCII.GetBytes(chars, bytes);
return TryParse(bytes, out result) || Guid.TryParse(chars, out result);
}
private static bool IsValid(ReadOnlySpan<char> chars)
{
if (chars.Length != Length) return false;
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] >= AsciiMapping.Length)
{
return false; // Not ASCII.
}
}
return true;
}
private static bool TryParse(Span<byte> bytes, out Guid result)
{
return TryDecodeUInt64(bytes.Slice(0, Length / 2), out var hi)
&& TryDecodeUInt64(bytes.Slice(Length / 2), out var lo)
&& TryWriteBytes(bytes.Slice(0, sizeof(long)), hi)
&& TryWriteBytes(bytes.Slice(sizeof(long)), lo)
&& TryReadUuid(bytes, out result);
}
private static void EncodeUInt64(Span<byte> bytes, ulong result)
{
var index = 0;
// Because a GUID is 128 bits and 26 characters with 5 bits
// each is 130, we limit the 1st and 13th character to 4 bits (hex).
bytes[index++] = Alphabet[(int)(result >> 60)];
result <<= 4;
while (index < bytes.Length)
{
// Each following character carries 5 bits each.
bytes[index++] = Alphabet[(int)(result >> 59)];
result <<= 5;
}
}
private static bool TryDecodeUInt64(Span<byte> bytes, out ulong result)
{
result = 0;
for (var i = 0; i < bytes.Length; i++)
{
var value = AsciiMapping[bytes[i]];
if (value == -1)
{
return false; // Invalid ASCII character.
}
result = (result << 5) | (uint) value;
}
return true;
}
private static int[] GenerateAsciiMapping()
{
const char start = '\x00', end = '\x7F';
var mapping = new int[end - start + 1];
for (var i = start; i <= end; i++)
{
mapping[i] = Array.IndexOf(Alphabet, (byte) char.ToLower(i));
}
mapping['o'] = mapping['O'] = 0;
mapping['i'] = mapping['I'] = mapping['l'] = mapping['L'] = 1;
return mapping;
}
private static bool TryWriteUuid(Span<byte> bytes, Guid value)
{
if (!value.TryWriteBytes(bytes)) return false;
bytes.Rotate(0, 6, 2, 4);
bytes.Rotate(1, 7, 3, 5);
bytes.Swap(8, 15);
bytes.Swap(9, 14);
bytes.Swap(10, 13);
bytes.Swap(11, 12);
return true;
}
private static bool TryReadUuid(Span<byte> bytes, out Guid result)
{
const int length = 16;
if (bytes.Length < length) return false;
bytes.Rotate(0, 4, 2, 6);
bytes.Rotate(1, 5, 3, 7);
bytes.Swap(8, 15);
bytes.Swap(9, 14);
bytes.Swap(10, 13);
bytes.Swap(11, 12);
result = new Guid(bytes.Slice(0, length));
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Rotate(this Span<byte> span, int a, int b, int c, int d)
{
var tmp = span[a];
span[a] = span[b];
span[b] = span[c];
span[c] = span[d];
span[d] = tmp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Swap(this Span<byte> span, int left, int right)
{
var tmp = span[left];
span[left] = span[right];
span[right] = tmp;
}
}