S h o r t S t o r i e s

// Tales from software development

C# StorageValue type part 1: Class

leave a comment »

I’ve been meaning to write a class for the past few years for dealing with memory storage values.

The clinical data interfaces that I write often need to read and write disk and memory storage values. For example, the Vitality HL7 MLLP software that receives and sends HL7 messages via TCP/IP, writes incoming messages to disk and so it monitors disk free space to ensure that there is always sufficient disk space available. The required disk free space is held as a configuration file value. I could use a long value for bytes, or an int value for MB, but it would be more convenient to have a datatype that allowed me to express the value using the most appropriate units, e.g. as “500MB” or “0.5GB” rather than 524288000.

Conversely, these interfaces typically log the process working set size before and after processing to aid diagnosing memory problems. Again, it’s possible just to log the number of bytes but it’s easier to read and understand a working set size of, for example, 18.56MB rather than 19464932.

So, the class must implement parsing methods to take values such as “1234”, “123.4KB”, “12.34MB”, “1.234GB”, “1.234GB” and to provide formatting methods to display storage values according to a format string or default to an appropriate formatted value.

Most of the code already existed as discrete utility methods in my code. I just needed to bring them all together into a single class and implement the IFormattable interface so that an instance of the class can be formatted in composite formatting methods such as string.Format().

The formatting supported for parsing is a numeric value optionally suffixed with B, KB, MB, GB, or TB. For example:

StorageValue s1 = StorageValue.Parse("150MB");

The IFormattable formatting supported for conversion to string values is any of the suffixes above, optionally with a leading +/- to enable or disable the display of the suffix, and an optional space in front of the suffix to indicate that a space should be placed between the numeric value and the suffix. If not format is specified then an appropriate unit will be used. Examples:

StorageValue workingSetSize = Process.GetCurrentProcess().WorkingSet64;

// Let the StorageValue class choose the most appropriate units:
applicationLog.WriteInfo(string.Format("Working set size: {0}", workingSetSize));

// Use KB:
applicationLog.WriteInfo(string.Format("Working set size: {0:KB}", workingSetSize));

// Use MB with a space between the value and the suffix:
applicationLog.WriteInfo(string.Format("Working set size: {0: MB}", workingSetSize));

// Use MB but don't display the units:
applicationLog.WriteInfo(string.Format("Working set size: {0:-MB} megabytes", workingSetSize));

One of the big design decisions that needs to be made is whether this type should be a class or a struct. A class is the easy option as its behaviour is likely to be more intuitive as it avoids some of the issues like immutability that structs raise. However, because this type is a thin wrapper around the System.Int64 type, a struct might offer more intuitive syntax and semantics. I decided to write the implementation as a class and then re-work it as a struct to see how the two implementations compared.

The initial implementation as a class is below and the struct implementation will follow in a second post. (Hint: Wait for the struct version, I think it’s better…)

(StorageClass.cs):

