Onde posso encontrar a função “clamp” no .NET?

92

Eu gostaria de fixar um valor xem um intervalo [a, b]:

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

Isso é bastante básico. Mas não vejo uma função "clamp" na biblioteca de classes - pelo menos não em System.Math.

(Para quem não sabe, "fixar" um valor é certificar-se de que ele está entre alguns valores máximo e mínimo. Se for maior que o valor máximo, ele será substituído pelo máximo, etc.)

Danvil
fonte
2
@Danvil: Não existe uma "biblioteca de classes C #". Você quer dizer "The .NET Framework".
John Saunders,
1
Ainda nada em C # 7.1?
Joce
1
@JohnSaunders Não acredito que isso seja estritamente verdade stackoverflow.com/questions/807880/…
Adam Naylor
Se eu perguntasse como "limitar" um valor, cada programador que falasse inglês no mundo saberia imediatamente o que eu quis dizer. Provavelmente, todo programador saberia. Depois de mais de 30 anos no negócio, tive que descobrir o que "clamp" significava hoje. Semelhante a "injeção de dependência" - "parametrização" é uma coisa tão óbvia que ninguém jamais escreveu um livro sobre isso.
Bob
@Bob Algumas palavras têm um significado histórico bem definido. Clamp é um deles. 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 "seria enganoso, especialmente que" limite "já tem um significado diferente em matemática.
Kaalus

Respostas:

135

Você pode escrever um método de extensão:

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;
}

Os métodos de extensão vão em classes estáticas - uma vez que esta é uma função de baixo nível, provavelmente deve ir em algum namespace central em seu projeto. Você pode então usar o método em qualquer arquivo de código que contenha uma diretiva de uso para o namespace, por exemplo

using Core.ExtensionMethods

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

.NET Core 2.0

A partir do .NET Core 2.0 System.Mathagora existe um Clampmétodo que pode ser usado:

using System;

int i = Math.Clamp(4, 1, 3);
Lee
fonte
1
Onde eu colocaria isso e chamar CompareTo mais lento do que comparar com <(para tipos integrais)?
Danvil
1
Em uma classe estática e na estrutura .NET (não tenho certeza sobre mono, compact, etc.), o genérico deve ser recompilado para o tipo e o CompareTo embutido, para que não haja nenhuma penalidade de desempenho.
Robert Fraser
1
@Frasier A menos que este seja um código ultrassensível ao desempenho, é improvável que você obtenha ganhos significativos de desempenho ao fazê-lo. Provavelmente ser genérico é mais útil do que economizar alguns microssegundos.
MgSam
4
O bom de restringir a versão genérica do IComparableé que não ocorre boxe. Isso deve ser executado muito rápido. Lembre-se de que com doublee float, o CompareTométodo corresponde a uma ordem total onde NaNé menor que todos os outros valores, inclusive NegativeInfinity. Portanto, não é equivalente ao <operador. Se você usou <com um tipo de ponto flutuante, você teria que considerar como tratar NaNtambém. Isso não é relevante para outros tipos numéricos.
Jeppe Stig Nielsen
1
Você precisaria considerar como tratar NaNem ambos os casos. A versão com <e >faria de saída NaNe usando NaNpara minou maxseria efetivamente fazer um one-sided grampo. Com CompareToele voltaria sempre NaNse maxfor NaN.
Herman
29

Basta usar Math.Mine Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7samurai
fonte
Isso se traduz em int a0 = x > a ? x : a; return a0 < b ? a0 : bque (embora forneça resultados corretos) não é exatamente ideal.
Sr. Smith
12
e por que isto?
d7samurai
4
@ d7samurai Se sabemos que min <= max, Math.Min(Math.Max(x, min), max)resulta em mais uma comparação do que o necessário se x <min.
Jim Balter,
@JimBalter, em teoria isso é verdade. Se você observar como CompareTo () é normalmente implementado, a resposta aceita pode levar até 6 comparações. Não sei, porém, se o compilador é inteligente o suficiente e inline o CompareTo () e remove as comparações supérfluas.
quinmars
1
Isso é bom para os casos em que você só precisa fazer isso uma vez, então uma função totalmente nova parece um exagero.
feos
26

Experimentar:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Clitóris
fonte
6
Ugh! Esses parênteses feios e redundantes! Se você vai ser um gênio do mal com os operadores ternários duplos, pelo menos faça de maneira adequada e livre-se deles também! 😂
XenoRo
8
@XenoRo Esses parênteses "redundantes" são o que o torna legível.
Claro
2
@Cleaner - 1) Se você está buscando legibilidade, ternários duplos seriam evitados e blocos IF seriam usados ​​em seu lugar. 2) Você não entende a piada, não é? xD
XenoRo
13

Não existe um, mas não é muito difícil fazer um. Encontrei um aqui: braçadeira

Isto é:

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;
    }

E pode ser usado como:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Jeremy B.
fonte
Esta solução é melhor do que a aceita. Sem ambigüidade.
aggsol
6
@CodeClown Esta solução resulta em uma comparação desnecessária quando valor> max, e a ordem do argumento invertido convida (e virtualmente garante) bugs. Não sei que ambiguidade você acha que deve ser evitada.
Jim Balter,
Para consistência com a implementação legada do Math.Clamp, recomendo mudar a ordem dos parâmetros mínimo / máximo:Clamp(T value, T min, T max)
josh poley
4

Apenas compartilhando a solução de Lee com os problemas e preocupações dos comentários tratados, sempre que possível:

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;
}

Diferenças:

Limitações: Sem grampos unilaterais. Se maxfor NaN, sempre retorna NaN(veja o comentário de Herman ).

XenoRo
fonte
Outra limitação é nameofque não funciona para C # 5 ou inferior.
RoLYroLLs
0

Usando as respostas anteriores, eu condensava no código abaixo para minhas necessidades. Isso também permitirá que você fixe um número apenas por seu mínimo ou máximo.

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
fonte
Porque não return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik
0

O código a seguir suporta a especificação de limites em qualquer ordem (ou seja bound1 <= bound2, ou bound2 <= bound1). Eu achei isso útil para fixar valores calculados a partir de equações lineares (y=mx+b ), onde a inclinação da linha pode ser crescente ou decrescente.

Eu sei: o código consiste em cinco operadores de expressão condicional extremamente feios . A questão é que funciona e os testes abaixo provam isso. Sinta-se à vontade para adicionar parênteses estritamente desnecessários, se desejar.

Você pode criar facilmente outras sobrecargas para outros tipos numéricos e basicamente copiar / colar os testes.

Aviso: comparar números de ponto flutuante não é simples. Este código não implementa doublecomparações de forma robusta. Use uma biblioteca de comparação de ponto flutuante para substituir os usos de operadores de comparação.

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;
    }
}

Testes 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
fonte
0

Se eu quiser validar o intervalo de um argumento em [min, max], uso a seguinte classe útil:

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;
}

A classe funciona para todos os objetos que são IComparable. Eu crio uma instância com um certo intervalo:

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

Eu posso validar um argumento

range.Validate(value);

ou fixe o argumento no intervalo:

var v = range.Validate(value);
Rabbid76
fonte