Où puis-je trouver la fonction «clamp» dans .NET?

93

Je voudrais fixer une valeur xà une plage [a, b]:

x = (x < a) ? a : ((x > b) ? b : x);

C'est assez basique. Mais je ne vois pas de fonction "clamp" dans la bibliothèque de classes - du moins pas dans System.Math.

(Pour ceux qui ne savent pas "bloquer" une valeur, il faut s'assurer qu'elle se situe entre des valeurs maximales et minimales. Si elle est supérieure à la valeur max, elle est remplacée par la valeur max, etc.)

Danvil
la source
2
@Danvil: Il n'y a pas de "bibliothèque de classes C #". Vous voulez dire "Le .NET Framework".
John Saunders
1
Toujours rien à partir de C # 7.1?
joce
1
@JohnSaunders Je ne pense pas que ce soit strictement vrai stackoverflow.com/questions/807880/…
Adam Naylor
Si je demandais comment "limiter" une valeur, chaque programmeur anglophone du monde saurait immédiatement ce que je voulais dire. Très probablement, tous les programmeurs le sauraient. Après plus de 30 ans dans l'entreprise, j'ai dû aller découvrir ce que «pince» signifiait aujourd'hui. Semblable à «l'injection de dépendances» - la «paramétrage» est une chose tellement évidente que personne n'a jamais écrit un livre là-dessus.
Bob
@Bob Certains mots ont une signification historique bien définie. Clamp en fait partie. en.wikipedia.org/wiki/Clamping_(graphics) ou khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml ou docs.microsoft.com/en-us/windows/win32/direct3dhlsl/… "Limite "serait trompeur, surtout que" limite "a déjà un sens différent en maths.
kaalus

Réponses:

137

Vous pouvez écrire une méthode d'extension:

public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
{
    if (val.CompareTo(min) < 0) return min;
    else if(val.CompareTo(max) > 0) return max;
    else return val;
}

Les méthodes d'extension vont dans des classes statiques - puisqu'il s'agit d'une fonction de bas niveau, elle devrait probablement aller dans un espace de noms de base de votre projet. Vous pouvez ensuite utiliser la méthode dans n'importe quel fichier de code contenant une directive using pour l'espace de noms, par exemple

using Core.ExtensionMethods

int i = 4.Clamp(1, 3);

.NET Core 2.0

À partir de .NET Core 2.0, System.Mathune Clampméthode peut être utilisée à la place:

using System;

int i = Math.Clamp(4, 1, 3);
Lee
la source
1
Où pourrais-je mettre cela et appeler CompareTo plus lentement que comparer avec <(pour les types intégraux)?
Danvil
1
Dans une classe statique, et dans le framework .NET (pas sûr de mono, compact, etc.), le générique doit être recompilé pour le type, et CompareTo en ligne, donc pas de pénalité de performances.
Robert Fraser
1
@Frasier Sauf s'il s'agit d'un code ultra sensible aux performances, il est peu probable que vous obteniez des gains de performances significatifs en le faisant. L'avoir générique est probablement plus utile que d'économiser quelques microsecondes.
MgSam
4
La bonne chose à propos de la contrainte à la version générique de IComparableest qu'aucune boxe ne se produit. Cela devrait fonctionner très vite. N'oubliez pas qu'avec doubleet float, la CompareTométhode correspond à un ordre total où NaNest inférieur à toutes les autres valeurs, y compris NegativeInfinity. Ce n'est donc pas équivalent à l' <opérateur. Si vous avez utilisé <avec un type à virgule flottante, vous devrez également réfléchir à la façon de traiter NaN. Cela n'est pas pertinent pour les autres types numériques.
Jeppe Stig Nielsen
1
Vous devrez réfléchir à la manière de traiter NaNdans les deux cas. La version avec <et >produirait NaNet utiliserait NaNpour minou maxferait effectivement une pince unilatérale. Avec CompareToça reviendrait toujours NaNsi maxc'est NaN.
Herman
29

Utilisez simplement Math.Minet Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7samurai
la source
Cela signifie int a0 = x > a ? x : a; return a0 < b ? a0 : bque (bien que donne des résultats corrects) n'est pas exactement idéal.
M. Smith
12
et pourquoi est-ce que?
d7samurai
4
@ d7samurai Si nous savons que min <= max, Math.Min(Math.Max(x, min), max)entraîne une comparaison de plus que nécessaire si x <min.
Jim Balter
@JimBalter, en théorie, c'est vrai. Si vous regardez comment CompareTo () est généralement implémenté, la réponse acceptée peut prendre jusqu'à 6 comparaisons. Je ne sais pas si le compilateur est assez intelligent et intègre le CompareTo () et supprime les comparaisons superflues.
quinmars
1
C'est bon pour les cas où vous n'avez besoin de le faire qu'une seule fois, puis une toute nouvelle fonction pour cela ressemble à une exagération.
feos
26

Essayer:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Clito
la source
6
Pouah! Ces vilaines parenthèses redondantes! Si vous voulez être un génie maléfique avec les opérateurs ternaires doubles, faites-le au moins correctement et débarrassez-vous de ceux-ci! 😂
XenoRo
8
@XenoRo Ces parenthèses "redondantes" sont ce qui le rend lisible.
Plus clair
2
@Cleaner - 1) Si vous optez pour la lisibilité, les doubles ternaires seraient évités et les blocs IF seraient utilisés à la place. 2) Vous ne comprenez pas la blague, n'est-ce pas? xD
XenoRo
13

Il n'y en a pas, mais ce n'est pas trop difficile d'en faire un. J'en ai trouvé un ici: pince

C'est:

