diff --git a/SysManager/SysManager.IntegrationTests/CleanupCategoryTests.cs b/SysManager/SysManager.IntegrationTests/CleanupCategoryTests.cs index 3e3fbff3..fa4a17f2 100644 --- a/SysManager/SysManager.IntegrationTests/CleanupCategoryTests.cs +++ b/SysManager/SysManager.IntegrationTests/CleanupCategoryTests.cs @@ -2,6 +2,7 @@ // Author: laurentiu021 · https://github.com/laurentiu021/SystemManager // License: MIT +using SysManager.Helpers; using SysManager.Models; namespace SysManager.IntegrationTests; @@ -12,13 +13,13 @@ public class CleanupCategoryTests [InlineData(0L, "0 B")] [InlineData(1L, "1 B")] [InlineData(1023L, "1023 B")] - [InlineData(1024L, "1 KB")] - [InlineData(1024L * 1024, "1 MB")] - [InlineData(1024L * 1024 * 1024, "1 GB")] - [InlineData(1024L * 1024 * 1024 * 1024, "1 TB")] + [InlineData(1024L, "1.0 KB")] + [InlineData(1024L * 1024, "1.0 MB")] + [InlineData(1024L * 1024 * 1024, "1.0 GB")] + [InlineData(1024L * 1024 * 1024 * 1024, "1.0 TB")] public void HumanSize_FormatsCorrectly(long bytes, string expected) { - Assert.Equal(expected, CleanupCategory.HumanSize(bytes)); + Assert.Equal(expected, FormatHelper.FormatSize(bytes)); } [Theory] @@ -27,13 +28,13 @@ public void HumanSize_FormatsCorrectly(long bytes, string expected) [InlineData(long.MinValue)] public void HumanSize_NegativeIsZero(long bytes) { - Assert.Equal("0 B", CleanupCategory.HumanSize(bytes)); + Assert.Equal("0 B", FormatHelper.FormatSize(bytes)); } [Fact] public void HumanSize_VeryLarge_DoesNotCrash() { - var s = CleanupCategory.HumanSize(long.MaxValue); + var s = FormatHelper.FormatSize(long.MaxValue); Assert.NotNull(s); Assert.Contains("TB", s); } @@ -71,7 +72,7 @@ public void Category_SizeDisplay_UsesHumanSize() Name = "X", Description = "Y", Paths = Array.Empty(), TotalSizeBytes = 2048 }; - Assert.Equal("2 KB", c.SizeDisplay); + Assert.Equal("2.0 KB", c.SizeDisplay); } [Fact] @@ -148,7 +149,7 @@ public void LargeFileEntry_SizeDisplay_UsesHumanSize() SizeBytes = 2048, LastModified = DateTime.Now }; - Assert.Equal("2 KB", e.SizeDisplay); + Assert.Equal("2.0 KB", e.SizeDisplay); } [Fact] diff --git a/SysManager/SysManager.Tests/CleanupCategoryHumanSizeBulkTests.cs b/SysManager/SysManager.Tests/CleanupCategoryHumanSizeBulkTests.cs index 1071968e..ecc787d2 100644 --- a/SysManager/SysManager.Tests/CleanupCategoryHumanSizeBulkTests.cs +++ b/SysManager/SysManager.Tests/CleanupCategoryHumanSizeBulkTests.cs @@ -2,6 +2,7 @@ // Author: laurentiu021 · https://github.com/laurentiu021/SystemManager // License: MIT +using SysManager.Helpers; using SysManager.Models; namespace SysManager.Tests; @@ -24,7 +25,7 @@ public static IEnumerable Powers() [MemberData(nameof(Powers))] public void HumanSize_PowersOfTwo_DoNotCrash(long n) { - var s = CleanupCategory.HumanSize(n); + var s = FormatHelper.FormatSize(n); Assert.False(string.IsNullOrWhiteSpace(s)); Assert.Matches(@"^[\d.,]+ (B|KB|MB|GB|TB)$", s); } @@ -41,7 +42,7 @@ public void HumanSize_PowersOfTwo_DoNotCrash(long n) [InlineData(1024L * 1024 * 1024 * 1024, "TB")] public void HumanSize_PicksCorrectUnit(long bytes, string expectedUnit) { - var s = CleanupCategory.HumanSize(bytes); + var s = FormatHelper.FormatSize(bytes); Assert.EndsWith(expectedUnit, s); } @@ -58,7 +59,7 @@ public void HumanSize_PicksCorrectUnit(long bytes, string expectedUnit) [InlineData(100_000_000_000L)] public void HumanSize_RealisticSizes_HaveNumericPrefix(long bytes) { - var s = CleanupCategory.HumanSize(bytes); + var s = FormatHelper.FormatSize(bytes); Assert.Matches(@"^\d", s); } @@ -68,7 +69,7 @@ public void HumanSize_RealisticSizes_HaveNumericPrefix(long bytes) [InlineData(long.MinValue, "0 B")] public void HumanSize_ZeroOrNegative_IsZeroBytes(long bytes, string expected) { - Assert.Equal(expected, CleanupCategory.HumanSize(bytes)); + Assert.Equal(expected, FormatHelper.FormatSize(bytes)); } [Theory] @@ -78,7 +79,7 @@ public void HumanSize_ZeroOrNegative_IsZeroBytes(long bytes, string expected) [InlineData(1024L * 1024 * 1024 * 1023)] public void HumanSize_NearBoundary_ValidFormat(long n) { - Assert.Matches(@"^[\d.,]+ (B|KB|MB|GB|TB)$", CleanupCategory.HumanSize(n)); + Assert.Matches(@"^[\d.,]+ (B|KB|MB|GB|TB)$", FormatHelper.FormatSize(n)); } [Theory] @@ -92,7 +93,7 @@ public void HumanSize_NearBoundary_ValidFormat(long n) [InlineData(15_500_000_000L)] public void HumanSize_FractionalResult_IsFormatted(long bytes) { - var s = CleanupCategory.HumanSize(bytes); + var s = FormatHelper.FormatSize(bytes); Assert.False(string.IsNullOrWhiteSpace(s)); } @@ -109,7 +110,7 @@ public void HumanSize_FractionalResult_IsFormatted(long bytes) [InlineData(512L)] public void HumanSize_SmallByteValues_StayInBytes(long n) { - var s = CleanupCategory.HumanSize(n); + var s = FormatHelper.FormatSize(n); Assert.EndsWith(" B", s); } @@ -117,8 +118,8 @@ public void HumanSize_SmallByteValues_StayInBytes(long n) public void HumanSize_SameInputSameOutput() { // Determinism - var a = CleanupCategory.HumanSize(123456789); - var b = CleanupCategory.HumanSize(123456789); + var a = FormatHelper.FormatSize(123456789); + var b = FormatHelper.FormatSize(123456789); Assert.Equal(a, b); } @@ -132,6 +133,6 @@ public void HumanSize_SameInputSameOutput() [InlineData(1024L * 1024 * 1024 * 1024 * 10, "TB")] public void HumanSize_LargeMultiples(long n, string unit) { - Assert.EndsWith(unit, CleanupCategory.HumanSize(n)); + Assert.EndsWith(unit, FormatHelper.FormatSize(n)); } } diff --git a/SysManager/SysManager.Tests/CleanupCategoryHumanSizeExtendedTests.cs b/SysManager/SysManager.Tests/CleanupCategoryHumanSizeExtendedTests.cs index 966bea6a..f753fd0e 100644 --- a/SysManager/SysManager.Tests/CleanupCategoryHumanSizeExtendedTests.cs +++ b/SysManager/SysManager.Tests/CleanupCategoryHumanSizeExtendedTests.cs @@ -1,4 +1,5 @@ -// SysManager · CleanupCategory.HumanSize extended boundary tests +// SysManager · FormatHelper.FormatSize extended boundary tests +using SysManager.Helpers; using SysManager.Models; namespace SysManager.Tests; @@ -8,69 +9,69 @@ public class CleanupCategoryHumanSizeExtendedTests [Fact] public void HumanSize_Zero_ReturnsZeroB() { - Assert.Equal("0 B", CleanupCategory.HumanSize(0)); + Assert.Equal("0 B", FormatHelper.FormatSize(0)); } [Fact] public void HumanSize_Negative_ReturnsZeroB() { - Assert.Equal("0 B", CleanupCategory.HumanSize(-100)); + Assert.Equal("0 B", FormatHelper.FormatSize(-100)); } [Fact] public void HumanSize_OneByte_Returns1B() { - Assert.Equal("1 B", CleanupCategory.HumanSize(1)); + Assert.Equal("1 B", FormatHelper.FormatSize(1)); } [Fact] public void HumanSize_1023Bytes_Returns1023B() { - Assert.Equal("1023 B", CleanupCategory.HumanSize(1023)); + Assert.Equal("1023 B", FormatHelper.FormatSize(1023)); } [Fact] public void HumanSize_1024Bytes_Returns1KB() { - Assert.Equal("1 KB", CleanupCategory.HumanSize(1024)); + Assert.Equal("1.0 KB", FormatHelper.FormatSize(1024)); } [Fact] public void HumanSize_1MB_Returns1MB() { - Assert.Equal("1 MB", CleanupCategory.HumanSize(1024 * 1024)); + Assert.Equal("1.0 MB", FormatHelper.FormatSize(1024 * 1024)); } [Fact] public void HumanSize_1GB_Returns1GB() { - Assert.Equal("1 GB", CleanupCategory.HumanSize(1024L * 1024 * 1024)); + Assert.Equal("1.0 GB", FormatHelper.FormatSize(1024L * 1024 * 1024)); } [Fact] public void HumanSize_1TB_Returns1TB() { - Assert.Equal("1 TB", CleanupCategory.HumanSize(1024L * 1024 * 1024 * 1024)); + Assert.Equal("1.0 TB", FormatHelper.FormatSize(1024L * 1024 * 1024 * 1024)); } [Fact] public void HumanSize_1point5GB_FormatsCorrectly() { long bytes = (long)(1.5 * 1024 * 1024 * 1024); - Assert.Equal("1.5 GB", CleanupCategory.HumanSize(bytes)); + Assert.Equal("1.5 GB", FormatHelper.FormatSize(bytes)); } [Fact] public void HumanSize_LargeValue_StaysInTB() { long bytes = 10L * 1024 * 1024 * 1024 * 1024; - Assert.Equal("10 TB", CleanupCategory.HumanSize(bytes)); + Assert.Equal("10.0 TB", FormatHelper.FormatSize(bytes)); } [Fact] public void HumanSize_MaxLong_DoesNotThrow() { - var result = CleanupCategory.HumanSize(long.MaxValue); + var result = FormatHelper.FormatSize(long.MaxValue); Assert.Contains("TB", result); } @@ -82,7 +83,7 @@ public void CleanupResult_Summary_NoErrors_FormatsCorrectly() BytesFreed = 1024 * 1024 * 50, FilesDeleted = 100 }; - Assert.Contains("50 MB", result.Summary); + Assert.Contains("50.0 MB", result.Summary); Assert.Contains("100", result.Summary); Assert.DoesNotContain("skipped", result.Summary); } @@ -139,7 +140,7 @@ public void CleanupCategory_SizeDisplay_ShowsHumanSize() TotalSizeBytes = 2048, FileCount = 5 }; - Assert.Equal("2 KB", cat.SizeDisplay); + Assert.Equal("2.0 KB", cat.SizeDisplay); } [Fact] diff --git a/SysManager/SysManager.Tests/ProcessManagerServiceTests.cs b/SysManager/SysManager.Tests/ProcessManagerServiceTests.cs index ae3d1985..ee67a502 100644 --- a/SysManager/SysManager.Tests/ProcessManagerServiceTests.cs +++ b/SysManager/SysManager.Tests/ProcessManagerServiceTests.cs @@ -117,7 +117,7 @@ public void OpenFileLocation_NonExistentPath_DoesNotThrow() public void ProcessEntry_MemoryDisplay_FormatsCorrectly() { var entry = new ProcessEntry { MemoryBytes = 52_428_800 }; // 50 MB - Assert.Equal("50 MB", entry.MemoryDisplay); + Assert.Equal("50.0 MB", entry.MemoryDisplay); } [Fact] diff --git a/SysManager/SysManager/Helpers/FormatHelper.cs b/SysManager/SysManager/Helpers/FormatHelper.cs index bafe9912..d975d246 100644 --- a/SysManager/SysManager/Helpers/FormatHelper.cs +++ b/SysManager/SysManager/Helpers/FormatHelper.cs @@ -11,13 +11,15 @@ namespace SysManager.Helpers; public static class FormatHelper { /// - /// Formats a byte count into a human-readable string (B, KB, MB, GB). + /// Formats a byte count into a human-readable string (B, KB, MB, GB, TB). /// public static string FormatSize(long bytes) => bytes switch { - >= 1L << 30 => $"{bytes / (double)(1L << 30):F1} GB", - >= 1L << 20 => $"{bytes / (double)(1L << 20):F1} MB", - >= 1L << 10 => $"{bytes / (double)(1L << 10):F1} KB", - _ => $"{bytes} B" + <= 0 => "0 B", + < 1L << 10 => $"{bytes} B", + < 1L << 20 => $"{bytes / (double)(1L << 10):F1} KB", + < 1L << 30 => $"{bytes / (double)(1L << 20):F1} MB", + < 1L << 40 => $"{bytes / (double)(1L << 30):F1} GB", + _ => $"{bytes / (double)(1L << 40):F1} TB" }; } diff --git a/SysManager/SysManager/Models/CleanupCategory.cs b/SysManager/SysManager/Models/CleanupCategory.cs index 95fb64fc..db756c03 100644 --- a/SysManager/SysManager/Models/CleanupCategory.cs +++ b/SysManager/SysManager/Models/CleanupCategory.cs @@ -3,6 +3,7 @@ // License: MIT using CommunityToolkit.Mvvm.ComponentModel; +using SysManager.Helpers; namespace SysManager.Models; @@ -22,17 +23,8 @@ public sealed partial class CleanupCategory : ObservableObject public TimeSpan? OlderThan { get; init; } public bool IsDestructiveHint { get; init; } - public string SizeDisplay => HumanSize(TotalSizeBytes); + public string SizeDisplay => FormatHelper.FormatSize(TotalSizeBytes); public string CountDisplay => $"{FileCount:N0} files"; - - public static string HumanSize(long bytes) - { - if (bytes <= 0) return "0 B"; - string[] u = { "B", "KB", "MB", "GB", "TB" }; - double v = bytes; var i = 0; - while (v >= 1024 && i < u.Length - 1) { v /= 1024; i++; } - return $"{v:0.#} {u[i]}"; - } } public sealed class CleanupResult @@ -41,7 +33,7 @@ public sealed class CleanupResult public int FilesDeleted { get; init; } public IReadOnlyList Errors { get; init; } = []; public string Summary => - $"Freed {CleanupCategory.HumanSize(BytesFreed)} across {FilesDeleted:N0} files" + + $"Freed {FormatHelper.FormatSize(BytesFreed)} across {FilesDeleted:N0} files" + (Errors.Count > 0 ? $" · {Errors.Count} skipped" : string.Empty); } @@ -52,6 +44,6 @@ public sealed class LargeFileEntry public required string Name { get; init; } public required long SizeBytes { get; init; } public required DateTime LastModified { get; init; } - public string SizeDisplay => CleanupCategory.HumanSize(SizeBytes); + public string SizeDisplay => FormatHelper.FormatSize(SizeBytes); public string LastModifiedDisplay => LastModified.ToString("dd MMM yyyy"); } diff --git a/SysManager/SysManager/Models/DiskUsageEntry.cs b/SysManager/SysManager/Models/DiskUsageEntry.cs index fe0c391c..95669694 100644 --- a/SysManager/SysManager/Models/DiskUsageEntry.cs +++ b/SysManager/SysManager/Models/DiskUsageEntry.cs @@ -3,6 +3,7 @@ // License: MIT using CommunityToolkit.Mvvm.ComponentModel; +using SysManager.Helpers; namespace SysManager.Models; @@ -23,5 +24,5 @@ public sealed partial class DiskUsageEntry : ObservableObject [ObservableProperty] private bool _isAccessDenied; /// Formatted size for display. - public string SizeDisplay => CleanupCategory.HumanSize(SizeBytes); + public string SizeDisplay => FormatHelper.FormatSize(SizeBytes); } diff --git a/SysManager/SysManager/Models/InstalledApp.cs b/SysManager/SysManager/Models/InstalledApp.cs index 9c9d1e7a..2d03dbea 100644 --- a/SysManager/SysManager/Models/InstalledApp.cs +++ b/SysManager/SysManager/Models/InstalledApp.cs @@ -4,6 +4,7 @@ using System.Windows.Media; using CommunityToolkit.Mvvm.ComponentModel; +using SysManager.Helpers; namespace SysManager.Models; @@ -27,5 +28,5 @@ public sealed partial class InstalledApp : ObservableObject [ObservableProperty] private string _quietUninstallString = ""; /// Formatted size for display. - public string SizeDisplay => SizeBytes > 0 ? CleanupCategory.HumanSize(SizeBytes) : "—"; + public string SizeDisplay => SizeBytes > 0 ? FormatHelper.FormatSize(SizeBytes) : "—"; } diff --git a/SysManager/SysManager/Models/ProcessEntry.cs b/SysManager/SysManager/Models/ProcessEntry.cs index ef42c8b1..ebfe617b 100644 --- a/SysManager/SysManager/Models/ProcessEntry.cs +++ b/SysManager/SysManager/Models/ProcessEntry.cs @@ -4,6 +4,7 @@ using System.Windows.Media; using CommunityToolkit.Mvvm.ComponentModel; +using SysManager.Helpers; namespace SysManager.Models; @@ -34,5 +35,5 @@ public sealed partial class ProcessEntry : ObservableObject [ObservableProperty] private bool _canOpenFileLocation; /// Formatted memory for display. - public string MemoryDisplay => CleanupCategory.HumanSize(MemoryBytes); + public string MemoryDisplay => FormatHelper.FormatSize(MemoryBytes); } diff --git a/SysManager/SysManager/Models/TuneUpResult.cs b/SysManager/SysManager/Models/TuneUpResult.cs index 162fb945..fb911883 100644 --- a/SysManager/SysManager/Models/TuneUpResult.cs +++ b/SysManager/SysManager/Models/TuneUpResult.cs @@ -2,6 +2,8 @@ // Author: laurentiu021 · https://github.com/laurentiu021/SystemManager // License: MIT +using SysManager.Helpers; + namespace SysManager.Models; /// @@ -36,7 +38,7 @@ public sealed class TuneUpResult public bool RamWarning => RamUsedPercent >= 85; // ── Summary helpers ──────────────────────────────────────────────── - public string FreedDisplay => CleanupCategory.HumanSize(TempBytesFreed); + public string FreedDisplay => FormatHelper.FormatSize(TempBytesFreed); public int WarningCount { diff --git a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs index 55f6f714..bc681a2e 100644 --- a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs +++ b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs @@ -54,9 +54,9 @@ public partial class DeepCleanupViewModel : ViewModelBase [ObservableProperty] private int _topCount = 100; public long TotalSelectedBytes => Categories.Where(c => c.IsSelected).Sum(c => c.TotalSizeBytes); - public string TotalSelectedDisplay => CleanupCategory.HumanSize(TotalSelectedBytes); + public string TotalSelectedDisplay => FormatHelper.FormatSize(TotalSelectedBytes); - public string LargeBytesScannedDisplay => CleanupCategory.HumanSize(LargeBytesScanned); + public string LargeBytesScannedDisplay => FormatHelper.FormatSize(LargeBytesScanned); public DeepCleanupViewModel() { @@ -185,10 +185,10 @@ private async Task ScanCoreAsync() c.PropertyChanged += OnCategoryPropertyChanged; foreach (var c in catList) Categories.Add(c); var total = cats.Sum(c => c.TotalSizeBytes); - ScanSummary = $"Found {CleanupCategory.HumanSize(total)} across {cats.Count} categories. Untick anything you want to keep."; + ScanSummary = $"Found {FormatHelper.FormatSize(total)} across {cats.Count} categories. Untick anything you want to keep."; ScanStatusLine = "Scan complete."; Log.Information("Deep cleanup scan completed: {Size} across {Count} categories", - CleanupCategory.HumanSize(total), cats.Count); + FormatHelper.FormatSize(total), cats.Count); OnPropertyChanged(nameof(TotalSelectedBytes)); OnPropertyChanged(nameof(TotalSelectedDisplay)); }