Browse Source

Merge pull request #1134 from akiyamaneko/factorial_improved

优化阶乘相关的逻辑
Golden Looly 5 years ago
parent
commit
d775ce9cca

+ 51 - 21
hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java

@@ -44,6 +44,14 @@ public class NumberUtil {
 	private static final int DEFAUT_DIV_SCALE = 10;
 
 	/**
+         * 0-20对应的阶乘,超过20的阶乘会超过Long.MAX_VALUE
+         */
+        private static final long[] FACTORIALS = new long[]{
+            1L, 1L, 2L, 6L, 24L, 120L, 720L, 5040L, 40320L, 362880L, 3628800L, 39916800L, 479001600L, 6227020800L,
+            87178291200L, 1307674368000L, 20922789888000L, 355687428096000L, 6402373705728000L, 121645100408832000L,
+            2432902008176640000L};
+	
+	/**
 	 * 提供精确的加法运算
 	 *
 	 * @param v1 被加数
@@ -1422,28 +1430,50 @@ public class NumberUtil {
 	 * @return 结果
 	 * @since 4.1.0
 	 */
-	public static long factorial(long start, long end) {
-		if (0L == start || start == end) {
-			return 1L;
-		}
-		if (start < end) {
-			return 0L;
-		}
-		return start * factorial(start - 1, end);
-	}
-
+        public static long factorial(long start, long end) {
+                // 负数没有阶乘
+                if(start < 0 || end < 0) {
+                        throw new IllegalArgumentException(String.format("Factorial start and end both must be >= 0, " +
+                            "but got start=%d, end=%d", start, end));
+                }
+                if (0L == start || start == end) {
+                        return 1L;
+                }
+                if (start < end) {
+                        return 0L;
+                }
+                return factorialMultiplyAndCheck(start, factorial(start - 1, end));
+        }
+	
 	/**
-	 * 计算阶乘
-	 * <p>
-	 * n! = n * (n-1) * ... * 2 * 1
-	 * </p>
-	 *
-	 * @param n 阶乘起始
-	 * @return 结果
-	 */
-	public static long factorial(long n) {
-		return factorial(n, 1);
-	}
+         * 计算范围阶乘中校验中间的计算是否存在溢出,factorial提前做了负数和0的校验,因此这里没有校验数字的正负
+         * @param a 乘数
+         * @param b 被乘数
+         * @return 如果 a * b的结果没有溢出直接返回,否则抛出异常
+         */
+        private static long factorialMultiplyAndCheck(long a, long b) {
+                if (a <= Long.MAX_VALUE / b) {
+                        return a * b;
+                }
+                throw new IllegalArgumentException(String.format("Overflow in multiplication: {%d} * {%d}", a, b));
+        }
+
+        /**
+         * 计算阶乘
+         * <p>
+         * n! = n * (n-1) * ... * 2 * 1
+         * </p>
+         *
+         * @param n 阶乘起始
+         * @return 结果
+         */
+        public static long factorial(long n) {
+                if (n < 0 || n > 20) {
+                        throw new IllegalArgumentException(String.format("Factorial must have n >= 0 and n <= 20 for n!, " +
+                            "but got n = %d", n));
+                }
+                return FACTORIALS[(int) n];
+        }
 
 	/**
 	 * 平方根算法<br>

+ 272 - 267
hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java

@@ -1,267 +1,272 @@
-package cn.hutool.core.util;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-
-/**
- * {@link NumberUtil} 单元测试类
- * 
- * @author Looly
- *
- */
-public class NumberUtilTest {
-	
-	@Test
-	public void addTest() {
-		Float a = 3.15f;
-		Double b = 4.22;
-		double result = NumberUtil.add(a, b).doubleValue();
-		Assert.assertEquals(7.37, result, 2);
-	}
-	
-	@Test
-	public void addTest2() {
-		double a = 3.15f;
-		double b = 4.22;
-		double result = NumberUtil.add(a, b);
-		Assert.assertEquals(7.37, result, 2);
-	}
-	
-	@Test
-	public void addTest3() {
-		float a = 3.15f;
-		double b = 4.22;
-		double result = NumberUtil.add(a, b, a, b).doubleValue();
-		Assert.assertEquals(14.74, result, 2);
-	}
-	
-	@Test
-	public void addTest4() {
-		BigDecimal result = NumberUtil.add(new BigDecimal("133"), new BigDecimal("331"));
-		Assert.assertEquals(new BigDecimal("464"), result);
-	}
-	
-	@Test
-	public void isIntegerTest() {
-		Assert.assertTrue(NumberUtil.isInteger("-12"));
-		Assert.assertTrue(NumberUtil.isInteger("256"));
-		Assert.assertTrue(NumberUtil.isInteger("0256"));
-		Assert.assertTrue(NumberUtil.isInteger("0"));
-		Assert.assertFalse(NumberUtil.isInteger("23.4"));
-	}
-	
-	@Test
-	public void isLongTest() {
-		Assert.assertTrue(NumberUtil.isLong("-12"));
-		Assert.assertTrue(NumberUtil.isLong("256"));
-		Assert.assertTrue(NumberUtil.isLong("0256"));
-		Assert.assertTrue(NumberUtil.isLong("0"));
-		Assert.assertFalse(NumberUtil.isLong("23.4"));
-	}
-	
-	@Test
-	public void isNumberTest() {
-		Assert.assertTrue(NumberUtil.isNumber("28.55"));
-		Assert.assertTrue(NumberUtil.isNumber("0"));
-		Assert.assertTrue(NumberUtil.isNumber("+100.10"));
-		Assert.assertTrue(NumberUtil.isNumber("-22.022"));
-		Assert.assertTrue(NumberUtil.isNumber("0X22"));
-	}
-
-	@Test
-	public void divTest() {
-		double result = NumberUtil.div(0, 1);
-		Assert.assertEquals(0.0, result, 0);
-	}
-	
-	@Test
-	public void roundTest() {
-
-		// 四舍
-		String round1 = NumberUtil.roundStr(2.674, 2);
-		String round2 = NumberUtil.roundStr("2.674", 2);
-		Assert.assertEquals("2.67", round1);
-		Assert.assertEquals("2.67", round2);
-
-		// 五入
-		String round3 = NumberUtil.roundStr(2.675, 2);
-		String round4 = NumberUtil.roundStr("2.675", 2);
-		Assert.assertEquals("2.68", round3);
-		Assert.assertEquals("2.68", round4);
-		
-		// 四舍六入五成双
-		String round31 = NumberUtil.roundStr(4.245, 2, RoundingMode.HALF_EVEN);
-		String round41 = NumberUtil.roundStr("4.2451", 2, RoundingMode.HALF_EVEN);
-		Assert.assertEquals("4.24", round31);
-		Assert.assertEquals("4.25", round41);
-
-		// 补0
-		String round5 = NumberUtil.roundStr(2.6005, 2);
-		String round6 = NumberUtil.roundStr("2.6005", 2);
-		Assert.assertEquals("2.60", round5);
-		Assert.assertEquals("2.60", round6);
-		
-		// 补0
-		String round7 = NumberUtil.roundStr(2.600, 2);
-		String round8 = NumberUtil.roundStr("2.600", 2);
-		Assert.assertEquals("2.60", round7);
-		Assert.assertEquals("2.60", round8);
-	}
-
-	@Test
-	public void roundStrTest() {
-		String roundStr = NumberUtil.roundStr(2.647, 2);
-		Assert.assertEquals(roundStr, "2.65");
-	}
-	
-	@Test
-	public void roundHalfEvenTest() {
-		String roundStr = NumberUtil.roundHalfEven(4.245, 2).toString();
-		Assert.assertEquals(roundStr, "4.24");
-		roundStr = NumberUtil.roundHalfEven(4.2450, 2).toString();
-		Assert.assertEquals(roundStr, "4.24");
-		roundStr = NumberUtil.roundHalfEven(4.2451, 2).toString();
-		Assert.assertEquals(roundStr, "4.25");
-		roundStr = NumberUtil.roundHalfEven(4.2250, 2).toString();
-		Assert.assertEquals(roundStr, "4.22");
-		
-		roundStr = NumberUtil.roundHalfEven(1.2050, 2).toString();
-		Assert.assertEquals(roundStr, "1.20");
-		roundStr = NumberUtil.roundHalfEven(1.2150, 2).toString();
-		Assert.assertEquals(roundStr, "1.22");
-		roundStr = NumberUtil.roundHalfEven(1.2250, 2).toString();
-		Assert.assertEquals(roundStr, "1.22");
-		roundStr = NumberUtil.roundHalfEven(1.2350, 2).toString();
-		Assert.assertEquals(roundStr, "1.24");
-		roundStr = NumberUtil.roundHalfEven(1.2450, 2).toString();
-		Assert.assertEquals(roundStr, "1.24");
-		roundStr = NumberUtil.roundHalfEven(1.2550, 2).toString();
-		Assert.assertEquals(roundStr, "1.26");
-		roundStr = NumberUtil.roundHalfEven(1.2650, 2).toString();
-		Assert.assertEquals(roundStr, "1.26");
-		roundStr = NumberUtil.roundHalfEven(1.2750, 2).toString();
-		Assert.assertEquals(roundStr, "1.28");
-		roundStr = NumberUtil.roundHalfEven(1.2850, 2).toString();
-		Assert.assertEquals(roundStr, "1.28");
-		roundStr = NumberUtil.roundHalfEven(1.2950, 2).toString();
-		Assert.assertEquals(roundStr, "1.30");
-	}
-
-	@Test
-	public void decimalFormatTest() {
-		long c = 299792458;// 光速
-
-		String format = NumberUtil.decimalFormat(",###", c);
-		Assert.assertEquals("299,792,458", format);
-	}
-	
-	@Test
-	public void decimalFormatMoneyTest() {
-		double c = 299792400.543534534;
-		
-		String format = NumberUtil.decimalFormatMoney(c);
-		Assert.assertEquals("299,792,400.54", format);
-		
-		double value = 0.5;
-		String money = NumberUtil.decimalFormatMoney(value);
-		Assert.assertEquals("0.50", money);
-	}
-
-	@Test
-	public void equalsTest() {
-		Assert.assertTrue(NumberUtil.equals(new BigDecimal("0.00"), BigDecimal.ZERO));
-	}
-	
-	@Test
-	public void formatPercentTest() {
-		String str = NumberUtil.formatPercent(0.33543545, 2);
-		Assert.assertEquals("33.54%", str);
-	}
-	
-	@Test
-	public void toBigDecimalTest() {
-		double a = 3.14;
-				
-		BigDecimal bigDecimal = NumberUtil.toBigDecimal(a);
-		Assert.assertEquals("3.14", bigDecimal.toString());
-	}
-
-	@Test
-	public void maxTest() {
-		int max = NumberUtil.max(5,4,3,6,1);
-		Assert.assertEquals(6, max);
-	}
-
-	@Test
-	public void minTest() {
-		int min = NumberUtil.min(5,4,3,6,1);
-		Assert.assertEquals(1, min);
-	}
-	
-	@Test
-	public void parseIntTest() {
-		int v1 = NumberUtil.parseInt("0xFF");
-		Assert.assertEquals(255, v1);
-		int v2 = NumberUtil.parseInt("010");
-		Assert.assertEquals(10, v2);
-		int v3 = NumberUtil.parseInt("10");
-		Assert.assertEquals(10, v3);
-		int v4 = NumberUtil.parseInt("   ");
-		Assert.assertEquals(0, v4);
-		int v5 = NumberUtil.parseInt("10F");
-		Assert.assertEquals(10, v5);
-		int v6 = NumberUtil.parseInt("22.4D");
-		Assert.assertEquals(22, v6);
-
-		int v7 = NumberUtil.parseInt("0");
-		Assert.assertEquals(0, v7);
-	}
-	
-	@Test
-	public void parseLongTest() {
-		long v1 = NumberUtil.parseLong("0xFF");
-		Assert.assertEquals(255L, v1);
-		long v2 = NumberUtil.parseLong("010");
-		Assert.assertEquals(10L, v2);
-		long v3 = NumberUtil.parseLong("10");
-		Assert.assertEquals(10L, v3);
-		long v4 = NumberUtil.parseLong("   ");
-		Assert.assertEquals(0L, v4);
-		long v5 = NumberUtil.parseLong("10F");
-		Assert.assertEquals(10L, v5);
-		long v6 = NumberUtil.parseLong("22.4D");
-		Assert.assertEquals(22L, v6);
-	}
-
-	@Test
-	public void factorialTest(){
-		long factorial = NumberUtil.factorial(0);
-		Assert.assertEquals(1, factorial);
-
-		factorial = NumberUtil.factorial(5, 0);
-		Assert.assertEquals(120, factorial);
-		factorial = NumberUtil.factorial(5, 1);
-		Assert.assertEquals(120, factorial);
-
-		Assert.assertEquals(5, NumberUtil.factorial(5, 4));
-	}
-
-	@Test
-	public void mulTest(){
-		final BigDecimal mul = NumberUtil.mul(new BigDecimal("10"), null);
-		Assert.assertEquals(BigDecimal.ZERO, mul);
-	}
-	
-	
-	@Test
-	public void isPowerOfTwoTest() {
-		Assert.assertEquals(false, NumberUtil.isPowerOfTwo(-1));
-		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(16));
-		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(65536));
-		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(1));
-		Assert.assertEquals(false, NumberUtil.isPowerOfTwo(17));
-	}
-}
+package cn.hutool.core.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * {@link NumberUtil} 单元测试类
+ * 
+ * @author Looly
+ *
+ */
+public class NumberUtilTest {
+	
+	@Test
+	public void addTest() {
+		Float a = 3.15f;
+		Double b = 4.22;
+		double result = NumberUtil.add(a, b).doubleValue();
+		Assert.assertEquals(7.37, result, 2);
+	}
+	
+	@Test
+	public void addTest2() {
+		double a = 3.15f;
+		double b = 4.22;
+		double result = NumberUtil.add(a, b);
+		Assert.assertEquals(7.37, result, 2);
+	}
+	
+	@Test
+	public void addTest3() {
+		float a = 3.15f;
+		double b = 4.22;
+		double result = NumberUtil.add(a, b, a, b).doubleValue();
+		Assert.assertEquals(14.74, result, 2);
+	}
+	
+	@Test
+	public void addTest4() {
+		BigDecimal result = NumberUtil.add(new BigDecimal("133"), new BigDecimal("331"));
+		Assert.assertEquals(new BigDecimal("464"), result);
+	}
+	
+	@Test
+	public void isIntegerTest() {
+		Assert.assertTrue(NumberUtil.isInteger("-12"));
+		Assert.assertTrue(NumberUtil.isInteger("256"));
+		Assert.assertTrue(NumberUtil.isInteger("0256"));
+		Assert.assertTrue(NumberUtil.isInteger("0"));
+		Assert.assertFalse(NumberUtil.isInteger("23.4"));
+	}
+	
+	@Test
+	public void isLongTest() {
+		Assert.assertTrue(NumberUtil.isLong("-12"));
+		Assert.assertTrue(NumberUtil.isLong("256"));
+		Assert.assertTrue(NumberUtil.isLong("0256"));
+		Assert.assertTrue(NumberUtil.isLong("0"));
+		Assert.assertFalse(NumberUtil.isLong("23.4"));
+	}
+	
+	@Test
+	public void isNumberTest() {
+		Assert.assertTrue(NumberUtil.isNumber("28.55"));
+		Assert.assertTrue(NumberUtil.isNumber("0"));
+		Assert.assertTrue(NumberUtil.isNumber("+100.10"));
+		Assert.assertTrue(NumberUtil.isNumber("-22.022"));
+		Assert.assertTrue(NumberUtil.isNumber("0X22"));
+	}
+
+	@Test
+	public void divTest() {
+		double result = NumberUtil.div(0, 1);
+		Assert.assertEquals(0.0, result, 0);
+	}
+	
+	@Test
+	public void roundTest() {
+
+		// 四舍
+		String round1 = NumberUtil.roundStr(2.674, 2);
+		String round2 = NumberUtil.roundStr("2.674", 2);
+		Assert.assertEquals("2.67", round1);
+		Assert.assertEquals("2.67", round2);
+
+		// 五入
+		String round3 = NumberUtil.roundStr(2.675, 2);
+		String round4 = NumberUtil.roundStr("2.675", 2);
+		Assert.assertEquals("2.68", round3);
+		Assert.assertEquals("2.68", round4);
+		
+		// 四舍六入五成双
+		String round31 = NumberUtil.roundStr(4.245, 2, RoundingMode.HALF_EVEN);
+		String round41 = NumberUtil.roundStr("4.2451", 2, RoundingMode.HALF_EVEN);
+		Assert.assertEquals("4.24", round31);
+		Assert.assertEquals("4.25", round41);
+
+		// 补0
+		String round5 = NumberUtil.roundStr(2.6005, 2);
+		String round6 = NumberUtil.roundStr("2.6005", 2);
+		Assert.assertEquals("2.60", round5);
+		Assert.assertEquals("2.60", round6);
+		
+		// 补0
+		String round7 = NumberUtil.roundStr(2.600, 2);
+		String round8 = NumberUtil.roundStr("2.600", 2);
+		Assert.assertEquals("2.60", round7);
+		Assert.assertEquals("2.60", round8);
+	}
+
+	@Test
+	public void roundStrTest() {
+		String roundStr = NumberUtil.roundStr(2.647, 2);
+		Assert.assertEquals(roundStr, "2.65");
+	}
+	
+	@Test
+	public void roundHalfEvenTest() {
+		String roundStr = NumberUtil.roundHalfEven(4.245, 2).toString();
+		Assert.assertEquals(roundStr, "4.24");
+		roundStr = NumberUtil.roundHalfEven(4.2450, 2).toString();
+		Assert.assertEquals(roundStr, "4.24");
+		roundStr = NumberUtil.roundHalfEven(4.2451, 2).toString();
+		Assert.assertEquals(roundStr, "4.25");
+		roundStr = NumberUtil.roundHalfEven(4.2250, 2).toString();
+		Assert.assertEquals(roundStr, "4.22");
+		
+		roundStr = NumberUtil.roundHalfEven(1.2050, 2).toString();
+		Assert.assertEquals(roundStr, "1.20");
+		roundStr = NumberUtil.roundHalfEven(1.2150, 2).toString();
+		Assert.assertEquals(roundStr, "1.22");
+		roundStr = NumberUtil.roundHalfEven(1.2250, 2).toString();
+		Assert.assertEquals(roundStr, "1.22");
+		roundStr = NumberUtil.roundHalfEven(1.2350, 2).toString();
+		Assert.assertEquals(roundStr, "1.24");
+		roundStr = NumberUtil.roundHalfEven(1.2450, 2).toString();
+		Assert.assertEquals(roundStr, "1.24");
+		roundStr = NumberUtil.roundHalfEven(1.2550, 2).toString();
+		Assert.assertEquals(roundStr, "1.26");
+		roundStr = NumberUtil.roundHalfEven(1.2650, 2).toString();
+		Assert.assertEquals(roundStr, "1.26");
+		roundStr = NumberUtil.roundHalfEven(1.2750, 2).toString();
+		Assert.assertEquals(roundStr, "1.28");
+		roundStr = NumberUtil.roundHalfEven(1.2850, 2).toString();
+		Assert.assertEquals(roundStr, "1.28");
+		roundStr = NumberUtil.roundHalfEven(1.2950, 2).toString();
+		Assert.assertEquals(roundStr, "1.30");
+	}
+
+	@Test
+	public void decimalFormatTest() {
+		long c = 299792458;// 光速
+
+		String format = NumberUtil.decimalFormat(",###", c);
+		Assert.assertEquals("299,792,458", format);
+	}
+	
+	@Test
+	public void decimalFormatMoneyTest() {
+		double c = 299792400.543534534;
+		
+		String format = NumberUtil.decimalFormatMoney(c);
+		Assert.assertEquals("299,792,400.54", format);
+		
+		double value = 0.5;
+		String money = NumberUtil.decimalFormatMoney(value);
+		Assert.assertEquals("0.50", money);
+	}
+
+	@Test
+	public void equalsTest() {
+		Assert.assertTrue(NumberUtil.equals(new BigDecimal("0.00"), BigDecimal.ZERO));
+	}
+	
+	@Test
+	public void formatPercentTest() {
+		String str = NumberUtil.formatPercent(0.33543545, 2);
+		Assert.assertEquals("33.54%", str);
+	}
+	
+	@Test
+	public void toBigDecimalTest() {
+		double a = 3.14;
+				
+		BigDecimal bigDecimal = NumberUtil.toBigDecimal(a);
+		Assert.assertEquals("3.14", bigDecimal.toString());
+	}
+
+	@Test
+	public void maxTest() {
+		int max = NumberUtil.max(5,4,3,6,1);
+		Assert.assertEquals(6, max);
+	}
+
+	@Test
+	public void minTest() {
+		int min = NumberUtil.min(5,4,3,6,1);
+		Assert.assertEquals(1, min);
+	}
+	
+	@Test
+	public void parseIntTest() {
+		int v1 = NumberUtil.parseInt("0xFF");
+		Assert.assertEquals(255, v1);
+		int v2 = NumberUtil.parseInt("010");
+		Assert.assertEquals(10, v2);
+		int v3 = NumberUtil.parseInt("10");
+		Assert.assertEquals(10, v3);
+		int v4 = NumberUtil.parseInt("   ");
+		Assert.assertEquals(0, v4);
+		int v5 = NumberUtil.parseInt("10F");
+		Assert.assertEquals(10, v5);
+		int v6 = NumberUtil.parseInt("22.4D");
+		Assert.assertEquals(22, v6);
+
+		int v7 = NumberUtil.parseInt("0");
+		Assert.assertEquals(0, v7);
+	}
+	
+	@Test
+	public void parseLongTest() {
+		long v1 = NumberUtil.parseLong("0xFF");
+		Assert.assertEquals(255L, v1);
+		long v2 = NumberUtil.parseLong("010");
+		Assert.assertEquals(10L, v2);
+		long v3 = NumberUtil.parseLong("10");
+		Assert.assertEquals(10L, v3);
+		long v4 = NumberUtil.parseLong("   ");
+		Assert.assertEquals(0L, v4);
+		long v5 = NumberUtil.parseLong("10F");
+		Assert.assertEquals(10L, v5);
+		long v6 = NumberUtil.parseLong("22.4D");
+		Assert.assertEquals(22L, v6);
+	}
+
+	@Test
+	public void factorialTest(){
+		long factorial = NumberUtil.factorial(0);
+		Assert.assertEquals(1, factorial);
+
+		Assert.assertEquals(1L, NumberUtil.factorial(1));
+		Assert.assertEquals(1307674368000L, NumberUtil.factorial(15));
+		Assert.assertEquals(2432902008176640000L, NumberUtil.factorial(20));
+
+		factorial = NumberUtil.factorial(5, 0);
+		Assert.assertEquals(120, factorial);
+		factorial = NumberUtil.factorial(5, 1);
+		Assert.assertEquals(120, factorial);
+    
+		Assert.assertEquals(5, NumberUtil.factorial(5, 4));
+		Assert.assertEquals(2432902008176640000L, NumberUtil.factorial(20, 0));
+	}
+
+	@Test
+	public void mulTest(){
+		final BigDecimal mul = NumberUtil.mul(new BigDecimal("10"), null);
+		Assert.assertEquals(BigDecimal.ZERO, mul);
+	}
+	
+	
+	@Test
+	public void isPowerOfTwoTest() {
+		Assert.assertEquals(false, NumberUtil.isPowerOfTwo(-1));
+		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(16));
+		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(65536));
+		Assert.assertEquals(true, NumberUtil.isPowerOfTwo(1));
+		Assert.assertEquals(false, NumberUtil.isPowerOfTwo(17));
+	}
+}