diff --git a/docs/input/live/progress.md b/docs/input/live/progress.md index ecb8587..ef46eaa 100644 --- a/docs/input/live/progress.md +++ b/docs/input/live/progress.md @@ -85,6 +85,8 @@ AnsiConsole.Progress() new PercentageColumn(), // Percentage new RemainingTimeColumn(), // Remaining time new SpinnerColumn(), // Spinner + new DownloadedColumn(), // Downloaded + new TransferSpeedColumn(), // Transfer speed }) .Start(ctx => { diff --git a/src/Spectre.Console/Internal/FileSize.cs b/src/Spectre.Console/Internal/FileSize.cs index 4a8254a..b47a612 100644 --- a/src/Spectre.Console/Internal/FileSize.cs +++ b/src/Spectre.Console/Internal/FileSize.cs @@ -3,33 +3,71 @@ namespace Spectre.Console; internal struct FileSize { public double Bytes { get; } - public FileSizeUnit Unit { get; } + public double Bits => Bytes * 8; + + public FileSizePrefix Prefix { get; } = FileSizePrefix.None; + + private readonly FileSizeBase _prefixBase = FileSizeBase.Binary; + + /// + /// If enabled, will display the output in bits, rather than bytes. + /// + private readonly bool _showBits = false; + public string Suffix => GetSuffix(); public FileSize(double bytes) { Bytes = bytes; - Unit = Detect(bytes); + Prefix = DetectPrefix(bytes); } - public FileSize(double bytes, FileSizeUnit unit) + public FileSize(double bytes, FileSizeBase @base) { Bytes = bytes; - Unit = unit; + _prefixBase = @base; + Prefix = DetectPrefix(bytes); + } + + public FileSize(double bytes, FileSizeBase @base, bool showBits) + { + Bytes = bytes; + _showBits = showBits; + + _prefixBase = @base; + Prefix = DetectPrefix(bytes); + } + + public FileSize(double bytes, FileSizePrefix prefix) + { + Bytes = bytes; + Prefix = prefix; + } + + public FileSize(double bytes, FileSizePrefix prefix, FileSizeBase @base, bool showBits) + { + Bytes = bytes; + _showBits = showBits; + + _prefixBase = @base; + Prefix = prefix; } public string Format(CultureInfo? culture = null) { - var @base = GetBase(Unit); - if (@base == 0) + var unitBase = Math.Pow((int)_prefixBase, (int)Prefix); + + if (_showBits) { - @base = 1; + var bits = Bits / unitBase; + return Prefix == FileSizePrefix.None ? + ((int)bits).ToString(culture ?? CultureInfo.InvariantCulture) + : bits.ToString("F1", culture ?? CultureInfo.InvariantCulture); } - var bytes = Bytes / @base; - - return Unit == FileSizeUnit.Byte - ? ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture) + var bytes = Bytes / unitBase; + return Prefix == FileSizePrefix.None ? + ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture) : bytes.ToString("F1", culture ?? CultureInfo.InvariantCulture); } @@ -50,36 +88,67 @@ internal struct FileSize private string GetSuffix() { - return (Bytes, Unit) switch + return (Bytes, Unit: Prefix, PrefixBase: _prefixBase, ShowBits: _showBits) switch { - (_, FileSizeUnit.KiloByte) => "KB", - (_, FileSizeUnit.MegaByte) => "MB", - (_, FileSizeUnit.GigaByte) => "GB", - (_, FileSizeUnit.TeraByte) => "TB", - (_, FileSizeUnit.PetaByte) => "PB", - (_, FileSizeUnit.ExaByte) => "EB", - (_, FileSizeUnit.ZettaByte) => "ZB", - (_, FileSizeUnit.YottaByte) => "YB", - (1, _) => "byte", - (_, _) => "bytes", + (_, FileSizePrefix.Kilo, FileSizeBase.Binary, false) => "KiB", + (_, FileSizePrefix.Mega, FileSizeBase.Binary, false) => "MiB", + (_, FileSizePrefix.Giga, FileSizeBase.Binary, false) => "GiB", + (_, FileSizePrefix.Tera, FileSizeBase.Binary, false) => "TiB", + (_, FileSizePrefix.Peta, FileSizeBase.Binary, false) => "PiB", + (_, FileSizePrefix.Exa, FileSizeBase.Binary, false) => "EiB", + (_, FileSizePrefix.Zetta, FileSizeBase.Binary, false) => "ZiB", + (_, FileSizePrefix.Yotta, FileSizeBase.Binary, false) => "YiB", + + (_, FileSizePrefix.Kilo, FileSizeBase.Binary, true) => "Kibit", + (_, FileSizePrefix.Mega, FileSizeBase.Binary, true) => "Mibit", + (_, FileSizePrefix.Giga, FileSizeBase.Binary, true) => "Gibit", + (_, FileSizePrefix.Tera, FileSizeBase.Binary, true) => "Tibit", + (_, FileSizePrefix.Peta, FileSizeBase.Binary, true) => "Pibit", + (_, FileSizePrefix.Exa, FileSizeBase.Binary, true) => "Eibit", + (_, FileSizePrefix.Zetta, FileSizeBase.Binary, true) => "Zibit", + (_, FileSizePrefix.Yotta, FileSizeBase.Binary, true) => "Yibit", + + (_, FileSizePrefix.Kilo, FileSizeBase.Decimal, false) => "KB", + (_, FileSizePrefix.Mega, FileSizeBase.Decimal, false) => "MB", + (_, FileSizePrefix.Giga, FileSizeBase.Decimal, false) => "GB", + (_, FileSizePrefix.Tera, FileSizeBase.Decimal, false) => "TB", + (_, FileSizePrefix.Peta, FileSizeBase.Decimal, false) => "PB", + (_, FileSizePrefix.Exa, FileSizeBase.Decimal, false) => "EB", + (_, FileSizePrefix.Zetta, FileSizeBase.Decimal, false) => "ZB", + (_, FileSizePrefix.Yotta, FileSizeBase.Decimal, false) => "YB", + + (_, FileSizePrefix.Kilo, FileSizeBase.Decimal, true) => "Kbit", + (_, FileSizePrefix.Mega, FileSizeBase.Decimal, true) => "Mbit", + (_, FileSizePrefix.Giga, FileSizeBase.Decimal, true) => "Gbit", + (_, FileSizePrefix.Tera, FileSizeBase.Decimal, true) => "Tbit", + (_, FileSizePrefix.Peta, FileSizeBase.Decimal, true) => "Pbit", + (_, FileSizePrefix.Exa, FileSizeBase.Decimal, true) => "Ebit", + (_, FileSizePrefix.Zetta, FileSizeBase.Decimal, true) => "Zbit", + (_, FileSizePrefix.Yotta, FileSizeBase.Decimal, true) => "Ybit", + + (1, _, _, true) => "bit", + (_, _, _, true) => "bits", + (1, _, _, false) => "byte", + (_, _, _, false) => "bytes", }; } - private static FileSizeUnit Detect(double bytes) + private FileSizePrefix DetectPrefix(double bytes) { - foreach (var unit in (FileSizeUnit[])Enum.GetValues(typeof(FileSizeUnit))) + if (_showBits) { - if (bytes < (GetBase(unit) * 1024)) + bytes *= 8; + } + + foreach (var prefix in (FileSizePrefix[])Enum.GetValues(typeof(FileSizePrefix))) + { + // Trying to find the largest unit, that the number of bytes can fit under. Ex. 40kb < 1mb + if (bytes < Math.Pow((int)_prefixBase, (int)prefix + 1)) { - return unit; + return prefix; } } - return FileSizeUnit.Byte; - } - - private static double GetBase(FileSizeUnit unit) - { - return Math.Pow(1024, (int)unit); + return FileSizePrefix.None; } } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/FileSizeBase.cs b/src/Spectre.Console/Internal/FileSizeBase.cs new file mode 100644 index 0000000..8dd73cb --- /dev/null +++ b/src/Spectre.Console/Internal/FileSizeBase.cs @@ -0,0 +1,17 @@ +namespace Spectre.Console; + +/// +/// Determines possible file size base prefixes. (base 2/base 10). +/// +public enum FileSizeBase +{ + /// + /// The SI prefix definition (base 10) of kilobyte, megabyte, etc. + /// + Decimal = 1000, + + /// + /// The IEC binary prefix definition (base 2) of kibibyte, mebibyte, etc. + /// + Binary = 1024, +} \ No newline at end of file diff --git a/src/Spectre.Console/Internal/FileSizePrefix.cs b/src/Spectre.Console/Internal/FileSizePrefix.cs new file mode 100644 index 0000000..36def53 --- /dev/null +++ b/src/Spectre.Console/Internal/FileSizePrefix.cs @@ -0,0 +1,14 @@ +namespace Spectre.Console; + +internal enum FileSizePrefix +{ + None = 0, + Kilo = 1, + Mega = 2, + Giga = 3, + Tera = 4, + Peta = 5, + Exa = 6, + Zetta = 7, + Yotta = 8, +} \ No newline at end of file diff --git a/src/Spectre.Console/Internal/FileSizeUnit.cs b/src/Spectre.Console/Internal/FileSizeUnit.cs deleted file mode 100644 index aca46e5..0000000 --- a/src/Spectre.Console/Internal/FileSizeUnit.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Spectre.Console; - -internal enum FileSizeUnit -{ - Byte = 0, - KiloByte = 1, - MegaByte = 2, - GigaByte = 3, - TeraByte = 4, - PetaByte = 5, - ExaByte = 6, - ZettaByte = 7, - YottaByte = 8, -} \ No newline at end of file diff --git a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs index 6162986..99e6939 100644 --- a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs @@ -10,10 +10,20 @@ public sealed class DownloadedColumn : ProgressColumn /// public CultureInfo? Culture { get; set; } + /// + /// Gets or sets the to use. + /// + public FileSizeBase Base { get; set; } = FileSizeBase.Binary; + + /// + /// Gets or sets a value indicating whether to display the transfer speed in bits. + /// + public bool ShowBits { get; set; } + /// public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { - var total = new FileSize(task.MaxValue); + var total = new FileSize(task.MaxValue, Base, ShowBits); if (task.IsFinished) { @@ -24,7 +34,7 @@ public sealed class DownloadedColumn : ProgressColumn } else { - var downloaded = new FileSize(task.Value, total.Unit); + var downloaded = new FileSize(task.Value, total.Prefix, Base, ShowBits); return new Markup(string.Format( "{0}[grey]/[/]{1} [grey]{2}[/]", diff --git a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs index 84f2743..5d253d1 100644 --- a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs +++ b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs @@ -10,6 +10,16 @@ public sealed class TransferSpeedColumn : ProgressColumn /// public CultureInfo? Culture { get; set; } + /// + /// Gets or sets the to use. + /// + public FileSizeBase Base { get; set; } = FileSizeBase.Binary; + + /// + /// Gets or sets a value indicating whether to display the transfer speed in bits. + /// + public bool ShowBits { get; set; } + /// public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { @@ -18,7 +28,14 @@ public sealed class TransferSpeedColumn : ProgressColumn return new Text("?/s"); } - var size = new FileSize(task.Speed.Value); - return new Markup(string.Format("{0}/s", size.ToString(suffix: true, Culture))); + if (task.IsFinished) + { + return new Markup(string.Empty, Style.Plain); + } + else + { + var size = new FileSize(task.Speed.Value, Base, ShowBits); + return new Markup(string.Format("{0}/s", size.ToString(suffix: true, Culture))); + } } } \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs new file mode 100644 index 0000000..92c7653 --- /dev/null +++ b/src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs @@ -0,0 +1,85 @@ +namespace Spectre.Console.Tests.Unit.Internal; + +public sealed class FileSizeTests +{ + [Theory] + [InlineData(0, "0 bytes")] + [InlineData(37, "37 bytes")] + [InlineData(512, "512 bytes")] + [InlineData(15 * 1024, "15.0 KiB")] + [InlineData(1024 * 512, "512.0 KiB")] + [InlineData(5 * 1024 * 1024, "5.0 MiB")] + [InlineData(9 * 1024 * 1024, "9.0 MiB")] + public void Binary_Unit_In_Bytes_Should_Return_Expected(double bytes, string expected) + { + // Given + var filesize = new FileSize(bytes, FileSizeBase.Binary); + + // When + var result = filesize.ToString(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(0, "0 bits")] + [InlineData(37, "296 bits")] + [InlineData(512, "4.0 Kibit")] + [InlineData(15 * 1024, "120.0 Kibit")] + [InlineData(1024 * 512, "4.0 Mibit")] + [InlineData(5 * 1024 * 1024, "40.0 Mibit")] + [InlineData(210 * 1024 * 1024, "1.6 Gibit")] + [InlineData(900 * 1024 * 1024, "7.0 Gibit")] + public void Binary_Unit_In_Bits_Should_Return_Expected(double bytes, string expected) + { + // Given + var filesize = new FileSize(bytes, FileSizeBase.Binary, showBits: true); + + // When + var result = filesize.ToString(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(0, "0 bytes")] + [InlineData(37, "37 bytes")] + [InlineData(512, "512 bytes")] + [InlineData(15 * 1024, "15.4 KB")] + [InlineData(1024 * 512, "524.3 KB")] + [InlineData(5 * 1024 * 1024, "5.2 MB")] + [InlineData(9 * 1024 * 1024, "9.4 MB")] + public void Decimal_Unit_In_Bytes_Should_Return_Expected(double bytes, string expected) + { + // Given + var filesize = new FileSize(bytes, FileSizeBase.Decimal); + + // When + var result = filesize.ToString(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(0, "0 bits")] + [InlineData(37, "296 bits")] + [InlineData(512, "4.1 Kbit")] + [InlineData(15 * 1024, "122.9 Kbit")] + [InlineData(1024 * 512, "4.2 Mbit")] + [InlineData(5 * 1024 * 1024, "41.9 Mbit")] + [InlineData(900 * 1024 * 1024, "7.5 Gbit")] + public void Decimal_Unit_In_Bits_Should_Return_Expected(double bytes, string expected) + { + // Given + var filesize = new FileSize(bytes, FileSizeBase.Decimal, showBits: true); + + // When + var result = filesize.ToString(); + + // Then + result.ShouldBe(expected); + } +} \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Tests/Unit/Live/Progress/DownloadedColumnTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Live/Progress/DownloadedColumnTests.cs index 6b2b76f..452eaad 100644 --- a/src/Tests/Spectre.Console.Tests/Unit/Live/Progress/DownloadedColumnTests.cs +++ b/src/Tests/Spectre.Console.Tests/Unit/Live/Progress/DownloadedColumnTests.cs @@ -6,15 +6,71 @@ public sealed class DownloadedColumnTests [InlineData(0, 1, "0/1 byte")] [InlineData(37, 101, "37/101 bytes")] [InlineData(101, 101, "101 bytes")] - [InlineData(512, 1024, "0.5/1.0 KB")] - [InlineData(1024, 1024, "1.0 KB")] - [InlineData(1024 * 512, 5 * 1024 * 1024, "0.5/5.0 MB")] - [InlineData(5 * 1024 * 1024, 5 * 1024 * 1024, "5.0 MB")] - public void Should_Return_Correct_Value(double value, double total, string expected) + [InlineData(512, 1024, "0.5/1.0 KiB")] + [InlineData(1024, 1024, "1.0 KiB")] + [InlineData(1024 * 512, 5 * 1024 * 1024, "0.5/5.0 MiB")] + [InlineData(5 * 1024 * 1024, 5 * 1024 * 1024, "5.0 MiB")] + public void Binary_Unit_In_Bytes_Should_Return_Expected(double value, double total, string expected) { // Given var fixture = new ProgressColumnFixture(value, total); fixture.Column.Culture = CultureInfo.InvariantCulture; + fixture.Column.Base = FileSizeBase.Binary; + fixture.Column.ShowBits = false; + + // When + var result = fixture.Render(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(512, 1024, "4.0/8.0 Kibit")] + [InlineData(1024, 1024, "8.0 Kibit")] + public void Binary_Unit_In_Bits_Should_Return_Expected(double value, double total, string expected) + { + // Given + var fixture = new ProgressColumnFixture(value, total); + fixture.Column.Culture = CultureInfo.InvariantCulture; + fixture.Column.Base = FileSizeBase.Binary; + fixture.Column.ShowBits = true; + + // When + var result = fixture.Render(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(500, 1000, "0.5/1.0 KB")] + [InlineData(1000, 1000, "1.0 KB")] + public void Decimal_Unit_In_Bytes_Should_Return_Expected(double value, double total, string expected) + { + // Given + var fixture = new ProgressColumnFixture(value, total); + fixture.Column.Culture = CultureInfo.InvariantCulture; + fixture.Column.Base = FileSizeBase.Decimal; + fixture.Column.ShowBits = false; + + // When + var result = fixture.Render(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(500, 1000, "4.0/8.0 Kbit")] + [InlineData(1000, 1000, "8.0 Kbit")] + public void Decimal_Unit_In_Bits_Should_Return_Expected(double value, double total, string expected) + { + // Given + var fixture = new ProgressColumnFixture(value, total); + fixture.Column.Culture = CultureInfo.InvariantCulture; + fixture.Column.Base = FileSizeBase.Decimal; + fixture.Column.ShowBits = true; // When var result = fixture.Render();