diff --git a/src/java.base/share/classes/java/lang/Math.java b/src/java.base/share/classes/java/lang/Math.java index 958073c7bd2..fd5c8bb0491 100644 --- a/src/java.base/share/classes/java/lang/Math.java +++ b/src/java.base/share/classes/java/lang/Math.java @@ -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. + *
+ * 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. + *
+ * 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. + *
+ * 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 diff --git a/src/java.base/share/classes/java/lang/StrictMath.java b/src/java.base/share/classes/java/lang/StrictMath.java index 6cec10e2f85..f08b016b629 100644 --- a/src/java.base/share/classes/java/lang/StrictMath.java +++ b/src/java.base/share/classes/java/lang/StrictMath.java @@ -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. + *
+ * 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. + *
+ * 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. + *
+ * 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 diff --git a/test/jdk/java/lang/Math/Clamp.java b/test/jdk/java/lang/Math/Clamp.java new file mode 100644 index 00000000000..5ddca508383 --- /dev/null +++ b/test/jdk/java/lang/Math/Clamp.java @@ -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; + } +}