using System; using System.Globalization; namespace Optimizer { /// /// Represents a byte size value with support for decimal (KiloByte) and /// binary values (KibiByte). /// public partial struct ByteSize : IComparable, IEquatable { public static readonly ByteSize MinValue = ByteSize.FromBits(long.MinValue); public static readonly ByteSize MaxValue = ByteSize.FromBits(long.MaxValue); public const long BitsInByte = 8; public const string BitSymbol = "b"; public const string ByteSymbol = "B"; public long Bits { get; private set; } public double Bytes { get; private set; } public string LargestWholeNumberBinarySymbol { get { // Absolute value is used to deal with negative values if (Math.Abs(this.PebiBytes) >= 1) return PebiByteSymbol; if (Math.Abs(this.TebiBytes) >= 1) return TebiByteSymbol; if (Math.Abs(this.GibiBytes) >= 1) return GibiByteSymbol; if (Math.Abs(this.MebiBytes) >= 1) return MebiByteSymbol; if (Math.Abs(this.KibiBytes) >= 1) return KibiByteSymbol; if (Math.Abs(this.Bytes) >= 1) return ByteSymbol; return BitSymbol; } } public string LargestWholeNumberDecimalSymbol { get { // Absolute value is used to deal with negative values if (Math.Abs(this.PetaBytes) >= 1) return PetaByteSymbol; if (Math.Abs(this.TeraBytes) >= 1) return TeraByteSymbol; if (Math.Abs(this.GigaBytes) >= 1) return GigaByteSymbol; if (Math.Abs(this.MegaBytes) >= 1) return MegaByteSymbol; if (Math.Abs(this.KiloBytes) >= 1) return KiloByteSymbol; if (Math.Abs(this.Bytes) >= 1) return ByteSymbol; return BitSymbol; } } public double LargestWholeNumberBinaryValue { get { // Absolute value is used to deal with negative values if (Math.Abs(this.PebiBytes) >= 1) return this.PebiBytes; if (Math.Abs(this.TebiBytes) >= 1) return this.TebiBytes; if (Math.Abs(this.GibiBytes) >= 1) return this.GibiBytes; if (Math.Abs(this.MebiBytes) >= 1) return this.MebiBytes; if (Math.Abs(this.KibiBytes) >= 1) return this.KibiBytes; if (Math.Abs(this.Bytes) >= 1) return this.Bytes; return this.Bits; } } public double LargestWholeNumberDecimalValue { get { // Absolute value is used to deal with negative values if (Math.Abs(this.PetaBytes) >= 1) return this.PetaBytes; if (Math.Abs(this.TeraBytes) >= 1) return this.TeraBytes; if (Math.Abs(this.GigaBytes) >= 1) return this.GigaBytes; if (Math.Abs(this.MegaBytes) >= 1) return this.MegaBytes; if (Math.Abs(this.KiloBytes) >= 1) return this.KiloBytes; if (Math.Abs(this.Bytes) >= 1) return this.Bytes; return this.Bits; } } public ByteSize(long bits) : this() { Bits = bits; Bytes = bits / BitsInByte; } public ByteSize(double bytes) : this() { // Get ceiling because bits are whole units Bits = (long)Math.Ceiling(bytes * BitsInByte); Bytes = bytes; } public static ByteSize FromBits(long value) { return new ByteSize(value); } public static ByteSize FromBytes(double value) { return new ByteSize(value); } /// /// Converts the value of the current object to a string. /// The prefix symbol (bit, byte, kilo, mebi, gibi, tebi) used is the /// largest prefix such that the corresponding value is greater than or /// equal to one. /// public override string ToString() { return this.ToString("0.##", CultureInfo.CurrentCulture); } public string ToString(string format) { return this.ToString(format, CultureInfo.CurrentCulture); } public string ToString(string format, IFormatProvider provider, bool useBinaryByte = false) { if (!format.Contains("#") && !format.Contains("0")) format = "0.## " + format; if (provider == null) provider = CultureInfo.CurrentCulture; Func has = s => format.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1; Func output = n => n.ToString(format, provider); // Binary if (has("PiB")) return output(this.PebiBytes); if (has("TiB")) return output(this.TebiBytes); if (has("GiB")) return output(this.GibiBytes); if (has("MiB")) return output(this.MebiBytes); if (has("KiB")) return output(this.KibiBytes); // Decimal if (has("PB")) return output(this.PetaBytes); if (has("TB")) return output(this.TeraBytes); if (has("GB")) return output(this.GigaBytes); if (has("MB")) return output(this.MegaBytes); if (has("KB")) return output(this.KiloBytes); // Byte and Bit symbol must be case-sensitive if (format.IndexOf(ByteSize.ByteSymbol) != -1) return output(this.Bytes); if (format.IndexOf(ByteSize.BitSymbol) != -1) return output(this.Bits); if (useBinaryByte) { return string.Format("{0} {1}", this.LargestWholeNumberBinaryValue.ToString(format, provider), this.LargestWholeNumberBinarySymbol); } else { return string.Format("{0} {1}", this.LargestWholeNumberDecimalValue.ToString(format, provider), this.LargestWholeNumberDecimalSymbol); } } public override bool Equals(object value) { if (value == null) return false; ByteSize other; if (value is ByteSize) other = (ByteSize)value; else return false; return Equals(other); } public bool Equals(ByteSize value) { return this.Bits == value.Bits; } public override int GetHashCode() { return this.Bits.GetHashCode(); } public int CompareTo(ByteSize other) { return this.Bits.CompareTo(other.Bits); } public ByteSize Add(ByteSize bs) { return new ByteSize(this.Bytes + bs.Bytes); } public ByteSize AddBits(long value) { return this + FromBits(value); } public ByteSize AddBytes(double value) { return this + ByteSize.FromBytes(value); } public ByteSize Subtract(ByteSize bs) { return new ByteSize(this.Bytes - bs.Bytes); } public static ByteSize operator +(ByteSize b1, ByteSize b2) { return new ByteSize(b1.Bytes + b2.Bytes); } public static ByteSize operator ++(ByteSize b) { return new ByteSize(b.Bytes + 1); } public static ByteSize operator -(ByteSize b) { return new ByteSize(-b.Bytes); } public static ByteSize operator -(ByteSize b1, ByteSize b2) { return new ByteSize(b1.Bytes - b2.Bytes); } public static ByteSize operator --(ByteSize b) { return new ByteSize(b.Bytes - 1); } public static bool operator ==(ByteSize b1, ByteSize b2) { return b1.Bits == b2.Bits; } public static bool operator !=(ByteSize b1, ByteSize b2) { return b1.Bits != b2.Bits; } public static bool operator <(ByteSize b1, ByteSize b2) { return b1.Bits < b2.Bits; } public static bool operator <=(ByteSize b1, ByteSize b2) { return b1.Bits <= b2.Bits; } public static bool operator >(ByteSize b1, ByteSize b2) { return b1.Bits > b2.Bits; } public static bool operator >=(ByteSize b1, ByteSize b2) { return b1.Bits >= b2.Bits; } public static ByteSize Parse(string s) { return Parse(s, NumberFormatInfo.CurrentInfo); } public static ByteSize Parse(string s, IFormatProvider formatProvider) { return Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, formatProvider); } public static ByteSize Parse(string s, NumberStyles numberStyles, IFormatProvider formatProvider) { // Arg checking if (string.IsNullOrWhiteSpace(s)) throw new ArgumentNullException("s", "String is null or whitespace"); // Get the index of the first non-digit character s = s.TrimStart(); // Protect against leading spaces var num = 0; var found = false; var numberFormatInfo = NumberFormatInfo.GetInstance(formatProvider); var decimalSeparator = Convert.ToChar(numberFormatInfo.NumberDecimalSeparator); var groupSeparator = Convert.ToChar(numberFormatInfo.NumberGroupSeparator); // Pick first non-digit number for (num = 0; num < s.Length; num++) if (!(char.IsDigit(s[num]) || s[num] == decimalSeparator || s[num] == groupSeparator)) { found = true; break; } if (found == false) throw new FormatException($"No byte indicator found in value '{s}'."); int lastNumber = num; // Cut the input string in half string numberPart = s.Substring(0, lastNumber).Trim(); string sizePart = s.Substring(lastNumber, s.Length - lastNumber).Trim(); // Get the numeric part double number; if (!double.TryParse(numberPart, numberStyles, formatProvider, out number)) throw new FormatException($"No number found in value '{s}'."); // Get the magnitude part switch (sizePart) { case "b": if (number % 1 != 0) // Can't have partial bits throw new FormatException($"Can't have partial bits for value '{s}'."); return FromBits((long)number); case "B": return FromBytes(number); } switch (sizePart.ToLowerInvariant()) { // Binary case "kib": return FromKibiBytes(number); case "mib": return FromMebiBytes(number); case "gib": return FromGibiBytes(number); case "tib": return FromTebiBytes(number); case "pib": return FromPebiBytes(number); // Decimal case "kb": return FromKiloBytes(number); case "mb": return FromMegaBytes(number); case "gb": return FromGigaBytes(number); case "tb": return FromTeraBytes(number); case "pb": return FromPetaBytes(number); default: throw new FormatException($"Bytes of magnitude '{sizePart}' is not supported."); } } public static bool TryParse(string s, out ByteSize result) { try { result = Parse(s); return true; } catch { result = new ByteSize(); return false; } } public static bool TryParse(string s, NumberStyles numberStyles, IFormatProvider formatProvider, out ByteSize result) { try { result = Parse(s, numberStyles, formatProvider); return true; } catch { result = new ByteSize(); return false; } } } }