Skip to content

Instantly share code, notes, and snippets.

@aradalvand
Last active August 2, 2023 23:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aradalvand/5e1b6f90de4324eaac11e20da29225aa to your computer and use it in GitHub Desktop.
Save aradalvand/5e1b6f90de4324eaac11e20da29225aa to your computer and use it in GitHub Desktop.
namespace Sqids;
public class SqidsOptions
{
public string Alphabet { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public HashSet<string> BlockList { get; set; } = new() { /* TODO */ };
public int MinLength { get; set; } = 10;
}
public class SqidsGenerator
{
private const int _minAlphabetLength = 5;
private readonly SqidsOptions _options;
public const int MinValue = 0;
public const int MaxValue = int.MaxValue;
public SqidsGenerator() : this(new()) { }
public SqidsGenerator(SqidsOptions options)
{
if (options.Alphabet.Length < _minAlphabetLength)
{
throw new ArgumentException("The alphabet must contain at least 5 characters.");
}
if (options.Alphabet.Distinct().Count() != options.Alphabet.Length)
{
throw new ArgumentException("The alphabet must not contain duplicate characters.");
}
if (options.MinLength < MinValue || options.MinLength > options.Alphabet.Length)
{
throw new ArgumentException($"The minimum length must be between {MinValue} and {options.Alphabet.Length}.");
}
var filteredBlockList = new HashSet<string>();
foreach (string word in options.BlockList)
{
if (word.Length >= 3)
{
var intersection = word.Intersect(options.Alphabet);
if (intersection.Count() == word.Length)
{
filteredBlockList.Add(word.ToLower());
}
}
}
options.Alphabet = Shuffle(options.Alphabet);
options.BlockList = filteredBlockList;
_options = options;
}
public string Encode(params int[] numbers)
{
if (numbers.Length == 0)
{
return "";
}
if (numbers.Any(n => n < MinValue || n > MaxValue))
{
throw new ArgumentException($"Encoding supports numbers between '{MinValue}' and '{MaxValue}'.");
}
return EncodeNumbers(numbers, false);
}
public int[] Decode(string id)
{
if (id == "")
{
return Array.Empty<int>();
}
foreach (char c in id)
{
if (!_options.Alphabet.Contains(c))
{
return Array.Empty<int>();
}
}
char prefix = id[0];
int offset = _options.Alphabet.IndexOf(prefix);
string alphabetTemp = _options.Alphabet.Substring(offset) + _options.Alphabet.Substring(0, offset);
char partition = alphabetTemp[1];
alphabetTemp = alphabetTemp.Substring(2);
string slicedId = id.Substring(1);
int partitionIndex = slicedId.IndexOf(partition);
if (partitionIndex > 0 && partitionIndex < slicedId.Length - 1)
{
slicedId = slicedId.Substring(partitionIndex + 1);
alphabetTemp = Shuffle(alphabetTemp);
}
var result = new List<int>();
while (slicedId.Length > 0)
{
char separator = alphabetTemp.Last();
string[] chunks = slicedId.Split(separator);
if (chunks.Length > 0)
{
string alphabetWithoutSeparator = alphabetTemp.Substring(0, alphabetTemp.Length - 1);
result.Add(ToNumber(chunks[0], alphabetWithoutSeparator));
if (chunks.Length > 1)
{
alphabetTemp = Shuffle(alphabetTemp);
}
}
slicedId = string.Join(separator, chunks.Skip(1));
}
return result.ToArray();
}
// Private:
private string EncodeNumbers(int[] numbers, bool partitioned = false)
{
int offset = (numbers.Length + (numbers.Select((n, i) => _options.Alphabet[n % _options.Alphabet.Length] + i).Sum())) % _options.Alphabet.Length;
string alphabetTemp = _options.Alphabet.Substring(offset) + _options.Alphabet.Substring(0, offset);
char prefix = alphabetTemp[0];
char partition = alphabetTemp[1];
alphabetTemp = alphabetTemp.Substring(2);
var result = new List<char> { prefix };
for (int i = 0; i < numbers.Length; i++)
{
int num = numbers[i];
string alphabetWithoutSeparator = alphabetTemp.Substring(0, alphabetTemp.Length - 1);
result.AddRange(ToId(num, alphabetWithoutSeparator));
if (i < numbers.Length - 1)
{
char separator = alphabetTemp.Last();
if (partitioned && i == 0)
{
result.Add(partition);
}
else
{
result.Add(separator);
}
alphabetTemp = Shuffle(alphabetTemp);
}
}
string id = new string(result.ToArray());
if (_options.MinLength > id.Length)
{
if (!partitioned)
{
int[] newNumbers = new int[] { 0 }.Concat(numbers).ToArray();
id = EncodeNumbers(newNumbers, true);
}
if (_options.MinLength > id.Length)
{
id = id.Insert(1, alphabetTemp.Substring(0, _options.MinLength - id.Length));
}
}
if (IsBlockedId(id))
{
int[] newNumbers = numbers;
if (partitioned)
{
if (numbers[0] + 1 > MaxValue)
{
throw new ArgumentOutOfRangeException("Ran out of range checking against the blocklist.");
}
else
{
newNumbers[0] += 1;
}
}
else
{
newNumbers = new int[] { 0 }.Concat(numbers).ToArray();
}
id = EncodeNumbers(newNumbers, true);
}
return id;
}
private bool IsBlockedId(string id)
{
string lowercaseId = id.ToLower();
foreach (string word in _options.BlockList)
{
if (word.Length <= lowercaseId.Length)
{
if (lowercaseId.Length <= 3 || word.Length <= 3)
{
if (lowercaseId == word)
{
return true;
}
}
else if (word.Any(char.IsDigit))
{
if (lowercaseId.StartsWith(word) || lowercaseId.EndsWith(word))
{
return true;
}
}
else if (lowercaseId.Contains(word))
{
return true;
}
}
}
return false;
}
private static string Shuffle(string alphabet)
{
char[] chars = alphabet.ToCharArray();
for (int i = 0, j = chars.Length - 1; j > 0; i++, j--)
{
int r = (i * j + chars[i] + chars[j]) % chars.Length;
(chars[i], chars[r]) = (chars[r], chars[i]);
}
return new string(chars);
}
private static string ToId(int num, string alphabet)
{
var id = new List<char>();
int result = num;
do
{
id.Insert(0, alphabet[result % alphabet.Length]);
result = result / alphabet.Length;
} while (result > 0);
return new string(id.ToArray());
}
private static int ToNumber(string id, string alphabet)
{
return id.Aggregate(0, (a, v) => a * alphabet.Length + alphabet.ToList().IndexOf(v));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment