Qual é a maneira mais elegante de limitar um número a um segmento?

127

Digamos x, ae bsão números. Eu preciso limitar xos limites do segmento [a, b].

Eu sei escrever Math.max(a, Math.min(x, b)), mas não acho que seja muito fácil de ler. Alguém tem uma maneira inteligente de escrever isso de uma maneira mais legível?

Samuel Rossille
fonte

Respostas:

197

A maneira como você faz isso é bastante padrão. Você pode definir uma clampfunção de utilitário :

/**
 * Returns a number whose value is limited to the given range.
 *
 * Example: limit the output of this computation to between 0 and 255
 * (x * 255).clamp(0, 255)
 *
 * @param {Number} min The lower boundary of the output range
 * @param {Number} max The upper boundary of the output range
 * @returns A number in the range [min, max]
 * @type Number
 */
Number.prototype.clamp = function(min, max) {
  return Math.min(Math.max(this, min), max);
};

(Embora a extensão de built-ins de idiomas geralmente seja mal vista)

Otto Allmendinger
fonte
OOps copiar / colar o meu erro de digitação (trocado máximo e mínimo)
Samuel Rossille
5
Não, eu peguei no site. A ordem de aninhamento / execução de Math.mine Math.maxé irrelevante, desde que o parâmetro de Math.miné maxe o parâmetro de Math.maxseja min.
Otto Allmendinger
23
A extensão de protótipos nativos ( Number.prototypeneste caso) é desaconselhada por vários motivos. Consulte "Má prática: extensão de protótipos nativos" em developer.mozilla.org/en-US/docs/Web/JavaScript/…
broofa
68

uma abordagem menos orientada à "matemática", mas também deve funcionar, dessa forma, o teste </> é exposto (talvez mais compreensível que o minimax), mas realmente depende do que você quer dizer com "legível"

function clamp(num, min, max) {
  return num <= min ? min : num >= max ? max : num;
}
dweeves
fonte
4
É melhor substituir <e> por <= e> = neste idioma, para que haja potencialmente menos uma comparação feita. Com essa correção, esta é definitivamente a minha resposta preferida: o máximo de minutos é confuso e propenso a erros.
Don escotilha
Esta é a melhor resposta. Muito mais rápido, muito mais simples, muito mais legível.
Tristan
5
Não é mais rápido. Math.min / solução max é mais rápido jsperf.com/math-clamp/9
Manuel Di Iorio
1
@ManuelDiIorio huh? na versão atual do Chrome, o ternário simples é duas vezes mais rápido que Math.min / max - e se você remover a sobrecarga da Math.random()chamada muito mais cara , essa abordagem simples é 10 vezes mais rápida .
mindplay.dk
48

Atualização para o ECMAScript 2017:

Math.clamp(x, lower, upper)

Mas observe que, atualmente, é uma proposta do Estágio 1 . Até que ele seja amplamente suportado, você pode usar um polyfill .

Tomas Nikodym
fonte
4
Se você deseja acompanhar esta proposta e outras, consulte: github.com/tc39/proposals
gfullam
5
Não encontrei essa proposta no repositório tc39 / propostas
Colin D
Para quem procura, a proposta está listada nas propostas do Estágio 1 como "Extensões matemáticas". A proposta real está aqui: github.com/rwaldron/proposal-math-extensions
WickyNilliams
14
Math.clip = function(number, min, max) {
  return Math.max(min, Math.min(number, max));
}
CAFxX
fonte
2
Esta solução é realmente muito mais rápida, embora eu considere estender o prototypemuito elegante quando se trata de realmente usar a função.
MaxArt
11

Isso não quer ser uma resposta "apenas use uma biblioteca", mas caso você esteja usando o Lodash, você pode usar .clamp:

_.clamp(yourInput, lowerBound, upperBound);

De modo a:

_.clamp(22, -10, 10); // => 10

Aqui está sua implementação, extraída da fonte Lodash :