namespace Vitality.Common.Types
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Text;

    /// <summary>
    /// Represents a memory or disk storage value.
    /// </summary>
    [Serializable]
    public class StorageValue : IFormattable
    {
        #region Private Constants

        /// <summary>
        /// Constant for one terabyte.
        /// </summary>
        private const long TeraByte = 1024 * 1024 * 1024 * 1024L;

        /// <summary>
        /// Constant for one gigabyte.
        /// </summary>
        private const long GigaByte = 1024 * 1024 * 1024L;

        /// <summary>
        /// Constant for one megabyte.
        /// </summary>
        private const long MegaByte = 1024 * 1024L;

        /// <summary>
        /// Constant for one kilobyte.
        /// </summary>
        private const long KiloByte = 1024L;

        /// <summary>
        /// Constant for one byte.
        /// </summary>
        private const long Byte = 1L;

        #endregion Private Constants

        #region Private Members

        /// <summary>
        /// The storage value in bytes.
        /// </summary>
        private long bytes;

        #endregion Private Members

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the StorageValue class.
        /// </summary>
        /// <param name="bytes">The number of bytes for the storage value.</param>
        public StorageValue(long bytes)
        {
            this.bytes = bytes;
        }

        #endregion Constructors

        #region Public Properties

        /// <summary>
        /// Gets or sets the number of bytes in the StorageValue.
        /// </summary>
        public long Bytes
        {
            get { return this.bytes; }
            set { this.bytes = value; }
        }

        #endregion Public Properties

        #region Public Static Methods

        /// <summary>
        /// Converts the string representation of a storage value to an instance of the StorageValue class.
        /// </summary>
        /// <param name="s">The string representing a storage value and optionally specifying B, KB, MB, GB, or TB as a suffix.</param>
        /// <returns>An instance of the StorageValue class.</returns>
        public static StorageValue Parse(string s)
        {
            long conversionFactor = 1;

            if (s.EndsWith("TB", StringComparison.OrdinalIgnoreCase))
            {
                conversionFactor = StorageValue.TeraByte;
                s = s.Substring(0, s.Length - 2);
            }
            else if (s.EndsWith("GB", StringComparison.OrdinalIgnoreCase))
            {
                conversionFactor = StorageValue.GigaByte;
                s = s.Substring(0, s.Length - 2);
            }
            else if (s.EndsWith("MB", StringComparison.OrdinalIgnoreCase))
            {
                conversionFactor = StorageValue.MegaByte;
                s = s.Substring(0, s.Length - 2);
            }
            else if (s.EndsWith("KB", StringComparison.OrdinalIgnoreCase))
            {
                conversionFactor = StorageValue.KiloByte;
                s = s.Substring(0, s.Length - 2);
            }
            else if (s.EndsWith("B", StringComparison.OrdinalIgnoreCase))
            {
                conversionFactor = StorageValue.Byte;
                s = s.Substring(0, s.Length - 1);
            }
            else
            {
                conversionFactor = StorageValue.Byte;
            }

            double value = double.Parse(s.Trim());

            return new StorageValue((long)(value * conversionFactor));
        }

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <param name="bytes">The storage value in bytes.</param>
        /// <returns>A formatted string representing the storage value.</returns>
        public static string ToString(long bytes)
        {
            return StorageValue.ToString(bytes, string.Empty, null);
        }

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <param name="bytes">The storage value in bytes.</param>
        /// <param name="format">The format string to use.</param>
        /// <returns>A formatted string representing the storage value.</returns>
        public static string ToString(long bytes, string format)
        {
            return StorageValue.ToString(bytes, format, null);
        }

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <param name="bytes">The storage value in bytes.</param>
        /// <param name="format">The format string to use.</param>
        /// <param name="formatProvider">The IFormatProvider to use.</param>
        /// <returns>A formatted string representing the storage value.</returns>
        public static string ToString(long bytes, string format, IFormatProvider formatProvider)
        {
            long conversionFactor = 1L;
            string suffix = string.Empty;
            bool includeSuffix = true;
            bool includeSpace = false;
            bool isUnitsSpecified = false;

            // Format: [+/-][ ]units
            // + : include factor (this is the default if not specified), e.g. "10MB"
            // - : omit factor, e.g. "10"
            // : a space before the units indicates that the value and units should be separated by a space, e.g. "10 MB" rather than "10MB"
            // units : TB, GB, MB, KB, B for, respectively, terabytes, gigabytes, megabytes, kilobytes, and bytes.
            //
            // Examples:
            //
            // "GB" --> "10GB"
            // "+GB" --> "10GB"
            // "+ GB" --> "10 GB"
            // "-GB" --> "10"
            //
            // Commas and decimal places will be used when necessary, e.g. "0.1TB", "102,400KB", etc/
            //
            // The factor argument is not case sensitive, e.g. "10mb", "10MB", "10Mb", and "10mB" are all valid.
            if (!string.IsNullOrEmpty(format))
            {
                if (format.StartsWith("+", StringComparison.OrdinalIgnoreCase))
                {
                    includeSuffix = true;
                    format = format.Substring(1);
                }
                else if (format.StartsWith("-", StringComparison.OrdinalIgnoreCase))
                {
                    includeSuffix = false;
                    format = format.Substring(1);
                }

                if (format.StartsWith(" ", StringComparison.OrdinalIgnoreCase))
                {
                    includeSpace = true;
                    format = format.Substring(1);
                }

                if (string.Compare(format, "TB", true) == 0)
                {
                    conversionFactor = StorageValue.TeraByte;
                    suffix = "TB";
                    isUnitsSpecified = true;
                }
                else if (string.Compare(format, "GB", true) == 0)
                {
                    conversionFactor = StorageValue.GigaByte;
                    suffix = "GB";
                    isUnitsSpecified = true;
                }
                else if (string.Compare(format, "MB", true) == 0)
                {
                    conversionFactor = StorageValue.MegaByte;
                    suffix = "MB";
                    isUnitsSpecified = true;
                }
                else if (string.Compare(format, "KB", true) == 0)
                {
                    conversionFactor = StorageValue.KiloByte;
                    suffix = "KB";
                    isUnitsSpecified = true;
                }
                else if (string.Compare(format, "B", true) == 0)
                {
                    conversionFactor = StorageValue.Byte;
                    suffix = "B";
                    isUnitsSpecified = true;
                }
            }

            if (!isUnitsSpecified)
            {
                // No valid explicit units given so attempt to determine the most suitable to use:
                if (bytes > StorageValue.TeraByte)
                {
                    conversionFactor = StorageValue.TeraByte;
                    suffix = "TB";
                }
                else if (bytes > StorageValue.GigaByte)
                {
                    conversionFactor = StorageValue.GigaByte;
                    suffix = "GB";
                }
                else if (bytes > StorageValue.MegaByte)
                {
                    conversionFactor = StorageValue.MegaByte;
                    suffix = "MB";
                }
                else if (bytes > StorageValue.KiloByte)
                {
                    conversionFactor = StorageValue.KiloByte;
                    suffix = "KB";
                }
                else
                {
                    conversionFactor = StorageValue.Byte;
                    suffix = "B";
                }
            }

            // Format the string according to the IFormatProvider the caller specified, otherwise; use the InvariantCulture:
            NumberFormatInfo numberFormatInfo = null;

            if (formatProvider != null)
            {
                NumberFormatInfo formatProviderNumberFormatInfo = formatProvider.GetFormat(typeof(NumberFormatInfo)) as NumberFormatInfo;

                if (formatProviderNumberFormatInfo != null)
                {
                    numberFormatInfo = formatProviderNumberFormatInfo;
                }
            }

            if (numberFormatInfo == null)
            {
                numberFormatInfo = CultureInfo.InvariantCulture.NumberFormat;
            }

            // Use the culture specific thousands and decimal characters:
            string numberFormatString = string.Format("{{0:#{0}##0{1}##}}{{1}}{{2}}", numberFormatInfo.NumberGroupSeparator, numberFormatInfo.NumberDecimalSeparator);

            return string.Format(numberFormatString, (double)(bytes / (conversionFactor * 1.0d)), includeSpace ? " " : string.Empty, includeSuffix ? suffix : string.Empty);
        }

        #endregion Public Static Methods

        #region Public Methods

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <returns>A formatted string representing the storage value.</returns>
        public override string ToString()
        {
            return StorageValue.ToString(this.bytes);
        }

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <param name="format">The format string to use.</param>
        /// <returns>A formatted string representing the storage value.</returns>
        public string ToString(string format)
        {
            return StorageValue.ToString(this.bytes, format);
        }

        /// <summary>
        /// Returns a string representation of the storage value.
        /// </summary>
        /// <param name="format">The format string to use.</param>
        /// <param name="formatProvider">The IFormatProvider to use.</param>
        /// <returns>A formatted string representing the storage value.</returns>
        public string ToString(string format, IFormatProvider formatProvider)
        {
            return StorageValue.ToString(this.bytes, format, formatProvider);
        }

        #endregion Public Methods
    }
}



Written by Sea Monkey

January 25, 2017 at 5:08 pm

Posted in Development

Tagged with ,

Leave a comment