Last active
August 2, 2023 23:23
-
-
Save aradalvand/5e1b6f90de4324eaac11e20da29225aa 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
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