/**
 * The base implementation of `_.clamp` which doesn't coerce arguments.
 *
 * @private
 * @param {number} number The number to clamp.
 * @param {number} [lower] The lower bound.
 * @param {number} upper The upper bound.
 * @returns {number} Returns the clamped number.
 */
function baseClamp(number, lower, upper) {
  if (number === number) {
    if (upper !== undefined) {
      number = number <= upper ? number : upper;
    }
    if (lower !== undefined) {
      number = number >= lower ? number : lower;
    }
  }
  return number;
}

Além disso, vale a pena notar que o Lodash disponibiliza métodos únicos como módulos autônomos; portanto, caso você precise apenas desse método, basta instalá-lo sem o restante da biblioteca:

npm i --save lodash.clamp
Nobita
fonte
6

Se você conseguir usar as funções de seta es6, também poderá usar uma abordagem de aplicativo parcial:

const clamp = (min, max) => (value) =>
    value < min ? min : value > max ? max : value;

clamp(2, 9)(8); // 8
clamp(2, 9)(1); // 2
clamp(2, 9)(10); // 9

or

const clamp2to9 = clamp(2, 9);
clamp2to9(8); // 8
clamp2to9(1); // 2
clamp2to9(10); // 9
bingles
fonte
A criação de uma função toda vez que você deseja fixar um número parece muito ineficiente
George
1
Provavelmente depende do seu caso de uso. Esta é apenas uma função de fábrica para criar o grampo com uma faixa definida. Você pode criar uma vez e reutilizar em todo o aplicativo. não precisa ser chamado várias vezes. Também pode não ser a ferramenta para todos os casos de uso em que você não precisa bloquear em um intervalo definido.
bingles 20/04
@ George, se você não precisar manter o intervalo, basta remover a função de seta interna e mover o parâmetro de valor para a função de seta externa
Alisson Nunes
6

Se você não deseja definir nenhuma função, escrevê-la como Math.min(Math.max(x, a), b)essa não é ruim.

Ricardo
fonte
0

No espírito da sensualidade das flechas, você pode criar um micro grampo / restrição / portão / & c. função usando parâmetros de descanso

var clamp = (...v) => v.sort((a,b) => a-b)[1];

Depois é só passar em três valores

clamp(100,-3,someVar);

Ou seja, novamente, se por sexy, você quer dizer 'curto'

stoke motor
fonte
você não deve usar matrizes e classificação por um clamp, perf vai tomar uma batida grave se você tem lógica complexa (mesmo que seja "sexy")
Andrew McOlash
0

Isso expande a opção ternária para if / else que minified é equivalente à opção ternária, mas não sacrifica a legibilidade.

const clamp = (value, min, max) => {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

Minimize para 35b (ou 43b se estiver usando function):

const clamp=(c,a,l)=>c<a?a:c>l?l:c;

Além disso, dependendo de quais ferramentas de perf ou navegador você usa, você obtém vários resultados, se a implementação baseada em matemática ou a implementação ternária é mais rápida. No caso de aproximadamente o mesmo desempenho, eu optaria pela legibilidade.

Michael Stramel
fonte
-5

Meu favorito:

[min,x,max].sort()[1]
user947856
fonte
12
Voto negativo porque não funciona. [1,5,19].sort()[1]retorna 19. Poderia ser corrigido assim: [min,x,max].sort(function(a, b) { return a - b; })[1]mas isso não é mais sexy. Além de ser muito usado, pode ser um problema de desempenho criar uma nova matriz apenas para comparar três números.
kayahr
5
Dar +1 de qualquer maneira, porque sexy é o que conta.
Don escotilha
3
IMHO isso é muito mais legível do que qualquer uma das outras opções, especialmente com funções de seta:[min, x, max].sort((a,b) => a-b)[1]
Zaius
4
A razão pela qual isso nem sempre funciona é porque o método de classificação não compara valores numéricos corretamente. (Trata-los como cordas)
John Leuenhagen
Acho que não há nada mais sexy do que uma função com nome apropriado que faz o trabalho de forma eficiente e de uma maneira obviamente correta. Mas isso pode ser uma questão de gosto.
BallpointBen