| File: src\Shared\DurationFormatter.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; namespace Aspire.Shared; internal enum DecimalDurationDisplay { /// <summary> /// Display decimal durations with optional decimal places (0.##). /// Trailing zeros after the decimal point are not shown. /// </summary> Optional, /// <summary> /// Display decimal durations with fixed decimal places (0.00). /// Always shows exactly two decimal places. /// </summary> Fixed } internal static class DurationFormatter { [DebuggerDisplay("Unit = {Unit}, Ticks = {Ticks}, IsDecimal = {IsDecimal}")] private sealed class UnitStep { public required string Unit { get; init; } public required long Ticks { get; init; } public required long Threshold { get; init; } public bool IsDecimal { get; init; } } private static readonly List<UnitStep> s_unitSteps = new List<UnitStep> { new UnitStep { Unit = "d", Ticks = TimeSpan.TicksPerDay, Threshold = TimeSpan.TicksPerDay }, new UnitStep { Unit = "h", Ticks = TimeSpan.TicksPerHour, Threshold = TimeSpan.TicksPerHour }, new UnitStep { Unit = "m", Ticks = TimeSpan.TicksPerMinute, Threshold = TimeSpan.TicksPerMinute }, new UnitStep { Unit = "s", Ticks = TimeSpan.TicksPerSecond, Threshold = TimeSpan.TicksPerSecond / 10, IsDecimal = true }, new UnitStep { Unit = "ms", Ticks = TimeSpan.TicksPerMillisecond, Threshold = TimeSpan.TicksPerMillisecond / 100, IsDecimal = true }, new UnitStep { Unit = "μs", Ticks = TimeSpan.TicksPerMicrosecond, Threshold = TimeSpan.TicksPerMicrosecond, IsDecimal = true }, }; public static string FormatDuration(TimeSpan duration, CultureInfo? culture = null, DecimalDurationDisplay decimalDisplay = DecimalDurationDisplay.Optional) { culture ??= CultureInfo.InvariantCulture; var (primaryUnit, secondaryUnit) = ResolveUnits(duration.Ticks); var ofPrevious = primaryUnit.Ticks / secondaryUnit.Ticks; var ticks = (double)duration.Ticks; if (primaryUnit.IsDecimal) { // If the unit is decimal based, display as a decimal with 2 decimal places for alignment (Fixed) // or with optional decimal places (Optional) to avoid trailing zeros var formatString = decimalDisplay == DecimalDurationDisplay.Fixed ? $"{{0:0.00}}{primaryUnit.Unit}" : $"{{0:0.##}}{primaryUnit.Unit}"; return string.Format(culture, formatString, ticks / primaryUnit.Ticks); } var primaryValue = Math.Floor(ticks / primaryUnit.Ticks); var primaryUnitString = $"{primaryValue}{primaryUnit.Unit}"; var secondaryValue = Math.Round((ticks / secondaryUnit.Ticks) % ofPrevious, MidpointRounding.AwayFromZero); var secondaryUnitString = $"{secondaryValue}{secondaryUnit.Unit}"; return secondaryValue == 0 ? primaryUnitString : $"{primaryUnitString} {secondaryUnitString}"; } public static string GetUnit(TimeSpan duration) { var (primaryUnit, secondaryUnit) = ResolveUnits(duration.Ticks); if (primaryUnit.IsDecimal) { return primaryUnit.Unit; } return secondaryUnit.Unit; } private static (UnitStep, UnitStep) ResolveUnits(long ticks) { for (var i = 0; i < s_unitSteps.Count; i++) { var step = s_unitSteps[i]; var result = i < s_unitSteps.Count - 1 && step.Threshold > ticks; if (!result) { return (step, i < s_unitSteps.Count - 1 ? s_unitSteps[i + 1] : step); } } return (s_unitSteps[^1], s_unitSteps[^1]); } }