From 8ec34a0322a8f6d6d316e39640bc5a0ad205ae20 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Mon, 1 Dec 2025 16:14:37 -0300 Subject: [PATCH 01/11] Update README with legacy framework support details Added support for all integral types in legacy frameworks. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 67fc1de..566cddf 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ - 🔢 **Multiple Numbers:** You can bundle more than one number into a single ID, and then decode the ID back into the same set of numbers. - 👁 **"Eye-safe":** Sqids makes sure that the IDs it generates do not contain common profanity, so you can safely use these IDs where end-users can see them (e.g. in URLs). - 🤹‍♀️ **Randomized Output:** Encoding sequential numbers (1, 2, 3...) yields completely different-looking IDs. -- 💪 **Supports All Integral Types:** Powered by .NET 7's [generic math](https://learn.microsoft.com/en-us/dotnet/standard/generics/math) — you could use Sqids to encode/decode numbers of any integral numeric type in .NET, including `int`, `long`, `ulong`, `byte`, etc. +- 💪 **Supports All Integral Types:** Powered by .NET 7's [generic math](https://learn.microsoft.com/en-us/dotnet/standard/generics/math) — you could use Sqids to encode/decode numbers of any integral numeric type in .NET, including `int`, `long`, `ulong`, `byte`, etc. This fork adds support to all integral types for legacy frameworks whith few changes as possible. - ⚡ **Blazingly Fast:** With an optimized span-based implementation that minimizes memory allocation and maximizes performance. - 🔍 **Meticulously Tested:** Sqids has a comprehensive test suite that covers numerous edge cases, so you can expect a bug-free experience. - ✅ **CLS-compliant:** Sqids can be used with any .NET language, not just C#. You can use Sqids just as easily with F#, for example. +- ❗ **ulong support:** Encoding UInt64/ulong is supported in legacy frameworks via a non CLS-compliant overload. ## Getting Started From a1afb4f01181974fb91cd5218b0ed72f9a3af2bd Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Tue, 9 Dec 2025 21:22:14 -0300 Subject: [PATCH 02/11] Backported support for all integral types to legacy frameworks via source generation Introduces an incremental source generator for SqidsEncoder that adds overloads supporting all integral types on legacy frameworks. Updates README with usage details, adds new tests for legacy encoding/decoding, and wires up the code generation project in the solution and main project. This enables drop-in support for byte, sbyte, short, ushort, int, uint, long, and ulong encoding/decoding in .NET Standard 2.0 (.NET Framework 4.7.2+) and .NET 6 environments. Extreme care was taken to avoid structural and code changes to the SqidsEncoder class (even comments were kept where they were, as a diff will show) to make it easy to keep it up to date with the main Sqids project if a PR is not accepted. --- README.md | 51 +- Sqids.sln | 19 +- .../Sqids.CodeGeneration.csproj | 14 + .../SqidsLegacyOverloadsGenerator.cs | 121 ++++ .../GenerateSqidsLegacyOverloadsAttribute.cs | 7 + src/Sqids/Sqids.csproj | 7 +- src/Sqids/SqidsEncoder.cs | 193 +++--- test/Sqids.Tests/LegacyEncodingTests.cs | 550 ++++++++++++++++++ 8 files changed, 856 insertions(+), 106 deletions(-) create mode 100644 src/Sqids.CodeGeneration/Sqids.CodeGeneration.csproj create mode 100644 src/Sqids.CodeGeneration/SqidsLegacyOverloadsGenerator.cs create mode 100644 src/Sqids/GenerateSqidsLegacyOverloadsAttribute.cs create mode 100644 test/Sqids.Tests/LegacyEncodingTests.cs diff --git a/README.md b/README.md index 566cddf..b0b9c9d 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ - 🔢 **Multiple Numbers:** You can bundle more than one number into a single ID, and then decode the ID back into the same set of numbers. - 👁 **"Eye-safe":** Sqids makes sure that the IDs it generates do not contain common profanity, so you can safely use these IDs where end-users can see them (e.g. in URLs). - 🤹‍♀️ **Randomized Output:** Encoding sequential numbers (1, 2, 3...) yields completely different-looking IDs. -- 💪 **Supports All Integral Types:** Powered by .NET 7's [generic math](https://learn.microsoft.com/en-us/dotnet/standard/generics/math) — you could use Sqids to encode/decode numbers of any integral numeric type in .NET, including `int`, `long`, `ulong`, `byte`, etc. This fork adds support to all integral types for legacy frameworks whith few changes as possible. +- 💪 **Supports All Integral Types:** Powered by .NET 7's [generic math](https://learn.microsoft.com/en-us/dotnet/standard/generics/math) and incremental source-generation on legacy frameworks — you could use Sqids to encode/decode numbers of any integral numeric type in .NET, including `int`, `long`, `ulong`, `byte`, etc. - ⚡ **Blazingly Fast:** With an optimized span-based implementation that minimizes memory allocation and maximizes performance. - 🔍 **Meticulously Tested:** Sqids has a comprehensive test suite that covers numerous edge cases, so you can expect a bug-free experience. - ✅ **CLS-compliant:** Sqids can be used with any .NET language, not just C#. You can use Sqids just as easily with F#, for example. -- ❗ **ulong support:** Encoding UInt64/ulong is supported in legacy frameworks via a non CLS-compliant overload. +- ❗ **Legacy support:** Encoding UInt64/ulong (and other non CLS-compliant) types is supported via source-generated overloads. ## Getting Started @@ -69,7 +69,7 @@ var sqids = new SqidsEncoder(); > **Note** > You can use any [integral numeric type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types) (e.g. `long`, `byte`, `short`, etc.) as the type argument. `int` is just the most common one, but if you need to encode/decode larger numbers, for example, you could use `long`/`ulong` instead. -If you're targeting an older framework than .NET 7, `SqidsEncoder` only supports `int`, and there is no generic type parameter you need to supply, so just: +If you're targeting an older framework than .NET 7, `SqidsEncoder` supports all integral types via overloads and suffixed methods (`DecodeByte`, `DecodeULong` etc.), and there is no generic type parameter you need to supply, so just: ```cs var sqids = new SqidsEncoder(); @@ -92,6 +92,51 @@ var numbers = sqids.Decode(id); // [1, 2, 3] > **Note** > Sqids also preserves the order when encoding/decoding multiple numbers. +#### Legacy support for all integral numeric types + +You can decode and encode any integral numeric type even when targeting frameworks bellow .NET 7 by using source-generated overloads and suffixed methods. + +Encoding a single number will call the `long`-based, CLS-Compliant `Encode` overload. Only when encoding an `ulong` value will it choose the non CSL-Compliant method. + +Decoding requires you to choose a specific suffixed `DecodeXXX` method which returns the desired type. + +```cs +var sqids = new SqidsEncoder(); + +// Single numbers +string idB = sqids.Encode(byte.MaxValue); +string idSB = sqids.Encode(sbyte.MaxValue); +string idS = sqids.Encode(short.MaxValue); +string idUS = sqids.Encode(ushort.MaxValue); // Non CLS-Compliant +string idI = sqids.Encode(int.MaxValue); +string idUI = sqids.Encode(uint.MaxValue); // Non CLS-Compliant +string idL = sqids.Encode(long.MaxValue); +string idUL = sqids.Encode(ulong.MaxValue); // Non CLS-Compliant +byte bID = sqids.DecodeByte(idB).Single(); +sbyte sbID = sqids.DecodeSByte(idSB).Single(); // Non CLS-Compliant +short sID = sqids.DecodeShort(idS).Single(); +ushort usID = sqids.DecodeUShort(idUS).Single(); // Non CLS-Compliant +int iID = sqids.Decode(idI).Single(); // or DecodeInt() +uint uiID = sqids.DecodeUInt(idUI).Single(); // Non CLS-Compliant +long lID = sqids.DecodeLong(idL).Single(); +ulong ulID = sqids.DecodeULong(idUL).Single(); // Non CLS-Compliant + +// Multiple numbers (showing different ways to call each overload) +string idB = sqids.Encode((byte[])[byte.MaxValue, 1]); +string idB2 = sqids.Encode(byte.MaxValue, (byte)1); +string idSB = sqids.Encode(sbyte.MaxValue, (sbyte)1); // Non CLS-Compliant +string idS = sqids.Encode(short.MaxValue, (short)1); +string idUS = sqids.Encode(ushort.MaxValue, (ushort)1); // Non CLS-Compliant +string idI = sqids.Encode(int.MaxValue, 1); +string idUI = sqids.Encode(uint.MaxValue, 1); // Non CLS-Compliant +string idL = sqids.Encode(long.MaxValue, 1); +string idUL = sqids.Encode(ulong.MaxValue, 1); // Non CLS-Compliant +``` + +> **Note** +> The non-suffixed `Decode()` method works exactly the same as in previous versions by decoding to `int` numbers. +> This makes this update a drop-in replacement, no chances needed to your current codebase unless you want to use other integral types in legacy frameworks. + ## Customizations: You can easily customize the alphabet (the characters that Sqids uses to encode the numbers), the minimum length of the IDs (how long the IDs should be at minimum), and the blocklist (the words that should not appear in the IDs), by passing an instance of `SqidsOptions` to the constructor of `SqidsEncoder`. diff --git a/Sqids.sln b/Sqids.sln index 9de449b..8be0ec9 100644 --- a/Sqids.sln +++ b/Sqids.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -11,14 +10,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B69EBB5-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sqids.Tests", "test\Sqids.Tests\Sqids.Tests.csproj", "{26D42DEF-5A42-436C-8B80-44AA4917BFC1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sqids.CodeGeneration", "src\Sqids.CodeGeneration\Sqids.CodeGeneration.csproj", "{04A8CCFC-1496-4F22-8399-922367E9260B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {387B307E-04C6-4B8E-BE50-03FF91307070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {387B307E-04C6-4B8E-BE50-03FF91307070}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -28,9 +26,20 @@ Global {26D42DEF-5A42-436C-8B80-44AA4917BFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {26D42DEF-5A42-436C-8B80-44AA4917BFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {26D42DEF-5A42-436C-8B80-44AA4917BFC1}.Release|Any CPU.Build.0 = Release|Any CPU + {04A8CCFC-1496-4F22-8399-922367E9260B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04A8CCFC-1496-4F22-8399-922367E9260B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04A8CCFC-1496-4F22-8399-922367E9260B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04A8CCFC-1496-4F22-8399-922367E9260B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {387B307E-04C6-4B8E-BE50-03FF91307070} = {FC64F776-DE51-4BFF-91D5-0ECFEEF5CCAC} {26D42DEF-5A42-436C-8B80-44AA4917BFC1} = {5B69EBB5-A05C-4DCB-9355-D010B0A093AE} + {04A8CCFC-1496-4F22-8399-922367E9260B} = {FC64F776-DE51-4BFF-91D5-0ECFEEF5CCAC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {49DFA9B2-8BDC-4F7C-AF2B-ED28635CAE4A} EndGlobalSection EndGlobal diff --git a/src/Sqids.CodeGeneration/Sqids.CodeGeneration.csproj b/src/Sqids.CodeGeneration/Sqids.CodeGeneration.csproj new file mode 100644 index 0000000..698750d --- /dev/null +++ b/src/Sqids.CodeGeneration/Sqids.CodeGeneration.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + latest + true + true + + + + + + + diff --git a/src/Sqids.CodeGeneration/SqidsLegacyOverloadsGenerator.cs b/src/Sqids.CodeGeneration/SqidsLegacyOverloadsGenerator.cs new file mode 100644 index 0000000..811b873 --- /dev/null +++ b/src/Sqids.CodeGeneration/SqidsLegacyOverloadsGenerator.cs @@ -0,0 +1,121 @@ +using System.Text; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Sqids.CodeGeneration; + +[Generator] +public class SqidsLegacyOverloadsGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) { + var provider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + transform: (ctx, _) => GetTarget(ctx)) + .Where(m => m is not null) + .Select((m, _) => m!.Value); + + context.RegisterSourceOutput(provider, Execute); + } + + private static (string Name, string Namespace)? GetTarget(GeneratorSyntaxContext ctx) { + var classDecl = (ClassDeclarationSyntax)ctx.Node; + var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl); + + if (symbol is null || !symbol.GetAttributes().Any(a => a.AttributeClass?.Name.StartsWith("GenerateSqidsLegacyOverloads") == true)) + return null; + + return (symbol.Name, symbol.ContainingNamespace.ToDisplayString()); + } + + private static void Execute(SourceProductionContext context, (string Name, string Namespace) target) { + // Format: (Suffix, CLS-Compliant) + var types = new (string Suffix, bool IsCls)[] { + ("Byte", true), + ("SByte", false), + ("Short", true), + ("UShort", false), + ("Int", true), + ("UInt", false), + ("Long", true), + ("ULong", false) + }; + + var sb = new StringBuilder(); + + // Note: usings may not be needed depending on context but included for safety + sb.AppendLine($$""" + // + #if !NET7_0_OR_GREATER + using System; + using System.Collections.Generic; + using System.Linq; + + namespace {{target.Namespace}}; + + public sealed partial class {{target.Name}} + { + """); + + foreach (var (suffix, isCls) in types) { + var t = suffix.ToLowerInvariant(); + var clsAttr = isCls ? "" : "\r\n [CLSCompliant(false)]"; + + // Encode Methods (int Encode methods are kept in the main class for increased readability) + if (t != "int") { + // Handle cast expression edge cases (identity for ulong, direct cast for uint, cast & Check() for others) + var castExpr = t == "ulong" ? "n" : t == "uint" ? "(ulong)n" : "(ulong)Check(n)"; + // Handle ulong collection expression edge case (no conversion needed) + var colExpr = t == "ulong" ? "[.. numbers]" : $"[.. numbers.Select(n => {castExpr})]"; + + static string GetEncodeXmlComment(bool isParams) => $$""" + /// Encodes {{(isParams ? "one or more" : "a collection of")}} numbers into a Sqids ID. + /// The {{(isParams ? "number or " : "")}}numbers to encode. + /// A string containing the encoded IDs, or an empty string if the `IEnumerable` passed is empty. + /// If any of the numbers passed is smaller than 0 (i.e. negative). + /// If the encoding reaches maximum re-generation attempts due to the blocklist. + """; + + sb.AppendLine($$""" + {{GetEncodeXmlComment(isParams: true)}}{{clsAttr}} + public string Encode(params {{t}}[] numbers) => EncodeCore({{colExpr}}); + + {{GetEncodeXmlComment(isParams: false)}}{{clsAttr}} + public string Encode(IEnumerable<{{t}}> numbers) => EncodeCore({{colExpr}}); + + """); + } + + static string GetDecodeXmlComment() => $$""" + /// Decodes an ID into numbers. + /// The encoded ID. + /// + /// An array containing the decoded number(s) (it would contain only one element + /// if the ID represents a single number); or an empty array if the input ID is null, + /// empty, or includes characters not found in the alphabet. + /// + """; + + // Decode Methods + // ulong returns IReadOnlyList directly so no need to cast + var retType = t == "ulong" ? "DecodeCore(id)" : $"[.. DecodeCore(id).Select(v => ({t})v)]"; + var retTypeSpan = t == "ulong" ? "DecodeCore(id.AsSpan())" : $"[.. DecodeCore(id.AsSpan()).Select(v => ({t})v)]"; + + sb.AppendLine($$""" + {{GetDecodeXmlComment()}}{{clsAttr}} + public IReadOnlyList<{{t}}> Decode{{suffix}}(ReadOnlySpan id) => {{retType}}; + + {{GetDecodeXmlComment()}}{{clsAttr}} + public IReadOnlyList<{{t}}> Decode{{suffix}}(string id) => {{retTypeSpan}}; + + """); + } + + sb.AppendLine("}"); + sb.AppendLine("#endif"); + + context.AddSource($"{target.Name}.Legacy.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + } +} diff --git a/src/Sqids/GenerateSqidsLegacyOverloadsAttribute.cs b/src/Sqids/GenerateSqidsLegacyOverloadsAttribute.cs new file mode 100644 index 0000000..93c9945 --- /dev/null +++ b/src/Sqids/GenerateSqidsLegacyOverloadsAttribute.cs @@ -0,0 +1,7 @@ +#if !NET7_0_OR_GREATER +namespace Sqids; + +[AttributeUsage(AttributeTargets.Class)] +internal class GenerateSqidsLegacyOverloadsAttribute : Attribute { } + +#endif diff --git a/src/Sqids/Sqids.csproj b/src/Sqids/Sqids.csproj index 1276da6..2319462 100644 --- a/src/Sqids/Sqids.csproj +++ b/src/Sqids/Sqids.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net6.0;net7.0;net8.0 @@ -23,6 +23,7 @@ true true snupkg + @@ -34,6 +35,10 @@ + + + + diff --git a/src/Sqids/SqidsEncoder.cs b/src/Sqids/SqidsEncoder.cs index 98636a1..4f2c789 100644 --- a/src/Sqids/SqidsEncoder.cs +++ b/src/Sqids/SqidsEncoder.cs @@ -1,5 +1,9 @@ #if NET7_0_OR_GREATER using System.Numerics; +using System.Runtime.CompilerServices; +#else +using System.Runtime.CompilerServices; +using T = System.UInt64; // Alias used to unify .NET 7+ and legacy code #endif namespace Sqids; @@ -18,7 +22,8 @@ public sealed class SqidsEncoder where T : unmanaged, IBinaryInteger, IMin /// /// The Sqids encoder/decoder. This is the main class. /// -public sealed class SqidsEncoder +[GenerateSqidsLegacyOverloads] // Will use source generation to add legacy overloads when targetting .NET Framework 4.6.1+ and up to .NET 6 +public sealed partial class SqidsEncoder #endif { private const int MinAlphabetLength = 3; @@ -33,14 +38,7 @@ public sealed class SqidsEncoder /// /// Initializes a new instance of with the default options. /// -#else - /// - /// Initializes a new instance of with the default options. - /// -#endif public SqidsEncoder() : this(new()) { } - -#if NET7_0_OR_GREATER /// /// Initializes a new instance of with custom options. /// @@ -51,7 +49,12 @@ public SqidsEncoder() : this(new()) { } /// /// /// + public SqidsEncoder(SqidsOptions options) #else + /// + /// Initializes a new instance of with the default options. + /// + public SqidsEncoder() : this(new SqidsOptions()) { } /// /// Initializes a new instance of with custom options. /// @@ -62,8 +65,8 @@ public SqidsEncoder() : this(new()) { } /// /// /// -#endif public SqidsEncoder(SqidsOptions options) +#endif { _ = options ?? throw new ArgumentNullException(nameof(options)); _ = options.Alphabet ?? throw new ArgumentNullException(nameof(options.Alphabet)); @@ -116,12 +119,13 @@ public SqidsEncoder(SqidsOptions options) blockList.Add(w); } - _blockList = blockList.ToArray(); // NOTE: Arrays are faster to iterate than HashSets, so we construct an array here. + _blockList = [..blockList]; // NOTE: Arrays are faster to iterate than HashSets, so we construct an array here. _alphabet = options.Alphabet.ToCharArray(); ConsistentShuffle(_alphabet); } +#if NET7_0_OR_GREATER /// /// Encodes a single number into a Sqids ID. /// @@ -129,31 +133,19 @@ public SqidsEncoder(SqidsOptions options) /// A string containing the encoded ID. /// If the number passed is smaller than 0 (i.e. negative). /// If the encoding reaches maximum re-generation attempts due to the blocklist. -#if NET7_0_OR_GREATER public string Encode(T number) -#else - public string Encode(int number) -#endif { #if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfLessThan(number, T.Zero, nameof(number)); #else -#if NET7_0 if (number < T.Zero) -#else - if (number < 0) -#endif throw new ArgumentOutOfRangeException( nameof(number), "Encoding is only supported for zero and positive numbers." ); - return Encode(stackalloc[] { number }); // NOTE: We use `stackalloc` here in order not to incur the cost of allocating an array on the heap, since we know the array will only have one element, we can use `stackalloc` safely. -#endif - -#if NET8_0_OR_GREATER - return Encode([number]); #endif + return EncodeCore([number]); // NOTE: Collection initialization takes care of `stackalloc` intricacies } /// @@ -163,11 +155,7 @@ public string Encode(int number) /// A string containing the encoded IDs, or an empty string if the array passed is empty. /// If any of the numbers passed is smaller than 0 (i.e. negative). /// If the encoding reaches maximum re-generation attempts due to the blocklist. -#if NET7_0_OR_GREATER public string Encode(params T[] numbers) -#else - public string Encode(params int[] numbers) -#endif { if (numbers.Length == 0) return string.Empty; @@ -176,18 +164,14 @@ public string Encode(params int[] numbers) #if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfLessThan(number, T.Zero, nameof(numbers)); #else -#if NET7_0 if (number < T.Zero) -#else - if (number < 0) -#endif throw new ArgumentOutOfRangeException( nameof(numbers), "Encoding is only supported for zero and positive numbers." ); #endif - return Encode(numbers.AsSpan()); + return EncodeCore(numbers.AsSpan()); } /// @@ -197,20 +181,74 @@ public string Encode(params int[] numbers) /// A string containing the encoded IDs, or an empty string if the `IEnumerable` passed is empty. /// If any of the numbers passed is smaller than 0 (i.e. negative). /// If the encoding reaches maximum re-generation attempts due to the blocklist. -#if NET7_0_OR_GREATER - public string Encode(IEnumerable numbers) => -#else - public string Encode(IEnumerable numbers) => -#endif - Encode(numbers.ToArray()); + public string Encode(IEnumerable numbers) => Encode([.. numbers]); + + /// + /// Decodes an ID into numbers. + /// + /// The encoded ID. + /// + /// An array containing the decoded number(s) (it would contain only one element + /// if the ID represents a single number); or an empty array if the input ID is null, + /// empty, or includes characters not found in the alphabet. + /// + public IReadOnlyList Decode(ReadOnlySpan id) => DecodeCore(id); - // TODO: Consider using `ArrayPool` if possible -#if NET7_0_OR_GREATER - private string Encode(ReadOnlySpan numbers, int increment = 0) #else - private string Encode(ReadOnlySpan numbers, int increment = 0) + // LEGACY API (Minimal overloads accepting any integral type and converting to ulong/"T") + + // Note: Overloads are source generated in a partial class. + // Since we use params[] arrays for the Encode methods, we don't need overloads for single values + + // Encoding/decoding overloads for int are kept here for "readability" purposes. + + /// Encodes one or more numbers into a Sqids ID. + /// The number or numbers to encode. + /// A string containing the encoded IDs, or an empty string if the `IEnumerable` passed is empty. + /// If any of the numbers passed is smaller than 0 (i.e. negative). + /// If the encoding reaches maximum re-generation attempts due to the blocklist. + public string Encode(params int[] numbers) => EncodeCore([.. numbers.Select(n => (T)Check(n))]); + + /// Encodes a collection of numbers into a Sqids ID. + /// The numbers to encode. + /// A string containing the encoded IDs, or an empty string if the `IEnumerable` passed is empty. + /// If any of the numbers passed is smaller than 0 (i.e. negative). + /// If the encoding reaches maximum re-generation attempts due to the blocklist. + public string Encode(IEnumerable numbers) => EncodeCore([.. numbers.Select(n => (T)Check(n))]); + + // For backwards compatibility, the default Decode methods still return int lists + + /// Decodes an ID into numbers. + /// The encoded ID. + /// + /// An array containing the decoded number(s) (it would contain only one element + /// if the ID represents a single number); or an empty array if the input ID is null, + /// empty, or includes characters not found in the alphabet. + /// + public IReadOnlyList Decode(ReadOnlySpan id) => [..DecodeCore(id).Select(i => (int)i)]; + + /// Decodes an ID into numbers. + /// The encoded ID. + /// + /// An array containing the decoded number(s) (it would contain only one element + /// if the ID represents a single number); or an empty array if the input ID is null, + /// empty, or includes characters not found in the alphabet. + /// + public IReadOnlyList Decode(string id) => [..DecodeCore(id.AsSpan()).Select(i => (int)i)]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long Check(long n) { + if (n < 0) throw new ArgumentOutOfRangeException(nameof(n), "Encoding is only supported for zero and positive numbers."); + return n; + } #endif + + // Encode/DecodeCore shares the same logic for legacy and modern (.NET 7+) implementations + private string EncodeCore(ReadOnlySpan numbers, int increment = 0) { + if (numbers.Length == 0) + return string.Empty; + if (increment > _alphabet.Length) throw new ArgumentException("Reached max attempts to re-generate the ID."); @@ -219,8 +257,9 @@ private string Encode(ReadOnlySpan numbers, int increment = 0) #if NET7_0_OR_GREATER offset += _alphabet[int.CreateChecked(numbers[i] % T.CreateChecked(_alphabet.Length))] + i; #else - offset += _alphabet[numbers[i] % _alphabet.Length] + i; + offset += _alphabet[(int)(numbers[i] % (ulong)_alphabet.Length)] + i; #endif + offset = (numbers.Length + offset) % _alphabet.Length; offset = (offset + increment) % _alphabet.Length; @@ -268,40 +307,23 @@ private string Encode(ReadOnlySpan numbers, int increment = 0) string result = builder.ToString(); if (IsBlockedId(result.AsSpan())) - result = Encode(numbers, increment + 1); + result = EncodeCore(numbers, increment + 1); return result; } - /// - /// Decodes an ID into numbers. - /// - /// The encoded ID. - /// - /// An array containing the decoded number(s) (it would contain only one element - /// if the ID represents a single number); or an empty array if the input ID is null, - /// empty, or includes characters not found in the alphabet. - /// -#if NET7_0_OR_GREATER - public IReadOnlyList Decode(ReadOnlySpan id) +#if NET8_0_OR_GREATER + private List DecodeCore(ReadOnlySpan id) #else - public IReadOnlyList Decode(ReadOnlySpan id) + private IReadOnlyList DecodeCore(ReadOnlySpan id) #endif { if (id.IsEmpty) -#if NET7_0_OR_GREATER - return Array.Empty(); -#else - return Array.Empty(); -#endif + return []; foreach (char c in id) if (!_alphabet.Contains(c)) -#if NET7_0_OR_GREATER - return Array.Empty(); -#else - return Array.Empty(); -#endif + return []; var alphabetSpan = _alphabet.AsSpan(); @@ -317,12 +339,7 @@ public IReadOnlyList Decode(ReadOnlySpan id) alphabetTemp.Reverse(); id = id[1..]; // NOTE: Exclude the prefix - -#if NET7_0_OR_GREATER var result = new List(); -#else - var result = new List(); -#endif while (!id.IsEmpty) { char separator = alphabetTemp[0]; @@ -345,20 +362,6 @@ public IReadOnlyList Decode(ReadOnlySpan id) return result; } - // NOTE: Implicit `string` => `Span` conversion was introduced in .NET Standard 2.1 (see https://learn.microsoft.com/en-us/dotnet/api/system.string.op_implicit), which means without this overload, calling `Decode` with a string on versions older than .NET Standard 2.1 would require calling `.AsSpan()` on the string, which is cringe. -#if NETSTANDARD2_0 - /// - /// Decodes an ID into numbers. - /// - /// The encoded ID. - /// - /// An array containing the decoded number(s) (it would contain only one element - /// if the ID represents a single number); or an empty array if the input ID is null, - /// empty, or includes characters not found in the alphabet. - /// - public IReadOnlyList Decode(string id) => Decode(id.AsSpan()); -#endif - private bool IsBlockedId(ReadOnlySpan id) { foreach (string word in _blockList) @@ -383,6 +386,7 @@ private bool IsBlockedId(ReadOnlySpan id) } // NOTE: Shuffles a span of characters in place. The shuffle produces consistent results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ConsistentShuffle(Span chars) { for (int i = 0, j = chars.Length - 1; j > 0; i++, j--) @@ -392,11 +396,7 @@ private static void ConsistentShuffle(Span chars) } } -#if NET7_0_OR_GREATER private static ReadOnlySpan ToId(T num, ReadOnlySpan alphabet) -#else - private static ReadOnlySpan ToId(int num, ReadOnlySpan alphabet) -#endif { var id = new StringBuilder(); var result = num; @@ -408,31 +408,30 @@ private static ReadOnlySpan ToId(int num, ReadOnlySpan alphabet) result /= T.CreateChecked(alphabet.Length); } while (result > T.Zero); #else + ulong alphabetLength = (ulong)alphabet.Length; do { - id.Insert(0, alphabet[result % alphabet.Length]); - result /= alphabet.Length; + id.Insert(0, alphabet[(int)(result % alphabetLength)]); + result /= alphabetLength; } while (result > 0); #endif return id.ToString().AsSpan(); // TODO: possibly avoid creating a string } -#if NET7_0_OR_GREATER private static T ToNumber(ReadOnlySpan id, ReadOnlySpan alphabet) -#else - private static int ToNumber(ReadOnlySpan id, ReadOnlySpan alphabet) -#endif { #if NET7_0_OR_GREATER T result = T.Zero; foreach (var character in id) result = result * T.CreateChecked(alphabet.Length) + T.CreateChecked(alphabet.IndexOf(character)); #else - int result = 0; + T result = 0; + ulong alphabetLength = (ulong)alphabet.Length; foreach (var character in id) - result = result * alphabet.Length + alphabet.IndexOf(character); + result = result * alphabetLength + (ulong)alphabet.IndexOf(character); #endif return result; } } + diff --git a/test/Sqids.Tests/LegacyEncodingTests.cs b/test/Sqids.Tests/LegacyEncodingTests.cs new file mode 100644 index 0000000..f2d3a4c --- /dev/null +++ b/test/Sqids.Tests/LegacyEncodingTests.cs @@ -0,0 +1,550 @@ +#if !NET7_0_OR_GREATER + +namespace Sqids.Tests; + +public class LegacyEncodingTests +{ + // NOTE: We replicate each original test of encoding and decoding for each integral type + // Superfluous tests (e.g., for int and negativity) are omitted. + + [Test] + public void EncodeAndDecode_SingleNumbers_AllIntegralTypes_ReturnsExactMatch() { + var sqids = new SqidsEncoder(); + string idB = sqids.Encode(byte.MaxValue); + string idSB = sqids.Encode(sbyte.MaxValue); + string idS = sqids.Encode(short.MaxValue); + string idUS = sqids.Encode(ushort.MaxValue); + string idI = sqids.Encode(int.MaxValue); + string idUI = sqids.Encode(uint.MaxValue); + string idL = sqids.Encode(long.MaxValue); + string idUL = sqids.Encode(ulong.MaxValue); // Non CLS-Compliant + byte bID = sqids.DecodeByte(idB).Single(); + sbyte sbID = sqids.DecodeSByte(idSB).Single(); // Non CLS-Compliant + short sID = sqids.DecodeShort(idS).Single(); + ushort usID = sqids.DecodeUShort(idUS).Single(); // Non CLS-Compliant + int iID = sqids.Decode(idI).Single(); // or DecodeInt() + uint uiID = sqids.DecodeUInt(idUI).Single(); // Non CLS-Compliant + long lID = sqids.DecodeLong(idL).Single(); + ulong ulID = sqids.DecodeULong(idUL).Single(); // Non CLS-Compliant + bID.ShouldBe(byte.MaxValue); + sbID.ShouldBe(sbyte.MaxValue); + sID.ShouldBe(short.MaxValue); + usID.ShouldBe(ushort.MaxValue); + iID.ShouldBe(int.MaxValue); + uiID.ShouldBe(uint.MaxValue); + lID.ShouldBe(long.MaxValue); + ulID.ShouldBe(ulong.MaxValue); + } + + [Test] + public void EncodeAndDecode_MultipleNumbers_AllIntegralTypes_ReturnsExactMatch() { + var sqids = new SqidsEncoder(); + string idB = sqids.Encode((byte[])[byte.MaxValue, 1]); + string idSB = sqids.Encode(sbyte.MaxValue, (sbyte)1); + string idS = sqids.Encode(short.MaxValue, (short)1); + string idUS = sqids.Encode(ushort.MaxValue, (ushort)1); + string idI = sqids.Encode(int.MaxValue, 1); + string idUI = sqids.Encode(uint.MaxValue, 1); + string idL = sqids.Encode(long.MaxValue, 1); + string idUL = sqids.Encode(ulong.MaxValue, 1); // Non CLS-Compliant + IEnumerable bID = sqids.DecodeByte(idB); + IEnumerable sbID = sqids.DecodeSByte(idSB); // Non CLS-Compliant + IEnumerable sID = sqids.DecodeShort(idS); + IEnumerable usID = sqids.DecodeUShort(idUS); // Non CLS-Compliant + IEnumerable iID = sqids.Decode(idI); // or DecodeInt() + IEnumerable uiID = sqids.DecodeUInt(idUI); // Non CLS-Compliant + IEnumerable lID = sqids.DecodeLong(idL); + IEnumerable ulID = sqids.DecodeULong(idUL); // Non CLS-Compliant + bID .ShouldBe([byte.MaxValue, (byte)1]); + sbID.ShouldBe([sbyte.MaxValue, (sbyte)1]); + sID .ShouldBe([short.MaxValue, (short)1]); + usID.ShouldBe([ushort.MaxValue, (ushort)1]); + iID .ShouldBe([int.MaxValue, 1]); + uiID.ShouldBe([uint.MaxValue, 1U]); + lID .ShouldBe([long.MaxValue, 1L]); + ulID.ShouldBe([ulong.MaxValue, 1UL]); + } + + + #region ULONG Tests + + // NOTE: Incremental + [TestCase(0UL, "bM")] + [TestCase(1UL, "Uk")] + [TestCase(2UL, "gb")] + [TestCase(3UL, "Ef")] + [TestCase(4UL, "Vq")] + [TestCase(5UL, "uw")] + [TestCase(6UL, "OI")] + [TestCase(7UL, "AX")] + [TestCase(8UL, "p6")] + [TestCase(9UL, "nJ")] + public void EncodeAndDecodeULong_SingleNumber_ReturnsExactMatch(ulong number, string id) + { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeULong(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new ulong[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new ulong[] { 0, 0 }, "SvIz")] + [TestCase(new ulong[] { 0, 1 }, "n3qa")] + [TestCase(new ulong[] { 0, 2 }, "tryF")] + [TestCase(new ulong[] { 0, 3 }, "eg6q")] + [TestCase(new ulong[] { 0, 4 }, "rSCF")] + [TestCase(new ulong[] { 0, 5 }, "sR8x")] + [TestCase(new ulong[] { 0, 6 }, "uY2M")] + [TestCase(new ulong[] { 0, 7 }, "74dI")] + [TestCase(new ulong[] { 0, 8 }, "30WX")] + [TestCase(new ulong[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new ulong[] { 0, 0 }, "SvIz")] + [TestCase(new ulong[] { 1, 0 }, "nWqP")] + [TestCase(new ulong[] { 2, 0 }, "tSyw")] + [TestCase(new ulong[] { 3, 0 }, "eX68")] + [TestCase(new ulong[] { 4, 0 }, "rxCY")] + [TestCase(new ulong[] { 5, 0 }, "sV8a")] + [TestCase(new ulong[] { 6, 0 }, "uf2K")] + [TestCase(new ulong[] { 7, 0 }, "7Cdk")] + [TestCase(new ulong[] { 8, 0 }, "3aWP")] + [TestCase(new ulong[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new ulong[] { }, "")] + public void EncodeAndDecodeULong_MultipleNumbers_ReturnsExactMatch(ulong[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeULong(id).ShouldBe(numbers); + } + + [TestCase(new ulong[] { 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, ulong.MaxValue })] + [TestCase(new ulong[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(ulong[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeULong(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region LONG Tests + + // NOTE: Incremental + [TestCase(0, "bM")] + [TestCase(1, "Uk")] + [TestCase(2, "gb")] + [TestCase(3, "Ef")] + [TestCase(4, "Vq")] + [TestCase(5, "uw")] + [TestCase(6, "OI")] + [TestCase(7, "AX")] + [TestCase(8, "p6")] + [TestCase(9, "nJ")] + public void EncodeAndDecodeLong_SingleNumber_ReturnsExactMatch(long number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeLong(id).ShouldBe([number]); + } + + + // NOTE: Simple case + [TestCase(new long[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new long[] { 0, 0 }, "SvIz")] + [TestCase(new long[] { 0, 1 }, "n3qa")] + [TestCase(new long[] { 0, 2 }, "tryF")] + [TestCase(new long[] { 0, 3 }, "eg6q")] + [TestCase(new long[] { 0, 4 }, "rSCF")] + [TestCase(new long[] { 0, 5 }, "sR8x")] + [TestCase(new long[] { 0, 6 }, "uY2M")] + [TestCase(new long[] { 0, 7 }, "74dI")] + [TestCase(new long[] { 0, 8 }, "30WX")] + [TestCase(new long[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new long[] { 0, 0 }, "SvIz")] + [TestCase(new long[] { 1, 0 }, "nWqP")] + [TestCase(new long[] { 2, 0 }, "tSyw")] + [TestCase(new long[] { 3, 0 }, "eX68")] + [TestCase(new long[] { 4, 0 }, "rxCY")] + [TestCase(new long[] { 5, 0 }, "sV8a")] + [TestCase(new long[] { 6, 0 }, "uf2K")] + [TestCase(new long[] { 7, 0 }, "7Cdk")] + [TestCase(new long[] { 8, 0 }, "3aWP")] + [TestCase(new long[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new long[] { }, "")] + public void EncodeAndDecodeLong_MultipleNumbers_ReturnsExactMatch(long[] numbers, string id) + { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeLong(id).ShouldBe(numbers); + } + + [TestCase(new long[] { 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, long.MaxValue })] + [TestCase(new long[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(long[] numbers) + { + var sqids = new SqidsEncoder(); + + sqids.DecodeLong(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region UINT Tests + + // NOTE: Incremental + [TestCase(0U, "bM")] + [TestCase(1U, "Uk")] + [TestCase(2U, "gb")] + [TestCase(3U, "Ef")] + [TestCase(4U, "Vq")] + [TestCase(5U, "uw")] + [TestCase(6U, "OI")] + [TestCase(7U, "AX")] + [TestCase(8U, "p6")] + [TestCase(9U, "nJ")] + public void EncodeAndDecodeUInt_SingleNumber_ReturnsExactMatch(uint number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeUInt(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new uint[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new uint[] { 0, 0 }, "SvIz")] + [TestCase(new uint[] { 0, 1 }, "n3qa")] + [TestCase(new uint[] { 0, 2 }, "tryF")] + [TestCase(new uint[] { 0, 3 }, "eg6q")] + [TestCase(new uint[] { 0, 4 }, "rSCF")] + [TestCase(new uint[] { 0, 5 }, "sR8x")] + [TestCase(new uint[] { 0, 6 }, "uY2M")] + [TestCase(new uint[] { 0, 7 }, "74dI")] + [TestCase(new uint[] { 0, 8 }, "30WX")] + [TestCase(new uint[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new uint[] { 0, 0 }, "SvIz")] + [TestCase(new uint[] { 1, 0 }, "nWqP")] + [TestCase(new uint[] { 2, 0 }, "tSyw")] + [TestCase(new uint[] { 3, 0 }, "eX68")] + [TestCase(new uint[] { 4, 0 }, "rxCY")] + [TestCase(new uint[] { 5, 0 }, "sV8a")] + [TestCase(new uint[] { 6, 0 }, "uf2K")] + [TestCase(new uint[] { 7, 0 }, "7Cdk")] + [TestCase(new uint[] { 8, 0 }, "3aWP")] + [TestCase(new uint[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new uint[] { }, "")] + public void EncodeAndDecodeUInt_MultipleNumbers_ReturnsExactMatch(uint[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeUInt(id).ShouldBe(numbers); + } + + [TestCase(new uint[] { 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, uint.MaxValue })] + [TestCase(new uint[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(uint[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeUInt(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region SHORT Tests + + // NOTE: Incremental + [TestCase(0, "bM")] + [TestCase(1, "Uk")] + [TestCase(2, "gb")] + [TestCase(3, "Ef")] + [TestCase(4, "Vq")] + [TestCase(5, "uw")] + [TestCase(6, "OI")] + [TestCase(7, "AX")] + [TestCase(8, "p6")] + [TestCase(9, "nJ")] + public void EncodeAndDecodeShort_SingleNumber_ReturnsExactMatch(short number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeShort(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new short[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new short[] { 0, 0 }, "SvIz")] + [TestCase(new short[] { 0, 1 }, "n3qa")] + [TestCase(new short[] { 0, 2 }, "tryF")] + [TestCase(new short[] { 0, 3 }, "eg6q")] + [TestCase(new short[] { 0, 4 }, "rSCF")] + [TestCase(new short[] { 0, 5 }, "sR8x")] + [TestCase(new short[] { 0, 6 }, "uY2M")] + [TestCase(new short[] { 0, 7 }, "74dI")] + [TestCase(new short[] { 0, 8 }, "30WX")] + [TestCase(new short[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new short[] { 0, 0 }, "SvIz")] + [TestCase(new short[] { 1, 0 }, "nWqP")] + [TestCase(new short[] { 2, 0 }, "tSyw")] + [TestCase(new short[] { 3, 0 }, "eX68")] + [TestCase(new short[] { 4, 0 }, "rxCY")] + [TestCase(new short[] { 5, 0 }, "sV8a")] + [TestCase(new short[] { 6, 0 }, "uf2K")] + [TestCase(new short[] { 7, 0 }, "7Cdk")] + [TestCase(new short[] { 8, 0 }, "3aWP")] + [TestCase(new short[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new short[] { }, "")] + public void EncodeAndDecodeShort_MultipleNumbers_ReturnsExactMatch(short[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeShort(id).ShouldBe(numbers); + } + + [TestCase(new short[] { 0, 0, 0, 1, 2, 3, 100, 1_000, short.MaxValue })] + [TestCase(new short[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(short[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeShort(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region USHORT Tests + + // NOTE: Incremental + [TestCase((ushort)0, "bM")] + [TestCase((ushort)1, "Uk")] + [TestCase((ushort)2, "gb")] + [TestCase((ushort)3, "Ef")] + [TestCase((ushort)4, "Vq")] + [TestCase((ushort)5, "uw")] + [TestCase((ushort)6, "OI")] + [TestCase((ushort)7, "AX")] + [TestCase((ushort)8, "p6")] + [TestCase((ushort)9, "nJ")] + public void EncodeAndDecodeUShort_SingleNumber_ReturnsExactMatch(ushort number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeUShort(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new ushort[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new ushort[] { 0, 0 }, "SvIz")] + [TestCase(new ushort[] { 0, 1 }, "n3qa")] + [TestCase(new ushort[] { 0, 2 }, "tryF")] + [TestCase(new ushort[] { 0, 3 }, "eg6q")] + [TestCase(new ushort[] { 0, 4 }, "rSCF")] + [TestCase(new ushort[] { 0, 5 }, "sR8x")] + [TestCase(new ushort[] { 0, 6 }, "uY2M")] + [TestCase(new ushort[] { 0, 7 }, "74dI")] + [TestCase(new ushort[] { 0, 8 }, "30WX")] + [TestCase(new ushort[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new ushort[] { 0, 0 }, "SvIz")] + [TestCase(new ushort[] { 1, 0 }, "nWqP")] + [TestCase(new ushort[] { 2, 0 }, "tSyw")] + [TestCase(new ushort[] { 3, 0 }, "eX68")] + [TestCase(new ushort[] { 4, 0 }, "rxCY")] + [TestCase(new ushort[] { 5, 0 }, "sV8a")] + [TestCase(new ushort[] { 6, 0 }, "uf2K")] + [TestCase(new ushort[] { 7, 0 }, "7Cdk")] + [TestCase(new ushort[] { 8, 0 }, "3aWP")] + [TestCase(new ushort[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new ushort[] { }, "")] + public void EncodeAndDecodeUShort_MultipleNumbers_ReturnsExactMatch(ushort[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeUShort(id).ShouldBe(numbers); + } + + [TestCase(new ushort[] { 0, 0, 0, 1, 2, 3, 100, 1_000, ushort.MaxValue })] + [TestCase(new ushort[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(ushort[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeUShort(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region BYTE Tests + + // NOTE: Incremental + [TestCase((byte)0, "bM")] + [TestCase((byte)1, "Uk")] + [TestCase((byte)2, "gb")] + [TestCase((byte)3, "Ef")] + [TestCase((byte)4, "Vq")] + [TestCase((byte)5, "uw")] + [TestCase((byte)6, "OI")] + [TestCase((byte)7, "AX")] + [TestCase((byte)8, "p6")] + [TestCase((byte)9, "nJ")] + public void EncodeAndDecodeByte_SingleNumber_ReturnsExactMatch(byte number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeByte(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new byte[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new byte[] { 0, 0 }, "SvIz")] + [TestCase(new byte[] { 0, 1 }, "n3qa")] + [TestCase(new byte[] { 0, 2 }, "tryF")] + [TestCase(new byte[] { 0, 3 }, "eg6q")] + [TestCase(new byte[] { 0, 4 }, "rSCF")] + [TestCase(new byte[] { 0, 5 }, "sR8x")] + [TestCase(new byte[] { 0, 6 }, "uY2M")] + [TestCase(new byte[] { 0, 7 }, "74dI")] + [TestCase(new byte[] { 0, 8 }, "30WX")] + [TestCase(new byte[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new byte[] { 0, 0 }, "SvIz")] + [TestCase(new byte[] { 1, 0 }, "nWqP")] + [TestCase(new byte[] { 2, 0 }, "tSyw")] + [TestCase(new byte[] { 3, 0 }, "eX68")] + [TestCase(new byte[] { 4, 0 }, "rxCY")] + [TestCase(new byte[] { 5, 0 }, "sV8a")] + [TestCase(new byte[] { 6, 0 }, "uf2K")] + [TestCase(new byte[] { 7, 0 }, "7Cdk")] + [TestCase(new byte[] { 8, 0 }, "3aWP")] + [TestCase(new byte[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new byte[] { }, "")] + public void EncodeAndDecodeByte_MultipleNumbers_ReturnsExactMatch(byte[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeByte(id).ShouldBe(numbers); + } + + [TestCase(new byte[] { 0, 0, 0, 1, 2, 3, 100, byte.MaxValue })] + [TestCase(new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(byte[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeByte(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion + + #region SBYTE Tests + + // NOTE: Incremental + [TestCase((sbyte)0, "bM")] + [TestCase((sbyte)1, "Uk")] + [TestCase((sbyte)2, "gb")] + [TestCase((sbyte)3, "Ef")] + [TestCase((sbyte)4, "Vq")] + [TestCase((sbyte)5, "uw")] + [TestCase((sbyte)6, "OI")] + [TestCase((sbyte)7, "AX")] + [TestCase((sbyte)8, "p6")] + [TestCase((sbyte)9, "nJ")] + public void EncodeAndDecodeSByte_SingleNumber_ReturnsExactMatch(sbyte number, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(number).ShouldBe(id); + sqids.DecodeSByte(id).ShouldBe([number]); + } + + // NOTE: Simple case + [TestCase(new sbyte[] { 1, 2, 3 }, "86Rf07")] + // NOTE: Incremental + [TestCase(new sbyte[] { 0, 0 }, "SvIz")] + [TestCase(new sbyte[] { 0, 1 }, "n3qa")] + [TestCase(new sbyte[] { 0, 2 }, "tryF")] + [TestCase(new sbyte[] { 0, 3 }, "eg6q")] + [TestCase(new sbyte[] { 0, 4 }, "rSCF")] + [TestCase(new sbyte[] { 0, 5 }, "sR8x")] + [TestCase(new sbyte[] { 0, 6 }, "uY2M")] + [TestCase(new sbyte[] { 0, 7 }, "74dI")] + [TestCase(new sbyte[] { 0, 8 }, "30WX")] + [TestCase(new sbyte[] { 0, 9 }, "moxr")] + // NOTE: Incremental + [TestCase(new sbyte[] { 0, 0 }, "SvIz")] + [TestCase(new sbyte[] { 1, 0 }, "nWqP")] + [TestCase(new sbyte[] { 2, 0 }, "tSyw")] + [TestCase(new sbyte[] { 3, 0 }, "eX68")] + [TestCase(new sbyte[] { 4, 0 }, "rxCY")] + [TestCase(new sbyte[] { 5, 0 }, "sV8a")] + [TestCase(new sbyte[] { 6, 0 }, "uf2K")] + [TestCase(new sbyte[] { 7, 0 }, "7Cdk")] + [TestCase(new sbyte[] { 8, 0 }, "3aWP")] + [TestCase(new sbyte[] { 9, 0 }, "m2xn")] + // NOTE: Empty array should encode into empty string + [TestCase(new sbyte[] { }, "")] + public void EncodeAndDecodeSByte_MultipleNumbers_ReturnsExactMatch(sbyte[] numbers, string id) { + var sqids = new SqidsEncoder(); + sqids.Encode(numbers).ShouldBe(id); + sqids.Encode(numbers.ToList()).ShouldBe(id); // NOTE: Selects the `IEnumerable` overload + sqids.DecodeSByte(id).ShouldBe(numbers); + } + + [TestCase(new sbyte[] { 0, 0, 0, 1, 2, 3, 100, sbyte.MaxValue })] + [TestCase(new sbyte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99 + })] + public void EncodeAndDecode_MultipleNumbers_RoundTripsSuccessfully(sbyte[] numbers) { + var sqids = new SqidsEncoder(); + + sqids.DecodeSByte(sqids.Encode(numbers)).ShouldBe(numbers); + } + #endregion +} + +#endif From c586f834ad945f2fa6c1da9de110cecced4180f0 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Tue, 9 Dec 2025 21:26:00 -0300 Subject: [PATCH 03/11] Fix typo in README regarding drop-in replacement --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0b9c9d..74f52af 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ string idUL = sqids.Encode(ulong.MaxValue, 1); // Non CLS-Compliant > **Note** > The non-suffixed `Decode()` method works exactly the same as in previous versions by decoding to `int` numbers. -> This makes this update a drop-in replacement, no chances needed to your current codebase unless you want to use other integral types in legacy frameworks. +> This makes this update a drop-in replacement, no changes needed to your current codebase unless you want to use other integral types in legacy frameworks. ## Customizations: From a9ffce5c56de111dc53db2b0315a0409d38029a6 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 11:39:39 -0300 Subject: [PATCH 04/11] Change CI to Windows and support multiple .NET versions Updated the GitHub Actions workflow to run on Windows and added support for multiple .NET versions. --- .github/workflows/dotnet.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..97a2c2a --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,33 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: windows-latest + strategy: + matrix: + dotnet-version: [ '8.0.x', '7.0.x', '6.0.x' ] + + steps: + - uses: actions/checkout@v5 + - name: Setup dotnet ${{ matrix.dotnet-version }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Display dotnet version + run: dotnet --version + - name: Restore dependencies + run: dotnet restore + - name: Build ${{ matrix.dotnet-version }} + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal --configuration Release From e09d88ba810ed78d9b9b7507f8086f88f5473352 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 15:06:50 -0300 Subject: [PATCH 05/11] Update dotnet.yml to use checkout@v4 and set Release config --- .github/workflows/dotnet.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 97a2c2a..3f1a5f0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -18,7 +18,7 @@ jobs: dotnet-version: [ '8.0.x', '7.0.x', '6.0.x' ] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Setup dotnet ${{ matrix.dotnet-version }} uses: actions/setup-dotnet@v4 with: @@ -28,6 +28,6 @@ jobs: - name: Restore dependencies run: dotnet restore - name: Build ${{ matrix.dotnet-version }} - run: dotnet build --no-restore + run: dotnet build --no-restore --configuration Release - name: Test run: dotnet test --no-build --verbosity normal --configuration Release From 8ba6790da9a4cb31ae1d9a227fc38ffb49063ccb Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 15:27:29 -0300 Subject: [PATCH 06/11] Trying to run only targeting NET 8 --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3f1a5f0..5a014f3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -15,7 +15,7 @@ jobs: runs-on: windows-latest strategy: matrix: - dotnet-version: [ '8.0.x', '7.0.x', '6.0.x' ] + dotnet-version: [ '8.0.x' ] steps: - uses: actions/checkout@v4 From f9a5105812d031c40c77be3a63f86cbc4a0e3ba6 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 15:33:27 -0300 Subject: [PATCH 07/11] Modify .NET workflow to support multiple versions Updated the .NET workflow to install multiple .NET versions and simplified the setup steps. --- .github/workflows/dotnet.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5a014f3..eb7b12f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,6 +1,3 @@ -# This workflow will build a .NET project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net - name: .NET on: @@ -11,23 +8,25 @@ on: jobs: build: - runs-on: windows-latest - strategy: - matrix: - dotnet-version: [ '8.0.x' ] steps: - uses: actions/checkout@v4 - - name: Setup dotnet ${{ matrix.dotnet-version }} + + # Instala TODAS as versões necessárias de uma vez + - name: Setup dotnet versions uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ matrix.dotnet-version }} - - name: Display dotnet version - run: dotnet --version + dotnet-version: | + 8.0.x + 7.0.x + 6.0.x + - name: Restore dependencies run: dotnet restore - - name: Build ${{ matrix.dotnet-version }} + + - name: Build run: dotnet build --no-restore --configuration Release + - name: Test run: dotnet test --no-build --verbosity normal --configuration Release From f90f0f6c5d376ff4ffd4661c3674313b409a606c Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 16:04:01 -0300 Subject: [PATCH 08/11] Add workflow_dispatch trigger to test workflow --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8105828..65a7744 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + workflow_dispatch: jobs: test: From 7c60e9e674694b5fa0f3dbd7c29cd5c4c603dd23 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 18:10:23 -0300 Subject: [PATCH 09/11] Removed new test workflow as existing one is working fine --- .github/workflows/dotnet.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index eb7b12f..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: .NET - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v4 - - # Instala TODAS as versões necessárias de uma vez - - name: Setup dotnet versions - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 7.0.x - 6.0.x - - - name: Restore dependencies - run: dotnet restore - - - name: Build - run: dotnet build --no-restore --configuration Release - - - name: Test - run: dotnet test --no-build --verbosity normal --configuration Release From d67dfe448f30e067dcdf2ee44fc5e0d95b4f7b6a Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Thu, 11 Dec 2025 19:42:32 -0300 Subject: [PATCH 10/11] Upgrade GitHub Actions to use latest .NET and checkout --- .github/workflows/test.yaml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 65a7744..157bdba 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,13 +12,22 @@ jobs: steps: - name: Checkout the repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x # NOTE: The `windows-latest` container already comes pre-installed with the .NET versions that we need, but we do need to use `setup-dotnet` (and pass it one of the versions — e.g. 7.0.x), so that the relevant binaries (e.g. `dotnet`) are actually added to PATH so we can use them. See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#preinstalled-software and https://github.com/actions/setup-dotnet + dotnet-version: | + 8.0.x + 7.0.x + 6.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release # TODO: Some .NET codebases with GitHub Actions workflows seem to run `dotnet restore`, `dotnet build` and `dotnet test` as separate steps. Why? - - name: Run the tests - run: dotnet test --configuration Release + - name: Test + run: dotnet test --no-build --verbosity normal --configuration Release From 089cddb1581a5e07b19073d2c2defceb4c6c7913 Mon Sep 17 00:00:00 2001 From: Felipe Machado Date: Mon, 22 Dec 2025 13:17:01 -0300 Subject: [PATCH 11/11] Add Sqids.Benchmarks project with initial benchmarks Introduces a new Sqids.Benchmarks project to the solution for performance testing. Includes a custom BenchmarkDotNet config and custom project variables that allow referencing the old and new versions of the package conditionally, and also allow to create conditional code based on these variables to properly compare performance across versions of the package. --- Sqids.sln | 6 ++ benchmarks/Sqids.Benchmarks/.editorconfig | 2 + benchmarks/Sqids.Benchmarks/Program.cs | 67 +++++++++++++++++++ .../Sqids.Benchmarks/Sqids.Benchmarks.csproj | 31 +++++++++ 4 files changed, 106 insertions(+) create mode 100644 benchmarks/Sqids.Benchmarks/.editorconfig create mode 100644 benchmarks/Sqids.Benchmarks/Program.cs create mode 100644 benchmarks/Sqids.Benchmarks/Sqids.Benchmarks.csproj diff --git a/Sqids.sln b/Sqids.sln index 8be0ec9..189fda5 100644 --- a/Sqids.sln +++ b/Sqids.sln @@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sqids.Tests", "test\Sqids.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sqids.CodeGeneration", "src\Sqids.CodeGeneration\Sqids.CodeGeneration.csproj", "{04A8CCFC-1496-4F22-8399-922367E9260B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sqids.Benchmarks", "benchmarks\Sqids.Benchmarks\Sqids.Benchmarks.csproj", "{44F4CB35-FD25-4D58-DB03-F1539F678FF2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,6 +32,10 @@ Global {04A8CCFC-1496-4F22-8399-922367E9260B}.Debug|Any CPU.Build.0 = Debug|Any CPU {04A8CCFC-1496-4F22-8399-922367E9260B}.Release|Any CPU.ActiveCfg = Release|Any CPU {04A8CCFC-1496-4F22-8399-922367E9260B}.Release|Any CPU.Build.0 = Release|Any CPU + {44F4CB35-FD25-4D58-DB03-F1539F678FF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44F4CB35-FD25-4D58-DB03-F1539F678FF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44F4CB35-FD25-4D58-DB03-F1539F678FF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44F4CB35-FD25-4D58-DB03-F1539F678FF2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/benchmarks/Sqids.Benchmarks/.editorconfig b/benchmarks/Sqids.Benchmarks/.editorconfig new file mode 100644 index 0000000..340c997 --- /dev/null +++ b/benchmarks/Sqids.Benchmarks/.editorconfig @@ -0,0 +1,2 @@ +[*.{cs,vb}] +dotnet_diagnostic.CA1050.severity = none \ No newline at end of file diff --git a/benchmarks/Sqids.Benchmarks/Program.cs b/benchmarks/Sqids.Benchmarks/Program.cs new file mode 100644 index 0000000..1fd2bc0 --- /dev/null +++ b/benchmarks/Sqids.Benchmarks/Program.cs @@ -0,0 +1,67 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; + +BenchmarkRunner.Run(); + +// Define the Custom Config +public class PackageComparisonConfig : ManualConfig +{ + public PackageComparisonConfig() { + var packageVersions = new[] { "3.2.0", "3.1.0" }; + + var runtimes = new Runtime[] + { + ClrRuntime.Net48, + CoreRuntime.Core70, + CoreRuntime.Core80 + }; + + foreach (var version in packageVersions) { + foreach (var runtime in runtimes) { + AddJob(Job.Default + .WithRuntime(runtime) + .WithMsBuildArguments($"/p:BenchmarksPackageVersion={version}") + .WithId($"{runtime.Name} - v{version}")); + } + } + } +} + +[Config(typeof(PackageComparisonConfig))] +[MemoryDiagnoser] +public class SquidsBenchmarks +{ +#if NET7_0_OR_GREATER + private readonly Sqids.SqidsEncoder sqid = new(); +#else + private readonly Sqids.SqidsEncoder sqid = new(); +#endif + +#if WITH_LONG_SUPPORT + static readonly long longValue = (long)int.MaxValue; +#else + static readonly int intValue = int.MaxValue; +#endif + + static readonly string EncodedValue = "UKrsQ1F"; + + [Benchmark] + public string SqidEncode() { +#if WITH_LONG_SUPPORT + var encoded = sqid.Encode(longValue); +#else + var encoded = sqid.Encode(intValue); +#endif + return encoded; + } + + [Benchmark] + public void SqidDecode() { + var decoded = sqid.Decode(EncodedValue)[0]; + if (decoded != int.MaxValue) + throw new Exception("Decoded value does not match original"); + } +} \ No newline at end of file diff --git a/benchmarks/Sqids.Benchmarks/Sqids.Benchmarks.csproj b/benchmarks/Sqids.Benchmarks/Sqids.Benchmarks.csproj new file mode 100644 index 0000000..0ce2a42 --- /dev/null +++ b/benchmarks/Sqids.Benchmarks/Sqids.Benchmarks.csproj @@ -0,0 +1,31 @@ + + + + Exe + latest + net48;net7.0;net8.0 + true + enable + enable + + 3.2.0 + + + + $(DefineConstants);WITH_LONG_SUPPORT + + + + + + + + + + + + + + + +