// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilMathFunctionLibrary.h" namespace { double EaseBackIn(double t) { const double s = 1.70158; return t * t * ((s + 1.0) * t - s); } double EaseBackOut(double t) { const double s = 1.70158; t -= 1.0; return t * t * ((s + 1.0) * t + s) + 1.0; } double EaseBackInOut(double t) { const double s = 1.70158 * 1.525; t *= 2.0; if (t < 1.0) { return 0.5 * (t * t * ((s + 1.0) * t - s)); } t -= 2.0; return 0.5 * (t * t * ((s + 1.0) * t + s) + 2.0); } double EaseElasticIn(double t) { if (t <= 0.0) { return 0.0; } if (t >= 1.0) { return 1.0; } const double p = 0.3; const double s = p / 4.0; t -= 1.0; return -(FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p)); } double EaseElasticOut(double t) { if (t <= 0.0) { return 0.0; } if (t >= 1.0) { return 1.0; } const double p = 0.3; const double s = p / 4.0; return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) + 1.0; } double EaseElasticInOut(double t) { if (t <= 0.0) { return 0.0; } if (t >= 1.0) { return 1.0; } const double p = 0.3 * 1.5; const double s = p / 4.0; t *= 2.0; if (t < 1.0) { t -= 1.0; return -0.5 * (FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p)); } t -= 1.0; return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) * 0.5 + 1.0; } double EaseBounceOut(double t) { const double n1 = 7.5625; const double d1 = 2.75; if (t < 1.0 / d1) { return n1 * t * t; } if (t < 2.0 / d1) { t -= 1.5 / d1; return n1 * t * t + 0.75; } if (t < 2.5 / d1) { t -= 2.25 / d1; return n1 * t * t + 0.9375; } t -= 2.625 / d1; return n1 * t * t + 0.984375; } double EaseBounceIn(double t) { return 1.0 - EaseBounceOut(1.0 - t); } double EaseBounceInOut(double t) { return t < 0.5 ? (1.0 - EaseBounceOut(1.0 - 2.0 * t)) * 0.5 : (1.0 + EaseBounceOut(2.0 * t - 1.0)) * 0.5; } } float UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(const FVector2D Position) { return FMath::PerlinNoise2D(Position); } float UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(const FVector& Position) { return FMath::PerlinNoise3D(Position); } float UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(const FVector& A, const FVector& B) { return FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(A.GetSafeNormal(), B.GetSafeNormal()), -1.0, 1.0))); } float UDirectiveUtilMathFunctionLibrary::EaseAlpha(const float Alpha, const EDirectiveUtilEaseType EaseType) { const double t = static_cast(FMath::Clamp(Alpha, 0.0f, 1.0f)); double Result = t; switch (EaseType) { case EDirectiveUtilEaseType::BackIn: Result = EaseBackIn(t); break; case EDirectiveUtilEaseType::BackOut: Result = EaseBackOut(t); break; case EDirectiveUtilEaseType::BackInOut: Result = EaseBackInOut(t); break; case EDirectiveUtilEaseType::ElasticIn: Result = EaseElasticIn(t); break; case EDirectiveUtilEaseType::ElasticOut: Result = EaseElasticOut(t); break; case EDirectiveUtilEaseType::ElasticInOut: Result = EaseElasticInOut(t); break; case EDirectiveUtilEaseType::BounceIn: Result = EaseBounceIn(t); break; case EDirectiveUtilEaseType::BounceOut: Result = EaseBounceOut(t); break; case EDirectiveUtilEaseType::BounceInOut: Result = EaseBounceInOut(t); break; } return static_cast(Result); } float UDirectiveUtilMathFunctionLibrary::EaseFloat(const float A, const float B, const float Alpha, const EDirectiveUtilEaseType EaseType) { return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType)); } FVector UDirectiveUtilMathFunctionLibrary::EaseVector(const FVector& A, const FVector& B, const float Alpha, const EDirectiveUtilEaseType EaseType) { return FMath::Lerp(A, B, static_cast(EaseAlpha(Alpha, EaseType))); } FRotator UDirectiveUtilMathFunctionLibrary::EaseRotator(const FRotator& A, const FRotator& B, const float Alpha, const EDirectiveUtilEaseType EaseType) { return FQuat::Slerp(A.Quaternion(), B.Quaternion(), EaseAlpha(Alpha, EaseType)).Rotator(); } FLinearColor UDirectiveUtilMathFunctionLibrary::EaseColor(const FLinearColor& A, const FLinearColor& B, const float Alpha, const EDirectiveUtilEaseType EaseType) { return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType)); } float UDirectiveUtilMathFunctionLibrary::RoundToDecimals(const float Value, int32 Decimals) { Decimals = FMath::Clamp(Decimals, 0, 10); if (Decimals == 0) { return FMath::RoundHalfFromZero(Value); } const double Factor = FMath::Pow(10.0, static_cast(Decimals)); return static_cast(FMath::RoundHalfFromZero(static_cast(Value) * Factor) / Factor); } FText UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(const float Value, int32 Decimals) { Decimals = FMath::Clamp(Decimals, 0, 10); FNumberFormattingOptions Options; Options.MinimumFractionalDigits = 0; Options.MaximumFractionalDigits = Decimals; Options.RoundingMode = ERoundingMode::HalfFromZero; return FText::AsNumber(Value, &Options); } FText UDirectiveUtilMathFunctionLibrary::FormatBytes(const int64 Bytes, int32 Decimals) { Decimals = FMath::Clamp(Decimals, 0, 3); static const TCHAR* Suffixes[] = { TEXT("B"), TEXT("KB"), TEXT("MB"), TEXT("GB"), TEXT("TB"), TEXT("PB") }; const bool bNegative = Bytes < 0; double Value = FMath::Abs(static_cast(Bytes)); int32 SuffixIndex = 0; while (Value >= 1024.0 && SuffixIndex < UE_ARRAY_COUNT(Suffixes) - 1) { Value /= 1024.0; ++SuffixIndex; } return FText::FromString(FString::Printf(TEXT("%s%.*f %s"), bNegative ? TEXT("-") : TEXT(""), SuffixIndex == 0 ? 0 : Decimals, Value, Suffixes[SuffixIndex])); } FText UDirectiveUtilMathFunctionLibrary::FormatDuration(const float Seconds, const bool bIncludeSeconds) { if (!FMath::IsFinite(Seconds)) { return FText::FromString(TEXT("0s")); } const int64 TotalSeconds = static_cast(FMath::Abs(static_cast(Seconds))); const bool bNegative = Seconds < 0.0f && TotalSeconds > 0; const int64 UnitValues[] = { TotalSeconds / 86400, (TotalSeconds / 3600) % 24, (TotalSeconds / 60) % 60, TotalSeconds % 60 }; static const TCHAR* UnitSuffixes[] = { TEXT("d"), TEXT("h"), TEXT("m"), TEXT("s") }; const int32 NumUnits = bIncludeSeconds ? 4 : 3; int32 FirstUnit = NumUnits - 1; for (int32 Index = 0; Index < NumUnits; ++Index) { if (UnitValues[Index] != 0) { FirstUnit = Index; break; } } int32 LastUnit = FirstUnit; for (int32 Index = NumUnits - 1; Index >= FirstUnit; --Index) { if (UnitValues[Index] != 0) { LastUnit = Index; break; } } FString Result = bNegative ? TEXT("-") : TEXT(""); for (int32 Index = FirstUnit; Index <= LastUnit; ++Index) { if (Index == FirstUnit) { Result += FString::Printf(TEXT("%lld%s"), UnitValues[Index], UnitSuffixes[Index]); } else { Result += FString::Printf(TEXT(" %02lld%s"), UnitValues[Index], UnitSuffixes[Index]); } } return FText::FromString(Result); } FText UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(const FDateTime& Timestamp) { const FTimespan Delta = FDateTime::Now() - Timestamp; const bool bFuture = Delta.GetTicks() < 0; // Round to whole seconds so clock-adjacent inputs (e.g. Now() + 2 hours) land in the intended bucket. const int64 SecondsAbs = static_cast(FMath::RoundToDouble(FMath::Abs(Delta.GetTotalSeconds()))); if (SecondsAbs < 60) { return FText::FromString(TEXT("just now")); } int64 Count; const TCHAR* Unit; if (SecondsAbs < 3600) { Count = SecondsAbs / 60; Unit = TEXT("minute"); } else if (SecondsAbs < 86400) { Count = SecondsAbs / 3600; Unit = TEXT("hour"); } else { Count = SecondsAbs / 86400; Unit = TEXT("day"); } const FString Quantity = FString::Printf(TEXT("%lld %s%s"), Count, Unit, Count == 1 ? TEXT("") : TEXT("s")); return FText::FromString(bFuture ? FString::Printf(TEXT("in %s"), *Quantity) : FString::Printf(TEXT("%s ago"), *Quantity)); } int64 UDirectiveUtilMathFunctionLibrary::GetIntArraySum(const TArray& Values) { int64 Sum = 0; for (const int32 Value : Values) { Sum += Value; } return Sum; } float UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } return static_cast(static_cast(GetIntArraySum(Values)) / Values.Num()); } float UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } TArray Sorted = Values; Sorted.Sort(); const int32 Middle = Sorted.Num() / 2; if (Sorted.Num() % 2 == 0) { return static_cast((static_cast(Sorted[Middle - 1]) + static_cast(Sorted[Middle])) * 0.5); } return static_cast(Sorted[Middle]); } float UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } const double Mean = static_cast(GetIntArraySum(Values)) / Values.Num(); double SquaredDeltaSum = 0.0; for (const int32 Value : Values) { const double Delta = static_cast(Value) - Mean; SquaredDeltaSum += Delta * Delta; } return static_cast(FMath::Sqrt(SquaredDeltaSum / Values.Num())); } float UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(const TArray& Values) { double Sum = 0.0; for (const float Value : Values) { Sum += static_cast(Value); } return static_cast(Sum); } float UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } double Sum = 0.0; for (const float Value : Values) { Sum += static_cast(Value); } return static_cast(Sum / Values.Num()); } float UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } TArray Sorted = Values; Sorted.Sort(); const int32 Middle = Sorted.Num() / 2; if (Sorted.Num() % 2 == 0) { return static_cast((static_cast(Sorted[Middle - 1]) + static_cast(Sorted[Middle])) * 0.5); } return Sorted[Middle]; } float UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(const TArray& Values) { if (Values.IsEmpty()) { return 0.0f; } double Sum = 0.0; for (const float Value : Values) { Sum += static_cast(Value); } const double Mean = Sum / Values.Num(); double SquaredDeltaSum = 0.0; for (const float Value : Values) { const double Delta = static_cast(Value) - Mean; SquaredDeltaSum += Delta * Delta; } return static_cast(FMath::Sqrt(SquaredDeltaSum / Values.Num())); } int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(const TArray& Weights) { float Total = 0.0f; for (const float Weight : Weights) { Total += FMath::Max(0.0f, Weight); } if (Total <= 0.0f) { return INDEX_NONE; } const float Roll = FMath::FRand() * Total; float Accumulated = 0.0f; int32 LastPositiveIndex = INDEX_NONE; for (int32 Index = 0; Index < Weights.Num(); ++Index) { const float Weight = FMath::Max(0.0f, Weights[Index]); if (Weight <= 0.0f) { continue; } LastPositiveIndex = Index; Accumulated += Weight; if (Roll < Accumulated) { return Index; } } return LastPositiveIndex; } int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(FRandomStream& Stream, const TArray& Weights) { float Total = 0.0f; for (const float Weight : Weights) { Total += FMath::Max(0.0f, Weight); } if (Total <= 0.0f) { return INDEX_NONE; } const float Roll = Stream.FRand() * Total; float Accumulated = 0.0f; int32 LastPositiveIndex = INDEX_NONE; for (int32 Index = 0; Index < Weights.Num(); ++Index) { const float Weight = FMath::Max(0.0f, Weights[Index]); if (Weight <= 0.0f) { continue; } LastPositiveIndex = Index; Accumulated += Weight; if (Roll < Accumulated) { return Index; } } return LastPositiveIndex; }