Skip to content

Commit

Permalink
DataSize implements IFormattable. More formatting methods (programmat…
Browse files Browse the repository at this point in the history
…ic precision and unit, optional automatic normalization). You can divide DataSize instances with /. More tests (100% coverage again).
  • Loading branch information
Aldaviva committed Jul 11, 2021
1 parent 1fb5d07 commit 918bf4f
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 28 deletions.
38 changes: 31 additions & 7 deletions DataSizeUnits/DataSize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ namespace DataSizeUnits {
/// <para><c>var kilobyte = new DataSize(1, Unit.Kilobyte);</c></para>
/// </summary>
[Serializable]
public struct DataSize: IComparable<DataSize> {
public struct DataSize: IComparable<DataSize>, IFormattable {

public double Quantity;
public Unit Unit;

private double AsBits => Quantity * CountBitsInUnit(Unit);

private static readonly DataSizeFormatter Formatter = new DataSizeFormatter();

/// <summary>
/// <para>Create a new instance with the given quantity of the given unit of data.</para>
/// <para><c>var kilobyte = new DataSize(1, Unit.Kilobyte);</c></para>
Expand Down Expand Up @@ -257,15 +259,29 @@ public override string ToString() {
/// </summary>
/// <param name="precision">Number of digits after the decimal place to use when formatting the quantity as a number. The
/// default for en-US is 2. To use the default for the current culture, pass the value <c>-1</c>, or call
/// <see cref="ToString()"/></param>.
/// <see cref="ToString()"/>.</param>
/// <param name="normalize"><c>true</c> to first normalize this instance to an automatically-chosen unit before converting it
/// to a string, or <c>false</c> (the default) to use the original unit this instance was defined with.</param>
/// <returns>String with the formatted data quantity and unit abbreviation, separated by a space.</returns>
public string ToString(int precision) {
var culture = (CultureInfo) CultureInfo.CurrentCulture.Clone();
if (precision >= 0) {
culture.NumberFormat.NumberDecimalDigits = precision;
public string ToString(int precision, bool normalize = false) {
if (normalize) {
return Normalize(Unit.IsMultipleOfBits()).ToString(precision);
} else {
var culture = (CultureInfo) CultureInfo.CurrentCulture.Clone();
if (precision >= 0) {
culture.NumberFormat.NumberDecimalDigits = precision;
}

return Quantity.ToString("N", culture) + " " + Unit.ToAbbreviation();
}
}

public string ToString(string format, IFormatProvider formatProvider = null) {
return Formatter.Format(format, this, Formatter);
}

return Quantity.ToString("N", culture) + " " + Unit.ToAbbreviation();
public string ToString(int precision, Unit unit) {
return ConvertToUnit(unit).ToString(precision);
}

public bool Equals(DataSize other) {
Expand Down Expand Up @@ -312,6 +328,14 @@ public int CompareTo(DataSize other) {
}
}

public static double operator /(DataSize a, DataSize b) {
if (!b.Quantity.Equals(0)) {
return a.AsBits / b.AsBits;
} else {
throw new DivideByZeroException();
}
}

public static bool operator ==(DataSize a, DataSize b) => a.Equals(b);

public static bool operator !=(DataSize a, DataSize b) => !a.Equals(b);
Expand Down
19 changes: 11 additions & 8 deletions DataSizeUnits/DataSizeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,21 @@ public string Format(string format, object arg, IFormatProvider formatProvider)
return null;
}

DataSize dataSize;
dataSize.Unit = Unit.Byte;

if (string.IsNullOrEmpty(format)) {
format = DefaultFormat;
}

try {
long bytes = Convert.ToInt64(arg);
dataSize.Quantity = bytes;
} catch (Exception) {
return HandleOtherFormats(format, arg);
DataSize dataSize;
if (arg is DataSize size) {
dataSize = size;
} else {
dataSize.Unit = Unit.Byte;
try {
long bytes = Convert.ToInt64(arg);
dataSize.Quantity = bytes;
} catch (Exception) {
return HandleOtherFormats(format, arg);
}
}

string unitString = Regex.Match(format, @"^[a-z]+", RegexOptions.IgnoreCase).Value;
Expand Down
3 changes: 2 additions & 1 deletion DataSizeUnits/DataSizeUnits.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Version>2.1.0</Version>
<Authors>Ben Hutchison</Authors>
<Company>Ben Hutchison</Company>
<PackageId>DataSizeUnits</PackageId>
<Product>DataSizeUnits</Product>
<Description>Convert and format data size units in .NET (bits, bytes, kilobits, kilobytes, and others).</Description>
<Copyright>2020 Ben Hutchison</Copyright>
<Copyright>2021 Ben Hutchison</Copyright>
<PackageProjectUrl>https://github.com/Aldaviva/DataSizeUnits</PackageProjectUrl>
<RepositoryUrl>https://github.com/Aldaviva/DataSizeUnits.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
27 changes: 26 additions & 1 deletion DataSizeUnits/Unit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static class UnitExtensions {
/// <param name="iec"><c>true</c> to return the IEC abbreviation (KiB, MiB, etc.), or <c>false</c> (the default) to return
/// the JEDEC abbreviation (KB, MB, etc.)</param>
/// <returns>The abbreviation for this unit.</returns>
public static string ToAbbreviation(this Unit unit, bool iec=false) {
public static string ToAbbreviation(this Unit unit, bool iec = false) {
switch (unit) {
case Unit.Byte:
return "B";
Expand Down Expand Up @@ -169,6 +169,31 @@ public static string ToName(this Unit unit, bool iec = false) {
}
}

public static bool IsMultipleOfBits(this Unit unit) {
switch (unit) {
case Unit.Byte:
case Unit.Kilobyte:
case Unit.Megabyte:
case Unit.Gigabyte:
case Unit.Terabyte:
case Unit.Petabyte:
case Unit.Exabyte:
return false;

case Unit.Bit:
case Unit.Kilobit:
case Unit.Megabit:
case Unit.Gigabit:
case Unit.Terabit:
case Unit.Petabit:
case Unit.Exabit:
return true;

default:
throw new ArgumentOutOfRangeException(nameof(unit), unit, null);
}
}

public static DataSize Quantity(this Unit unit, double quantity) {
return new DataSize(quantity, unit);
}
Expand Down
43 changes: 37 additions & 6 deletions Tests/DataSizeFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using DataSizeUnits;
using Xunit;

Expand All @@ -12,11 +13,11 @@ public class DataSizeFormatterTests {
[MemberData(nameof(UnitData))]
public void FormatUsingCustomFormatter(ulong inputBytes, string formatSyntax, string expectedOutput) {
string formatString = "{0:" + formatSyntax + "}";
string actual = string.Format(new DataSizeFormatter(), formatString, inputBytes);
string actual = string.Format(new DataSizeFormatter(), formatString, inputBytes);
Assert.Equal(expectedOutput, actual);
}

public static TheoryData<ulong, string, string> FormatData = new TheoryData<ulong, string, string> {
public static TheoryData<ulong, string, string> FormatData = new() {
{ 0, "", "0.00 B" },
{ 0, "A", "0.00 B" },
{ 1024, "A", "1.00 KB" },
Expand All @@ -28,7 +29,7 @@ public void FormatUsingCustomFormatter(ulong inputBytes, string formatSyntax, st
{ 1024L * 1024 * 1024 * 1024 * 1024 * 1024, "A", "1.00 EB" }
};

public static TheoryData<ulong, string, string> PrecisionData = new TheoryData<ulong, string, string> {
public static TheoryData<ulong, string, string> PrecisionData = new() {
{ 9_995_326_316_544, "A", "9.09 TB" },
{ 9_995_326_316_544, "A0", "9 TB" },
{ 9_995_326_316_544, "1", "9.1 TB" },
Expand All @@ -37,7 +38,7 @@ public void FormatUsingCustomFormatter(ulong inputBytes, string formatSyntax, st
{ 9_995_326_316_544, "A3", "9.091 TB" }
};

public static TheoryData<ulong, string, string> UnitData = new TheoryData<ulong, string, string> {
public static TheoryData<ulong, string, string> UnitData = new() {
{ 9_995_326_316_544, "B0", "9,995,326,316,544 B" },
{ 9_995_326_316_544, "K0", "9,761,060,856 KB" },
{ 9_995_326_316_544, "KB0", "9,761,060,856 KB" },
Expand Down Expand Up @@ -82,13 +83,31 @@ public void ConvertAndFormatUsingToString() {
Assert.Equal("1.41 MB", actual);
}

[Theory, MemberData(nameof(OtherData))]
[Fact]
public void FormatUsingToStringAndFormatProvider() {
string actual = new DataSize(1474560).ToString("K1", CultureInfo.CurrentCulture);
Assert.Equal("1,440.0 KB", actual);
}

[Fact]
public void FormatUsingPrecisionAndUnit() {
string actual = new DataSize(1474560).ToString(2, Unit.Kilobyte);
Assert.Equal("1,440.00 KB", actual);
}

[Fact]
public void FormatUsingToStringAndNormalize() {
string actual = new DataSize(1474560).ToString(2, true);
Assert.Equal("1.41 MB", actual);
}

[Theory] [MemberData(nameof(OtherData))]
public void HandleOtherFormats(object otherInput, string expectedOutput) {
string actualOutput = string.Format(new DataSizeFormatter(), "{0:D}", otherInput);
Assert.Equal(expectedOutput, actualOutput);
}

public static TheoryData<object, string> OtherData = new TheoryData<object, string> {
public static TheoryData<object, string> OtherData = new() {
{ new DateTime(1988, 8, 17, 16, 30, 0), "Wednesday, August 17, 1988" },
{ new Unformattable(), "unformattable" },
{ null, "" }
Expand Down Expand Up @@ -120,6 +139,18 @@ public void NegativeNumbers() {
Assert.Equal("-1 KB", actual);
}

[Fact]
public void ZeroBytes() {
string actual = string.Format(new DataSizeFormatter(), "{0:A1}", 0);
Assert.Equal("0.0 B", actual);
}

[Fact]
public void DataSizeArg() {
string actual = string.Format(new DataSizeFormatter(), "{0:A1}", new DataSize(0, Unit.Kilobyte));
Assert.Equal("0.0 B", actual);
}

}

internal class Unformattable {
Expand Down
13 changes: 10 additions & 3 deletions Tests/DataSizeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,22 @@ public void Multiplication() {
}

[Fact]
public void Division() {
DataSize actual = new DataSize(6, Unit.Megabyte) / 3;
public void DivisionByDouble() {
DataSize actual = new DataSize(6, Unit.Megabyte) / 3.0;
Assert.Equal(2, actual.Quantity);
Assert.Equal(Unit.Megabyte, actual.Unit);
}

[Fact]
public void DivisionByDatasize() {
double actual = new DataSize(6, Unit.Megabyte) / new DataSize(3, Unit.Megabyte);
Assert.Equal(2, actual);
}

[Fact]
public void DivisionByZero() {
Assert.Throws<DivideByZeroException>(() => new DataSize(1) / 0);
Assert.Throws<DivideByZeroException>(() => new DataSize(1) / 0.0);
Assert.Throws<DivideByZeroException>(() => new DataSize(1) / new DataSize(0));
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion Tests/Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

Expand Down
23 changes: 22 additions & 1 deletion Tests/UnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DataSizeUnits;
using System;
using DataSizeUnits;
using Xunit;

namespace Tests {
Expand Down Expand Up @@ -87,6 +88,26 @@ public void CreateDataSizeFromUnit() {
Assert.Equal(Unit.Megabyte, actual.Unit);
}

[Fact]
public void IsMultipleOfBits() {
Assert.False(Unit.Byte.IsMultipleOfBits());
Assert.False(Unit.Kilobyte.IsMultipleOfBits());
Assert.False(Unit.Megabyte.IsMultipleOfBits());
Assert.False(Unit.Gigabyte.IsMultipleOfBits());
Assert.False(Unit.Terabyte.IsMultipleOfBits());
Assert.False(Unit.Petabyte.IsMultipleOfBits());
Assert.False(Unit.Exabyte.IsMultipleOfBits());
Assert.True(Unit.Bit.IsMultipleOfBits());
Assert.True(Unit.Kilobit.IsMultipleOfBits());
Assert.True(Unit.Megabit.IsMultipleOfBits());
Assert.True(Unit.Gigabit.IsMultipleOfBits());
Assert.True(Unit.Terabit.IsMultipleOfBits());
Assert.True(Unit.Petabit.IsMultipleOfBits());
Assert.True(Unit.Exabit.IsMultipleOfBits());

Assert.Throws<ArgumentOutOfRangeException>(() => ((Unit) 999999).IsMultipleOfBits());
}

}

}

0 comments on commit 918bf4f

Please sign in to comment.