// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilMathFunctionLibrary.h" #include namespace { float GetUsableStatisticsWeight(const float Weight) { return FMath::IsFinite(Weight) && Weight > 0.0f ? Weight : 0.0f; } template ValueType SelectStatisticsNth(TArray& Values, const int32 NthIndex) { int32 Left = 0; int32 Right = Values.Num() - 1; int32 RemainingDepth = FMath::FloorLog2(static_cast(Values.Num())) * 2; while (Left < Right) { if (RemainingDepth-- <= 0) { Values.Sort(); return Values[NthIndex]; } const int32 Middle = Left + (Right - Left) / 2; if (Values[Middle] < Values[Left]) { Values.Swap(Middle, Left); } if (Values[Right] < Values[Left]) { Values.Swap(Right, Left); } if (Values[Right] < Values[Middle]) { Values.Swap(Right, Middle); } const ValueType Pivot = Values[Middle]; int32 LessEnd = Left; int32 Current = Left; int32 GreaterStart = Right; while (Current <= GreaterStart) { if (Values[Current] < Pivot) { Values.Swap(LessEnd++, Current++); } else if (Pivot < Values[Current]) { Values.Swap(Current, GreaterStart--); } else { ++Current; } } if (NthIndex < LessEnd) { Right = LessEnd - 1; } else if (NthIndex > GreaterStart) { Left = GreaterStart + 1; } else { return Values[NthIndex]; } } return Values[Left]; } template double CalculateStatisticsMedian(TArray& Values) { const int32 Middle = Values.Num() / 2; const ValueType UpperMiddle = SelectStatisticsNth(Values, Middle); if (Values.Num() % 2 != 0) { return static_cast(UpperMiddle); } ValueType LowerMiddle = Values[0]; for (int32 Index = 1; Index < Middle; ++Index) { LowerMiddle = FMath::Max(LowerMiddle, Values[Index]); } return (static_cast(LowerMiddle) + static_cast(UpperMiddle)) * 0.5; } } 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 double AbsoluteSeconds = FMath::Abs(static_cast(Seconds)); const int64 TotalSeconds = AbsoluteSeconds >= static_cast(TNumericLimits::Max()) ? TNumericLimits::Max() : static_cast(AbsoluteSeconds); const int64 VisibleSeconds = bIncludeSeconds ? TotalSeconds : (TotalSeconds / 60) * 60; const bool bNegative = Seconds < 0.0f && VisibleSeconds > 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 first so timestamps near the current second stay in the expected 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 WorkingValues = Values; return static_cast(CalculateStatisticsMedian(WorkingValues)); } 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 WorkingValues = Values; if (WorkingValues.ContainsByPredicate([](const float Value) { return FMath::IsNaN(Value); })) { return std::numeric_limits::quiet_NaN(); } return static_cast(CalculateStatisticsMedian(WorkingValues)); } 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())); } bool UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage(const TArray& Angles, float& AverageAngle, float& ResultantStrength) { AverageAngle = 0.0f; ResultantStrength = 0.0f; if (Angles.IsEmpty()) { return false; } double SineSum = 0.0; double CosineSum = 0.0; for (const float Angle : Angles) { if (!FMath::IsFinite(Angle)) { return false; } const double Radians = FMath::DegreesToRadians(FMath::Fmod(static_cast(Angle), 360.0)); SineSum += FMath::Sin(Radians); CosineSum += FMath::Cos(Radians); } const double Magnitude = FMath::Sqrt(SineSum * SineSum + CosineSum * CosineSum); ResultantStrength = static_cast(FMath::Clamp(Magnitude / Angles.Num(), 0.0, 1.0)); if (ResultantStrength <= UE_DOUBLE_SMALL_NUMBER) { ResultantStrength = 0.0f; return false; } AverageAngle = static_cast(FMath::RadiansToDegrees(FMath::Atan2(SineSum, CosineSum))); return true; } bool UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(const TArray& Values, const TArray& Weights, float& Average) { Average = 0.0f; if (Values.IsEmpty() || Values.Num() != Weights.Num()) { return false; } double WeightedSum = 0.0; double WeightSum = 0.0; for (int32 Index = 0; Index < Values.Num(); ++Index) { if (!FMath::IsFinite(Values[Index])) { return false; } const double Weight = GetUsableStatisticsWeight(Weights[Index]); WeightedSum += static_cast(Values[Index]) * Weight; WeightSum += Weight; } if (WeightSum <= 0.0) { return false; } Average = static_cast(WeightedSum / WeightSum); return FMath::IsFinite(Average); } bool UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(const TArray& Values, const TArray& Weights, FVector& Average) { Average = FVector::ZeroVector; if (Values.IsEmpty() || Values.Num() != Weights.Num()) { return false; } FVector RunningAverage = FVector::ZeroVector; double WeightSum = 0.0; for (int32 Index = 0; Index < Values.Num(); ++Index) { if (Values[Index].ContainsNaN()) { return false; } const double Weight = GetUsableStatisticsWeight(Weights[Index]); if (Weight > 0.0) { const double NewWeightSum = WeightSum + Weight; RunningAverage = FMath::LerpStable(RunningAverage, Values[Index], Weight / NewWeightSum); WeightSum = NewWeightSum; } } if (WeightSum <= 0.0 || RunningAverage.ContainsNaN()) { return false; } Average = RunningAverage; return true; } bool UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(const TArray& Values, const float OutputMinimum, const float OutputMaximum, TArray& NormalizedValues) { TArray ValuesCopy; const TArray* SourceValues = &Values; if (&Values == &NormalizedValues) { ValuesCopy = Values; SourceValues = &ValuesCopy; } NormalizedValues.Reset(); if (SourceValues->IsEmpty() || !FMath::IsFinite(OutputMinimum) || !FMath::IsFinite(OutputMaximum)) { return false; } float InputMinimum = (*SourceValues)[0]; float InputMaximum = (*SourceValues)[0]; for (const float Value : *SourceValues) { if (!FMath::IsFinite(Value)) { return false; } InputMinimum = FMath::Min(InputMinimum, Value); InputMaximum = FMath::Max(InputMaximum, Value); } NormalizedValues.SetNumUninitialized(SourceValues->Num()); if (InputMinimum == InputMaximum) { NormalizedValues.Init(OutputMinimum, SourceValues->Num()); return true; } const double Scale = (static_cast(OutputMaximum) - OutputMinimum) / (static_cast(InputMaximum) - InputMinimum); for (int32 Index = 0; Index < SourceValues->Num(); ++Index) { NormalizedValues[Index] = static_cast(OutputMinimum + (static_cast((*SourceValues)[Index]) - InputMinimum) * Scale); } return true; } bool UDirectiveUtilMathFunctionLibrary::NormalizeWeights(const TArray& Weights, TArray& NormalizedWeights) { TArray WeightsCopy; const TArray* SourceWeights = &Weights; if (&Weights == &NormalizedWeights) { WeightsCopy = Weights; SourceWeights = &WeightsCopy; } NormalizedWeights.Reset(); if (SourceWeights->IsEmpty()) { return false; } double WeightSum = 0.0; for (const float Weight : *SourceWeights) { WeightSum += GetUsableStatisticsWeight(Weight); } if (WeightSum <= 0.0) { return false; } NormalizedWeights.SetNumUninitialized(SourceWeights->Num()); for (int32 Index = 0; Index < SourceWeights->Num(); ++Index) { NormalizedWeights[Index] = static_cast(GetUsableStatisticsWeight((*SourceWeights)[Index]) / WeightSum); } return true; } bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(const TArray& Values, const float Percentile, float& Value) { Value = 0.0f; if (Values.IsEmpty() || !FMath::IsFinite(Percentile)) { return false; } for (const float Candidate : Values) { if (!FMath::IsFinite(Candidate)) { return false; } } TArray WorkingValues = Values; const double Position = FMath::Clamp(static_cast(Percentile), 0.0, 100.0) * 0.01 * (WorkingValues.Num() - 1); const int32 LowerIndex = FMath::FloorToInt(Position); const int32 UpperIndex = FMath::CeilToInt(Position); const float LowerValue = SelectStatisticsNth(WorkingValues, LowerIndex); if (LowerIndex == UpperIndex) { Value = LowerValue; return true; } const float UpperValue = SelectStatisticsNth(WorkingValues, UpperIndex); Value = static_cast(FMath::Lerp( static_cast(LowerValue), static_cast(UpperValue), Position - LowerIndex)); return true; } bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(const TArray& Values, float& RootMeanSquare) { RootMeanSquare = 0.0f; if (Values.IsEmpty()) { return false; } double SquaredSum = 0.0; for (const float Value : Values) { if (!FMath::IsFinite(Value)) { return false; } SquaredSum += static_cast(Value) * Value; } RootMeanSquare = static_cast(FMath::Sqrt(SquaredSum / Values.Num())); return FMath::IsFinite(RootMeanSquare); }