Skip to content

Commit

Permalink
Add Vec3 with string parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Tisawesomeness committed Sep 21, 2023
1 parent 55dad2f commit 847cc7c
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.tisawesomeness.minecord.mc.pos;

import com.tisawesomeness.minecord.util.Mth;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import javax.annotation.Nullable;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Getter
@AllArgsConstructor
@EqualsAndHashCode
public class Vec3 {

private static final String LEFT_BRACKETS = "([{<";
private static final String RIGHT_BRACKETS = ")]}>";

// Regex used to parse a coordinate
// prefix: `([xyz] *[=:]? *)`, matches "x", "x:", "x=", with optional spaces
// num: `([+-]?\d*\.?\d+)`, matches a number with optional decimal part, ".9" matches but "9." doesn't
// sep: `(?: +| *[,/] *|(?=[xyz]))`, matches a separator, which can be either:
// - 1+ spaces
// - ',' or '/' with optional spaces
// - nothing, as long as the next number begins with a prefix (next char is in [xyz])
// Complete pattern is `{prefix}?{num}{sep}{prefix}?{num}{sep}{prefix}?{num}`
// Prefixes are groups 1, 3, 5, numbers are groups 4, 5, 6, separator is non-capturing
private static final Pattern PARSE_REGEX = Pattern.compile("([xyz] *[=:]? *)?([+-]?\\d*\\.?\\d+)(?: +| *[,/] *|(?=[xyz]))([xyz] *[=:]? *)?([+-]?\\d*\\.?\\d+)(?: +| *[,/] *|(?=[xyz]))([xyz] *[=:]? *)?([+-]?\\d*\\.?\\d+)", Pattern.CASE_INSENSITIVE);

private final double x;
private final double y;
private final double z;

public static Optional<Vec3> parse(String str) {
// Shortest possible input is "1 2 3", reject shorter strings
if (str.length() < 5) {
return Optional.empty();
}

// Regex doesn't take into account brackets, any extra chars will cause match to fail
Optional<String> trimmedOpt = trimBrackets(str);
if (!trimmedOpt.isPresent()) {
return Optional.empty();
}
String trimmed = trimmedOpt.get().trim(); // This trim is necessary

// Entire trimmed string must match to prevent "a3, 4, 5" from parsing
Matcher matcher = PARSE_REGEX.matcher(trimmed);
if (!matcher.matches()) {
return Optional.empty();
}

// Case 1: no prefixes used (ex: "73, 4, -5")
String prefix1 = matcher.group(1);
String prefix2 = matcher.group(3);
String prefix3 = matcher.group(5);
if (prefix1 == null && prefix2 == null && prefix3 == null) {
String numStr1 = matcher.group(2);
String numStr2 = matcher.group(4);
String numStr3 = matcher.group(6);
return parse(numStr1, numStr2, numStr3);
}

// Case 2: all prefixes used (ex: "x=73, y=4, z=-5")
// If prefixes only used on some numbers, input is ambiguous and rejected
// x, y, and z can be processed in any order, but if any strings are left null, they'll fail in parse(String...)
String xStr = null;
String yStr = null;
String zStr = null;
for (int i = 0; i < 3; i++) {
String prefix = matcher.group(2 * i + 1);
if (prefix == null) {
return Optional.empty();
}
String numStr = matcher.group(2 * i + 2);
// [xyz] is always the first character matched in the prefix group
char maybeXyz = Character.toLowerCase(prefix.charAt(0));
switch (maybeXyz) {
case 'x':
xStr = numStr;
break;
case 'y':
yStr = numStr;
break;
case 'z':
zStr = numStr;
break;
default:
throw new AssertionError("unreachable");
}
}
return parse(xStr, yStr, zStr);
}

private static Optional<Vec3> parse(@Nullable String xStr, @Nullable String yStr, @Nullable String zStr) {
OptionalDouble xOpt = Mth.safeParseDouble(xStr);
if (!xOpt.isPresent()) {
return Optional.empty();
}
OptionalDouble yOpt = Mth.safeParseDouble(yStr);
if (!yOpt.isPresent()) {
return Optional.empty();
}
OptionalDouble zOpt = Mth.safeParseDouble(zStr);
if (!zOpt.isPresent()) {
return Optional.empty();
}
return Optional.of(new Vec3(xOpt.getAsDouble(), yOpt.getAsDouble(), zOpt.getAsDouble()));
}

// Trims brackets in the first/last character if they exist, returning empty if brackets are mismatched
private static Optional<String> trimBrackets(String str) {
assert str.length() >= 2;
char first = str.charAt(0);
char last = str.charAt(str.length() - 1);

// If first character isn't a bracket, no need to trim brackets
int bracketId = LEFT_BRACKETS.indexOf(first);
if (bracketId == -1) {
return Optional.of(str);
}
// Brackets must match
if (last == RIGHT_BRACKETS.charAt(bracketId)) {
return Optional.of(str.substring(1, str.length() - 1));
} else {
return Optional.empty();
}
}

public Vec3i round() {
return new Vec3i((int) Math.round(x), (int) Math.round(y), (int) Math.round(z));
}

@Override
public String toString() {
return x + ", " + y + ", " + z;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.concurrent.ThreadLocalRandom;
Expand Down Expand Up @@ -244,6 +245,22 @@ private static MessageDigest getDigest() {
return MessageDigest.getInstance("SHA-1");
}

/**
* Parses a double from a string.
* @param str The string to parse, may or may not be a double
* @return The double if present, empty if null
*/
public static OptionalDouble safeParseDouble(@Nullable String str) {
if (str == null) {
return OptionalDouble.empty();
}
try {
return OptionalDouble.of(Double.parseDouble(str));
} catch (NumberFormatException ignore) {
return OptionalDouble.empty();
}
}



/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.tisawesomeness.minecord.mc.pos;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;

public class Vec3Test {

@ParameterizedTest
@ValueSource(strings = {
"37, -5, 1056",
"37,-5,1056",
"37 / -5 / 1056",
"37 -5 1056",
"37 -5 1056",
"x = 37, y = -5, z = 1056",
"x=37, y=-5, z=1056",
"x=37,y=-5,z=1056",
"X=37, Y=-5, Z=1056",
"x:37, y:-5, z:1056",
"x: 37, y: -5, z: 1056",
"y: -5, z: 1056, x: 37",
"x37y-5z1056",
"(37, -5, 1056)",
"(37 -5 1056)",
"<37, -5, 1056>",
"[37, -5, 1056]",
"{37, -5, 1056}",
"(x:37, y:-5, z:1056)",
"37.0, -5.0, 1056.0",
"37.2, -4.9, 1055.5"
})
public void testParse(String input) {
assertThat(Vec3.parse(input).map(Vec3::round))
.contains(new Vec3i(37, -5, 1056));
}
@ParameterizedTest
@ValueSource(strings = {
"",
"nonsense",
"x",
"x:",
"37, -5",
"37,-51056",
"37, -5, 1056, 783",
"37, -, 1056",
"a, -5, 1056",
"37, a, 1056",
"37, -5, a",
"=37, y=-5, z=1056",
"x:37, :-5, z:1056",
"y:37, y:-5, z:1056",
"(37, -5, 1056>",
"((37, -5, 1056)",
"(37, -5, 1056))",
"37, -5, 1056)",
"(37, -5, 1056",
"(invalid37, -5, 1056)",
"37,,-5,1056",
"x:37 ,-5.0 / 1056 ",
"37, y=-5, z=1056"
})
public void testParseInvalid(String input) {
assertThat(Vec3.parse(input))
.isEmpty();
}

@Test
public void testRound() {
assertThat(new Vec3(2.3, 4.5, -1.7).round())
.isEqualTo(new Vec3i(2, 5, -2));
}
@Test
public void testRoundZero() {
assertThat(new Vec3(0.0, 0.0, 0.0).round())
.isEqualTo(new Vec3i(0, 0, 0));
}

}

0 comments on commit 847cc7c

Please sign in to comment.