// Copyright Epic Games, Inc. All Rights Reserved. #include "Insights/Common/TimeUtils.h" #include #include // #include "Insights/Log.h" namespace TimeUtils { //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeValue(const double Duration, const int32 NumDigits) { if (std::isnan(Duration)) { return TEXT("NaN"); } #if !PLATFORM_USE_GENERIC_STRING_IMPLEMENTATION FString Str = FString::Printf(TEXT("%.*f"), NumDigits, Duration); #else // proper resolution is tracked as UE-79534 TCHAR FormatString[32]; FCString::Snprintf(FormatString, sizeof(FormatString), TEXT("%%.%df"), NumDigits); FString Str = FString::Printf(FormatString, Duration); #endif if (NumDigits == 1) { Str.RemoveFromEnd(TEXT(".0")); } else { while (Str.RemoveFromEnd(TEXT("0"))) { /* keep removing the ending 0 */ } Str.RemoveFromEnd(TEXT(".")); } return Str; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeAuto(const double InDuration, const int32 NumDigits) { // TestTimeAutoFormatting(); double Duration = InDuration; if (std::isnan(Duration)) { return TEXT("NaN"); } if (Duration == 0.0) { return TEXT("0"); } FString Str; if (FMath::IsNegativeDouble(Duration)) { Str = TEXT("-"); Duration = -Duration; } if (Duration == DBL_MAX || Duration == std::numeric_limits::infinity()) { Str += TEXT("∞"); } else if (Duration < TimeUtils::Picosecond) { // (0 .. 1ps) Str = TEXT("~0"); } else if (Duration < TimeUtils::Nanosecond) { // [1ps .. 1ns) if (Duration >= 999.5 * TimeUtils::Picosecond) { Str += TEXT("1 ns"); } else { const double Picoseconds = Duration * 1000000000000.0 + 0.5; const int32 IntPicoseconds = static_cast(Picoseconds); ensure(IntPicoseconds <= 999); // if (IntPicoseconds > 999) //{ // Str += TEXT("1 ns"); // } // else { Str += FString::Printf(TEXT("%d ps"), IntPicoseconds); } } } else if (Duration < TimeUtils::Microsecond) { // [1ns .. 1µs) if (Duration >= 999.5 * TimeUtils::Nanosecond) { Str += TEXT("1 µs"); } else { const double Nanoseconds = Duration * 1000000000.0 + 0.5; const int32 IntNanoseconds = static_cast(Nanoseconds); ensure(IntNanoseconds <= 999); // if (IntNanoseconds > 999) //{ // Str += TEXT("1 µs"); // } // else { Str += FString::Printf(TEXT("%d ns"), IntNanoseconds); } } } else if (Duration < TimeUtils::Milisecond) { // [1µs .. 1ms) const double Microseconds = Duration * 1000000.0; if (Microseconds >= 999.95) { Str += TEXT("1 ms"); } else { Str += FormatTimeValue(Microseconds, NumDigits); Str += TEXT(" µs"); } } else if (Duration < TimeUtils::Second) { // [1ms .. 1s) const double Miliseconds = Duration * 1000.0; if (Miliseconds >= 999.95) { Str += TEXT("1s"); } else { Str += FormatTimeValue(Miliseconds, NumDigits); Str += TEXT(" ms"); } } else if (Duration < TimeUtils::Minute) { // [1s .. 1m) if (Duration >= 59.95) { Str += TEXT("1m"); } else { Str += FormatTimeValue(Duration, NumDigits); Str += TEXT("s"); } } else if (Duration < TimeUtils::Hour) { // [1m .. 1h) const double Minutes = FMath::FloorToDouble(Duration / TimeUtils::Minute); Str += FString::Printf(TEXT("%dm"), static_cast(Minutes)); Duration -= Minutes * TimeUtils::Minute; if (NumDigits <= 1) { const double Seconds = FMath::FloorToDouble(Duration / TimeUtils::Second); if (Seconds > 0.5) { Str += FString::Printf(TEXT(" %ds"), static_cast(Seconds)); } } else { Str += TEXT(" "); Str += FormatTimeValue(Duration, NumDigits - 1); Str += TEXT("s"); } } else if (Duration < TimeUtils::Day) { // [1h .. 1d) const double Hours = FMath::FloorToDouble(Duration / TimeUtils::Hour); Str += FString::Printf(TEXT("%dh"), static_cast(Hours)); Duration -= Hours * TimeUtils::Hour; const double Minutes = FMath::FloorToDouble(Duration / TimeUtils::Minute); if (Minutes > 0.5) { Str += FString::Printf(TEXT(" %dm"), static_cast(Minutes)); } } else { // [1d .. ∞) const double Days = FMath::FloorToDouble(Duration / TimeUtils::Day); Str += FString::Printf(TEXT("%dd"), static_cast(Days)); Duration -= Days * TimeUtils::Day; const double Hours = FMath::FloorToDouble(Duration / TimeUtils::Hour); if (Hours > 0.5) { Str += FString::Printf(TEXT(" %dh"), static_cast(Hours)); } } return Str; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeMs(const double InDuration, const int32 NumDigits, bool bAddTimeUnit) { if (std::isnan(InDuration)) { return TEXT("NaN"); } if (FMath::IsNearlyZero(InDuration, TimeUtils::Picosecond)) { return TEXT("0"); } double Duration = InDuration; FString Str; if (FMath::IsNegativeDouble(Duration)) { Duration = -Duration; Str = TEXT("-"); } if (Duration == DBL_MAX || Duration == std::numeric_limits::infinity()) { Str += TEXT("∞"); } else { #if !PLATFORM_USE_GENERIC_STRING_IMPLEMENTATION Str += FString::Printf(TEXT("%.*f"), NumDigits, Duration * 1000.0); #else // proper resolution is tracked as UE-79534 TCHAR FormatString[32]; FCString::Snprintf(FormatString, sizeof(FormatString), TEXT("%%.%df"), NumDigits); Str += FString::Printf(FormatString, Duration * 1000.0); #endif if (bAddTimeUnit) { Str += TEXT(" ms"); } } return Str; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTime(const double InTime, const double Precision) { if (std::isnan(InTime)) { return TEXT("NaN"); } if (FMath::IsNearlyZero(InTime, FMath::Max(TimeUtils::Picosecond, Precision / 10.0f))) { return TEXT("0"); } double Time = InTime; FString Str; if (FMath::IsNegativeDouble(Time)) { Time = -Time; Str = TEXT("-"); } if (Time == DBL_MAX || Time == std::numeric_limits::infinity()) { Str += TEXT("∞"); return Str; } bool bIsSpaceNeeded = false; int32 Days = static_cast(Time / TimeUtils::Day); if (Days > 0) { Str += FString::Printf(TEXT("%dd"), Days); bIsSpaceNeeded = true; Time -= static_cast(Days) * TimeUtils::Day; } if (Precision >= TimeUtils::Day) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } int32 Hours = static_cast(Time / TimeUtils::Hour); if (Hours > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dh"), Hours); bIsSpaceNeeded = true; Time -= static_cast(Hours) * TimeUtils::Hour; } if (Precision >= TimeUtils::Hour) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } int32 Minutes = static_cast(Time / TimeUtils::Minute); if (Minutes > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dm"), Minutes); bIsSpaceNeeded = true; Time -= static_cast(Minutes) * TimeUtils::Minute; } if (Precision >= TimeUtils::Minute) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } // TestOptimizationIssue(); ////float Log10 = -FMath::LogX(10.0f, static_cast(Precision)); // double Log10 = -log10(Precision); // int32 Digits = (Log10 > 0) ? FMath::CeilToInt(Log10) : 0; static const double DigitThresholds[] = { TimeUtils::Second, // 0 digits TimeUtils::Milisecond * 100, // 1 digit TimeUtils::Milisecond * 10, // 2 digits TimeUtils::Milisecond, // 3 digits TimeUtils::Microsecond * 100, // 4 digits TimeUtils::Microsecond * 10, // 5 digits TimeUtils::Microsecond, // 6 digits TimeUtils::Nanosecond * 100, // 7 digits TimeUtils::Nanosecond * 10, // 8 digits TimeUtils::Nanosecond, // 9 digits TimeUtils::Picosecond * 100, // 10 digits TimeUtils::Picosecond * 10, // 11 digits TimeUtils::Picosecond, // 12 digits 0 // 13 digits }; int32 Digits = 0; while (Precision < DigitThresholds[Digits]) { Digits++; } if (Digits == 0) { int32 Seconds = static_cast(Time / TimeUtils::Second + 0.5); if (Seconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%ds"), Seconds); } else if (!bIsSpaceNeeded) { Str += TEXT("~0"); } } // else if (Digits <= 9) //{ // if (bIsSpaceNeeded) // { // Str += TEXT(" "); // } // int32 Seconds = static_cast(Time / TimeUtils::Second); // Time -= static_cast(Seconds) * TimeUtils::Second; // int64 SubSeconds = static_cast(Time * FMath::Pow(10.0f, Digits) + 0.5); // Str += FString::Printf(TEXT("%d.%0*llds"), Seconds, Digits, SubSeconds); // } else { if (bIsSpaceNeeded) { Str += TEXT(" "); } #if !PLATFORM_USE_GENERIC_STRING_IMPLEMENTATION Str += FString::Printf(TEXT("%.*fs"), Digits, Time); #else // proper resolution is tracked as UE-79534 TCHAR FormatString[32]; FCString::Snprintf(FormatString, sizeof(FormatString), TEXT("%%.%dfs"), Digits); Str += FString::Printf(FormatString, Time); #endif } return Str; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeHMS(const double Time, const double Precision) { // TODO: DD:HH:MM:SS.mmm.uuu.nnn.ppp return FormatTime(Time, Precision); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SplitTime(const double InTime, FTimeSplit& OutTimeSplit) { double Time = InTime; OutTimeSplit.bIsZero = false; OutTimeSplit.bIsNegative = false; OutTimeSplit.bIsInfinite = false; OutTimeSplit.bIsNaN = false; if (std::isnan(InTime)) { OutTimeSplit.bIsNaN = true; return; } if (FMath::IsNearlyZero(Time, TimeUtils::Picosecond)) { OutTimeSplit.bIsZero = true; for (int32 Index = 0; Index < 8; ++Index) { OutTimeSplit.Units[Index] = 0; } return; } if (FMath::IsNegativeDouble(Time)) { Time = -Time; OutTimeSplit.bIsNegative = true; } if (Time == DBL_MAX || Time == std::numeric_limits::infinity()) { OutTimeSplit.bIsInfinite = true; for (int32 Index = 0; Index < 8; ++Index) { OutTimeSplit.Units[Index] = 0; } return; } bool bIsZero = true; // assume true and see if any split unit is != 0 const double Days = FMath::FloorToDouble(Time / TimeUtils::Day); OutTimeSplit.Days = static_cast(Days); if (OutTimeSplit.Days > 0) { Time -= Days * TimeUtils::Day; bIsZero = false; } const double Hours = FMath::FloorToDouble(Time / TimeUtils::Hour); OutTimeSplit.Hours = static_cast(Hours); if (OutTimeSplit.Hours > 0) { Time -= Hours * TimeUtils::Hour; } const double Minutes = FMath::FloorToDouble(Time / TimeUtils::Minute); OutTimeSplit.Minutes = static_cast(Minutes); if (OutTimeSplit.Minutes > 0) { Time -= Minutes * TimeUtils::Minute; bIsZero = false; } const double Seconds = FMath::FloorToDouble(Time / TimeUtils::Second); OutTimeSplit.Seconds = static_cast(Seconds); if (OutTimeSplit.Seconds > 0) { Time -= Seconds * TimeUtils::Second; bIsZero = false; } const double Miliseconds = FMath::FloorToDouble(Time / TimeUtils::Milisecond); OutTimeSplit.Miliseconds = static_cast(Miliseconds); if (OutTimeSplit.Miliseconds > 0) { Time -= Miliseconds * TimeUtils::Milisecond; bIsZero = false; } const double Microseconds = FMath::FloorToDouble(Time / TimeUtils::Microsecond); OutTimeSplit.Microseconds = static_cast(Microseconds); if (OutTimeSplit.Microseconds > 0) { Time -= Microseconds * TimeUtils::Microsecond; bIsZero = false; } const double Nanoseconds = FMath::FloorToDouble(Time / TimeUtils::Nanosecond); OutTimeSplit.Nanoseconds = static_cast(Nanoseconds); if (OutTimeSplit.Nanoseconds > 0) { Time -= Nanoseconds * TimeUtils::Nanosecond; bIsZero = false; } const double Picoseconds = FMath::FloorToDouble(Time / TimeUtils::Picosecond); OutTimeSplit.Picoseconds = static_cast(Picoseconds); if (OutTimeSplit.Picoseconds > 0) { Time -= Picoseconds * TimeUtils::Picosecond; bIsZero = false; } OutTimeSplit.bIsZero = bIsZero; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeSplit(const FTimeSplit& InTimeSplit, const double Precision) { FString Str; if (InTimeSplit.bIsNaN) { Str = TEXT("NaN"); return Str; } if (InTimeSplit.bIsZero) { Str = TEXT("0"); return Str; } if (InTimeSplit.bIsNegative) { Str = TEXT("-"); } if (InTimeSplit.bIsInfinite) { Str += TEXT("∞"); return Str; } bool bIsSpaceNeeded = false; if (InTimeSplit.Days > 0) { Str += FString::Printf(TEXT("%dd"), InTimeSplit.Days); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Day) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Hours > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dh"), InTimeSplit.Hours); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Hour) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Minutes > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dm"), InTimeSplit.Minutes); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Minute) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Seconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%ds"), InTimeSplit.Seconds); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Second) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Miliseconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dms"), InTimeSplit.Miliseconds); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Milisecond) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Microseconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dµs"), InTimeSplit.Microseconds); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Microsecond) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Nanoseconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dns"), InTimeSplit.Nanoseconds); bIsSpaceNeeded = true; } if (Precision >= TimeUtils::Nanosecond) { if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } if (InTimeSplit.Picoseconds > 0) { if (bIsSpaceNeeded) { Str += TEXT(" "); } Str += FString::Printf(TEXT("%dps"), InTimeSplit.Picoseconds); bIsSpaceNeeded = true; } if (!bIsSpaceNeeded) { Str = TEXT("~0"); } return Str; } //////////////////////////////////////////////////////////////////////////////////////////////////// FString FormatTimeSplit(const double Time, const double Precision) { FTimeSplit TimeSplit; SplitTime(Time, TimeSplit); return FormatTimeSplit(TimeSplit, Precision); } //////////////////////////////////////////////////////////////////////////////////////////////////// void TestTimeFormatting() { double T1 = 1 * TimeUtils::Day + 2 * TimeUtils::Hour + 3 * TimeUtils::Minute + 4.567890123456789; FString S1 = FormatTime(T1, TimeUtils::Day); // UE_LOG(TraceInsights, Log, TEXT("D-T: %s"), *S1); FString S2 = FormatTime(T1, TimeUtils::Hour); // UE_LOG(TraceInsights, Log, TEXT("H-T: %s"), *S2); FString S3 = FormatTime(T1, TimeUtils::Minute); // UE_LOG(TraceInsights, Log, TEXT("M-T: %s"), *S3); for (double P = 10.0; P >= TimeUtils::Picosecond; P /= 10.0) { FString SP = FormatTime(T1, P); // UE_LOG(TraceInsights, Log, TEXT("P:%g T: %s"), P, *SP); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void TestTimeAutoFormatting() { static bool Once = false; if (Once) return; Once = true; struct FTestTimeAutoData { double T; const TCHAR* Msg; }; static const FTestTimeAutoData Data[] = { { TimeUtils::Minute, TEXT("1m")}, { TimeUtils::Second * 59.99, TEXT("59.99s")}, { TimeUtils::Second * 59.95, TEXT("59.95s")}, { 0, TEXT("[threshold 1m / 59.9s]")}, { TimeUtils::Second * 59.94, TEXT("59.94s")}, { TimeUtils::Second * 59.9, TEXT("59.9s")}, { TimeUtils::Second * 10, TEXT("10s")}, { TimeUtils::Second, TEXT("1s")}, { TimeUtils::Milisecond * 999.99, TEXT("999.99ms")}, { TimeUtils::Milisecond * 999.95, TEXT("999.95ms")}, { 0, TEXT("[threshold 1s / 999.9 ms]")}, { TimeUtils::Milisecond * 999.94, TEXT("999.94ms")}, { TimeUtils::Milisecond * 999.9, TEXT("999.9ms")}, { TimeUtils::Milisecond * 999, TEXT("999ms")}, { TimeUtils::Milisecond * 100, TEXT("100ms")}, { TimeUtils::Milisecond * 10, TEXT("10ms")}, { TimeUtils::Milisecond * 1.55, TEXT("1.55ms")}, { TimeUtils::Milisecond * 1.5, TEXT("1.5ms")}, { TimeUtils::Milisecond * 1.05, TEXT("1.05ms")}, { TimeUtils::Milisecond, TEXT("1ms")}, {TimeUtils::Microsecond * 999.99, TEXT("999.99µs")}, {TimeUtils::Microsecond * 999.95, TEXT("999.95µs")}, { 0, TEXT("[threshold 1 ms / 999.9 µs]")}, {TimeUtils::Microsecond * 999.94, TEXT("999.94µs")}, { TimeUtils::Microsecond * 999.9, TEXT("999.9µs")}, { TimeUtils::Microsecond * 999, TEXT("999µs")}, { TimeUtils::Microsecond * 100, TEXT("100µs")}, { TimeUtils::Microsecond * 10, TEXT("10µs")}, { TimeUtils::Microsecond, TEXT("1µs")}, { TimeUtils::Nanosecond * 999.9, TEXT("999.9ns")}, { TimeUtils::Nanosecond * 999.5, TEXT("999.5ns")}, { 0, TEXT("[threshold 1 µs / 999 ns]")}, { TimeUtils::Nanosecond * 999.4, TEXT("999.4ns")}, { TimeUtils::Nanosecond * 999, TEXT("999ns")}, { TimeUtils::Nanosecond * 100, TEXT("100ns")}, { TimeUtils::Nanosecond * 10, TEXT("10ns")}, { TimeUtils::Nanosecond, TEXT("1ns")}, { TimeUtils::Picosecond * 999.9, TEXT("999.9ps")}, { TimeUtils::Picosecond * 999.5, TEXT("999.5ps")}, { 0, TEXT("[threshold 1 ns / 999 ps]")}, { TimeUtils::Picosecond * 999.4, TEXT("999.4ps")}, { TimeUtils::Picosecond * 999, TEXT("999ps")}, { TimeUtils::Picosecond * 100, TEXT("100ps")}, { TimeUtils::Picosecond * 10, TEXT("10ps")}, { TimeUtils::Picosecond, TEXT("1ps")}, { TimeUtils::Picosecond * 0.1, TEXT("0.1ps")}, }; const int32 DataCount = sizeof(Data) / sizeof(FTestTimeAutoData); for (int32 Index = 0; Index < DataCount; ++Index) { FString Str = FormatTimeAuto(Data[Index].T); // UE_LOG(TraceInsights, Log, TEXT("%s : %s"), Data[Index].Msg, *Str); } } //////////////////////////////////////////////////////////////////////////////////////////////////// int32 GetNumDigits(const double Precision) { // float Log10 = -log10(Precision); float Log10 = -FMath::LogX(10.0f, static_cast(Precision)); int32 D = (Log10 > 0) ? FMath::CeilToInt(Log10) : 0; return D; } PRAGMA_DISABLE_OPTIMIZATION int32 GetNumDigitsOptDisabled(const double Precision) { // double Log10 = -log10(Precision); float Log10 = -FMath::LogX(10.0f, static_cast(Precision)); int32 D = (Log10 > 0) ? FMath::CeilToInt(Log10) : 0; return D; } PRAGMA_ENABLE_OPTIMIZATION PRAGMA_DISABLE_OPTIMIZATION void TestOptimizationIssue() { constexpr double Ns = 0.000000001; int32 D1 = GetNumDigits(Ns); int32 D2 = GetNumDigitsOptDisabled(Ns); // UE_LOG(TraceInsights, Log, TEXT("D1 = %d, D2 = %d"), D1, D2); ensure(D1 == 9); // 10 ? ensure(D1 == D2); } PRAGMA_ENABLE_OPTIMIZATION //////////////////////////////////////////////////////////////////////////////////////////////////// } // namespace TimeUtils