Last active
April 19, 2023 12:22
-
-
Save khellang/4993fcfbf8fb2ecdeccc2c822567037c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MIT License | |
// | |
// Copyright (c) 2018 Kristian Hellang | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
using System; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
public static class CompactGuid | |
{ | |
private static ReadOnlySpan<byte> Alphabet => new[] | |
{ | |
(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', | |
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', (byte)'h', (byte)'j', (byte)'k', | |
(byte)'m', (byte)'n', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'v', (byte)'w', (byte)'x', | |
(byte)'y', (byte)'z' | |
}; | |
private static readonly string EmptyString = new string('0', Length); | |
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 ToString(Guid value) | |
{ | |
if (value == Guid.Empty) | |
{ | |
return EmptyString; | |
} | |
Span<byte> target = stackalloc byte[Length]; | |
Encoder.Encode(value, target); | |
return Encoding.ASCII.GetString(target); | |
} | |
/// <summary> | |
/// Tries to parse a 26-character, Base32-encoded representation of a <see cref="Guid"/>. | |
/// </summary> | |
/// <param name="value">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> value, out Guid result) | |
{ | |
if (value.Length != Length) | |
{ | |
result = default; | |
return false; | |
} | |
Span<byte> source = stackalloc byte[Length]; | |
Encoding.ASCII.GetBytes(value, source); | |
return Decoder.TryDecode(source, out result); | |
} | |
private static Span<ulong> AsSpan<T>(ref T value) where T : unmanaged | |
{ | |
return MemoryMarshal.Cast<T, ulong>(MemoryMarshal.CreateSpan(ref value, 1)); | |
} | |
private static class Encoder | |
{ | |
public static void Encode(Guid value, Span<byte> target) | |
{ | |
var longs = AsSpan(ref value); | |
EncodeUInt64(target.Slice(0, Length / 2), longs[0]); | |
EncodeUInt64(target.Slice(Length / 2), longs[1]); | |
} | |
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 class Decoder | |
{ | |
private static readonly int[] AsciiMapping = GenerateAsciiMapping(); | |
public static bool TryDecode(ReadOnlySpan<byte> bytes, out Guid result) | |
{ | |
result = default; | |
var longs = AsSpan(ref result); | |
return TryDecodeUInt64(bytes.Slice(0, Length / 2), ref longs[0]) | |
&& TryDecodeUInt64(bytes.Slice(Length / 2), ref longs[1]); | |
} | |
private static bool TryDecodeUInt64(ReadOnlySpan<byte> bytes, ref ulong result) | |
{ | |
for (var i = 0; i < bytes.Length; i++) | |
{ | |
var index = bytes[i]; | |
if (index >= AsciiMapping.Length) | |
{ | |
return false; // Not ASCII. | |
} | |
var value = AsciiMapping[index]; | |
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] = Alphabet.IndexOf((byte)char.ToLower(i)); | |
} | |
mapping['o'] = mapping['O'] = 0; | |
mapping['i'] = mapping['I'] = mapping['l'] = mapping['L'] = 1; | |
return mapping; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Buffers.Binary; | |
using System.Runtime.InteropServices; | |
public static class UuidConverter | |
{ | |
public static Guid ToUuid(this Guid value) | |
{ | |
var decomposed = new Decomposed(value); | |
decomposed.Data1 = decomposed.Data1.ToBigEndian(); | |
decomposed.Data2 = decomposed.Data2.ToBigEndian(); | |
decomposed.Data3 = decomposed.Data3.ToBigEndian(); | |
return decomposed.Value; | |
} | |
public static Guid ToGuid(this Guid value) | |
{ | |
var decomposed = new Decomposed(value); | |
decomposed.Data1 = decomposed.Data1.FromBigEndian(); | |
decomposed.Data2 = decomposed.Data2.FromBigEndian(); | |
decomposed.Data3 = decomposed.Data3.FromBigEndian(); | |
return decomposed.Value; | |
} | |
private static uint ToBigEndian(this uint data) | |
{ | |
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data; | |
} | |
private static ushort ToBigEndian(this ushort data) | |
{ | |
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data; | |
} | |
private static uint FromBigEndian(this uint data) | |
{ | |
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data; | |
} | |
private static ushort FromBigEndian(this ushort data) | |
{ | |
return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(data) : data; | |
} | |
[StructLayout(LayoutKind.Explicit)] | |
private struct Decomposed | |
{ | |
[FieldOffset(00)] public Guid Value; | |
[FieldOffset(00)] public uint Data1; | |
[FieldOffset(04)] public ushort Data2; | |
[FieldOffset(06)] public ushort Data3; | |
[FieldOffset(08)] public ulong Data4; | |
public Decomposed(Guid value) : this() => Value = value; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment