From 918bf4fd193f6acbe4fdc30c88bd125374ad69fe Mon Sep 17 00:00:00 2001 From: Ben Hutchison Date: Sun, 11 Jul 2021 11:50:17 -0700 Subject: [PATCH] DataSize implements IFormattable. More formatting methods (programmatic precision and unit, optional automatic normalization). You can divide DataSize instances with /. More tests (100% coverage again). --- DataSizeUnits/DataSize.cs | 38 +++++++++++++++++++++----- DataSizeUnits/DataSizeFormatter.cs | 19 +++++++------ DataSizeUnits/DataSizeUnits.csproj | 3 ++- DataSizeUnits/Unit.cs | 27 ++++++++++++++++++- Tests/DataSizeFormatterTests.cs | 43 +++++++++++++++++++++++++----- Tests/DataSizeTests.cs | 13 ++++++--- Tests/Tests.csproj | 2 +- Tests/UnitTests.cs | 23 +++++++++++++++- 8 files changed, 140 insertions(+), 28 deletions(-) diff --git a/DataSizeUnits/DataSize.cs b/DataSizeUnits/DataSize.cs index a98178f..f89bd73 100644 --- a/DataSizeUnits/DataSize.cs +++ b/DataSizeUnits/DataSize.cs @@ -9,13 +9,15 @@ namespace DataSizeUnits { /// var kilobyte = new DataSize(1, Unit.Kilobyte); /// [Serializable] - public struct DataSize: IComparable { + public struct DataSize: IComparable, IFormattable { public double Quantity; public Unit Unit; private double AsBits => Quantity * CountBitsInUnit(Unit); + private static readonly DataSizeFormatter Formatter = new DataSizeFormatter(); + /// /// Create a new instance with the given quantity of the given unit of data. /// var kilobyte = new DataSize(1, Unit.Kilobyte); @@ -257,15 +259,29 @@ public override string ToString() { /// /// 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 -1, or call - /// . + /// . + /// true to first normalize this instance to an automatically-chosen unit before converting it + /// to a string, or false (the default) to use the original unit this instance was defined with. /// String with the formatted data quantity and unit abbreviation, separated by a space. - 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) { @@ -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); diff --git a/DataSizeUnits/DataSizeFormatter.cs b/DataSizeUnits/DataSizeFormatter.cs index 0b8bcc3..3ff38ac 100644 --- a/DataSizeUnits/DataSizeFormatter.cs +++ b/DataSizeUnits/DataSizeFormatter.cs @@ -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; diff --git a/DataSizeUnits/DataSizeUnits.csproj b/DataSizeUnits/DataSizeUnits.csproj index b39b409..1dbaaff 100644 --- a/DataSizeUnits/DataSizeUnits.csproj +++ b/DataSizeUnits/DataSizeUnits.csproj @@ -4,10 +4,11 @@ netstandard2.0 2.1.0 Ben Hutchison + Ben Hutchison DataSizeUnits DataSizeUnits Convert and format data size units in .NET (bits, bytes, kilobits, kilobytes, and others). - 2020 Ben Hutchison + 2021 Ben Hutchison https://github.com/Aldaviva/DataSizeUnits https://github.com/Aldaviva/DataSizeUnits.git git diff --git a/DataSizeUnits/Unit.cs b/DataSizeUnits/Unit.cs index c49238d..61a4619 100644 --- a/DataSizeUnits/Unit.cs +++ b/DataSizeUnits/Unit.cs @@ -88,7 +88,7 @@ public static class UnitExtensions { /// true to return the IEC abbreviation (KiB, MiB, etc.), or false (the default) to return /// the JEDEC abbreviation (KB, MB, etc.) /// The abbreviation for this unit. - 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"; @@ -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); } diff --git a/Tests/DataSizeFormatterTests.cs b/Tests/DataSizeFormatterTests.cs index b0cfd72..952efa6 100644 --- a/Tests/DataSizeFormatterTests.cs +++ b/Tests/DataSizeFormatterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using DataSizeUnits; using Xunit; @@ -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 FormatData = new TheoryData { + public static TheoryData FormatData = new() { { 0, "", "0.00 B" }, { 0, "A", "0.00 B" }, { 1024, "A", "1.00 KB" }, @@ -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 PrecisionData = new TheoryData { + public static TheoryData 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" }, @@ -37,7 +38,7 @@ public void FormatUsingCustomFormatter(ulong inputBytes, string formatSyntax, st { 9_995_326_316_544, "A3", "9.091 TB" } }; - public static TheoryData UnitData = new TheoryData { + public static TheoryData 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" }, @@ -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 OtherData = new TheoryData { + public static TheoryData OtherData = new() { { new DateTime(1988, 8, 17, 16, 30, 0), "Wednesday, August 17, 1988" }, { new Unformattable(), "unformattable" }, { null, "" } @@ -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 { diff --git a/Tests/DataSizeTests.cs b/Tests/DataSizeTests.cs index dbe7234..bb821a2 100644 --- a/Tests/DataSizeTests.cs +++ b/Tests/DataSizeTests.cs @@ -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(() => new DataSize(1) / 0); + Assert.Throws(() => new DataSize(1) / 0.0); + Assert.Throws(() => new DataSize(1) / new DataSize(0)); } [Fact] diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 66f5348..83112b4 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 latest diff --git a/Tests/UnitTests.cs b/Tests/UnitTests.cs index 1e36872..5a16187 100644 --- a/Tests/UnitTests.cs +++ b/Tests/UnitTests.cs @@ -1,4 +1,5 @@ -using DataSizeUnits; +using System; +using DataSizeUnits; using Xunit; namespace Tests { @@ -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(() => ((Unit) 999999).IsMultipleOfBits()); + } + } } \ No newline at end of file