Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 91 additions & 32 deletions src/Spectre.Console/Internal/FileSize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,61 @@ 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;

/// <summary>
/// If enabled, will display the output in bits, rather than bytes.
/// </summary>
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, FileSizeBase prefix)
{
Bytes = bytes;
_prefixBase = prefix;
Prefix = DetectPrefix(bytes);
}

public FileSize(double bytes, FileSizeBase prefix, bool showBits)
{
Bytes = bytes;
_prefixBase = prefix;
_showBits = showBits;
Prefix = DetectPrefix(bytes);
}

public FileSize(double bytes, FileSizeUnit unit)
public FileSize(double bytes, FileSizePrefix prefix)
{
Bytes = bytes;
Unit = unit;
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);
}

Expand All @@ -50,36 +78,67 @@ public string ToString(bool suffix = true, CultureInfo? culture = null)

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;
}
}
17 changes: 17 additions & 0 deletions src/Spectre.Console/Internal/FileSizeBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Spectre.Console;

/// <summary>
/// Determines possible file size base prefixes. (base 2/base 10).
/// </summary>
public enum FileSizeBase
{
/// <summary>
/// The SI prefix definition (base 10) of kilobyte, megabyte, etc.
/// </summary>
Decimal = 1000,

/// <summary>
/// The IEC binary prefix definition (base 2) of kibibyte, mebibyte, etc.
/// </summary>
Binary = 1024,
}
14 changes: 14 additions & 0 deletions src/Spectre.Console/Internal/FileSizePrefix.cs
Original file line number Diff line number Diff line change
@@ -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,
}
14 changes: 0 additions & 14 deletions src/Spectre.Console/Internal/FileSizeUnit.cs

This file was deleted.

14 changes: 12 additions & 2 deletions src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ public sealed class DownloadedColumn : ProgressColumn
/// </summary>
public CultureInfo? Culture { get; set; }

/// <summary>
/// Gets or sets the <see cref="FileSizeBase"/> to use.
/// </summary>
public FileSizeBase Base { get; set; } = FileSizeBase.Binary;

/// <summary>
/// Gets or sets a value indicating whether to display the transfer speed in bits.
/// </summary>
public bool DisplayBits { get; set; }

/// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var total = new FileSize(task.MaxValue);
var total = new FileSize(task.MaxValue, Base, DisplayBits);

if (task.IsFinished)
{
Expand All @@ -24,7 +34,7 @@ public override IRenderable Render(RenderOptions options, ProgressTask task, Tim
}
else
{
var downloaded = new FileSize(task.Value, total.Unit);
var downloaded = new FileSize(task.Value, total.Prefix);

return new Markup(string.Format(
"{0}[grey]/[/]{1} [grey]{2}[/]",
Expand Down
12 changes: 11 additions & 1 deletion src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ public sealed class TransferSpeedColumn : ProgressColumn
/// </summary>
public CultureInfo? Culture { get; set; }

/// <summary>
/// Gets or sets the <see cref="FileSizeBase"/> to use.
/// </summary>
public FileSizeBase Base { get; set; } = FileSizeBase.Binary;

/// <summary>
/// Gets or sets a value indicating whether to display the transfer speed in bits.
/// </summary>
public bool DisplayBits { get; set; }

/// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
Expand All @@ -18,7 +28,7 @@ public override IRenderable Render(RenderOptions options, ProgressTask task, Tim
return new Text("?/s");
}

var size = new FileSize(task.Speed.Value);
var size = new FileSize(task.Speed.Value, Base, DisplayBits);
return new Markup(string.Format("{0}/s", size.ToString(suffix: true, Culture)));
}
}
85 changes: 85 additions & 0 deletions src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs
Original file line number Diff line number Diff line change
@@ -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, displayBits: true);

Check failure on line 37 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 37 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 37 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 37 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 37 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

// 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, displayBits: true);

Check failure on line 77 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 77 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 77 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 77 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

Check failure on line 77 in src/Tests/Spectre.Console.Tests/Unit/Internal/FileSizeTests.cs

View workflow job for this annotation

GitHub Actions / Build

The best overload for 'FileSize' does not have a parameter named 'displayBits'

// When
var result = filesize.ToString();

// Then
result.ShouldBe(expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ 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")]
[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 Should_Return_Correct_Value(double value, double total, string expected)
{
// Given
Expand Down
Loading