public static T Clamp<T>(T value, T max, T min)
    where T : System.IComparable<T> {
        T result = value;
        if (value.CompareTo(max) > 0)
            result = max;
        if (value.CompareTo(min) < 0)
            result = min;
        return result;
    }

Et il peut être utilisé comme:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Jeremy B.
la source
Cette solution est meilleure que celle acceptée. Aucune ambiguïté.
aggsol
6
@CodeClown Cette solution entraîne une comparaison inutile lorsque value> max, et l'ordre des arguments inversé invite (et garantit pratiquement) des bogues. Je ne sais pas quelle ambiguïté vous pensez être évitée.
Jim Balter
Par souci de cohérence avec l'implémentation héritée de Math.Clamp, il est recommandé de changer l'ordre des paramètres min / max:Clamp(T value, T min, T max)
josh poley
4

Je partage simplement la solution de Lee avec les problèmes et préoccupations des commentaires traités, dans la mesure du possible:

public static T Clamped<T>(this T value, T min, T max) where T : IComparable<T> {
    if (value == null) throw new ArgumentNullException(nameof(value), "is null.");
    if (min == null) throw new ArgumentNullException(nameof(min), "is null.");
    if (max == null) throw new ArgumentNullException(nameof(max), "is null.");
    //If min <= max, clamp
    if (min.CompareTo(max) <= 0) return value.CompareTo(min) < 0 ? min : value.CompareTo(max) > 0 ? max : value;
    //If min > max, clamp on swapped min and max
    return value.CompareTo(max) < 0 ? max : value.CompareTo(min) > 0 ? min : value;
}

Différences:

Limitations: pas de pinces unilatérales. Si maxest NaN, renvoie toujours NaN(voir le commentaire d'Herman ).

XenoRo
la source
Une autre limitation est nameofne fonctionne pas pour C # 5 ou inférieur.
RoLYroLLs
0

En utilisant les réponses précédentes, je l'ai condensé au code ci-dessous pour mes besoins. Cela vous permettra également de fixer un nombre uniquement par son min ou son max.

public static class IComparableExtensions
{
    public static T Clamped<T>(this T value, T min, T max) 
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value.ClampedMaximum(max);
    }

    public static T ClampedMinimum<T>(this T value, T min)
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value;
    }

    public static T ClampedMaximum<T>(this T value, T max)
        where T : IComparable<T>
    {
        return value.CompareTo(max) > 0 ? max : value;
    }
}
Bobby Speirs
la source
Pourquoi pas return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik
0

Le code ci-dessous prend en charge la spécification de limites dans n'importe quel ordre (c'est bound1 <= bound2-à- dire ou bound2 <= bound1). J'ai trouvé cela utile pour les valeurs de serrage calculées à partir d'équations linéaires ( y=mx+b) où la pente de la ligne peut être croissante ou décroissante.

Je sais: le code se compose de cinq opérateurs d'expression conditionnelle super-laids . Le fait est que cela fonctionne et les tests ci-dessous le prouvent. N'hésitez pas à ajouter des parenthèses strictement inutiles si vous le souhaitez.

Vous pouvez facilement créer d'autres surcharges pour d'autres types numériques et copier / coller les tests.

Attention: comparer des nombres à virgule flottante n'est pas simple. Ce code n'implémente pas les doublecomparaisons de manière robuste. Utilisez une bibliothèque de comparaison à virgule flottante pour remplacer les utilisations des opérateurs de comparaison.

public static class MathExtensions
{
    public static double Clamp(this double value, double bound1, double bound2)
    {
        return bound1 <= bound2 ? value <= bound1 ? bound1 : value >= bound2 ? bound2 : value : value <= bound2 ? bound2 : value >= bound1 ? bound1 : value;
    }
}

Tests xUnit / FluentAssertions:

public class MathExtensionsTests
{
    [Theory]
    [InlineData(0, 0, 0, 0)]
    [InlineData(0, 0, 2, 0)]
    [InlineData(-1, 0, 2, 0)]
    [InlineData(1, 0, 2, 1)]
    [InlineData(2, 0, 2, 2)]
    [InlineData(3, 0, 2, 2)]
    [InlineData(0, 2, 0, 0)]
    [InlineData(-1, 2, 0, 0)]
    [InlineData(1, 2, 0, 1)]
    [InlineData(2, 2, 0, 2)]
    [InlineData(3, 2, 0, 2)]
    public void MustClamp(double value, double bound1, double bound2, double expectedValue)
    {
        value.Clamp(bound1, bound2).Should().Be(expectedValue);
    }
}
NathanAldenSr
la source
0

Si je veux valider la plage d'un argument dans [min, max], j'utilise la classe pratique suivante:

public class RangeLimit<T> where T : IComparable<T>
{
    public T Min { get; }
    public T Max { get; }
    public RangeLimit(T min, T max)
    {
        if (min.CompareTo(max) > 0)
            throw new InvalidOperationException("invalid range");
        Min = min;
        Max = max;
    }

    public void Validate(T param)
    {
        if (param.CompareTo(Min) < 0 || param.CompareTo(Max) > 0)
            throw new InvalidOperationException("invalid argument");
    }

    public T Clamp(T param) => param.CompareTo(Min) < 0 ? Min : param.CompareTo(Max) > 0 ? Max : param;
}

La classe fonctionne pour tous les objets qui le sont IComparable. Je crée une instance avec une certaine plage:

RangeLimit<int> range = new RangeLimit<int>(0, 100);

Je valide soit un argument

range.Validate(value);

ou fixez l'argument à la plage:

var v = range.Validate(value);
Lapin76
la source