diff --git a/src/Humanizer.Tests.Shared/Bytes/ToFullWordsTests.cs b/src/Humanizer.Tests.Shared/Bytes/ToFullWordsTests.cs index 9730f2e48..db88528b2 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ToFullWordsTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ToFullWordsTests.cs @@ -26,13 +26,13 @@ namespace Humanizer.Tests.Bytes { public class ToFullWordsTests - { + { [Fact] public void ReturnsSingularBit() { Assert.Equal("1 bit", ByteSize.FromBits(1).ToFullWords()); } - + [Fact] public void ReturnsPluralBits() { @@ -98,5 +98,13 @@ public void ReturnsPluralTerabytes() { Assert.Equal("10 terabytes", ByteSize.FromTerabytes(10).ToFullWords()); } + + [Theory] + [InlineData(229376, "B", "229376 bytes")] + [InlineData(229376, "# KB", "224 kilobytes")] + public void ToFullWordsFormatted(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, ByteSize.FromBytes(input).ToFullWords(format)); + } } } diff --git a/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ByteSizeExtensionsTests.cs b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ByteSizeExtensionsTests.cs new file mode 100644 index 000000000..5554d0de7 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ByteSizeExtensionsTests.cs @@ -0,0 +1,77 @@ +using Xunit; + +namespace Humanizer.Tests.Localisation.fr.Bytes +{ + [UseCulture("fr-FR")] + public class ByteSizeExtensionsTests + { + [Theory] + [InlineData(2, null, "2 To")] + [InlineData(2, "GB", "2048 Go")] + [InlineData(2.123, "#.#", "2,1 To")] + public void HumanizesTerabytes(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Terabytes().Humanize(format)); + } + + [Theory] + [InlineData(0, null, "0 b")] + [InlineData(0, "GB", "0 Go")] + [InlineData(2, null, "2 Go")] + [InlineData(2, "MB", "2048 Mo")] + [InlineData(2.123, "#.##", "2,12 Go")] + public void HumanizesGigabytes(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Gigabytes().Humanize(format)); + } + + [Theory] + [InlineData(0, null, "0 b")] + [InlineData(0, "MB", "0 Mo")] + [InlineData(2, null, "2 Mo")] + [InlineData(2, "KB", "2048 Ko")] + [InlineData(2.123, "#", "2 Mo")] + public void HumanizesMegabytes(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Megabytes().Humanize(format)); + } + + [Theory] + [InlineData(0, null, "0 b")] + [InlineData(0, "KB", "0 Ko")] + [InlineData(2, null, "2 Ko")] + [InlineData(2, "B", "2048 o")] + [InlineData(2.123, "#.####", "2,123 Ko")] + public void HumanizesKilobytes(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Kilobytes().Humanize(format)); + } + + [Theory] + [InlineData(0, null, "0 b")] + [InlineData(0, "#.##", "0 b")] + [InlineData(0, "#.## B", "0 o")] + [InlineData(0, "B", "0 o")] + [InlineData(2, null, "2 o")] + [InlineData(2000, "KB", "1,95 Ko")] + [InlineData(2123, "#.##", "2,07 Ko")] + [InlineData(10000000, "KB", "9765,63 Ko")] + [InlineData(10000000, "#,##0 KB", "9 766 Ko")] + [InlineData(10000000, "#,##0.# KB", "9 765,6 Ko")] + public void HumanizesBytes(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Bytes().Humanize(format)); + } + + [Theory] + [InlineData(0, null, "0 b")] + [InlineData(0, "b", "0 b")] + [InlineData(2, null, "2 b")] + [InlineData(12, "B", "1,5 o")] + [InlineData(10000, "#.# KB", "1,2 Ko")] + public void HumanizesBits(long input, string format, string expectedValue) + { + Assert.Equal(expectedValue, input.Bits().Humanize(format)); + } + } +} diff --git a/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToFullWordsTests.cs b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToFullWordsTests.cs new file mode 100644 index 000000000..a1bea5985 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToFullWordsTests.cs @@ -0,0 +1,89 @@ +using Humanizer.Bytes; +using Xunit; + +namespace Humanizer.Tests.Localisation.fr.Bytes +{ + [UseCulture("fr-FR")] + public class ToFullWordsTests + { + [Fact] + public void ReturnsSingularBit() + { + Assert.Equal("1 bit", ByteSize.FromBits(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralBits() + { + Assert.Equal("2 bits", ByteSize.FromBits(2).ToFullWords()); + } + + [Fact] + public void ReturnsSingularByte() + { + Assert.Equal("1 octet", ByteSize.FromBytes(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralBytes() + { + Assert.Equal("10 octets", ByteSize.FromBytes(10).ToFullWords()); + } + + [Fact] + public void ReturnsSingularKiloByte() + { + Assert.Equal("1 kilooctet", ByteSize.FromKilobytes(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralKilobytes() + { + Assert.Equal("10 kilooctets", ByteSize.FromKilobytes(10).ToFullWords()); + } + + [Fact] + public void ReturnsSingularMegabyte() + { + Assert.Equal("1 mégaoctet", ByteSize.FromMegabytes(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralMegabytes() + { + Assert.Equal("10 mégaoctets", ByteSize.FromMegabytes(10).ToFullWords()); + } + + [Fact] + public void ReturnsSingularGigabyte() + { + Assert.Equal("1 gigaoctet", ByteSize.FromGigabytes(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralGigabytes() + { + Assert.Equal("10 gigaoctets", ByteSize.FromGigabytes(10).ToFullWords()); + } + + [Fact] + public void ReturnsSingularTerabyte() + { + Assert.Equal("1 téraoctet", ByteSize.FromTerabytes(1).ToFullWords()); + } + + [Fact] + public void ReturnsPluralTerabytes() + { + Assert.Equal("10 téraoctets", ByteSize.FromTerabytes(10).ToFullWords()); + } + + [Theory] + [InlineData(229376, "B", "229376 octets")] + [InlineData(229376, "# KB", "224 kilooctets")] + public void ToFullWordsFormatted(double input, string format, string expectedValue) + { + Assert.Equal(expectedValue, ByteSize.FromBytes(input).ToFullWords(format)); + } + } +} diff --git a/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToStringTests.cs b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToStringTests.cs new file mode 100644 index 000000000..54deee964 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/fr/Bytes/ToStringTests.cs @@ -0,0 +1,87 @@ +using Humanizer.Bytes; +using Xunit; + +namespace Humanizer.Tests.Localisation.fr.Bytes +{ + [UseCulture("fr-FR")] + public class ToStringTests + { + [Fact] + public void ReturnsLargestMetricSuffix() + { + Assert.Equal("10,5 Ko", ByteSize.FromKilobytes(10.5).ToString()); + } + + [Fact] + public void ReturnsDefaultNumberFormat() + { + Assert.Equal("10,5 Ko", ByteSize.FromKilobytes(10.5).ToString("KB")); + } + + [Fact] + public void ReturnsProvidedNumberFormat() + { + Assert.Equal("10,1234 Ko", ByteSize.FromKilobytes(10.1234).ToString("#.#### KB")); + } + + [Fact] + public void ReturnsBits() + { + Assert.Equal("10 b", ByteSize.FromBits(10).ToString("##.#### b")); + } + + [Fact] + public void ReturnsBytes() + { + Assert.Equal("10 o", ByteSize.FromBytes(10).ToString("##.#### B")); + } + + [Fact] + public void ReturnsKilobytes() + { + Assert.Equal("10 Ko", ByteSize.FromKilobytes(10).ToString("##.#### KB")); + } + + [Fact] + public void ReturnsMegabytes() + { + Assert.Equal("10 Mo", ByteSize.FromMegabytes(10).ToString("##.#### MB")); + } + + [Fact] + public void ReturnsGigabytes() + { + Assert.Equal("10 Go", ByteSize.FromGigabytes(10).ToString("##.#### GB")); + } + + [Fact] + public void ReturnsTerabytes() + { + Assert.Equal("10 To", ByteSize.FromTerabytes(10).ToString("##.#### TB")); + } + + [Fact] + public void ReturnsSelectedFormat() + { + Assert.Equal("10,0 To", ByteSize.FromTerabytes(10).ToString("0.0 TB")); + } + + [Fact] + public void ReturnsLargestMetricPrefixLargerThanZero() + { + Assert.Equal("512 Ko", ByteSize.FromMegabytes(.5).ToString("#.#")); + } + + [Fact] + public void ReturnsLargestMetricPrefixLargerThanZeroForNegativeValues() + { + Assert.Equal("-512 Ko", ByteSize.FromMegabytes(-.5).ToString("#.#")); + } + + [Fact] + public void ReturnsBytesViaGeneralFormat() + { + Assert.Equal("10 o", $"{ByteSize.FromBytes(10)}"); + } + } +} diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index 42d465383..de5d8865f 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -22,7 +22,8 @@ using System; using System.Globalization; -using System.Text.RegularExpressions; +using Humanizer.Configuration; +using Humanizer.Localisation; namespace Humanizer.Bytes { @@ -61,72 +62,74 @@ public struct ByteSize : IComparable, IEquatable, IComparabl public double Gigabytes { get; private set; } public double Terabytes { get; private set; } - public string LargestWholeNumberSymbol + public string LargestWholeNumberSymbol => GetLargestWholeNumberSymbol(); + + public string GetLargestWholeNumberSymbol(IFormatProvider provider = null) { - get - { - // Absolute value is used to deal with negative values - if (Math.Abs(Terabytes) >= 1) - { - return TerabyteSymbol; - } + var cultureFormatter = Configurator.GetFormatter(provider as CultureInfo); - if (Math.Abs(Gigabytes) >= 1) - { - return GigabyteSymbol; - } + // Absolute value is used to deal with negative values + if (Math.Abs(Terabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Terabyte, Terabytes, toSymbol: true); + } - if (Math.Abs(Megabytes) >= 1) - { - return MegabyteSymbol; - } + if (Math.Abs(Gigabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Gigabyte, Gigabytes, toSymbol: true); + } - if (Math.Abs(Kilobytes) >= 1) - { - return KilobyteSymbol; - } + if (Math.Abs(Megabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Megabyte, Megabytes, toSymbol: true); + } - if (Math.Abs(Bytes) >= 1) - { - return ByteSymbol; - } + if (Math.Abs(Kilobytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Kilobyte, Kilobytes, toSymbol: true); + } - return BitSymbol; + if (Math.Abs(Bytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Byte, Bytes, toSymbol: true); } + + return cultureFormatter.DataUnitHumanize(DataUnit.Bit, Bits, toSymbol: true); } - public string LargestWholeNumberFullWord + public string LargestWholeNumberFullWord => GetLargestWholeNumberFullWord(); + + public string GetLargestWholeNumberFullWord(IFormatProvider provider = null) { - get - { - // Absolute value is used to deal with negative values - if (Math.Abs(Terabytes) >= 1) - { - return Terabyte; - } + var cultureFormatter = Configurator.GetFormatter(provider as CultureInfo); - if (Math.Abs(Gigabytes) >= 1) - { - return Gigabyte; - } + // Absolute value is used to deal with negative values + if (Math.Abs(Terabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Terabyte, Terabytes, toSymbol: false); + } - if (Math.Abs(Megabytes) >= 1) - { - return Megabyte; - } + if (Math.Abs(Gigabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Gigabyte, Gigabytes, toSymbol: false); + } - if (Math.Abs(Kilobytes) >= 1) - { - return Kilobyte; - } + if (Math.Abs(Megabytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Megabyte, Megabytes, toSymbol: false); + } - if (Math.Abs(Bytes) >= 1) - { - return Byte; - } + if (Math.Abs(Kilobytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Kilobyte, Kilobytes, toSymbol: false); + } - return Bit; + if (Math.Abs(Bytes) >= 1) + { + return cultureFormatter.DataUnitHumanize(DataUnit.Byte, Bytes, toSymbol: false); } + + return cultureFormatter.DataUnitHumanize(DataUnit.Bit, Bits, toSymbol: false); } public double LargestWholeNumberValue @@ -222,7 +225,7 @@ public string ToString(IFormatProvider provider) if (provider == null) provider = CultureInfo.CurrentCulture; - return string.Format("{0} {1}", LargestWholeNumberValue.ToString(provider), LargestWholeNumberSymbol); + return string.Format("{0} {1}", LargestWholeNumberValue.ToString(provider), GetLargestWholeNumberSymbol(provider)); } public string ToString(string format) @@ -231,6 +234,11 @@ public string ToString(string format) } public string ToString(string format, IFormatProvider provider) + { + return ToString(format, provider, toSymbol: true); + } + + private string ToString(string format, IFormatProvider provider, bool toSymbol) { if (format == null) format = "G"; @@ -253,34 +261,42 @@ public string ToString(string format, IFormatProvider provider) bool has(string s) => culture.CompareInfo.IndexOf(format, s, CompareOptions.IgnoreCase) != -1; string output(double n) => n.ToString(format, provider); + var cultureFormatter = Configurator.GetFormatter(provider as CultureInfo); + if (has(TerabyteSymbol)) { + format = format.Replace(TerabyteSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Terabyte, Terabytes, toSymbol)); return output(Terabytes); } if (has(GigabyteSymbol)) { + format = format.Replace(GigabyteSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Gigabyte, Gigabytes, toSymbol)); return output(Gigabytes); } if (has(MegabyteSymbol)) { + format = format.Replace(MegabyteSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Megabyte, Megabytes, toSymbol)); return output(Megabytes); } if (has(KilobyteSymbol)) { + format = format.Replace(KilobyteSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Kilobyte, Kilobytes, toSymbol)); return output(Kilobytes); } // Byte and Bit symbol look must be case-sensitive if (format.IndexOf(ByteSymbol, StringComparison.Ordinal) != -1) { + format = format.Replace(ByteSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Byte, Bytes, toSymbol)); return output(Bytes); } if (format.IndexOf(BitSymbol, StringComparison.Ordinal) != -1) { + format = format.Replace(BitSymbol, cultureFormatter.DataUnitHumanize(DataUnit.Bit, Bits, toSymbol)); return output(Bits); } @@ -290,7 +306,7 @@ public string ToString(string format, IFormatProvider provider) ? "0" : formattedLargeWholeNumberValue; - return string.Format("{0} {1}", formattedLargeWholeNumberValue, LargestWholeNumberSymbol); + return string.Format("{0} {1}", formattedLargeWholeNumberValue, toSymbol ? GetLargestWholeNumberSymbol(provider) : GetLargestWholeNumberFullWord(provider)); } /// @@ -299,12 +315,9 @@ public string ToString(string format, IFormatProvider provider) /// tera) used is the largest metric prefix such that the corresponding /// value is greater than or equal to one. /// - public string ToFullWords() + public string ToFullWords(string format = null, IFormatProvider provider = null) { - var byteSizeAsFullWords = string.Format("{0} {1}", LargestWholeNumberValue, LargestWholeNumberFullWord); - - // If there is more than one unit, make the word plural - return LargestWholeNumberValue > 1 ? byteSizeAsFullWords + "s" : byteSizeAsFullWords; + return ToString(format, provider, toSymbol: false); } public override bool Equals(object value) @@ -471,8 +484,8 @@ public static bool TryParse(string s, out ByteSize result) // Acquiring culture specific decimal separator - var decSep = Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator); - + var decSep = Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator); + // Pick first non-digit number for (num = 0; num < s.Length; num++) { diff --git a/src/Humanizer/Localisation/DataUnit.cs b/src/Humanizer/Localisation/DataUnit.cs new file mode 100644 index 000000000..c615ceecf --- /dev/null +++ b/src/Humanizer/Localisation/DataUnit.cs @@ -0,0 +1,15 @@ +namespace Humanizer.Localisation +{ + /// + /// Units of data + /// + public enum DataUnit + { + Bit, + Byte, + Kilobyte, + Megabyte, + Gigabyte, + Terabyte, + } +} diff --git a/src/Humanizer/Localisation/Formatters/DefaultFormatter.cs b/src/Humanizer/Localisation/Formatters/DefaultFormatter.cs index 2efe8d73b..d3b410990 100644 --- a/src/Humanizer/Localisation/Formatters/DefaultFormatter.cs +++ b/src/Humanizer/Localisation/Formatters/DefaultFormatter.cs @@ -71,6 +71,18 @@ public virtual string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords return GetResourceForTimeSpan(timeUnit, unit, toWords); } + /// + public virtual string DataUnitHumanize(DataUnit dataUnit, double count, bool toSymbol = true) + { + var resourceKey = toSymbol ? $"DataUnit_{dataUnit}Symbol" : $"DataUnit_{dataUnit}"; + var resourceValue = Format(resourceKey); + + if (!toSymbol && count > 1) + resourceValue += 's'; + + return resourceValue; + } + private string GetResourceForDate(TimeUnit unit, Tense timeUnitTense, int count) { var resourceKey = ResourceKeys.DateHumanize.GetResourceKey(unit, timeUnitTense: timeUnitTense, count: count); diff --git a/src/Humanizer/Localisation/Formatters/IFormatter.cs b/src/Humanizer/Localisation/Formatters/IFormatter.cs index 398fbd4fd..64e06c3dd 100644 --- a/src/Humanizer/Localisation/Formatters/IFormatter.cs +++ b/src/Humanizer/Localisation/Formatters/IFormatter.cs @@ -42,5 +42,14 @@ public interface IFormatter /// /// string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false); + + /// + /// Returns the string representation of the provided DataUnit, either as a symbol or full word + /// + /// Data unit + /// Number of said units, to adjust for singular/plural forms + /// Indicates whether the data unit should be expressed as symbol or full word + /// String representation of the provided DataUnit + string DataUnitHumanize(DataUnit dataUnit, double count, bool toSymbol = true); } } diff --git a/src/Humanizer/Properties/Resources.fr.resx b/src/Humanizer/Properties/Resources.fr.resx index f55793bbe..5af112816 100644 --- a/src/Humanizer/Properties/Resources.fr.resx +++ b/src/Humanizer/Properties/Resources.fr.resx @@ -300,4 +300,40 @@ un an + + bit + + + b + + + octet + + + o + + + kilooctet + + + Ko + + + mégaoctet + + + Mo + + + gigaoctet + + + Go + + + téraoctet + + + To + \ No newline at end of file diff --git a/src/Humanizer/Properties/Resources.resx b/src/Humanizer/Properties/Resources.resx index 678f0d5cf..8683674b6 100644 --- a/src/Humanizer/Properties/Resources.resx +++ b/src/Humanizer/Properties/Resources.resx @@ -675,4 +675,40 @@ NNW + + bit + + + b + + + byte + + + B + + + kilobyte + + + KB + + + megabyte + + + MB + + + gigabyte + + + GB + + + terabyte + + + TB + \ No newline at end of file