diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/binary/HexUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/binary/HexUtil.java index 2394a5580b..4bcd9566d0 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/binary/HexUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/binary/HexUtil.java @@ -208,7 +208,7 @@ public class HexUtil extends Hex { * @param value float值 * @return 16进制字符串 */ - public static String toHex(float value) { + public static String toHex(final float value) { return Integer.toHexString(Float.floatToIntBits(value)); } @@ -218,7 +218,7 @@ public class HexUtil extends Hex { * @param value 16进制字符串 * @return 16进制字符串float值 */ - public static float hexToFloat(String value) { + public static float hexToFloat(final String value) { return Float.intBitsToFloat(Integer.parseUnsignedInt(removeHexPrefix(value), 16)); } @@ -228,7 +228,7 @@ public class HexUtil extends Hex { * @param value double值 * @return 16进制字符串 */ - public static String toHex(double value) { + public static String toHex(final double value) { return Long.toHexString(Double.doubleToLongBits(value)); } @@ -238,7 +238,7 @@ public class HexUtil extends Hex { * @param value 16进制字符串 * @return 16进制字符串double值 */ - public static double hexToDouble(String value) { + public static double hexToDouble(final String value) { return Double.longBitsToDouble(Long.parseUnsignedLong(removeHexPrefix(value), 16)); } @@ -288,7 +288,7 @@ public class HexUtil extends Hex { * * * @param hexStr Hex字符串 - * @param prefix 自定义前缀,如0x + * @param prefix 自定义前缀,如0x,此前缀每个16进制数前都添加 * @return 格式化后的字符串 */ public static String format(final String hexStr, final String prefix) { diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/CityHash.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/CityHash.java index ea4d4c560c..082bf23eaf 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/CityHash.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/CityHash.java @@ -26,8 +26,8 @@ import java.util.Arrays; * 它们分别根据字串计算 64 和 128 位的散列值。这些算法不适用于加密,但适合用在散列表等处。 * *

- * 代码来自:https://github.com/rolandhe/string-tools
- * 原始算法:https://github.com/google/cityhash + * 代码来自:string-tools
+ * 原始算法:cityhash * * @author hexiufeng * @since 5.2.5 diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/AbstractMetroHash.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/AbstractMetroHash.java index 15563d925f..7b9b36a63a 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/AbstractMetroHash.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/AbstractMetroHash.java @@ -23,9 +23,9 @@ import java.nio.ByteBuffer; * 除了卓越的性能外,他们还以算法生成而著称。 * *

- * 官方实现:https://github.com/jandrewrogers/MetroHash - * 官方文档:http://www.jandrewrogers.com/2015/05/27/metrohash/ - * 来自:https://github.com/postamar/java-metrohash/ + * 官方实现:MetroHash + * 官方文档:metrohash + * 来自:java-metrohash * * @author Marius Posta * @param 返回值类型,为this类型 diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash.java index da2c3c0353..60e94c1b95 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash.java @@ -24,9 +24,9 @@ import java.nio.ByteOrder; * 除了卓越的性能外,他们还以算法生成而著称。 * *

- * 官方实现:https://github.com/jandrewrogers/MetroHash - * 官方文档:http://www.jandrewrogers.com/2015/05/27/metrohash/ - * 来自:https://github.com/postamar/java-metrohash/ + * 官方实现:MetroHash + * 官方文档:metrohash + * 来自:java-metrohash * * @param 返回值类型,为this类型 * @author Marius Posta diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash128.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash128.java index 3d38fbbff4..d5f764c601 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash128.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash128.java @@ -27,9 +27,10 @@ import java.nio.ByteOrder; * 除了卓越的性能外,他们还以算法生成而著称。 * *

- * 官方实现:https://github.com/jandrewrogers/MetroHash - * 官方文档:http://www.jandrewrogers.com/2015/05/27/metrohash/ - * 来自:https://github.com/postamar/java-metrohash/ + * 官方实现:MetroHash + * 官方文档:metrohash + * 来自:java-metrohash + * * @author Marius Posta */ public class MetroHash128 extends AbstractMetroHash implements Hash128 { @@ -37,7 +38,7 @@ public class MetroHash128 extends AbstractMetroHash implements Has /** * 创建 {@code MetroHash128}对象 * - * @param seed 种子 + * @param seed 种子 * @return {@code MetroHash128}对象 */ public static MetroHash128 of(final long seed) { diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash64.java b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash64.java index c207e9708b..364f62bb7f 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash64.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/codec/hash/metro/MetroHash64.java @@ -26,9 +26,10 @@ import java.nio.ByteOrder; * 除了卓越的性能外,他们还以算法生成而著称。 * *

- * 官方实现:https://github.com/jandrewrogers/MetroHash - * 官方文档:http://www.jandrewrogers.com/2015/05/27/metrohash/ - * 来自:https://github.com/postamar/java-metrohash/ + * 官方实现:MetroHash + * 官方文档:metrohash + * 来自:java-metrohash + * * @author Marius Posta */ public class MetroHash64 extends AbstractMetroHash implements Hash64 { @@ -36,7 +37,7 @@ public class MetroHash64 extends AbstractMetroHash implements Hash6 /** * 创建 {@code MetroHash64}对象 * - * @param seed 种子 + * @param seed 种子 * @return {@code MetroHash64}对象 */ public static MetroHash64 of(final long seed) { @@ -82,9 +83,9 @@ public class MetroHash64 extends AbstractMetroHash implements Hash6 @Override public MetroHash64 write(final ByteBuffer output, final ByteOrder byteOrder) { - if(ByteOrder.LITTLE_ENDIAN == byteOrder){ + if (ByteOrder.LITTLE_ENDIAN == byteOrder) { writeLittleEndian(hash, output); - } else{ + } else { output.asLongBuffer().put(hash); } return this; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/RadixUtilTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/RadixUtilTest.java index 35e595aa39..669e2b1eb5 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/codec/RadixUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/RadixUtilTest.java @@ -18,7 +18,7 @@ package cn.hutool.v7.core.codec; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; public class RadixUtilTest { @Test @@ -29,4 +29,151 @@ public class RadixUtilTest { RadixUtil.decode(radixs, bad); }); } + + @Test + public void testEncodeInt() { + // Test binary conversion (base 2) + assertEquals("1010", RadixUtil.encode("01", 10)); + + // Test base 3 conversion + assertEquals("101", RadixUtil.encode("012", 10)); + + // Test base 16 conversion + assertEquals("A", RadixUtil.encode("0123456789ABCDEF", 10)); + + // Test with 34-radix + assertEquals("Y", RadixUtil.encode(RadixUtil.RADIXS_34, 32)); + + // Test with 59-radix + assertEquals("b", RadixUtil.encode(RadixUtil.RADIXS_59, 11)); + } + + @Test + public void testEncodeLong() { + // Test binary conversion (base 2) + assertEquals("1010", RadixUtil.encode("01", 10L)); + + // Test base 3 conversion + assertEquals("101", RadixUtil.encode("012", 10L)); + + // Test larger number + assertEquals("RR", RadixUtil.encode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 999L)); + + // Test with shuffle 34-radix + assertEquals("R", RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_34, 22)); + } + + @Test + public void testDecodeToInt() { + // Test binary decoding (base 2) + assertEquals(10, RadixUtil.decodeToInt("01", "1010")); + + // Test base 3 decoding + assertEquals(10, RadixUtil.decodeToInt("012", "101")); + + // Test base 16 decoding + assertEquals(10, RadixUtil.decodeToInt("0123456789ABCDEF", "A")); + + // Test with 34-radix + assertEquals(30, RadixUtil.decodeToInt(RadixUtil.RADIXS_34, "W")); + } + + @Test + public void testDecode() { + // Test binary decoding (base 2) + assertEquals(10L, RadixUtil.decode("01", "1010")); + + // Test base 3 decoding + assertEquals(10L, RadixUtil.decode("012", "101")); + + // Test base 16 decoding + assertEquals(10L, RadixUtil.decode("0123456789ABCDEF", "A")); + + // Test with 59-radix + assertEquals(11L, RadixUtil.decode(RadixUtil.RADIXS_59, "b")); + + // Test with shuffle 59-radix + assertEquals(1L, RadixUtil.decode(RadixUtil.RADIXS_SHUFFLE_59, "vh")); + } + + @Test + public void testEncodeDecodeRoundTrip() { + // Test round trip for various bases + assertEquals(42, RadixUtil.decodeToInt("01", RadixUtil.encode("01", 42))); + assertEquals(123, RadixUtil.decodeToInt("0123456789", RadixUtil.encode("0123456789", 123))); + assertEquals(255, RadixUtil.decodeToInt("0123456789ABCDEF", RadixUtil.encode("0123456789ABCDEF", 255))); + + // Test with predefined radixes + assertEquals(1000, RadixUtil.decodeToInt(RadixUtil.RADIXS_34, RadixUtil.encode(RadixUtil.RADIXS_34, 1000))); + assertEquals(2000, RadixUtil.decodeToInt(RadixUtil.RADIXS_59, RadixUtil.encode(RadixUtil.RADIXS_59, 2000))); + assertEquals(500, RadixUtil.decodeToInt(RadixUtil.RADIXS_SHUFFLE_34, RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_34, 500))); + assertEquals(750, RadixUtil.decodeToInt(RadixUtil.RADIXS_SHUFFLE_59, RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_59, 750))); + } + + @Test + public void testEdgeCases() { + // Test zero + assertEquals("0", RadixUtil.encode("01", 0)); + assertEquals(0, RadixUtil.decodeToInt("01", "0")); + + // Test single digit numbers + assertEquals("1", RadixUtil.encode("01", 1)); + assertEquals(1, RadixUtil.decodeToInt("01", "1")); + + // Test with larger numbers + assertEquals("11111111", RadixUtil.encode("01", 255)); // 255 in binary + assertEquals(255, RadixUtil.decodeToInt("01", "11111111")); + + // Test negative numbers (using the special handling in encode) + assertEquals(4294967254L, RadixUtil.decode("01", RadixUtil.encode("01", -42))); + } + + @SuppressWarnings("ConstantValue") + @Test + public void testPredefinedRadixes() { + // Test 34-radix constants + assertNotNull(RadixUtil.RADIXS_34); + assertEquals(34, RadixUtil.RADIXS_34.length()); + assertFalse(RadixUtil.RADIXS_34.contains("I")); + assertFalse(RadixUtil.RADIXS_34.contains("O")); + + // Test 59-radix constants + assertNotNull(RadixUtil.RADIXS_59); + assertEquals(59, RadixUtil.RADIXS_59.length()); + assertFalse(RadixUtil.RADIXS_59.contains("I")); + assertFalse(RadixUtil.RADIXS_59.contains("O")); + assertFalse(RadixUtil.RADIXS_59.contains("l")); + + // Test shuffle radixes + assertNotNull(RadixUtil.RADIXS_SHUFFLE_34); + assertEquals(34, RadixUtil.RADIXS_SHUFFLE_34.length()); + + assertNotNull(RadixUtil.RADIXS_SHUFFLE_59); + assertEquals(59, RadixUtil.RADIXS_SHUFFLE_59.length()); + } + + @Test + public void testInvalidInputs() { + // Test invalid radix (too short) + assertThrows(RuntimeException.class, () -> RadixUtil.encode("0", 10)); // only 1 char + + // Test decode with null radix + assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode(null, "10")); + + // Test decode with empty string + assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode("01", "")); + + // Test decode with invalid character (already tested in original method) + final String radixs = "0123456789ABC"; // base 13 + final String bad = "1X3"; // 'X' 不在 radix 中 + assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode(radixs, bad)); + } + + @Test + public void testLongValueEncodeDecode() { + final long testValue = 1234567890L; + final String encoded = RadixUtil.encode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", testValue); + final long decoded = RadixUtil.decode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", encoded); + assertEquals(testValue, decoded); + } } diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base32Test.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base32Test.java similarity index 96% rename from hutool-core/src/test/java/cn/hutool/v7/core/codec/Base32Test.java rename to hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base32Test.java index 8dfc261336..3a0716f0d7 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base32Test.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base32Test.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package cn.hutool.v7.core.codec; +package cn.hutool.v7.core.codec.binary; -import cn.hutool.v7.core.codec.binary.Base32; import cn.hutool.v7.core.util.ByteUtil; import cn.hutool.v7.core.util.RandomUtil; import org.junit.jupiter.api.Assertions; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base58Test.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base58Test.java similarity index 95% rename from hutool-core/src/test/java/cn/hutool/v7/core/codec/Base58Test.java rename to hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base58Test.java index 6654bf1e04..9e9698c8cc 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base58Test.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base58Test.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package cn.hutool.v7.core.codec; +package cn.hutool.v7.core.codec.binary; -import cn.hutool.v7.core.codec.binary.Base58; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base62Test.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base62Test.java similarity index 96% rename from hutool-core/src/test/java/cn/hutool/v7/core/codec/Base62Test.java rename to hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base62Test.java index e9d6d24634..015ca4dfe4 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base62Test.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base62Test.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package cn.hutool.v7.core.codec; +package cn.hutool.v7.core.codec.binary; -import cn.hutool.v7.core.codec.binary.Base62; import cn.hutool.v7.core.util.RandomUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base64Test.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base64Test.java similarity index 98% rename from hutool-core/src/test/java/cn/hutool/v7/core/codec/Base64Test.java rename to hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base64Test.java index 12c58b2525..38f9481931 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/codec/Base64Test.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/Base64Test.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package cn.hutool.v7.core.codec; +package cn.hutool.v7.core.codec.binary; -import cn.hutool.v7.core.codec.binary.Base64; import cn.hutool.v7.core.util.ByteUtil; import cn.hutool.v7.core.util.CharsetUtil; import cn.hutool.v7.core.util.RandomUtil; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/HexUtilTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/HexUtilTest.java new file mode 100644 index 0000000000..0dded93a48 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/binary/HexUtilTest.java @@ -0,0 +1,202 @@ +package cn.hutool.v7.core.codec.binary; + +import org.junit.jupiter.api.Test; + +import java.awt.Color; + +import static org.junit.jupiter.api.Assertions.*; + +public class HexUtilTest { + + @Test + public void testEncodeColor() { + final Color red = new Color(255, 0, 0); + assertEquals("#ff0000", HexUtil.encodeColor(red)); + + final Color green = new Color(0, 255, 0); + assertEquals("#00ff00", HexUtil.encodeColor(green)); + + final Color blue = new Color(0, 0, 255); + assertEquals("#0000ff", HexUtil.encodeColor(blue)); + + final Color black = new Color(0, 0, 0); + assertEquals("#000000", HexUtil.encodeColor(black)); + + final Color white = new Color(255, 255, 255); + assertEquals("#ffffff", HexUtil.encodeColor(white)); + + // Test with single digit values (should be padded with 0) + final Color testColor = new Color(1, 16, 255); + assertEquals("#0110ff", HexUtil.encodeColor(testColor)); + } + + @Test + public void testEncodeColorWithPrefix() { + final Color red = new Color(255, 0, 0); + assertEquals("0xff0000", HexUtil.encodeColor(red, "0x")); + assertEquals("#ff0000", HexUtil.encodeColor(red, "#")); + assertEquals("ff0000", HexUtil.encodeColor(red, "")); + } + + @Test + public void testDecodeColor() { + assertEquals(new Color(255, 0, 0), HexUtil.decodeColor("#ff0000")); + assertEquals(new Color(0, 255, 0), HexUtil.decodeColor("#00ff00")); + assertEquals(new Color(0, 0, 255), HexUtil.decodeColor("#0000ff")); + assertEquals(new Color(255, 0, 0), HexUtil.decodeColor("0xff0000")); + } + + @Test + public void testIsHexNumber() { + assertTrue(HexUtil.isHexNumber("ff")); + assertTrue(HexUtil.isHexNumber("FF")); + assertTrue(HexUtil.isHexNumber("0xff")); + assertTrue(HexUtil.isHexNumber("0XFF")); + assertTrue(HexUtil.isHexNumber("#ff")); + assertTrue(HexUtil.isHexNumber("123abc")); + assertTrue(HexUtil.isHexNumber("0x123abc")); + assertTrue(HexUtil.isHexNumber("#123abc")); + + assertFalse(HexUtil.isHexNumber("")); + assertFalse(HexUtil.isHexNumber(null)); + assertFalse(HexUtil.isHexNumber("gg")); // g is not hex digit + assertFalse(HexUtil.isHexNumber("-ff")); + assertFalse(HexUtil.isHexNumber("ff-")); + assertFalse(HexUtil.isHexNumber("12 34")); // space not allowed + } + + @Test + public void testToUnicodeHex() { + assertEquals("\\u4f60", HexUtil.toUnicodeHex('你')); + assertEquals("\\u0048", HexUtil.toUnicodeHex('H')); + assertEquals("\\u0065", HexUtil.toUnicodeHex('e')); + assertEquals("\\u006c", HexUtil.toUnicodeHex('l')); + assertEquals("\\u006f", HexUtil.toUnicodeHex('o')); + + // Test with integer values + assertEquals("\\u0041", HexUtil.toUnicodeHex(65)); // 'A' + assertEquals("\\u0000", HexUtil.toUnicodeHex(0)); + assertEquals("\\uffff", HexUtil.toUnicodeHex(65535)); // max char value + } + + @Test + public void testToHexFromInt() { + assertEquals("ff", HexUtil.toHex(255)); + assertEquals("0", HexUtil.toHex(0)); + assertEquals("10", HexUtil.toHex(16)); + assertEquals("64", HexUtil.toHex(100)); + assertEquals("ffff", HexUtil.toHex(65535)); + } + + @Test + public void testHexToInt() { + assertEquals(255, HexUtil.hexToInt("ff")); + assertEquals(255, HexUtil.hexToInt("0xff")); + assertEquals(255, HexUtil.hexToInt("#ff")); + assertEquals(0, HexUtil.hexToInt("0")); + assertEquals(16, HexUtil.hexToInt("10")); + assertEquals(100, HexUtil.hexToInt("64")); + assertEquals(65535, HexUtil.hexToInt("ffff")); + assertEquals(65535, HexUtil.hexToInt("0xffff")); + } + + @Test + public void testToHexFromLong() { + assertEquals("ff", HexUtil.toHex(255L)); + assertEquals("0", HexUtil.toHex(0L)); + assertEquals("10", HexUtil.toHex(16L)); + assertEquals("ffffffffffffffff", HexUtil.toHex(-1L)); + } + + @Test + public void testHexToLong() { + assertEquals(255L, HexUtil.hexToLong("ff")); + assertEquals(255L, HexUtil.hexToLong("0xff")); + assertEquals(0L, HexUtil.hexToLong("0")); + assertEquals(16L, HexUtil.hexToLong("10")); + assertThrows(NumberFormatException.class, ()-> HexUtil.hexToLong("ffffffffffffffff")); + } + + @Test + public void testToHexFromFloat() { + assertEquals("40490fdb", HexUtil.toHex((float) Math.PI)); + assertEquals("0", HexUtil.toHex(0.0f)); + assertEquals("3f800000", HexUtil.toHex(1.0f)); + } + + @Test + public void testHexToFloat() { + assertEquals(Math.PI, HexUtil.hexToFloat("40490fdb"), 0.0001f); + assertEquals(0.0f, HexUtil.hexToFloat("0"), 0.0001f); + assertEquals(1.0f, HexUtil.hexToFloat("3f800000"), 0.0001f); + } + + @Test + public void testToHexFromDouble() { + assertEquals("400921fb54442d18", HexUtil.toHex(Math.PI)); + assertEquals("0", HexUtil.toHex(0.0)); + assertEquals("3ff0000000000000", HexUtil.toHex(1.0)); + } + + @Test + public void testHexToDouble() { + assertEquals(Math.PI, HexUtil.hexToDouble("400921fb54442d18"), 0.0001); + assertEquals(0.0, HexUtil.hexToDouble("0"), 0.0001); + assertEquals(1.0, HexUtil.hexToDouble("3ff0000000000000"), 0.0001); + } + + @Test + public void testAppendHex() { + StringBuilder sb = new StringBuilder(); + HexUtil.appendHex(sb, (byte) 255, true); // lowercase + assertEquals("ff", sb.toString()); + + sb = new StringBuilder(); + HexUtil.appendHex(sb, (byte) 255, false); // uppercase + assertEquals("FF", sb.toString()); + + sb = new StringBuilder(); + HexUtil.appendHex(sb, (byte) 0, true); + assertEquals("00", sb.toString()); + + sb = new StringBuilder(); + HexUtil.appendHex(sb, (byte) 16, true); + assertEquals("10", sb.toString()); + } + + @Test + public void testToBigInteger() { + assertNull(HexUtil.toBigInteger(null)); + assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("ff")); + assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("0xff")); + assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("#ff")); + assertEquals(new java.math.BigInteger("0", 16), HexUtil.toBigInteger("0")); + assertEquals(new java.math.BigInteger("1234abcd", 16), HexUtil.toBigInteger("1234abcd")); + } + + @Test + public void testFormat() { + assertEquals("", HexUtil.format("")); + assertEquals("a", HexUtil.format("a")); + assertEquals("ab", HexUtil.format("ab")); + assertEquals("ab cd", HexUtil.format("abcd")); + assertEquals("ab cd ef", HexUtil.format("abcdef")); + assertEquals("ab cd ef 12", HexUtil.format("abcdef12")); + } + + @Test + public void testFormatWithPrefix() { + assertEquals("0xab", HexUtil.format("ab", "0x")); + assertEquals("0xab 0xcd", HexUtil.format("abcd", "0x")); + assertEquals("#ab", HexUtil.format("ab", "#")); + assertEquals("#ab #cd", HexUtil.format("abcd", "#")); + } + + @Test + public void testFormatWithPrefixAndSeparator() { + assertEquals("0xab 0xcd", HexUtil.format("abcd", "0x", " ")); + assertEquals("0xab:0xcd", HexUtil.format("abcd", "0x", ":")); + assertEquals("ab-cd", HexUtil.format("abcd", "", "-")); + assertEquals("ab cd ef 12", HexUtil.format("abcdef12", null, null)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/codec/hash/ConsistentHashTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/codec/hash/ConsistentHashTest.java new file mode 100644 index 0000000000..8189f45f64 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/v7/core/codec/hash/ConsistentHashTest.java @@ -0,0 +1,214 @@ +package cn.hutool.v7.core.codec.hash; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ConsistentHashTest { + + @Test + public void testConstructorWithDefaultHashFunction() { + final List nodes = Arrays.asList("node1", "node2", "node3"); + final ConsistentHash consistentHash = new ConsistentHash<>(3, nodes); + + assertNotNull(consistentHash); + // Cannot directly access numberOfReplicas and circle as they are private + // We'll test the functionality instead + final String node = consistentHash.get("testKey"); + assertNotNull(node); + assertTrue(nodes.contains(node)); + } + + @Test + public void testConstructorWithCustomHashFunction() { + final Hash32 customHash = Object::hashCode; + final List nodes = Arrays.asList("server1", "server2", "server3"); + final ConsistentHash consistentHash = new ConsistentHash<>(customHash, 2, nodes); + + assertNotNull(consistentHash); + // Test functionality instead of accessing private fields + final String node = consistentHash.get("testKey"); + assertNotNull(node); + assertTrue(nodes.contains(node)); + } + + @Test + public void testAddNode() { + final List nodes = Collections.singletonList("initial"); + final ConsistentHash consistentHash = new ConsistentHash<>(2, nodes); + + // Test that we can add a new node + consistentHash.add("newNode"); + + // Verify that the new node can be retrieved for some keys + final String result = consistentHash.get("someKey"); + assertNotNull(result); + } + + @Test + public void testRemoveNode() { + final List nodes = Arrays.asList("node1", "node2", "node3"); + final ConsistentHash consistentHash = new ConsistentHash<>(2, nodes); + + // Initially, there should be nodes + final String initialResult = consistentHash.get("testKey"); + assertNotNull(initialResult); + + // Remove a node + consistentHash.remove("node2"); + + // After removal, there should still be nodes to handle requests + final String resultAfterRemoval = consistentHash.get("testKey"); + assertNotNull(resultAfterRemoval); + // The result might be different, but should not be null if other nodes exist + } + + @Test + public void testGetNode() { + final List nodes = Arrays.asList("server1", "server2", "server3"); + final ConsistentHash consistentHash = new ConsistentHash<>(3, nodes); + + // Test that we can get a node for a key + final String node = consistentHash.get("key1"); + assertNotNull(node); + assertTrue(nodes.contains(node)); + + // Test with different keys + final String node2 = consistentHash.get("key2"); + assertNotNull(node2); + assertTrue(nodes.contains(node2)); + } + + @Test + public void testGetNodeWithEmptyCircle() { + final ConsistentHash consistentHash = new ConsistentHash<>(2, Collections.emptyList()); + + // Should return null when there are no nodes + final String node = consistentHash.get("anyKey"); + assertNull(node); + } + + @Test + public void testConsistency() { + final List nodes = Arrays.asList("server1", "server2", "server3", "server4", "server5"); + final ConsistentHash consistentHash = new ConsistentHash<>(10, nodes); + + // Test that the same key always maps to the same node + final String node1 = consistentHash.get("consistentKey"); + final String node2 = consistentHash.get("consistentKey"); + final String node3 = consistentHash.get("consistentKey"); + + assertEquals(node1, node2); + assertEquals(node2, node3); + } + + @Test + public void testLoadDistribution() { + final List nodes = Arrays.asList("server1", "server2", "server3"); + final ConsistentHash consistentHash = new ConsistentHash<>(10, nodes); + + // Map many keys to nodes to test distribution + final int[] hits = new int[3]; + for (int i = 0; i < 100; i++) { + final String node = consistentHash.get("key" + i); + switch (node) { + case "server1": + hits[0]++; + break; + case "server2": + hits[1]++; + break; + case "server3": + hits[2]++; + break; + } + } + + // Verify that all servers got some traffic (with higher replica count, distribution should be relatively balanced) + for (final int hitCount : hits) { + assertTrue(hitCount > 0, "Each server should receive some load"); + } + } + + @Test + public void testNodeAdditionDoesNotDisruptTooMuch() { + final List initialNodes = Arrays.asList("server1", "server2"); + final ConsistentHash consistentHash = new ConsistentHash<>(10, initialNodes); + + // Map keys to nodes with initial setup + final String[] initialMappings = new String[50]; + for (int i = 0; i < 50; i++) { + initialMappings[i] = consistentHash.get("key" + i); + } + + // Add a new node + consistentHash.add("server3"); + + // Map the same keys again + int remappedCount = 0; + for (int i = 0; i < 50; i++) { + final String newMapping = consistentHash.get("key" + i); + if (!initialMappings[i].equals(newMapping)) { + remappedCount++; + } + } + + // Verify that not ALL keys are remapped (which would happen with naive modulo hashing) + assertTrue(remappedCount < 50, "Not all keys should be remapped when adding a new node"); + // In a well-distributed consistent hash, typically only a fraction of keys are remapped + assertTrue(remappedCount <= 20, "Remapping should be minimal"); + } + + @Test + public void testNodeRemovalDoesNotDisruptTooMuch() { + final List initialNodes = Arrays.asList("server1", "server2", "server3"); + final ConsistentHash consistentHash = new ConsistentHash<>(10, initialNodes); + + // Map keys to nodes with initial setup + final String[] initialMappings = new String[50]; + for (int i = 0; i < 50; i++) { + initialMappings[i] = consistentHash.get("key" + i); + } + + // Remove a node + consistentHash.remove("server3"); + + // Map the same keys again + int remappedCount = 0; + for (int i = 0; i < 50; i++) { + final String newMapping = consistentHash.get("key" + i); + if (!newMapping.equals(initialMappings[i])) { + remappedCount++; + } + } + + // Verify that not ALL keys are remapped + assertTrue(remappedCount < 50, "Not all keys should be remapped when removing a node"); + } + + @Test + public void testSingleNode() { + final List nodes = Collections.singletonList("onlyServer"); + final ConsistentHash consistentHash = new ConsistentHash<>(5, nodes); + + // All keys should map to the same server + for (int i = 0; i < 20; i++) { + assertEquals("onlyServer", consistentHash.get("key" + i)); + } + } + + @Test + public void testManyVirtualNodes() { + final List nodes = Arrays.asList("node1", "node2"); + final ConsistentHash consistentHash = new ConsistentHash<>(100, nodes); // Many replicas + + // Should still work normally + final String node = consistentHash.get("testKey"); + assertNotNull(node); + assertTrue(nodes.contains(node)); + } +}