8301226: Add clamp() methods to java.lang.Math and to StrictMath

Reviewed-by: qamai, darcy
This commit is contained in:
Tagir F. Valeev 2023-02-14 05:39:07 +00:00
parent 13b1ebba27
commit 94e7cc8587
3 changed files with 500 additions and 1 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1994, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1994, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -2191,6 +2191,135 @@ public final class Math {
return (a <= b) ? a : b;
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned.
* <p>
* While the original value of type long may not fit into the int type,
* the bounds have the int type, so the result always fits the int type.
* This allows to use method to safely cast long value to int with
* saturation.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if {@code min > max}
*
* @since 21
*/
public static int clamp(long value, int min, int max) {
if (min > max) {
throw new IllegalArgumentException(min + " > " + max);
}
return (int) Math.min(max, Math.max(value, min));
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if {@code min > max}
*
* @since 21
*/
public static long clamp(long value, long min, long max) {
if (min > max) {
throw new IllegalArgumentException(min + " > " + max);
}
return Math.min(max, Math.max(value, min));
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned. If value is NaN, the result is also NaN.
* <p>
* Unlike the numerical comparison operators, this method considers
* negative zero to be strictly smaller than positive zero.
* E.g., {@code clamp(-0.0, 0.0, 1.0)} returns 0.0.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if either of {@code min} and {@code max}
* arguments is NaN, or {@code min > max}, or {@code min} is +0.0, and
* {@code max} is -0.0.
*
* @since 21
*/
public static double clamp(double value, double min, double max) {
// This unusual condition allows keeping only one branch
// on common path when min < max and neither of them is NaN.
// If min == max, we should additionally check for +0.0/-0.0 case,
// so we're still visiting the if statement.
if (!(min < max)) { // min greater than, equal to, or unordered with respect to max; NaN values are unordered
if (Double.isNaN(min)) {
throw new IllegalArgumentException("min is NaN");
}
if (Double.isNaN(max)) {
throw new IllegalArgumentException("max is NaN");
}
if (Double.compare(min, max) > 0) {
throw new IllegalArgumentException(min + " > " + max);
}
// Fall-through if min and max are exactly equal (or min = -0.0 and max = +0.0)
// and none of them is NaN
}
return Math.min(max, Math.max(value, min));
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned. If value is NaN, the result is also NaN.
* <p>
* Unlike the numerical comparison operators, this method considers
* negative zero to be strictly smaller than positive zero.
* E.g., {@code clamp(-0.0f, 0.0f, 1.0f)} returns 0.0f.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if either of {@code min} and {@code max}
* arguments is NaN, or {@code min > max}, or {@code min} is +0.0f, and
* {@code max} is -0.0f.
*
* @since 21
*/
public static float clamp(float value, float min, float max) {
// This unusual condition allows keeping only one branch
// on common path when min < max and neither of them is NaN.
// If min == max, we should additionally check for +0.0/-0.0 case,
// so we're still visiting the if statement.
if (!(min < max)) { // min greater than, equal to, or unordered with respect to max; NaN values are unordered
if (Float.isNaN(min)) {
throw new IllegalArgumentException("min is NaN");
}
if (Float.isNaN(max)) {
throw new IllegalArgumentException("max is NaN");
}
if (Float.compare(min, max) > 0) {
throw new IllegalArgumentException(min + " > " + max);
}
// Fall-through if min and max are exactly equal (or min = -0.0 and max = +0.0)
// and none of them is NaN
}
return Math.min(max, Math.max(value, min));
}
/**
* Returns the fused multiply add of the three arguments; that is,
* returns the exact product of the first two arguments summed

View File

@ -1759,6 +1759,95 @@ public final class StrictMath {
return Math.min(a, b);
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned.
* <p>
* While the original value of type long may not fit into the int type,
* the bounds have the int type, so the result always fits the int type.
* This allows to use method to safely cast long value to int with
* saturation.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if {@code min > max}
*
* @since 21
*/
public static int clamp(long value, int min, int max) {
return Math.clamp(value, min, max);
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if {@code min > max}
*
* @since 21
*/
public static long clamp(long value, long min, long max) {
return Math.clamp(value, min, max);
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned. If value is NaN, the result is also NaN.
* <p>
* Unlike the numerical comparison operators, this method considers
* negative zero to be strictly smaller than positive zero.
* E.g., {@code clamp(-0.0, 0.0, 1.0)} returns 0.0.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if either of {@code min} and {@code max}
* arguments is NaN, or {@code min > max}, or {@code min} is +0.0, and
* {@code max} is -0.0.
*
* @since 21
*/
public static double clamp(double value, double min, double max) {
return Math.clamp(value, min, max);
}
/**
* Clamps the value to fit between min and max. If the value is less
* than {@code min}, then {@code min} is returned. If the value is greater
* than {@code max}, then {@code max} is returned. Otherwise, the original
* value is returned. If value is NaN, the result is also NaN.
* <p>
* Unlike the numerical comparison operators, this method considers
* negative zero to be strictly smaller than positive zero.
* E.g., {@code clamp(-0.0f, 0.0f, 1.0f)} returns 0.0f.
*
* @param value value to clamp
* @param min minimal allowed value
* @param max maximal allowed value
* @return a clamped value that fits into {@code min..max} interval
* @throws IllegalArgumentException if either of {@code min} and {@code max}
* arguments is NaN, or {@code min > max}, or {@code min} is +0.0f, and
* {@code max} is -0.0f.
*
* @since 21
*/
public static float clamp(float value, float min, float max) {
return Math.clamp(value, min, max);
}
/**
* Returns the fused multiply add of the three arguments; that is,
* returns the exact product of the first two arguments summed

View File

@ -0,0 +1,281 @@
/*
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/* @test
@bug 8301226
@summary Add clamp() methods to java.lang.Math
*/
public class Clamp {
public static void main(String[] args) {
int failures = 0;
failures += testIntClamp();
failures += testLongClamp();
failures += testDoubleClamp();
failures += testFloatClamp();
if (failures > 0) {
System.err.println("Testing clamp incurred " + failures + " failures.");
throw new RuntimeException();
}
}
private static int testIntClamp() {
int failures = 0;
long[][] tests = {
// value, min, max, expected
{0, 1, 2, 1},
{0, 0, 2, 0},
{1, 0, 2, 1},
{2, 0, 2, 2},
{3, 0, 2, 2},
{0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0},
{Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE},
{Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE},
{Long.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE},
{Long.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE},
{0, 1, 1, 1},
{Long.MAX_VALUE, 1, 1, 1}
};
long[][] exceptionTests = {
// value, min, max
{1, 2, 0},
{1, Integer.MAX_VALUE, Integer.MIN_VALUE}
};
for (long[] test : tests) {
long value = test[0];
int min = Math.toIntExact(test[1]);
int max = Math.toIntExact(test[2]);
int expected = Math.toIntExact(test[3]);
failures += checkEquals("(int) Math.clamp(" + value + ", " + min + ", " + max + ")", Math.clamp(value, min, max), expected);
failures += checkEquals("(int) StrictMath.clamp(" + value + ", " + min + ", " + max + ")", StrictMath.clamp(value, min, max), expected);
}
for (long[] test : exceptionTests) {
long value = test[0];
int min = Math.toIntExact(test[1]);
int max = Math.toIntExact(test[2]);
failures += checkIllegalArgumentException("(int) Math.clamp(" + value + ", " + min + ", " + max + ")",
() -> Math.clamp(value, min, max));
failures += checkIllegalArgumentException("(int) StrictMath.clamp(" + value + ", " + min + ", " + max + ")",
() -> StrictMath.clamp(value, min, max));
}
return failures;
}
private static int testLongClamp() {
int failures = 0;
long[][] tests = {
// value, min, max, expected
{0L, 1L, 2L, 1L},
{0L, 0L, 2L, 0L},
{1L, 0L, 2L, 1L},
{2L, 0L, 2L, 2L},
{3L, 0L, 2L, 2L},
{0L, Long.MIN_VALUE, Long.MAX_VALUE, 0},
{Long.MIN_VALUE, Long.MIN_VALUE, Long.MAX_VALUE, Long.MIN_VALUE},
{Long.MAX_VALUE, Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE},
{0, 1, 1, 1},
{Long.MAX_VALUE, 1, 1, 1}
};
long[][] exceptionTests = {
// value, min, max
{1L, 2L, 0L},
{1, Long.MAX_VALUE, Long.MIN_VALUE}
};
for (long[] test : tests) {
long value = test[0];
long min = test[1];
long max = test[2];
long expected = test[3];
failures += checkEquals("(long) Math.clamp(" + value + ", " + min + ", " + max + ")", Math.clamp(value, min, max), expected);
failures += checkEquals("(long) StrictMath.clamp(" + value + ", " + min + ", " + max + ")", StrictMath.clamp(value, min, max), expected);
}
for (long[] test : exceptionTests) {
long value = test[0];
long min = test[1];
long max = test[2];
failures += checkIllegalArgumentException("(long) Math.clamp(" + value + ", " + min + ", " + max + ")",
() -> Math.clamp(value, min, max));
failures += checkIllegalArgumentException("(long) StrictMath.clamp(" + value + ", " + min + ", " + max + ")",
() -> StrictMath.clamp(value, min, max));
}
return failures;
}
private static int testDoubleClamp() {
int failures = 0;
double[][] tests = {
// value, min, max, expected
{-0.1, 0.0, 0.5, 0.0},
{-0.0, 0.0, 0.5, 0.0},
{0.0, 0.0, 0.5, 0.0},
{Double.MIN_VALUE, 0.0, 0.5, Double.MIN_VALUE},
{0.2, 0.0, 0.5, 0.2},
{Math.nextDown(0.5), 0.0, 0.5, Math.nextDown(0.5)},
{0.5, 0.0, 0.5, 0.5},
{Math.nextUp(0.5), 0.0, 0.5, 0.5},
{0.6, 0.0, 0.5, 0.5},
{Double.MAX_VALUE, 0.0, Double.POSITIVE_INFINITY, Double.MAX_VALUE},
{Double.POSITIVE_INFINITY, 0.0, Double.MAX_VALUE, Double.MAX_VALUE},
{-Double.MAX_VALUE, Double.NEGATIVE_INFINITY, 0.0, -Double.MAX_VALUE},
{Double.NEGATIVE_INFINITY, -Double.MAX_VALUE, 0.0, -Double.MAX_VALUE},
{-1.0, -0.0, 0.0, -0.0},
{-0.0, -0.0, 0.0, -0.0},
{0.0, -0.0, 0.0, 0.0},
{1.0, -0.0, 0.0, 0.0},
{-1.0, 0.0, 0.0, 0.0},
{-0.0, 0.0, 0.0, 0.0},
{0.0, 0.0, 0.0, 0.0},
{1.0, 0.0, 0.0, 0.0},
{-1.0, -0.0, -0.0, -0.0},
{-0.0, -0.0, -0.0, -0.0},
{0.0, -0.0, -0.0, -0.0},
{1.0, -0.0, -0.0, -0.0},
{Double.NaN, 0.0, 1.0, Double.NaN},
{Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN}
};
double[][] exceptionTests = {
// value, min, max
{0.0, Double.NaN, Double.NaN},
{0.0, 0.0, Double.NaN},
{0.0, Double.NaN, 0.0},
{Double.NaN, 1.0, 0.0},
{0.0, 0.0, -0.0},
{0.0, 1.0, -1.0}
};
for (double[] test : tests) {
double value = test[0];
double min = test[1];
double max = test[2];
double expected = test[3];
failures += checkEquals("(double) Math.clamp(" + value + ", " + min + ", " + max + ")", Math.clamp(value, min, max), expected);
failures += checkEquals("(double) StrictMath.clamp(" + value + ", " + min + ", " + max + ")", StrictMath.clamp(value, min, max), expected);
}
for (double[] test : exceptionTests) {
double value = test[0];
double min = test[1];
double max = test[2];
failures += checkIllegalArgumentException("(double) Math.clamp(" + value + ", " + min + ", " + max + ")",
() -> Math.clamp(value, min, max));
failures += checkIllegalArgumentException("(double) StrictMath.clamp(" + value + ", " + min + ", " + max + ")",
() -> StrictMath.clamp(value, min, max));
}
return failures;
}
private static int testFloatClamp() {
int failures = 0;
float[][] tests = {
// value, min, max, expected
{-0.1f, 0.0f, 0.5f, 0.0f},
{-0.0f, 0.0f, 0.5f, 0.0f},
{0.0f, 0.0f, 0.5f, 0.0f},
{Float.MIN_VALUE, 0.0f, 0.5f, Float.MIN_VALUE},
{0.2f, 0.0f, 0.5f, 0.2f},
{Math.nextDown(0.5f), 0.0f, 0.5f, Math.nextDown(0.5f)},
{0.5f, 0.0f, 0.5f, 0.5f},
{Math.nextUp(0.5f), 0.0f, 0.5f, 0.5f},
{0.6f, 0.0f, 0.5f, 0.5f},
{Float.MAX_VALUE, 0.0f, Float.POSITIVE_INFINITY, Float.MAX_VALUE},
{Float.POSITIVE_INFINITY, 0.0f, Float.MAX_VALUE, Float.MAX_VALUE},
{-Float.MAX_VALUE, Float.NEGATIVE_INFINITY, 0.0f, -Float.MAX_VALUE},
{Float.NEGATIVE_INFINITY, -Float.MAX_VALUE, 0.0f, -Float.MAX_VALUE},
{-1.0f, -0.0f, 0.0f, -0.0f},
{-0.0f, -0.0f, 0.0f, -0.0f},
{0.0f, -0.0f, 0.0f, 0.0f},
{1.0f, -0.0f, 0.0f, 0.0f},
{-1.0f, 0.0f, 0.0f, 0.0f},
{-0.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 0.0f, 0.0f},
{1.0f, 0.0f, 0.0f, 0.0f},
{-1.0f, -0.0f, -0.0f, -0.0f},
{-0.0f, -0.0f, -0.0f, -0.0f},
{0.0f, -0.0f, -0.0f, -0.0f},
{1.0f, -0.0f, -0.0f, -0.0f},
{Float.NaN, 0.0f, 1.0f, Float.NaN},
{Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NaN}
};
float[][] exceptionTests = {
// value, min, max
{0.0f, Float.NaN, Float.NaN},
{0.0f, 0.0f, Float.NaN},
{0.0f, Float.NaN, 0.0f},
{Float.NaN, 1.0f, 0.0f},
{0.0f, 0.0f, -0.0f},
{0.0f, 1.0f, -1.0f}
};
for (float[] test : tests) {
float value = test[0];
float min = test[1];
float max = test[2];
float expected = test[3];
failures += checkEquals("(float) Math.clamp(" + value + ", " + min + ", " + max + ")", Math.clamp(value, min, max), expected);
failures += checkEquals("(float) StrictMath.clamp(" + value + ", " + min + ", " + max + ")", StrictMath.clamp(value, min, max), expected);
}
for (float[] test : exceptionTests) {
float value = test[0];
float min = test[1];
float max = test[2];
failures += checkIllegalArgumentException("(float) Math.clamp(" + value + ", " + min + ", " + max + ")",
() -> Math.clamp(value, min, max));
failures += checkIllegalArgumentException("(float) StrictMath.clamp(" + value + ", " + min + ", " + max + ")",
() -> StrictMath.clamp(value, min, max));
}
return failures;
}
private static int checkIllegalArgumentException(String what, Runnable r) {
try {
r.run();
}
catch (IllegalArgumentException ex) {
return 0;
}
System.err.println(what+": missing expected exception");
return 1;
}
private static int checkEquals(String what, double actual, double expected) {
if (Double.doubleToLongBits(actual) != Double.doubleToLongBits(expected)) {
System.err.println(what + ": expected = " + expected + "; actual = " + actual);
return 1;
}
return 0;
}
private static int checkEquals(String what, long actual, long expected) {
if (actual != expected) {
System.err.println(what + ": expected = " + expected + "; actual = " + actual);
return 1;
}
return 0;
}
}