Skip to content

Commit 36b57d8

Browse files
sovdeethEfnilite
andauthored
Add tolerance to divisible by condition (#7931)
* add tolerance to divisible by * Apply suggestions from code review Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> * Update CondIsDivisibleBy.java --------- Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com>
1 parent aa84c88 commit 36b57d8

File tree

2 files changed

+62
-10
lines changed

2 files changed

+62
-10
lines changed

src/main/java/ch/njol/skript/conditions/CondIsDivisibleBy.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ch.njol.skript.conditions;
22

33
import ch.njol.skript.Skript;
4+
import ch.njol.skript.config.Node;
45
import ch.njol.skript.doc.Description;
56
import ch.njol.skript.doc.Examples;
67
import ch.njol.skript.doc.Name;
@@ -11,33 +12,44 @@
1112
import ch.njol.util.Kleenean;
1213
import org.bukkit.event.Event;
1314
import org.jetbrains.annotations.Nullable;
15+
import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer;
1416

1517
@Name("Is Evenly Divisible By")
16-
@Description("Check if a number is evenly divisible by another number.")
18+
@Description("""
19+
Checks if a number is evenly divisible by another number.
20+
An optional tolerance can be provided to counteract floating point error. The default tolerance is 1e-10.
21+
Any input smaller than the tolerance is considered to be 0.
22+
This means divisors that are too small will always return false, and dividends that are too small will always return true.
23+
""")
1724
@Examples({
1825
"if 5 is evenly divisible by 5:",
1926
"if 11 cannot be evenly divided by 10:",
27+
"if 0.3 can be evenly divided by 0.1 with a tolerance of 0.0000001:"
2028
})
21-
@Since("2.10")
22-
public class CondIsDivisibleBy extends Condition {
29+
@Since("2.10, INSERT VERSION (tolerance)")
30+
public class CondIsDivisibleBy extends Condition implements SyntaxRuntimeErrorProducer {
2331

2432
static {
2533
Skript.registerCondition(CondIsDivisibleBy.class,
26-
"%numbers% (is|are) evenly divisible by %number%",
27-
"%numbers% (isn't|is not|aren't|are not) evenly divisible by %number%",
28-
"%numbers% can be evenly divided by %number%",
29-
"%numbers% (can't|can[ ]not) be evenly divided by %number%");
34+
"%numbers% (is|are) evenly divisible by %number% [with [a] tolerance [of] %-number%]",
35+
"%numbers% (isn't|is not|aren't|are not) evenly divisible by %number% [with [a] tolerance [of] %-number%]",
36+
"%numbers% can be evenly divided by %number% [with [a] tolerance [of] %-number%]",
37+
"%numbers% (can't|can[ ]not) be evenly divided by %number% [with [a] tolerance [of] %-number%]");
3038
}
31-
@SuppressWarnings("null")
39+
3240
private Expression<Number> dividend;
33-
@SuppressWarnings("null")
3441
private Expression<Number> divisor;
42+
private @Nullable Expression<Number> epsilon;
43+
private Node node;
3544

3645
@Override
46+
@SuppressWarnings("unchecked")
3747
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
3848
dividend = (Expression<Number>) exprs[0];
3949
divisor = (Expression<Number>) exprs[1];
50+
epsilon = (Expression<Number>) exprs[2];
4051
setNegated(matchedPattern == 1 || matchedPattern == 3);
52+
node = getParser().getNode();
4153
return true;
4254
}
4355

@@ -47,7 +59,36 @@ public boolean check(Event event) {
4759
if (divisorNumber == null)
4860
return isNegated();
4961
double divisor = divisorNumber.doubleValue();
50-
return dividend.check(event, dividendNumber -> (dividendNumber.doubleValue() % divisor == 0), isNegated());
62+
if (divisor == 0) {
63+
// Division by zero never passes
64+
return isNegated();
65+
}
66+
67+
Number epsilonNumber = epsilon != null ? epsilon.getSingle(event) : Skript.EPSILON;
68+
if (epsilonNumber == null) {
69+
epsilonNumber = Skript.EPSILON;
70+
}
71+
double epsilon = epsilonNumber.doubleValue();
72+
73+
if (epsilon <= 0 || Double.isNaN(epsilon)) {
74+
error("Tolerance must be a positive, non-zero number, but was " + epsilonNumber + ".");
75+
return isNegated();
76+
}
77+
78+
if (divisor < epsilon) {
79+
// If the divisor is smaller than the tolerance, we consider it to be 0
80+
return isNegated();
81+
}
82+
83+
return dividend.check(event, dividendNumber -> {
84+
double remainder = Math.abs(dividendNumber.doubleValue() % divisor);
85+
return remainder <= epsilon || remainder >= divisor - epsilon;
86+
}, isNegated());
87+
}
88+
89+
@Override
90+
public Node getNode() {
91+
return node;
5192
}
5293

5394
@Override

src/test/skript/tests/syntaxes/conditions/CondIsDivisibleBy.sk

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,14 @@ test "is divisible":
1414
assert {_num} can be divided by 5 to fail with "You cannot divide by a string!"
1515
set {_numlist::*} to "5", "10", and "15"
1616
assert {_numlist::*} can be divided by 5 to fail with "You cannot divide by a string!"
17+
18+
test "tolerances":
19+
assert 0.3 can be evenly divided by 0.1 with "floating point error with 0.3 / 0.1"
20+
assert 0.3 can be evenly divided by 0.1 with tolerance 0.01 with "floating point error with 0.3 / 0.1 at high tolerance"
21+
assert 0.3 can be evenly divided by 0.1 with tolerance {_} with "floating point error with 0.3 / 0.1 at none tolerance"
22+
assert 300000 is evenly divisible by 1/3 with "floating point error with 300000 / 1/3"
23+
assert 3000000 is not evenly divisible by 1/3 with "unexpected floating point success with 3000000 / 1/3"
24+
assert 3000000 is evenly divisible by 1/3 with tolerance 0.0001 with "unexpected floating point failure with 3000000 / 1/3 at high tolerance"
25+
assert 5 is evenly divisible by 2.5 with tolerance 3 to fail with "too small divisor passed"
26+
assert 0 is evenly divisible by 2 with tolerance 1 with "too small dividend failed"
27+

0 commit comments

Comments
 (0)