Repository: dotnet/BenchmarkDotNet

Issue:
Custom order of parameters when using complex types
I originally had the following `int` parameter:
```cs
[Params(1_000, 10_000, 100_000, 1_000_000, 10_000_000)]
public int N;
```

This sorted properly using the natural order of the `int`.

I then wanted to customize the value names (1K, 1M, etc instead), so I had to wrap the `int` in a complex type to override `ToString()` (just a simple class with an `int` and a `string`, and then using a `ParamsSource` of a list).

I now had correct names, but since the parameter is now a complex type, it appears that the display name is used to do an alphabetical sort instead. This causes the order to be something like "100K, 10K, 10M, 1K, etc" , which I definitely don't want.

I can see the current implementation of this here: https://github.com/dotnet/BenchmarkDotNet/blob/56c66d01f0a26df676c6bf4ed4219afdc7695c96/src/BenchmarkDotNet/Parameters/ParameterComparer.cs#L31

As far as I can tell, there doesn't seem to be a way to get both a customized display but also ordering according to the integer (or some other custom ordering), without doing something extreme like having to provide an implementation of `IOrderer` for example (which doesn't seem at all feasible to me).

I see a mention of a proposed `ParamsOrderPolicy` [here](https://github.com/dotnet/BenchmarkDotNet/issues/660#issuecomment-368267370), but I think that doesn't provide enough flexibility.

I'm proposing a change in `ParameterComparer`, where complex params are tested if they're `IComparable`, and then simply use that interface to provide the order behavior.

I'm willing to work on this, as I feel it's a must have feature.


Hints:
> where complex params are tested if they're IComparable, and then simply use that interface to provide the order behavior.

It's a great idea, thanks for the suggestion!

> I'm willing to work on this, as I feel it's a must have feature.

PR is welcome!
Great! Will work on a PR soon.

Patch:
diff --git a/docs/articles/samples/IntroComparableComplexParam.md b/docs/articles/samples/IntroComparableComplexParam.md
new file mode 100644
index 0000000000..16319bab9c
--- /dev/null
+++ b/docs/articles/samples/IntroComparableComplexParam.md
@@ -0,0 +1,20 @@
+---
+uid: BenchmarkDotNet.Samples.IntroComparableComplexParam
+---
+
+## Sample: IntroComparableComplexParam
+
+You can implement `IComparable` (the non generic version) on your complex parameter class if you want custom ordering behavior for your parameter.
+
+One use case for this is having a parameter class that overrides `ToString()`, but also providing a custom ordering behavior that isn't the alphabetical order of the result of `ToString()`.
+
+### Source code
+
+[!code-csharp[IntroComparableComplexParam.cs](../../../samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs)]
+
+### Links
+
+* @docs.parameterization
+* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroComparableComplexParam
+
+---
\ No newline at end of file
diff --git a/docs/articles/samples/toc.yml b/docs/articles/samples/toc.yml
index 9d30bb4c64..098e457dd8 100644
--- a/docs/articles/samples/toc.yml
+++ b/docs/articles/samples/toc.yml
@@ -14,6 +14,8 @@
   href: IntroCategoryBaseline.md
 - name: IntroColdStart
   href: IntroColdStart.md
+- name: IntroComparableComplexParam
+  href: IntroComparableComplexParam.md
 - name: IntroConfigSource
   href: IntroConfigSource.md
 - name: IntroConfigUnion
diff --git a/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs b/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs
new file mode 100644
index 0000000000..4bb5cffb22
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroComparableComplexParam.cs
@@ -0,0 +1,37 @@
+﻿using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+
+namespace BenchmarkDotNet.Samples
+{
+    public class IntroComparableComplexParam
+    {
+        [ParamsSource(nameof(ValuesForA))]
+        public ComplexParam A { get; set; }
+
+        public IEnumerable<ComplexParam> ValuesForA => new[] { new ComplexParam(1, "First"), new ComplexParam(2, "Second") };
+
+        [Benchmark]
+        public object Benchmark() => A;
+
+        // Only non generic IComparable is required to provide custom order behavior, but implementing IComparable<> too is customary.
+        public class ComplexParam : IComparable<ComplexParam>, IComparable
+        {
+            public ComplexParam(int value, string name)
+            {
+                Value = value;
+                Name = name;
+            }
+
+            public int Value { get; set; }
+
+            public string Name { get; set; }
+
+            public override string ToString() => Name;
+
+            public int CompareTo(ComplexParam other) => other == null ? 1 : Value.CompareTo(other.Value);
+
+            public int CompareTo(object obj) => obj is ComplexParam other ? CompareTo(other) : throw new ArgumentException();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Parameters/ParameterComparer.cs b/src/BenchmarkDotNet/Parameters/ParameterComparer.cs
index e5ae86d57b..a1363de073 100644
--- a/src/BenchmarkDotNet/Parameters/ParameterComparer.cs
+++ b/src/BenchmarkDotNet/Parameters/ParameterComparer.cs
@@ -7,16 +7,6 @@ internal class ParameterComparer : IComparer<ParameterInstances>
     {
         public static readonly ParameterComparer Instance = new ParameterComparer();
 
-        // We will only worry about common, basic types, i.e. int, long, double, etc
-        // (e.g. you can't write [Params(10.0m, 20.0m, 100.0m, 200.0m)], the compiler won't let you!)
-        private static readonly Comparer PrimitiveComparer = new Comparer().
-            Add((string x, string y) => string.CompareOrdinal(x, y)).
-            Add((int x, int y) => x.CompareTo(y)).
-            Add((long x, long y) => x.CompareTo(y)).
-            Add((short x, short y) => x.CompareTo(y)).
-            Add((float x, float y) => x.CompareTo(y)).
-            Add((double x, double y) => x.CompareTo(y));
-
         public int Compare(ParameterInstances x, ParameterInstances y)
         {
             if (x == null && y == null) return 0;
@@ -24,35 +14,25 @@ public int Compare(ParameterInstances x, ParameterInstances y)
             if (x == null) return -1;
             for (int i = 0; i < Math.Min(x.Count, y.Count); i++)
             {
-                int compareTo = PrimitiveComparer.CompareTo(x[i]?.Value, y[i]?.Value);
+                var compareTo = CompareValues(x[i]?.Value, y[i]?.Value);
                 if (compareTo != 0)
                     return compareTo;
             }
             return string.CompareOrdinal(x.DisplayInfo, y.DisplayInfo);
         }
 
-        private class Comparer
+        private int CompareValues(object x, object y)
         {
-            private readonly Dictionary<Type, Func<object, object, int>> comparers =
-                new Dictionary<Type, Func<object, object, int>>();
-
-            public Comparer Add<T>(Func<T, T, int> compareFunc)
-            {
-                comparers.Add(typeof(T), (x, y) => compareFunc((T)x, (T)y));
-                return this;
-            }
-
-            public int CompareTo(object x, object y)
+            // Detect IComparable implementations.
+            // This works for all primitive types in addition to user types that implement IComparable.
+            if (x != null && y != null && x.GetType() == y.GetType() &&
+                x is IComparable xComparable)
             {
-                return x != null && y != null && x.GetType() == y.GetType() && comparers.TryGetValue(GetComparisonType(x), out var comparer)
-                    ? comparer(x, y)
-                    : string.CompareOrdinal(x?.ToString(), y?.ToString());
+                return xComparable.CompareTo(y);
             }
 
-            private static Type GetComparisonType(object x) =>
-                x.GetType().IsEnum
-                ? x.GetType().GetEnumUnderlyingType()
-                : x.GetType();
+            // Anything else.
+            return string.CompareOrdinal(x?.ToString(), y?.ToString());
         }
     }
 }
\ No newline at end of file


Test patch:
diff --git a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs
index f67e6b3d30..0f01045b5c 100644
--- a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs
+++ b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs
@@ -122,5 +122,82 @@ public void AlphaNumericComparisionTest()
             Assert.Equal(1000, sortedData[2].Items[0].Value);
             Assert.Equal(2000, sortedData[3].Items[0].Value);
         }
+
+        [Fact]
+        public void IComparableComparisionTest()
+        {
+            var comparer = ParameterComparer.Instance;
+
+            var originalData = new[]
+            {
+                new ParameterInstances(new[]
+                {
+                    new ParameterInstance(sharedDefinition, new ComplexParameter(1, "first"), null)
+                }),
+                new ParameterInstances(new[]
+                {
+                    new ParameterInstance(sharedDefinition, new ComplexParameter(3, "third"), null)
+                }),
+                new ParameterInstances(new[]
+                {
+                    new ParameterInstance(sharedDefinition, new ComplexParameter(2, "second"), null)
+                }),
+                new ParameterInstances(new[]
+                {
+                    new ParameterInstance(sharedDefinition, new ComplexParameter(4, "fourth"), null)
+                })
+            };
+
+            var sortedData = originalData.OrderBy(d => d, comparer).ToArray();
+
+            // Check that we sort by numeric value, not string order!!
+            Assert.Equal(1, ((ComplexParameter)sortedData[0].Items[0].Value).Value);
+            Assert.Equal(2, ((ComplexParameter)sortedData[1].Items[0].Value).Value);
+            Assert.Equal(3, ((ComplexParameter)sortedData[2].Items[0].Value).Value);
+            Assert.Equal(4, ((ComplexParameter)sortedData[3].Items[0].Value).Value);
+        }
+
+        private class ComplexParameter : IComparable<ComplexParameter>, IComparable
+        {
+            public ComplexParameter(int value, string name)
+            {
+                Value = value;
+                Name = name;
+            }
+
+            public int Value { get; }
+
+            public string Name { get; }
+
+            public override string ToString()
+            {
+                return Name;
+            }
+
+            public int CompareTo(ComplexParameter other)
+            {
+                if (other == null)
+                {
+                    return 1;
+                }
+
+                return Value.CompareTo(other.Value);
+            }
+
+            public int CompareTo(object obj)
+            {
+                if (obj == null)
+                {
+                    return 1;
+                }
+
+                if (obj is not ComplexParameter other)
+                {
+                    throw new ArgumentException();
+                }
+
+                return CompareTo(other);
+            }
+        }
     }
 }
\ No newline at end of file