diff --git a/src/main/java/io/socket/yeast/Yeast.java b/src/main/java/io/socket/yeast/Yeast.java new file mode 100644 index 0000000..f41bf80 --- /dev/null +++ b/src/main/java/io/socket/yeast/Yeast.java @@ -0,0 +1,58 @@ +package io.socket.yeast; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * A Java implementation of yeast. https://github.com/unshiftio/yeast + */ +public class Yeast { + private static char[] alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".toCharArray(); + + private static int length = alphabet.length; + + private static Map map = new HashMap(length); + static { + for (int i = 0; i < length; i++) { + map.put(alphabet[i], i); + } + } + + private static int seed = 0; + + private static String prev; + + public static String encode(long num) { + final StringBuilder encoded = new StringBuilder(); + + do { + encoded.insert(0, alphabet[(int)(num % length)]); + num = (long)Math.floor(num / length); + } while (num > 0); + + return encoded.toString(); + } + + public static long decode(String str) { + long decoded = 0; + + for (char c : str.toCharArray()) { + decoded = decoded * length + map.get(c); + } + + return decoded; + } + + public static String yeast() { + String now = encode(new Date().getTime()); + + if (!now.equals(prev)) { + seed = 0; + prev = now; + return now; + } + + return now + "." + encode(seed++); + } +} diff --git a/src/test/java/io/socket/yeast/YeastTest.java b/src/test/java/io/socket/yeast/YeastTest.java new file mode 100644 index 0000000..738c37b --- /dev/null +++ b/src/test/java/io/socket/yeast/YeastTest.java @@ -0,0 +1,76 @@ +package io.socket.yeast; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Arrays; +import java.util.Date; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +; + +@RunWith(JUnit4.class) +public class YeastTest { + + private void waitUntilNextMillisecond() { + long now = new Date().getTime(); + while (new Date().getTime() == now) { /* do nothing */ } + } + + @Test + public void prependsIteratedSeedWhenSamePreviousId() { + waitUntilNextMillisecond(); + + String[] ids = new String[] { Yeast.yeast(), Yeast.yeast(), Yeast.yeast() }; + assertThat(ids[0], not(containsString("."))); + assertThat(ids[1], containsString(".0")); + assertThat(ids[2], containsString(".1")); + } + + @Test + public void resetsTheSeed() { + waitUntilNextMillisecond(); + + String[] ids = new String[] { Yeast.yeast(), Yeast.yeast(), Yeast.yeast() }; + assertThat(ids[0], not(containsString("."))); + assertThat(ids[1], containsString(".0")); + assertThat(ids[2], containsString(".1")); + + waitUntilNextMillisecond(); + + ids = new String[] { Yeast.yeast(), Yeast.yeast(), Yeast.yeast() }; + assertThat(ids[0], not(containsString("."))); + assertThat(ids[1], containsString(".0")); + assertThat(ids[2], containsString(".1")); + } + + @Test + public void doesNotCollide() { + int length = 30000; + String[] ids = new String[length]; + + for (int i = 0; i < length; i++) ids[i] = Yeast.yeast(); + + Arrays.sort(ids); + + for (int i = 0; i < length - 1; i++) { + assertThat(ids[i], not(equalTo(ids[i + 1]))); + } + } + + @Test + public void canConvertIdToTimestamp() { + waitUntilNextMillisecond(); + + long now = new Date().getTime(); + String id = Yeast.yeast(); + + assertThat(Yeast.encode(now), equalTo(id)); + assertThat(Yeast.decode(id), equalTo(now)); + } +}