I just ran afoul of this defect and am necroing this thread as it’s Google’s most prominent result.
To validate what I was seeing, I created the following test and inspected the values during PIE as well as printing them to the log.
//Note: the float version of the function is deprecated and defaults to the double variant.
KismetMathLibrary.cpp
Anyway, here’s a rudimentary “fix” along with basic test that passes all but the 1e6 / 3
test case. @twiddle I’d love for some input incase I’ve missed something obvious.
Flawed 1st attempt - Ignore
#define EPSILON 0.0001f
UCLASS(meta = (BlueprintThreadSafe))
class VR_BASE_API UMathBPFL : public UBlueprintFunctionLibrary {
GENERATED_BODY()
public:
/// <summary>Returns the number of times the divisor goes into the dividend along with the remainder.</summary>
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Math", meta = (DisplayName = "Division (Whole, Remainder)"))
static void Division(float dividend, float divisor, float& outWhole, float& outRem) {
if (divisor != 0.0f) {
const float quotient = dividend / divisor;
if (quotient >= 0) {
outWhole = FMath::Floor(quotient);
} else {
outWhole = FMath::CeilToFloat(quotient);
}
outRem = quotient - outWhole;
} else {
UE_LOG(MathExtensions, Warning, TEXT("Attempted to divide by zero - Returning 0 instead of collapsing the universe."));
outWhole = 0.0f;
outRem = 0.0f;
}
}
UFUNCTION(BlueprintCallable, Category = "Math", meta = (DisplayName = "Test Division"))
static void TestDivision() {
struct TestCase {
const FString description;
float dividend;
float divisor;
float expectedWhole;
float expectedRem;
};
TestCase cases[] = {
//Standard cases
{TEXT("5 / 2"), 5.0f, 2.0f, 2.0f, 0.5f},
{TEXT("2 / 5"), 2.0f, 5.0f, 0.0f, 0.4f},
{TEXT("-5 / 2"), -5.0f, 2.0f, -2.0f, -0.5f},
{TEXT("-2 / 5"), -2.0f, 5.0f, 0.0f, -0.4f},
{TEXT("-5 / -2"), -5.0f, -2.0f, 2.0f, 0.5f},
{TEXT("-2 / -5"), -2.0f, -5.0f, 0.0f, 0.4f},
{TEXT("0.7 / 0.05"), 0.7f, 0.05f, 14.0f, 0.0f},
{TEXT("0.05 / 0.7"), 0.05f, 0.7f, 0.0f, 0.0714f},
//Zero dividend
{TEXT("0 / 5"), 0.0f, 5.0f, 0.0f, 0.0f},
{TEXT("0 / -5"), 0.0f, -5.0f, 0.0f, 0.0f},
{TEXT("0 / 0.05"), 0.0f, 0.05f, 0.0f, 0.0f},
//Division by zero
{TEXT("5 / 0"), 5.0f, 0.0f, 0.0f, 0.0f},
{TEXT("-5 / 0"), -5.0f, 0.0f, 0.0f, 0.0f},
{TEXT("0 / 0"), 0.0f, 0.0f, 0.0f, 0.0f},
//Negative remainders with small quotients
{TEXT("-0.3 / 0.1"), -0.3f, 0.1f, -3.0f, 0.0f},
{TEXT("-0.3 / 0.2"), -0.3f, 0.2f, -1.0f, -0.5f},
{TEXT("-0.1 / 0.3"), -0.1f, 0.3f, 0.0f, -0.3333f},
//Close to whole number
{TEXT("1.9999 / 1.0"), 1.9999f, 1.0f, 1.0f, 0.9999f},
{TEXT("-1.9999 / 1.0"), -1.9999f, 1.0f, -1.0f, -0.9999f},
//Perfectly divisible
{TEXT("10 / 2"), 10.0f, 2.0f, 5.0f, 0.0f},
{TEXT("-10 / 2"), -10.0f, 2.0f, -5.0f, 0.0f},
{TEXT("10 / -2"), 10.0f, -2.0f, -5.0f, 0.0f},
//Fractional divisors
{TEXT("4.5 / 0.5"), 4.5f, 0.5f, 9.0f, 0.0f},
{TEXT("7.25 / 0.25"), 7.25f, 0.25f, 29.0f, 0.0f},
{TEXT("-3.75 / 0.25"), -3.75f, 0.25f, -15.0f, 0.0f},
//Very small and large values
{TEXT("1e-6 / 1.0"), 1e-6f, 1.0f, 0.0f, 1e-6f},
{TEXT("1e6 / 2"), 1000000.0f, 2.0f, 500000.0f, 0.0f},
{TEXT("1e6 / 3"), 1000000.0f, 3.0f, 333333.0f, 0.333333f},
//Arbitrary remainder checks
{TEXT("1 / 3"), 1.0f, 3.0f, 0.0f, 0.3333f},
{TEXT("1 / 7"), 1.0f, 7.0f, 0.0f, 0.142857f},
{TEXT("0.3 / 0.1"), 0.3f, 0.1f, 3.0f, 0.0f}
};
UE_LOG(MathExtensions, Warning, TEXT("Starting Division tests."));
for (const auto& test : cases) {
float calculatedWhole = -999.0f;
float calculatedRem = -999.0f;
//The function being tested
Division(test.dividend, test.divisor, calculatedWhole, calculatedRem);
bool wholeResult = std::abs(calculatedWhole - test.expectedWhole) < EPSILON;
bool remResult = std::abs(calculatedRem - test.expectedRem) < EPSILON;
if (!wholeResult || !remResult) {
FString message = FString::Printf(
TEXT("Calculation mismatch! Expected: %s -> Whole: %f Rem: %f"),
*test.description, calculatedWhole, calculatedRem);
UE_LOG(MathExtensions, Warning, TEXT("%s"), *message);
}
}
UE_LOG(MathExtensions, Warning, TEXT("All Division tests finished."));
}
};