diff --git a/build.gradle b/build.gradle index 829256f1..fd1ab2ae 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { allprojects { group = "com.dumbdogdiner" - version = "3.0.2" + version = "3.1.0" // java plugin is applied in subprojects apply plugin: "jacoco" diff --git a/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/LocationUtil.java b/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/LocationUtil.java new file mode 100644 index 00000000..e630135e --- /dev/null +++ b/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/LocationUtil.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.bukkit.util; + +import com.dumbdogdiner.stickyapi.math.vector.Vector3; +import org.bukkit.Location; +import org.bukkit.World; +import org.jetbrains.annotations.NotNull; + +/** + * Utilities for converting {@link Vector3}s into Bukkit's {@link Location} object. + */ +public final class LocationUtil { + private LocationUtil() {} + + /** + * Convert the target {@link Vector3} into a Bukkit {@link Location}. + * @param world The world this location should be contained in + * @param vector The target vector + * @return A new {@link Location} in the target world. + */ + public static @NotNull Location toLocation(@NotNull World world, @NotNull Vector3 vector) { + return new Location(world, vector.getX(), vector.getY(), vector.getZ()); + } + + /** + * Convert the target {@link Location} into a {@link Vector3}. + * @param location The target location + * @return A new {@link Location} in the target world. + */ + public static @NotNull Vector3 toVector3(@NotNull Location location) { + return new Vector3(location.getX(), location.getY(), location.getZ()); + } +} diff --git a/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/PlayerSelector.java b/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/PlayerSelector.java index f71aa801..76c14515 100644 --- a/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/PlayerSelector.java +++ b/bukkit/src/main/java/com/dumbdogdiner/stickyapi/bukkit/util/PlayerSelector.java @@ -4,7 +4,7 @@ */ package com.dumbdogdiner.stickyapi.bukkit.util; -import com.dumbdogdiner.stickyapi.common.util.MathUtil; +import com.dumbdogdiner.stickyapi.math.RandomUtil; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; @@ -101,7 +101,7 @@ public static Predicate inRange(Location center, double radius) { */ public static Player selectRandom(Predicate condition) { List list = selectPlayers(condition); - return list.get(MathUtil.randomInt(list.size())); + return RandomUtil.randomElement(list); } /** diff --git a/common/build.gradle b/common/build.gradle index 0462850a..473b51a4 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -9,6 +9,7 @@ plugins { dependencies { // Depend on the config project api project(":config") // api - transistively expose config dependency when implementing :common + api project(":math") // api - transistively expose api dependency when implementing :common testImplementation project(":config").sourceSets.test.output // LocaleProviderTest - add snakeyaml so YamlProvider (from the :config project) can work properly diff --git a/common/src/main/java/com/dumbdogdiner/stickyapi/common/arguments/Arguments.java b/common/src/main/java/com/dumbdogdiner/stickyapi/common/arguments/Arguments.java index e442bd55..bef460b9 100644 --- a/common/src/main/java/com/dumbdogdiner/stickyapi/common/arguments/Arguments.java +++ b/common/src/main/java/com/dumbdogdiner/stickyapi/common/arguments/Arguments.java @@ -12,9 +12,9 @@ import java.util.List; import com.dumbdogdiner.stickyapi.common.util.Debugger; -import com.dumbdogdiner.stickyapi.common.util.NumberUtil; import com.dumbdogdiner.stickyapi.common.util.TimeUtil; +import com.dumbdogdiner.stickyapi.math.NumberUtil; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -356,7 +356,7 @@ public Arguments requiredTimeString(@NotNull String name) { private Arguments optionalIntImplementation(@NotNull String name, @NotNull Integer fallback) { debug.print("Looking for optional integer " + name + "..."); - if (unparsedArgs.size() > position && NumberUtil.isNumeric(unparsedArgs.get(position))) { + if (unparsedArgs.size() > position && NumberUtil.isInteger(unparsedArgs.get(position), false)) { parsedArgs.put(name, unparsedArgs.get(position)); position++; debug.print("Found int at position " + String.valueOf(position) + " - new args size = " @@ -405,7 +405,7 @@ public Arguments optionalInt(@NotNull String name, @NotNull Integer fallback) { public Arguments requiredInt(@NotNull String name) { debug.print("Looking for optional required " + name + "..."); - if (unparsedArgs.size() > position && NumberUtil.isNumeric(unparsedArgs.get(position))) { + if (unparsedArgs.size() > position && NumberUtil.isInteger(unparsedArgs.get(position), false)) { parsedArgs.put(name, unparsedArgs.get(position)); position++; debug.print("Found int at position " + String.valueOf(position) + " - new args size = " diff --git a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java index b20c6cb3..eecf1513 100644 --- a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java +++ b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java @@ -15,10 +15,10 @@ import java.util.Random; /** - *

* Util class for commonly used math operations. - *

+ * @deprecated in favor of the stickyapi:math module. */ +@Deprecated(since = "3.1") public class MathUtil { private MathUtil() { } diff --git a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/NumberUtil.java b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/NumberUtil.java index 1515543e..09579b4b 100644 --- a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/NumberUtil.java +++ b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/NumberUtil.java @@ -10,7 +10,9 @@ *

* Provides extra functionality for Java Number classes. *

- */ + * @deprecated in favor of the stickyapi:math module. +*/ +@Deprecated(since = "3.1") public final class NumberUtil { private NumberUtil() { } diff --git a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java index 6e5922ce..067649c9 100644 --- a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java +++ b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.UUID; +import com.dumbdogdiner.stickyapi.math.RandomUtil; import com.google.common.base.Preconditions; import net.md_5.bungee.api.ChatColor; @@ -333,16 +334,16 @@ public static String randomObfuscatedString(int min, int max, int minRunBeforeSp }; StringBuilder obfuscated = new StringBuilder(); - int len = MathUtil.randomInt(min, max); + int len = RandomUtil.randomInt(min, max); int charsSinceSpace = 0; while (obfuscated.length() < len) { if (minRunBeforeSpace > 0 && charsSinceSpace > minRunBeforeSpace && // Set a 5% probability of the character being a space - MathUtil.randomInt(1, 100) <= 5) { + RandomUtil.probability(0.05)) { obfuscated.append(' '); charsSinceSpace = 0; } else { - obfuscated.append(MathUtil.randomElement(choices)); + obfuscated.append(RandomUtil.randomElement(choices)); charsSinceSpace++; } } diff --git a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/TimeUtil.java b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/TimeUtil.java index a13faa49..d5f8f266 100644 --- a/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/TimeUtil.java +++ b/common/src/main/java/com/dumbdogdiner/stickyapi/common/util/TimeUtil.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Optional; +import com.dumbdogdiner.stickyapi.math.NumberUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -286,7 +287,7 @@ public static Timestamp toTimestamp(@NotNull String timePeriod) { return null; // If it's numeric, lets do some extra checks! - if (NumberUtil.isNumeric(timePeriod)) { + if (NumberUtil.isInteger(timePeriod, false)) { // Return null if it's greater 12 characters long if (timePeriod.length() > 12) return null; diff --git a/common/src/test/java/com/dumbdogdiner/stickyapi/common/nbt/NbtNumberTagTest.java b/common/src/test/java/com/dumbdogdiner/stickyapi/common/nbt/NbtNumberTagTest.java index 9f84a4f0..91f077c4 100644 --- a/common/src/test/java/com/dumbdogdiner/stickyapi/common/nbt/NbtNumberTagTest.java +++ b/common/src/test/java/com/dumbdogdiner/stickyapi/common/nbt/NbtNumberTagTest.java @@ -4,6 +4,7 @@ */ package com.dumbdogdiner.stickyapi.common.nbt; +import com.dumbdogdiner.stickyapi.math.RandomUtil; import com.google.gson.JsonPrimitive; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; @@ -11,8 +12,6 @@ import java.text.MessageFormat; -import static com.dumbdogdiner.stickyapi.common.util.MathUtil.randomDouble; -import static com.dumbdogdiner.stickyapi.common.util.MathUtil.randomInt; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -23,8 +22,8 @@ class NbtNumberTagTest { @BeforeEach public void setUp(){ - i = randomInt(-256, 4096); - f = (float) randomDouble(-256, 8192); + i = RandomUtil.randomInt(-256, 4096); + f = (float) RandomUtil.randomDouble(-256, 8192); } @RepeatedTest(REPEAT) diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..e69de29b diff --git a/math/README.md b/math/README.md new file mode 100644 index 00000000..02082f2c --- /dev/null +++ b/math/README.md @@ -0,0 +1,3 @@ +# stickyapi:math + +Provides a lightweight mathematics library for general purpose use. diff --git a/math/build.gradle b/math/build.gradle new file mode 100644 index 00000000..8e7d76d5 --- /dev/null +++ b/math/build.gradle @@ -0,0 +1,4 @@ +dependencies { + // TODO: Investigate why this isn't provided by common. + implementation "com.google.guava:guava:30.1.1-jre" +} diff --git a/math/lombok.config b/math/lombok.config new file mode 100644 index 00000000..189c0bef --- /dev/null +++ b/math/lombok.config @@ -0,0 +1,3 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/Interpolation.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/Interpolation.java new file mode 100644 index 00000000..5f8b24bb --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/Interpolation.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import com.google.common.base.Preconditions; + +/** + * Utility class for interpolating between values. + * TODO: Inaccuracy issues for longs. + */ +public final class Interpolation { + private Interpolation() {} + + /** + * Clamp the value `t` between `min` and `max`. + * @param max The minimum value + * @param min The maximum value + * @param t The value to clamp + * @return The clamped value between `max` and `min`. + */ + public static double clamp(Number min, Number max, Number t) { + Preconditions.checkNotNull(min); + Preconditions.checkNotNull(max); + Preconditions.checkNotNull(t); + return Double.min(max.doubleValue(), Double.max(min.doubleValue(), t.doubleValue())); + } + + /** + * Linearly interpolate between a and b with time parameter t. + * `t` is clamped between `0` and `1`. + * @param a The first value + * @param b The second value + * @param t The time parameter + * @return The linearly interpolated value between `a` and `b`. + */ + public static double lerp(Number a, Number b, Number t) { + return lerpUnclamped(a, b, clamp(0, 1, t)); + } + + /**s + * Linearly interpolate between a and b with time parameter t. + * `t` is not clamped between `0` and `1`. + * @param a The first value + * @param b The second value + * @param t The time parameter + * @return The linearly interpolated value. + */ + public static double lerpUnclamped(Number a, Number b, Number t) { + Preconditions.checkNotNull(a); + Preconditions.checkNotNull(b); + Preconditions.checkNotNull(t); + return b.doubleValue() * t.doubleValue() + (1 - t.doubleValue()) * a.doubleValue(); + } + + /** + * Sinusoidaly interpolate between a and b with time parameter t. + * @param a The first value + * @param b The second value + * @param t The time parameter + * @return The sinusoidaly interpolated value. + */ + public static double sinusoidal(Number a, Number b, Number t) { + // calculate sinusoidal param + double param = Math.pow(Math.sin(t.doubleValue() * Math.PI / 2), 2); + return lerp(a, b, param); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/NumberUtil.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/NumberUtil.java new file mode 100644 index 00000000..ea2a4608 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/NumberUtil.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.regex.Pattern; + +/** + * Utility class for working with numbers. + */ +public final class NumberUtil { + private NumberUtil() {} + + /** + * Test whether the specified number is within the specified range. + * @param number The specified number + * @param min The minimum value (inclusive) + * @param max The maximum value (inclusive) + * @return true if given number is in the given range. + */ + public static boolean inRange(Number number, Number min, Number max) { + return number.doubleValue() >= min.doubleValue() && number.doubleValue() <= max.doubleValue(); + } + + /** + * Round a double value. + * @param value the value that should be rounded + * @param places amount of decimal places + * @return {@link Double} + */ + public static double round(double value, int places) { + // compute power of ten equal to places + long factor = (long) Math.pow(10, places); + value *= factor; + // chop off remaining decimal places + long tmp = Math.round(value); + // divide back down to get fixed number of places + return (double) tmp / factor; + } + + /** + * Test if the target string can be considered an integer. Checks if every character is a unicode digit. + *
+	 * NumberUtil.isInteger("123")         = true
+	 * NumberUtil.isInteger("-123")        = true
+	 * NumberUtil.isInteger(null)          = false
+	 * NumberUtil.isInteger("")            = false
+	 * NumberUtil.isInteger("  ")          = false
+	 * NumberUtil.isInteger("12 3")        = false
+	 * NumberUtil.isInteger("ab2c")        = false
+	 * NumberUtil.isInteger("12-3")        = false
+	 * NumberUtil.isInteger("12.3")        = false
+	 * NumberUtil.isInteger("-123", false) = false
+	 * 
+ * @param string The string to check + * @param signed Whether to allow signed numbers + * @return true if the target string can be considered an integer. + */ + public static boolean isInteger(@Nullable String string, boolean signed) { + // an empty or null string is very definitely not an integer + if (string == null || string.length() == 0) { + return false; + } + // sign is allowed, but the first char is neither a sign or a digit + if (signed && !Character.isDigit(string.charAt(0)) && string.charAt(0) != '-') { + return false; + } + // iterate over chars and check if they are digits + // starts from position 1 if the string is allowed to be signed + for (int i = signed ? 1 : 0; i < string.length(); i++) { + if (!Character.isDigit(string.charAt(i))) { + return false; + } + } + // made it here, must be an integer + return true; + } + + /** + * Test if the target string can be considered an integer by checking if every character is a unicode digit. + * By default, this method allows signed numbers. + * @param string The target string + * @return true if the target string can be considered a signed integer. + */ + public static boolean isInteger(@Nullable String string) { + return isInteger(string, true); + } + + // format: sign value decimal exponent + private static final Pattern NUMERIC_REGEX = Pattern.compile("-?[0-9]+\\.?[0-9]+(?:e-?[0-9]+)?$"); + + /** + * Checks if the string is considered numeric. This method accounts for signed integers + * and engineering notation (i.e. 12e3). + *
+	 * NumberUtil.isNumeric("123")         = true
+	 * NumberUtil.isNumeric("-123")        = true
+	 * NumberUtil.isNumeric("12.3")        = true
+	 * NumberUtil.isNumeric("12e3")        = true
+	 * NumberUtil.isNumeric("12e-3")       = true
+	 * NumberUtil.isNumeric(null)          = false
+	 * NumberUtil.isNumeric("")            = false
+	 * NumberUtil.isNumeric("  ")          = false
+	 * NumberUtil.isNumeric("12 3")        = false
+	 * NumberUtil.isNumeric("ab2c")        = false
+	 * NumberUtil.isNumeric("12-3")        = false
+	 * 
+ * @param string the string to check + * @return true if the string is numeric + */ + public static boolean isNumeric(@Nullable String string) { + // check for null + if (string == null) { + return false; + } + // match string with regex + return NUMERIC_REGEX.matcher(string).matches(); + } + + /** + * Utility method for checking if two doubles are very very almost equal. Useful for getting rid + * of floating point inaccuracies. + * @param a The first double + * @param b The second double + * @param epsilon The maximum difference between them + * @return true if the numbers are within epsilon of each other. + */ + public static boolean almostEquals(double a, double b, double epsilon) { + return Math.abs(a - b) < epsilon; + } + + /** + * Utility method for checking if two doubles are very very almost equal. Useful for getting rid + * of floating point inaccuracies. + * @param a The first double + * @param b The second double + * @return true if the numbers are within 1 part in a million of each other. + */ + public static boolean almostEquals(double a, double b) { + return almostEquals(a, b, 10e-6); + } + + /** + * Get a number as the percentage of another. + * + * @param x The number who's percentage of the total this method will return + * @param total The total + * @return The percentage value + */ + public static double getPercentage(Number x, Number total) { + return x.doubleValue() / total.doubleValue() * 100; + } + + /** + * Get a number as the percentage of another. + * + * @param x The number who's percentage of the total this method will return + * @param total The total + * @param places The number of decimal places to round to + * @return A rounded percentage value + */ + public static double getPercentage(Number x, Number total, int places) { + return round(getPercentage(x, total), places); + } + + /** + * Get a number as the percentage of another. + * + * @param x The number who's percentage of the total this method will return + * @param total The total + * @param places The number of decimal places to return + * @return A formatted {@link String} containing the percentage to the specified number of decimal places. + */ + public static String getPercentageString(Number x, Number total, int places) { + return getPercentage(x, total, places) + "%"; + } + + /** + * Get a number as the percentage of another. + * @param x The number who's percentage of the total this method will return + * @param total The total + * @return A formatted {@link String} containing the percentage to 2 decimal places. + */ + public static String getPercentageString(Number x, Number total) { + return getPercentageString(x, total, 2); + } + + /** + * Try to return long as an int, capped at int max and int min + * + * @param lng The long to convert + * @return {@link Integer} + */ + public static int longToInt(long lng) { + try { + return Math.toIntExact(lng); + } catch (ArithmeticException ae) { + switch (Long.compare(lng, 0)) { + case 1: + return Integer.MAX_VALUE; + case 0: + return 0; + case -1: + return Integer.MIN_VALUE; + default: + throw new ArithmeticException(); // Somehow Long.compare is broken?? This should be impossible + } + } + } + + /** + * Convert a number of bytes to a human-readable value. + * TODO: How the absolute fuck does this work? + * @param bytes the value that should be converted + * @return a human-readable byte value + */ + public static String bytesToReadable(long bytes) { + long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); + if (absB < 1024) { + return bytes + " B"; + } + long value = absB; + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + value *= Long.signum(bytes); + return String.format("%.1f %ciB", value / 1024.0, ci.current()); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/Ordinal.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/Ordinal.java new file mode 100644 index 00000000..de67062f --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/Ordinal.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import com.google.common.base.Preconditions; + +/** + * Utility class for generating ordinals e.g. `1st, 2nd, 3rd` + */ +public final class Ordinal { + private Ordinal() {} + + /** + * Get the appropriate ordinal for the specified number. + * @param number The number to fetch the ordinal for + * @return A {@link String} containing the appropriate ordinal. + */ + public static String getOrdinal(int number) { + Preconditions.checkArgument(number > 0); + // store modulo 10 and 100 of target - useful for calculating things + int mod10 = number % 10; + int mod100 = number % 100; + // st is used with numbers ending in 1 (e.g. 1st, pronounced first) + if (mod10 == 1 && mod100 != 11) { + return "st"; + } + // nd is used with numbers ending in 2 (e.g. 92nd, pronounced ninety-second) + if (mod10 == 2 && mod100 != 12) { + return "nd"; + } + // rd is used with numbers ending in 3 (e.g. 33rd, pronounced thirty-third) + // As an exception to the above rules, all the "teen" numbers ending with 11, 12 or 13 use -th (e.g. 11th, pronounced eleventh, 112th, pronounced one hundred [and] twelfth) + if (mod10 == 3 && mod100 != 13) { + return "rd"; + } + // th is used for all other numbers (e.g. 9th, pronounced ninth). + return "th"; + } + + /** + * Return a number formatted with its ordinal. + * @param number The number to format with its ordinal + * @return A {@link String} containing the number with its ordinal. + */ + public static String withOrdinal(int number) { + return number + getOrdinal(number); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/RandomUtil.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/RandomUtil.java new file mode 100644 index 00000000..5b5737a8 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/RandomUtil.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import com.dumbdogdiner.stickyapi.math.vector.Vector2; +import com.dumbdogdiner.stickyapi.math.vector.Vector3; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Chars; +import com.google.common.primitives.Ints; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.Random; + +/** + * Utility class for dealing with randomness. + */ +public class RandomUtil { + private RandomUtil() { + } + + private static final Random random = new Random(); + + /** + * Get a random number between 0 and the specified maximum value (inclusive). + * @param max The maximum value + * @return A random integer between 0 and the specified maximum value. + */ + public static int randomInt(int max) { + Preconditions.checkArgument(max >= 0, "Argument must be positive"); + // add 1 to method call - not inclusive, so make it inclusive. + return random.nextInt(max + 1); + } + + /** + * Get a random number between 0 and the specified maximum value (exclusive). + * @param max The maximum value + * @return A random integer between 0 and the specified maximum value. + */ + public static int randomIntExclusive(int max) { + Preconditions.checkArgument(max >= 0, "Argument must be positive"); + return random.nextInt(max); + } + + /** + * Get a random number within the specified inclusive range. + * @param min The minimum value (inclusive) + * @param max The maximum value (inclusive) + * @return A random integer within the specified range + * @throws IllegalArgumentException if min is greater than max. + */ + public static int randomInt(int min, int max) { + // validate argument + Preconditions.checkArgument(min <= max, "The minimum value may not be greater than the maximum."); + // exception case + if (min == max) + return min; + return min + randomInt(max - min); + } + + /** + * Get a random number between 0 and 1. This method is exclusive, as doubles are + * (approximately) continuous. While it could generate a value equal to the maximum, + * this is really unlikely + * @return A random double between 0 and 1. + */ + public static double randomDouble() { + return random.nextDouble(); + } + + /** + * Get a random number between 0 and the specified maximum value. This method + * is exclusive, as doubles are (approximately) continuous. While it could generate + * a value equal to the maximum, this is really unlikely. + * @param max maximum value + * @return A random double between 0 and the specified maximum value. + */ + public static double randomDouble(double max) { + return max * random.nextDouble(); + } + + /** + * Get a random double within the specified inclusive range. This method + * is exclusive, as doubles are (approximately) continuous. While it could generate + * a value equal to the maximum, this is really unlikely. + * @param min minimum value + * @param max maximum value + * @return A random double between min and max. + * @throws IllegalArgumentException when min is greater than max + */ + public static double randomDouble(double min, double max) { + // validate argument + Preconditions.checkArgument(min <= max, "The minimum value may not be greater than the maximum."); + // exception case + if (min == max) + return min; + // don't need to add 1 here since doubles will be inclusive as they + // are continuous, unlike integers. + return min + randomDouble(max - min); + } + + /** + * Get a random element from the target array. + * @param array The target array + * @return A random element from the target array. + */ + public static @Nullable T randomElement(@NotNull T @NotNull [] array) { + // exception - array is empty + if (array.length < 1) return null; + // pick random index, return element at index + // have to minus 1 here since this would throw an out of bounds exception + return array[randomIntExclusive(array.length)]; + } + + /** + * Get a random element from the target list. + * @param list The target list + * @return A random element from the target list. + */ + public static @Nullable T randomElement(@NotNull List list) { + // exception - list is empty + if (list.isEmpty()) return null; + // pick random index, return element at index + // have to minus 1 here since this would throw an out of bounds exception + return list.get(randomIntExclusive(list.size())); + } + + /** + * @see #randomElement(List) + * Primitive values cannot be used with type generics, so these methods exist for ease of use. + * @since 3.0 + */ + public static char randomElement(char [] choices) { + return Objects.requireNonNull(randomElement(Chars.asList(choices))); + } + + /** + * @see #randomElement(List) + * Primitive values cannot be used with type generics, so these methods exist for ease of use. + * @since 3.0 + */ + public static int randomElement(int [] choices) { + return Objects.requireNonNull(randomElement(Ints.asList(choices))); + } + + /** + * Return a random boolean with probability p of being true. + * @param probability The probability of returning + * @return A random boolean with probability p of being true. + */ + public static boolean probability(double probability) { + return randomDouble() > probability; + } + + /** + * @return A random angle between 0 and 2pi. + */ + public static double randomAngle() { + return randomDouble(0, Math.PI * 2); + } + + /** + * @return A random angle between -pi and pi. + */ + public static double randomDualAngle() { + return randomDouble(-Math.PI, Math.PI); + } + + /** + * Generate a random {@link Vector2} with magnitude r centered about center. + * @param r The radius, or magnitude of the vector + * @param center The vector around which the output vector will be centered + * @return A random {@link Vector2} with magnitude r. + */ + public static Vector2 randomVector2(double r, Vector2 center) { + return Vector2.fromPolar(r, randomAngle()).add(center); + } + + /** + * Generate a random {@link Vector2} with magnitude r. + * @param r The radius, or magnitude of the vector + * @return A random {@link Vector2} with magnitude r. + */ + public static Vector2 randomVector2(double r) { + return randomVector2(r, Vector2.zero()); + } + + /** + * Generate a random unit {@link Vector2}. This vector will always + * have a magnitude of 1. + * @return A random {@link Vector2} with magnitude 1. + */ + public static Vector2 randomVector2() { + return randomVector2(1); + } + + /** + * Generate a random {@link Vector3} with magnitude r centered about center. + * @param r The radius, or magnitude of the vector + * @param center The vector around which the output vector will be centered + * @return A random {@link Vector3} with magnitude r. + */ + public static Vector3 randomVector3(double r, Vector3 center) { + return Vector3.fromPolar(r, randomAngle(), randomAngle()).add(center); + } + + /** + * Generate a random {@link Vector3} with magnitude r. + * @param r The radius, or magnitude of the vector + * @return A random {@link Vector3} with magnitude r. + */ + public static Vector3 randomVector3(double r) { + return randomVector3(r, Vector3.zero()); + } + + /** + * Generate a random unit {@link Vector3}. This vector will always + * have a magnitude of 1. + * @return A random {@link Vector3} with magnitude 1. + */ + public static Vector3 randomVector3() { + return randomVector3(1); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/CubicBezier.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/CubicBezier.java new file mode 100644 index 00000000..5af2c393 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/CubicBezier.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.function; + +import com.dumbdogdiner.stickyapi.math.vector.Vector2; + +/** + * Represents a cubic bezier curve. + */ +public class CubicBezier { + private final Vector2 a; + private final Vector2 b; + private final Vector2 c; + private final Vector2 d; + + /** + * Construct a new cubic bezier with the specified parameters + * @param a The first parameter + * @param b The second parameter + * @param c The third parameter + * @param d The fourth parameter + */ + public CubicBezier(double a, double b, double c, double d) { + this(new Vector2(a, b), new Vector2(c, d)); + } + + /** + * Construct a new cubic bezier with the specified vector parameters. + * @param b The first vector + * @param c The second vector + */ + public CubicBezier(Vector2 b, Vector2 c) { + this(Vector2.zero(), b, c, Vector2.one()); + } + + /** + * Construct a raw cubic bezier with the specified vector parameters. + * @param a The first vector + * @param b The second vector + * @param c The third vector + * @param d The fourth vector + */ + public CubicBezier(Vector2 a, Vector2 b, Vector2 c, Vector2 d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + /** + * Evaluate this cubic bezier for time val. + * @param val The time parameter + * @return A value based on the cubic bezier. + */ + public Vector2 evaluateToVector(Number val) { + double t = val.doubleValue(); + double q = 1 - t; + + // B(t) = (1 - t)^3P0 + (1-t)^2*tP1 etc. + return this.a.scale(Math.pow(q, 3)).add( + this.b.scale(3 * Math.pow(q, 2) * t) + ).add( + this.c.scale(3 * q * Math.pow(t, 2)) + ).add( + this.d.scale(Math.pow(t, 3)) + ); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/Parametric2.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/Parametric2.java new file mode 100644 index 00000000..8b512faa --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/function/Parametric2.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.function; + +import com.dumbdogdiner.stickyapi.math.vector.Vector2; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +/** + * Represents a parametric curve. + */ +public class Parametric2 { + final Function x; + final Function y; + + /** + * Construct a new parametric curve from the specified functions. + * @param x The function describing the x co-ordinate + * @param y The function describing the y co-ordinate + */ + public Parametric2( + Function<@NotNull Double, @NotNull Double> x, + Function<@NotNull Double, @NotNull Double> y + ) { + this.x = x; + this.y = y; + } + + /** + * Evaluate this parametric curve at time t. + * @param t The time parameter + * @return A {@link Vector2} representing the parametric output. + */ + public Vector2 evaluate(double t) { + return new Vector2( + this.x.apply(t), + this.y.apply(t) + ); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Averages.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Averages.java new file mode 100644 index 00000000..9a5cc607 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Averages.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.stats; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility class for computing various types of average. + */ +public final class Averages { + private Averages() {} + + /** + * Compute the mean of the target dataset. + * @param dataset The target dataset + * @return The mean of the target dataset + */ + public static double getMean(@NotNull List<@NotNull Number> dataset) { + Preconditions.checkNotNull(dataset); + Preconditions.checkArgument(!dataset.isEmpty(), "Cannot compute mean of an empty dataset"); + double acc = 0; + // iterate over dataset and add values to accumulator + for (Number val : dataset) { + Preconditions.checkNotNull(val, "Cannot compute mean of a dataset containing a null value"); + acc += val.doubleValue(); + } + // return acc / length + return acc / dataset.size(); + } + + /** + * Compute the mean of the target dataset. + * @param dataset The target dataset + * @return The mean of the target dataset + */ + public static double getMean(int[] dataset) { + return getMean(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the mean of the target dataset. + * @param dataset The target dataset + * @return The mean of the target dataset + */ + public static double getMean(long[] dataset) { + return getMean(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the mean of the target dataset. + * @param dataset The target dataset + * @return The mean of the target dataset + */ + public static double getMean(double[] dataset) { + return getMean(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the median of the target dataset. Not guaranteed to produce a value in the dataset. + * @param dataset The target dataset + * @return The median of the target dataset. + */ + public static double getMedian(@NotNull List<@NotNull Number> dataset) { + Preconditions.checkNotNull(dataset); + Preconditions.checkArgument(!dataset.isEmpty(), "Cannot compute median of an empty dataset"); + + // sort the dataset + dataset = dataset.stream().sorted().collect(Collectors.toList()); + // compute the center index of the dataset + // integer division rounds down so this works + int center = dataset.size() / 2; + // median exists within the dataset + if (dataset.size() % 2 == 1) { + Number value = dataset.get(center); + // ensure value is not null + Preconditions.checkNotNull(value, "Cannot compute median of a dataset containing a null value"); + return value.doubleValue(); + } + // median is between two values - TODO: comment about the minus sign below vvvv + Number upper = dataset.get(center); + Number lower = dataset.get(center - 1); + Preconditions.checkNotNull(upper,"Cannot compute median of a dataset containing a null value"); + Preconditions.checkNotNull(lower,"Cannot compute median of a dataset containing a null value"); + return (upper.doubleValue() + lower.doubleValue()) / 2; + } + + /** + * Compute the median of the target dataset. Not guaranteed to produce a value in the dataset. + * @param dataset The target dataset + * @return The median of the target dataset. + */ + public static double getMedian(int[] dataset) { + return getMedian(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the median of the target dataset. Not guaranteed to produce a value in the dataset. + * @param dataset The target dataset + * @return The median of the target dataset. + */ + public static double getMedian(long[] dataset) { + return getMedian(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the median of the target dataset. Not guaranteed to produce a value in the dataset. + * @param dataset The target dataset + * @return The median of the target dataset. + */ + public static double getMedian(double[] dataset) { + return getMedian(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the mode (most common value) of the dataset. This method + * does not account for multiple modes. + * @param dataset The target dataset + * @return The mode of the dataset. + */ + public static double getMode(@NotNull List<@NotNull Number> dataset) { + Preconditions.checkNotNull(dataset); + Preconditions.checkArgument(!dataset.isEmpty(), "Cannot compute mode of an empty dataset"); + double mode = -1; + int maxCount = 0; + // iterate over the dataset + for (Number target : dataset) { + int count = 0; + // iterate over the dataset again and compare + for (Number number : dataset) { + // only need to check for null here, since this will catch the outer loop anyway. + Preconditions.checkNotNull(number, "Cannot compute mode of a dataset containing a null value"); + if (number.equals(target)) count++; + } + // if count of this value is greater than the max, set it as new max + if (count > maxCount) { + maxCount = count; + mode = target.doubleValue(); + } + } + // return the mode + return mode; + } + + /** + * Compute the mode (most common value) of the dataset. This method + * does not account for multiple modes. + * @param dataset The target dataset + * @return The mode of the dataset. + */ + public static double getMode(int[] dataset) { + return getMode(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the mode (most common value) of the dataset. This method + * does not account for multiple modes. + * @param dataset The target dataset + * @return The mode of the dataset. + */ + public static double getMode(long[] dataset) { + return getMode(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } + + /** + * Compute the mode (most common value) of the dataset. This method + * does not account for multiple modes. + * @param dataset The target dataset + * @return The mode of the dataset. + */ + public static double getMode(double[] dataset) { + return getMode(Arrays.stream(dataset).boxed().collect(Collectors.toList())); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Distribution.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Distribution.java new file mode 100644 index 00000000..2935ddb6 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/Distribution.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.stats; + +import com.dumbdogdiner.stickyapi.math.NumberUtil; +import com.google.common.base.Preconditions; + +/** + * Utility class for dealing with distribution of data. + */ +public final class Distribution { + private Distribution() {} + + /** + * Calculate the expected value of the target dataset. This is just an alias for the mean + * of the dataset. + * @param dataset The target dataset + * @return The expected value of the target dataset. + */ + public static double expectation(double[] dataset) { + return Averages.getMean(dataset); + } + + /** + * Compute the variance of the target dataset. This is a measure of + * how much the data varies about its mean value. + * @param dataset The target dataset + * @return The variation of the target dataset. + */ + public static double variance(double[] dataset) { + // compute squares of dataset + double[] squares = new double[dataset.length]; + for (int i = 0; i < dataset.length; i++) { + squares[i] = Math.pow(dataset[i], 2); + } + // Var(X) = E(X^2) - E(X)^2 + return expectation(squares) - Math.pow(expectation(dataset), 2); + } + + /** + * Compute the standard deviation of the target dataset. This is a measure + * of how much the data varies about its mean value. + * @param dataset The target dataset + * @return The standard deviation of the target dataset. + */ + public static double standardDeviation(double[] dataset) { + return Math.sqrt(variance(dataset)); + } + + /** + * Compute the covariance of two datasets. + * @param x The first dataset + * @param y The second dataset + * @return The covariance of the two datasets. + */ + public static double covariance(double[] x, double[] y) { + Preconditions.checkArgument(x.length == y.length, "Datasets must be of equal size to compute covariance."); + double[] products = new double[x.length]; + // compute product of values + for (int i = 0; i < x.length; i++) { + products[i] = x[i] * y[i]; + } + // cov(X, Y) = E(XY) - E(X)E(Y) + return expectation(products) - expectation(x) * expectation(y); + } + + /** + * Compute the Pearson correlation coefficient (PCC) of the target datasets. + * This is a measure of how much two datasets correlate with each other. + * @param x The first dataset + * @param y The second dataset + * @return The PCC of the target datasets. + */ + public static double pcc(double[] x, double[] y) { + Preconditions.checkArgument(x.length == y.length, "Datasets must be of equal size to compute PCC."); + return covariance(x, y) / (standardDeviation(x) * standardDeviation(y)); + } + + /** + * Test if two datasets are associated with the specified confidence. + * @param x The first dataset + * @param y The second dataset + * @param confidence The confidence to test with + * @return true if the two datasets are associated. + */ + public static boolean isAssociated(double[] x, double[] y, double confidence) { + Preconditions.checkArgument(NumberUtil.inRange(confidence, 0, 1)); + return Math.abs(pcc(x, y)) > confidence; + } + + /** + * Test if two datasets are associated with the specified confidence. + * @param x The first dataset + * @param y The second dataset + * @return true if the two datasets are associated. + */ + public static boolean isAssociated(double[] x, double[] y) { + return isAssociated(x, y, 0.8); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/package-info.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/package-info.java new file mode 100644 index 00000000..30ff1388 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/stats/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides methods and utilities for computing statistics on datasets. + */ +package com.dumbdogdiner.stickyapi.math.stats; diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2.java new file mode 100644 index 00000000..764b183e --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +import lombok.Getter; + +import java.util.Objects; + +/** + * Represents a 2x2 mathematical, immutable matrix. The matrix has the following layout: + *
+ * (a b)
+ * (c d)
+ * 
+ */ +public class Matrix2 implements Cloneable { + /** + * Generate an anticlockwise rotation matrix for the target angle. + * @param theta The target angle in radians + * @return A {@link Matrix2} representing a rotation theta radians about the origin. + */ + public static Matrix2 getRotationMatrix(double theta) { + return new Matrix2(Math.cos(theta), -Math.sin(theta), Math.sin(theta), Math.cos(theta)); + } + + /** + * Generate an enlargement matrix for the target scale factor. + * @param k The target scale factor + * @return A {@link Matrix2} representing an enlargement scale factor k centered at the origin. + */ + public static Matrix2 getEnlargementMatrix(double k) { + return new Matrix2(k, 0, 0, k); + } + + /** + * Generate a new identity matrix. + * @return A {@link Matrix2} representing an identity matrix. + */ + public static Matrix2 getIdentityMatrix() { + return getEnlargementMatrix(1); + } + + @Getter + private final double a; + @Getter + private final double b; + @Getter + private final double c; + @Getter + private final double d; + + /** + * Construct a new matrix with the following parameters. + * @param a The a parameter of the matrix + * @param b The b parameter of the matrix + * @param c The c parameter of the matrix + * @param d The d parameter of the matrix + */ + public Matrix2(double a, double b, double c, double d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Matrix2 matrix2 = (Matrix2) o; + return Double.compare(matrix2.a, a) == 0 && Double.compare(matrix2.b, b) == 0 && Double.compare(matrix2.c, c) == 0 && Double.compare(matrix2.d, d) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(a, b, c, d); + } + + /** + * Add the target matrix to this matrix. + * @param target The target matrix + * @return The resulting {@link Matrix2}. + */ + public Matrix2 add(Matrix2 target) { + return new Matrix2( + this.a + target.a, + this.b + target.b, + this.c + target.c, + this.b + target.b + ); + } + + /** + * Subtract the target matrix from this matrix. + * @param target The target matrix + * @return The resulting {@link Matrix2}. + */ + public Matrix2 subtract(Matrix2 target) { + return new Matrix2( + this.a - target.a, + this.b - target.b, + this.c - target.c, + this.d - target.d + ); + } + + /** + * Scale this matrix by the target scalar. + * @param k The target scalar + * @return The resulting scaled {@link Matrix2}. + */ + public Matrix2 scale(double k) { + return new Matrix2( + this.a * k, + this.b * k, + this.c * k, + this.d * k + ); + } + + /** + * Pre-multiply the target matrix with this matrix. + * @param target The matrix being pre-multiplied. + * @return The resulting {@link Matrix2} + */ + public Matrix2 preMultiply(Matrix2 target) { + return new Matrix2( + target.a * this.a + target.b * this.c, + target.a * this.b + target.b * this.d, + target.c * this.a + target.d * this.c, + target.c * this.b + target.d * this.d + ); + } + + /** + * Post-multiply the target matrix with this matrix. This is equivalent + * to pre-multiplying the input matrix with this matrix. + * @param target The matrix being post-multiplied. + * @return The resulting {@link Matrix2} + */ + public Matrix2 postMultiply(Matrix2 target) { + return target.preMultiply(this); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix3.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix3.java new file mode 100644 index 00000000..fce09cb4 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/Matrix3.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +public class Matrix3 { +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2.java new file mode 100644 index 00000000..94a08daf --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +import com.dumbdogdiner.stickyapi.math.vector.Vector2; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Applies transformations sequentially. + */ +public class TransformationBuilder2 { + @Getter + private final List transformations = new ArrayList<>(); + + /** + * Apply the transformation to this builder. + * @param transformation The transformation to apply + * @return The {@link TransformationBuilder2} with the new matrix applied. + */ + public TransformationBuilder2 apply(Matrix2 transformation) { + this.transformations.add(transformation); + return this; + } + + /** + * @return The translation builder as a {@link Matrix2} + */ + public Matrix2 toMatrix() { + Matrix2 out = Matrix2.getIdentityMatrix(); + for (Matrix2 matrix : this.transformations) { + out = out.postMultiply(matrix); + } + return out; + } + + /** + * Transform the target matrix through the translation represented by this builder. + * @param input The input vector + * @return The output {@link Vector2} + */ + public Vector2 transform(Vector2 input) { + Matrix2 transform = this.toMatrix(); + return new Vector2( + transform.getA() * input.getX() + transform.getB() * input.getY(), + transform.getC() * input.getX() + transform.getD() * input.getY() + ); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TranslationBuilder3.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TranslationBuilder3.java new file mode 100644 index 00000000..c8844bac --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/TranslationBuilder3.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +/** + * Applies transformations sequentially. + */ +public class TranslationBuilder3 { +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/package-info.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/package-info.java new file mode 100644 index 00000000..1a3ac53c --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/transform/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides implementations of mathematical matrices, used for translating vectors. + */ +package com.dumbdogdiner.stickyapi.math.transform; diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector.java new file mode 100644 index 00000000..a5c80bf5 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.vector; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; + +/** + * Base abstract class for an immutable vector. + * @param + */ +abstract class Vector implements Cloneable { + /** + * @return The number of dimensions this vector has. + */ + protected abstract int getDimensions(); + + /** + * Get an array of values representing this vector. + * @return An array of values representing this vector's components. + */ + protected abstract double[] getValues(); + + /** + * Get the value of the target dimension. + * @param index The index of the dimension + * @return The value of the target dimension. + */ + protected double getDimension(int index) { + Preconditions.checkArgument(index < this.getDimensions(), "Index out of range for vector dimension."); + return this.getValues()[index]; + } + + @Override + public boolean equals(Object vector) { + // check if vector is null or is not a vector + if (!(vector instanceof Vector)) { + return false; + } + // iterate over each dimension and append square to accumulator + for (int i = 0; i < this.getDimensions(); i++) { + if (this.getDimension(i) != ((Vector)vector).getDimension(i)) { + return false; + } + } + // all dimensions equal - must be same vector. + return true; + } + + @Override + public int hashCode() { + // use two prime numbers because they're hot and sexy and help with uniqueness + int hash = 7; + // iterate over each dimension and append hash to accumulator + for (int i = 0; i < this.getDimensions(); i++) { + hash = 31 * hash + Double.hashCode(this.getDimension(i)); + } + return hash; + } + + /** + * Add the target vector to this vector. + * @param vector The target vector + * @return A new {@link Vector}. + */ + abstract @NotNull Vector add(@NotNull Vector vector); + + /** + * Subtract the target vector from this vector. + * @param vector The target vector + * @return A new {@link Vector}. + */ + abstract @NotNull Vector subtract(@NotNull Vector vector); + + /** + * Return the vector from this vector to the target vector. + * @param vector The target vector + * @return The vector from this vector to the target vector. + */ + @NotNull Vector to(@NotNull Vector vector) { + return vector.subtract(this); + }; + + /** + * Scale this vector by the given scalar. + * @param scalar The scalar to scale by + * @return The scaled vector. + */ + abstract @NotNull Vector scale(@NotNull Number scalar); + + /** + * @return The magnitude of this vector. + */ + public double abs() { + double acc = 0; + // iterate over each dimension and append square to accumulator + for (int i = 0; i < this.getDimensions(); i++) { + acc += Math.pow(this.getDimension(i), 2); + } + // return square root of accumulator + return Math.sqrt(acc); + } + + /** + * @return A unit vector of this vector. + */ + @NotNull Vector normalize() { + return this.scale(this.abs()); + } + + /** + * Compute the dot product of two vectors. + * @param vec The other vectors + * @return The dot product of the two vectors. + */ + double dot(@NotNull Vector vec) { + // ensure dimensions are equal. + Preconditions.checkNotNull(vec); + Preconditions.checkArgument(this.getDimensions() == vec.getDimensions(), "Cannot compute dot product of vectors with mismatching dimensions."); + double acc = 0; + // iterate over each dimension and append it to accumulator + for (int i = 0; i < this.getDimensions(); i++) { + acc += this.getDimension(i) * vec.getDimension(i); + } + // return the accumulator value. + return acc; + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector2.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector2.java new file mode 100644 index 00000000..705f9bce --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector2.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.vector; + +import com.google.common.base.Preconditions; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a two-dimensional immutable vector. + */ +public class Vector2 extends Vector { + /** + * @return A zero vector. + */ + public static Vector2 zero() { + return new Vector2(0, 0); + } + + /** + * @return A one vector. + */ + public static Vector2 one() { + return new Vector2(1, 1); + } + + /** + * Create a new Vector2 from polar co-ordinates. + * @param r The radius of the vector + * @param theta The angle from the initial line + * @return A {@link Vector2} + */ + public static Vector2 fromPolar(double r, double theta) { + return new Vector2(r * Math.cos(theta), r * Math.sin(theta)); + } + + /** + * The x value of this vector. + */ + @Getter + private final double x; + + /** + * The y value of this vector. + */ + @Getter + private final double y; + + /** + * Construct a new 2D vector. + * @param x The x value + * @param y The y value + */ + public Vector2(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Construct a new 2D vector. + * @param x The x value + * @param y The y value + */ + public Vector2(@NotNull Number x, @NotNull Number y) { + // ensure arguments are not null + Preconditions.checkNotNull(x); + Preconditions.checkNotNull(y); + this.x = x.doubleValue(); + this.y = y.doubleValue(); + } + + /** + * Construct a new 2D vector from an existing vector. + * @param target The existing vector + */ + public Vector2(@NotNull Vector2 target) { + Preconditions.checkNotNull(target); + this.x = target.x; + this.y = target.y; + } + + @Override + protected int getDimensions() { + return 2; + } + + @Override + protected double[] getValues() { + return new double[]{this.x, this.y}; + } + + @Override + @NotNull + public Vector2 add(@NotNull Vector vector) { + Preconditions.checkNotNull(vector); + return new Vector2(this.x + vector.getDimension(0), this.y + vector.getDimension(1)); + } + + @Override + @NotNull + Vector2 subtract(@NotNull Vector vector) { + Preconditions.checkNotNull(vector); + return new Vector2(this.x - vector.getDimension(0), this.y - vector.getDimension(1)); + } + + @Override + @NotNull + public Vector2 scale(@NotNull Number scalar) { + Preconditions.checkNotNull(scalar); + return new Vector2(this.x * scalar.doubleValue(), this.y * scalar.doubleValue()); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector3.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector3.java new file mode 100644 index 00000000..c46dd299 --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/Vector3.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.vector; + +import com.google.common.base.Preconditions; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a three-dimensional immutable vector. + */ +public class Vector3 extends Vector { + /** + * @return A zero vector. + */ + public static Vector3 zero() { + return new Vector3(0, 0, 0); + } + + /** + * Create a new Vector3 from polar co-ordinates. + * @param r The radius of the vector + * @param theta The 1st angle from the initial line + * @param phi The 2nd angle from the initial line + * @return A {@link Vector3} + */ + public static Vector3 fromPolar(double r, double theta, double phi) { + return new Vector3( + r * Math.sin(theta) * Math.cos(phi), + r * Math.sin(theta) * Math.sin(phi), + r * Math.cos(theta) + ); + } + + /** + * The x value of this vector. + */ + @Getter + private final double x; + + /** + * The y value of this vector. + */ + @Getter + private final double y; + + /** + * The z value of this vector. + */ + @Getter + private final double z; + + /** + * Construct a new 3D vector. + * @param x The x value + * @param y The y value + * @param z The z value + */ + public Vector3(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Construct a new 3D vector. + * @param x The x value + * @param y The y value + * @param z The z value + */ + public Vector3(@NotNull Number x, @NotNull Number y, @NotNull Number z) { + // ensure arguments are not null + Preconditions.checkNotNull(x); + Preconditions.checkNotNull(y); + Preconditions.checkNotNull(z); + this.x = x.doubleValue(); + this.y = y.doubleValue(); + this.z = z.doubleValue(); + } + + /** + * Construct a new 3D vector from an existing vector. + * @param target The existing vector + */ + public Vector3(@NotNull Vector3 target) { + Preconditions.checkNotNull(target); + this.x = target.x; + this.y = target.y; + this.z = target.z; + } + + @Override + protected int getDimensions() { + return 3; + } + + @Override + protected double[] getValues() { + return new double[]{this.x, this.y, this.z}; + } + + @Override + @NotNull + public Vector3 add(@NotNull Vector vector) { + return new Vector3(this.x + vector.getDimension(0), this.y + vector.getDimension(1), this.z + vector.getDimension(2)); + } + + @Override + @NotNull + Vector3 subtract(@NotNull Vector vector) { + return new Vector3(this.x - vector.getDimension(0), this.y - vector.getDimension(1), this.z - vector.getDimension(2)); + } + + @Override + @NotNull + Vector3 scale(@NotNull Number scalar) { + return new Vector3(this.x * scalar.doubleValue(), this.y * scalar.doubleValue(), this.z * scalar.doubleValue()); + } +} diff --git a/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/package-info.java b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/package-info.java new file mode 100644 index 00000000..47e58a7a --- /dev/null +++ b/math/src/main/java/com/dumbdogdiner/stickyapi/math/vector/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides implementations of mathematically oriented, immutable vectors. + */ +package com.dumbdogdiner.stickyapi.math.vector; diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/InterpolationTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/InterpolationTest.java new file mode 100644 index 00000000..b3cf8c0a --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/InterpolationTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class InterpolationTest { + @Test + void testUnclampedLerp() { + // test lerp 0 => 1, t = 0.5 + Assertions.assertEquals(0.5, Interpolation.lerpUnclamped(0, 1, 0.5)); + // test lerp 0 => 2, t = 0.5 + Assertions.assertEquals(1.0, Interpolation.lerpUnclamped(0, 2, 0.5)); + // test lerp 0 => 1, t = 2 + Assertions.assertEquals(2.0, Interpolation.lerpUnclamped(0, 1, 2)); + } + + @Test + void testClamp() { + // test clamp 0 => 1, t = 0.5 + Assertions.assertEquals(0.5, Interpolation.clamp(0, 1, 0.5)); + // test clamp 0 => 2, t = 0.5 + Assertions.assertEquals(0.5, Interpolation.clamp(0, 2, 0.5)); + // test clamp 0 => 1, t = 2 + Assertions.assertEquals(1.0, Interpolation.clamp(0, 1, 2)); + } + + @Test + void testLerp() { + // test clamp 0 => 1, t = 0.5 + Assertions.assertEquals(0.5, Interpolation.lerp(0, 1, 0.5)); + // test clamp 0 => 2, t = 0.5 + Assertions.assertEquals(1.0, Interpolation.lerp(0, 2, 0.5)); + // test clamp 0 => 1, t = 2 + Assertions.assertEquals(1.0, Interpolation.lerp(0, 1, 2)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/NumberUtilTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/NumberUtilTest.java new file mode 100644 index 00000000..128fd073 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/NumberUtilTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class NumberUtilTest { + @Test + void testIsSignedInteger() { + // truthy cases + Assertions.assertTrue(NumberUtil.isInteger("123")); + Assertions.assertTrue(NumberUtil.isInteger("-123")); + // falsey cases + Assertions.assertFalse(NumberUtil.isInteger("")); + Assertions.assertFalse(NumberUtil.isInteger(" ")); + Assertions.assertFalse(NumberUtil.isInteger("12 3")); + Assertions.assertFalse(NumberUtil.isInteger("ab2c")); + Assertions.assertFalse(NumberUtil.isInteger("12-3")); + Assertions.assertFalse(NumberUtil.isInteger("12.3")); + } + + @Test + void testIsUnsignedInteger() { + // truthy cases + Assertions.assertTrue(NumberUtil.isInteger("123", false)); + // falsey cases + Assertions.assertFalse(NumberUtil.isInteger("-123", false)); + Assertions.assertFalse(NumberUtil.isInteger("", false)); + Assertions.assertFalse(NumberUtil.isInteger(" ", false)); + Assertions.assertFalse(NumberUtil.isInteger("12 3", false)); + Assertions.assertFalse(NumberUtil.isInteger("ab2c", false)); + Assertions.assertFalse(NumberUtil.isInteger("12-3", false)); + Assertions.assertFalse(NumberUtil.isInteger("12.3", false)); + } + + @Test + void testIsNumeric() { + // truthy cases + Assertions.assertTrue(NumberUtil.isNumeric("123")); + Assertions.assertTrue(NumberUtil.isNumeric("-123")); + Assertions.assertTrue(NumberUtil.isNumeric("-123.123")); + Assertions.assertTrue(NumberUtil.isNumeric("-123e123")); + Assertions.assertTrue(NumberUtil.isNumeric("-123e-123")); + Assertions.assertTrue(NumberUtil.isNumeric("-123.123e-123")); + // falsey cases + Assertions.assertFalse(NumberUtil.isInteger("", false)); + Assertions.assertFalse(NumberUtil.isInteger(" ", false)); + Assertions.assertFalse(NumberUtil.isInteger("12 3", false)); + Assertions.assertFalse(NumberUtil.isInteger("ab2c", false)); + Assertions.assertFalse(NumberUtil.isInteger("12-3", false)); + } + + @Test + void testIntHelper() { + Assertions.assertEquals(NumberUtil.longToInt(1L), 1); + } + + @Test + void testIntHelperMax() { + Assertions.assertEquals(NumberUtil.longToInt(((long)Integer.MAX_VALUE) + 1L), Integer.MAX_VALUE); + } + + @Test + void testIntHelperMin() { + Assertions.assertEquals(NumberUtil.longToInt(((long) Integer.MIN_VALUE) - 1L), Integer.MIN_VALUE); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/OrdinalTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/OrdinalTest.java new file mode 100644 index 00000000..117618d5 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/OrdinalTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class OrdinalTest { + @Test + void testFirst() { + Assertions.assertEquals("st", Ordinal.getOrdinal(1)); + // mod 10 + Assertions.assertEquals("st", Ordinal.getOrdinal(21)); + // mod 100 + Assertions.assertEquals("st", Ordinal.getOrdinal(101)); + } + + @Test + void testSecond() { + Assertions.assertEquals("nd", Ordinal.getOrdinal(2)); + // mod 10 + Assertions.assertEquals("nd", Ordinal.getOrdinal(22)); + // mod 100 + Assertions.assertEquals("nd", Ordinal.getOrdinal(102)); + } + + @Test + void testThird() { + Assertions.assertEquals("rd", Ordinal.getOrdinal(3)); + // mod 10 + Assertions.assertEquals("rd", Ordinal.getOrdinal(23)); + // mod 100 + Assertions.assertEquals("rd", Ordinal.getOrdinal(103)); + } + + @Test + void testTeenException() { + Assertions.assertEquals("th", Ordinal.getOrdinal(11)); + Assertions.assertEquals("th", Ordinal.getOrdinal(12)); + Assertions.assertEquals("th", Ordinal.getOrdinal(13)); + // mod 100 + Assertions.assertEquals("th", Ordinal.getOrdinal(111)); + } + + @Test + void testGeneral() { + Assertions.assertEquals("th", Ordinal.getOrdinal(4)); + // mod 10 + Assertions.assertEquals("th", Ordinal.getOrdinal(14)); + // mod 100 + Assertions.assertEquals("th", Ordinal.getOrdinal(104)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/RandomUtilTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/RandomUtilTest.java new file mode 100644 index 00000000..f22243f3 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/RandomUtilTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test;; + +// TODO: Random element tests +class RandomUtilTest { + @RepeatedTest(100) + void testMaxRandomInt() { + // this method is inclusive, so test for inclusivity. + Assertions.assertTrue(RandomUtil.randomInt(5) <= 5); + } + + @RepeatedTest(100) + void testMaxRandomIntExclusive() { + // this method is exclusive, so test for exclusivity. + Assertions.assertTrue(RandomUtil.randomIntExclusive(5) < 5); + } + + @Test + void testRandomIntNonPositiveArgument() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + RandomUtil.randomInt(-1); + }); + } + + @RepeatedTest(100) + void testRangedRandomInt() { + // this method is inclusive, so test for inclusivity. + int value = RandomUtil.randomInt(3, 7); + Assertions.assertTrue(value >= 3 && value <= 7); + } + + @Test + void testRangedRandomIntIllegalRange() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + RandomUtil.randomInt(1, 0); + }); + } + + @RepeatedTest(100) + void testRangedRandomIntNegativeRange() { + Assertions.assertDoesNotThrow(() -> { + RandomUtil.randomInt(-5, 5); + }); + } + + @RepeatedTest(100) + void testMaxRandomDouble() { + // this method is exclusive, so test for exclusivity. + Assertions.assertTrue(RandomUtil.randomDouble(5) < 5); + } + + @RepeatedTest(100) + void testRangedRandomDouble() { + // this method is inclusive, so test for inclusivity. + double value = RandomUtil.randomDouble(3, 7); + Assertions.assertTrue(value >= 3 && value <= 7); + } + + @RepeatedTest(100) + void testRandomAngle() { + double value = RandomUtil.randomAngle(); + Assertions.assertTrue(value >= 0 && value <= Math.PI * 2); + } + + @RepeatedTest(100) + void testRandomDualAngle() { + double value = RandomUtil.randomDualAngle(); + Assertions.assertTrue(value >= -Math.PI && value <= Math.PI); + } + + @RepeatedTest(100) + void testRandomVector2() { + Assertions.assertTrue(NumberUtil.almostEquals(RandomUtil.randomVector2().abs(), 1)); + Assertions.assertTrue(NumberUtil.almostEquals(RandomUtil.randomVector2(2).abs(), 2)); + } + + @RepeatedTest(100) + void testRandomVector3() { + System.out.println(RandomUtil.randomVector3().abs()); + Assertions.assertTrue(NumberUtil.almostEquals(RandomUtil.randomVector3().abs(), 1)); + Assertions.assertTrue(NumberUtil.almostEquals(RandomUtil.randomVector3(2).abs(), 2)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/function/Parametric2Test.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/function/Parametric2Test.java new file mode 100644 index 00000000..de7f989e --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/function/Parametric2Test.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.function; + +import com.dumbdogdiner.stickyapi.math.vector.Vector2; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Parametric2Test { + @Test + void testParametric2() { + Parametric2 curve = new Parametric2( + (t) -> Math.pow(t,2), + (t) -> Math.pow(t, 2) - 3 * t + ); + Assertions.assertEquals(new Vector2(9, 0), curve.evaluate(3)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/AveragesTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/AveragesTest.java new file mode 100644 index 00000000..75605f48 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/AveragesTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.stats; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +class AveragesTest { + @Test + void testMean() { + // primitive lists + int[] primitiveIntDataset = {1, 2, 3}; + long[] primitiveLongDataset = {1, 2, 3}; + double[] primitiveDoubleDataset = {1, 2, 3}; + Assertions.assertEquals(2, Averages.getMean(primitiveIntDataset)); + Assertions.assertEquals(2, Averages.getMean(primitiveLongDataset)); + Assertions.assertEquals(2, Averages.getMean(primitiveDoubleDataset)); + // boxed list + List dataset = Arrays.stream(primitiveDoubleDataset).boxed().collect(Collectors.toList()); + Assertions.assertEquals(2, Averages.getMean(dataset)); + } + + @Test + void testMedian() { + // primitive lists + int[] primitiveIntDataset = {1, 2, 3}; + long[] primitiveLongDataset = {1, 2, 3}; + double[] primitiveDoubleDataset = {1, 2, 3}; + Assertions.assertEquals(2, Averages.getMedian(primitiveIntDataset)); + Assertions.assertEquals(2, Averages.getMedian(primitiveLongDataset)); + Assertions.assertEquals(2, Averages.getMedian(primitiveDoubleDataset)); + // boxed list + List dataset = Arrays.stream(primitiveDoubleDataset).boxed().collect(Collectors.toList()); + Assertions.assertEquals(2, Averages.getMedian(dataset)); + + // intermediate values + primitiveIntDataset = new int[]{1, 2, 3, 4}; + primitiveLongDataset = new long[]{1, 2, 3, 4}; + primitiveDoubleDataset = new double[]{1, 2, 3, 4}; + Assertions.assertEquals(2.5, Averages.getMedian(primitiveIntDataset)); + Assertions.assertEquals(2.5, Averages.getMedian(primitiveLongDataset)); + Assertions.assertEquals(2.5, Averages.getMedian(primitiveDoubleDataset)); + + dataset = Arrays.stream(primitiveDoubleDataset).boxed().collect(Collectors.toList()); + Assertions.assertEquals(2.5, Averages.getMedian(dataset)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/DistributionTest.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/DistributionTest.java new file mode 100644 index 00000000..2049c3e8 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/stats/DistributionTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.stats; + +import com.dumbdogdiner.stickyapi.math.NumberUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DistributionTest { + static final double[] DATASET_X = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + static final double[] DATASET_Y = {9, 8, 7, 6, 5, 4, 3, 2, 1}; + + @Test + void testExpectation() { + Assertions.assertEquals(5, Distribution.expectation(DATASET_X)); + Assertions.assertEquals(5, Distribution.expectation(DATASET_Y)); + } + + @Test + void testVariance() { + Assertions.assertEquals(6.666666666666668, Distribution.variance(DATASET_X)); + Assertions.assertEquals(6.666666666666668, Distribution.variance(DATASET_Y)); + } + + @Test + void testStandardDeviation() { + Assertions.assertEquals(2.5819888974716116, Distribution.standardDeviation(DATASET_X)); + Assertions.assertEquals(2.5819888974716116, Distribution.standardDeviation(DATASET_Y)); + } + + @Test + void testPCC() { + Assertions.assertTrue(NumberUtil.almostEquals(-1, Distribution.pcc(DATASET_X, DATASET_Y))); + } + + @Test + void testIsAssociated() { + Assertions.assertTrue(NumberUtil.almostEquals(-1, Distribution.pcc(DATASET_X, DATASET_Y))); + Assertions.assertTrue(Distribution.isAssociated(DATASET_X, DATASET_Y)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2Test.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2Test.java new file mode 100644 index 00000000..fad9a830 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/Matrix2Test.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Matrix2Test { + @Test + void testMatrix2Equals() { + Matrix2 a = Matrix2.getIdentityMatrix(); + Assertions.assertEquals(new Matrix2(1, 0, 0 ,1), a); + } + + @Test + void testMatrix2Identity() { + Matrix2 a = new Matrix2(1, 2, 3, 4); + Assertions.assertEquals(a, a.preMultiply(Matrix2.getIdentityMatrix())); + Assertions.assertEquals(a, a.postMultiply(Matrix2.getIdentityMatrix())); + } + + @Test + void testMatrix2Add() { + Matrix2 a = new Matrix2(1, 2, 3, 4); + Matrix2 b = new Matrix2(4, 3, 2, 1); + Assertions.assertEquals(new Matrix2(5, 5, 5, 5), a.add(b)); + } + + @Test + void testMatrix2Subtract() { + Matrix2 a = new Matrix2(4, 8, 7, 4); + Matrix2 b = new Matrix2(5, 4, 6, 5); + Assertions.assertEquals(new Matrix2(-1, 4, 1, -1), a.subtract(b)); + } + + @Test + void testMatrix2Scale() { + Matrix2 a = new Matrix2(1, 7, -6, 2); + Assertions.assertEquals(a, a.scale(1)); + Assertions.assertEquals(new Matrix2(2, 14, -12, 4), a.scale(2)); + Assertions.assertEquals(new Matrix2(-1, -7, 6, -2), a.scale(-1)); + } + + @Test + void testMatrixPreMultiply() { + Matrix2 a = new Matrix2(1, 2, 3, 4); + Matrix2 b = new Matrix2(4, 3, 2, 1); + Assertions.assertEquals(new Matrix2(13, 20, 5, 8), a.preMultiply(b)); + } + + @Test + void testMatrixPostMultiply() { + Matrix2 a = new Matrix2(1, 2, 3, 4); + Matrix2 b = new Matrix2(4, 3, 2, 1); + Assertions.assertEquals(new Matrix2(8, 5, 20, 13), a.postMultiply(b)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2Test.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2Test.java new file mode 100644 index 00000000..9b19ac6b --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/transform/TransformationBuilder2Test.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.transform; + +import com.dumbdogdiner.stickyapi.math.NumberUtil; +import com.dumbdogdiner.stickyapi.math.vector.Vector2; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TransformationBuilder2Test { + @Test + void testSimpleTransformation() { + // enlargement s.f. 2 + TransformationBuilder2 builder = new TransformationBuilder2().apply(Matrix2.getEnlargementMatrix(2)); + Assertions.assertEquals(new Vector2(2, -4), builder.transform(new Vector2(1, -2))); + } + + @Test + void testRepeatedTransformation() { + // enlargement s.f. 2, then rotate pi radians + TransformationBuilder2 builder = new TransformationBuilder2() + .apply(Matrix2.getEnlargementMatrix(2)) + .apply(Matrix2.getEnlargementMatrix(2)); + Assertions.assertEquals(new Vector2(8, -16), builder.transform(new Vector2(2, -4))); + } + + @Test + void testInverseTransformation() { + // enlargement s.f. 2, then back again + TransformationBuilder2 builder = new TransformationBuilder2() + .apply(Matrix2.getEnlargementMatrix(2)) + .apply(Matrix2.getEnlargementMatrix(0.5)); + Vector2 a = new Vector2(2, -4); + Assertions.assertEquals(a, builder.transform(a)); + } + + @Test + void testRotationTransformation() { + TransformationBuilder2 builder = new TransformationBuilder2() + .apply(Matrix2.getRotationMatrix(Math.PI)); + // translate vectors + Vector2 original = new Vector2(1, 1); + Vector2 translated = builder.transform(original); + // due to floating point inaccuracy + Assertions.assertTrue(NumberUtil.almostEquals(-1, translated.getX())); + Assertions.assertTrue(NumberUtil.almostEquals(-1, translated.getY())); + } + + @Test + void testCompositeTransformation() { + TransformationBuilder2 builder = new TransformationBuilder2() + .apply(Matrix2.getEnlargementMatrix(2)) + .apply(Matrix2.getRotationMatrix(Math.PI)); + // translate vectors + Vector2 original = new Vector2(1, 1); + Vector2 translated = builder.transform(original); + // due to floating point inaccuracy + Assertions.assertTrue(NumberUtil.almostEquals(-2, translated.getX())); + Assertions.assertTrue(NumberUtil.almostEquals(-2, translated.getY())); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector2Test.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector2Test.java new file mode 100644 index 00000000..3e1f99a6 --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector2Test.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.vector; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Vector2Test { + @Test + void testVector2Equal() { + Assertions.assertEquals(new Vector2(1, 2), new Vector2(1, 2)); + } + + @Test + void testVector2Zero() { + Assertions.assertEquals(new Vector2(0 ,0), Vector2.zero()); + } + + @Test + void testVector2Add() { + Vector2 a = new Vector2(0, 1); + Vector2 b = new Vector2(1, 1); + Assertions.assertEquals(new Vector2(1, 2), a.add(b)); + } + + @Test + void testVector2Subtract() { + Vector2 a = new Vector2(0, 1); + Vector2 b = new Vector2(1, 1); + Assertions.assertEquals(new Vector2(-1, 0), a.subtract(b)); + } + + @Test + void testVector2Scale() { + Vector2 a = new Vector2(10, 5); + // check unitary scale + Assertions.assertEquals(a, a.scale(1)); + // check 2 scale + Assertions.assertEquals(new Vector2(20, 10), a.scale(2)); + // check negative scale + Assertions.assertEquals(new Vector2(-10, -5), a.scale(-1)); + } + + @Test + void testVector2To() { + Vector2 a = new Vector2(0, 1); + Vector2 b = new Vector2(1, 0); + Assertions.assertEquals(new Vector2(1, -1), a.to(b)); + } +} diff --git a/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector3Test.java b/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector3Test.java new file mode 100644 index 00000000..517afa1c --- /dev/null +++ b/math/src/test/java/com/dumbdogdiner/stickyapi/math/vector/Vector3Test.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.math.vector; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Vector3Test { + @Test + void testVector3Equal() { + Assertions.assertEquals(new Vector3(1, 2, 3), new Vector3(1, 2, 3)); + } + + @Test + void testVector3Zero() { + Assertions.assertEquals(new Vector3(0 ,0, 0), Vector3.zero()); + } + + @Test + void testVector3Add() { + Vector3 a = new Vector3(0, 1, 2); + Vector3 b = new Vector3(1, 1, 1); + Assertions.assertEquals(new Vector3(1, 2, 3), a.add(b)); + } + + @Test + void testVector3Subtract() { + Vector3 a = new Vector3(0, 1, 2); + Vector3 b = new Vector3(1, 1, 1); + Assertions.assertEquals(new Vector3(-1, 0, 1), a.subtract(b)); + } + + @Test + void testVector3Scale() { + Vector3 a = new Vector3(10, 5, 20); + // check unitary scale + Assertions.assertEquals(a, a.scale(1)); + // check 2 scale + Assertions.assertEquals(new Vector3(20, 10, 40), a.scale(2)); + // check negative scale + Assertions.assertEquals(new Vector3(-10, -5, -20), a.scale(-1)); + } + + @Test + void testVector3To() { + Vector3 a = new Vector3(0, 1, 2); + Vector3 b = new Vector3(1, 1, 1); + // TODO: skye do some math and work out what this should be you lazy hoe + Assertions.assertEquals(new Vector3(1, 0, -1), a.to(b)); + } +} diff --git a/settings.gradle b/settings.gradle index d107c168..6395b49b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,5 +2,6 @@ rootProject.name = "stickyapi" include ":common" include ":common:serverversion" // subproject that's shaded into common include ":config" // common subproject; not included in common jar +include ":math" include ":bukkit" include ":bungee"