Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Unit Selection #831

Closed
AndyNetDuma opened this issue Sep 4, 2020 · 6 comments
Closed

Dynamic Unit Selection #831

AndyNetDuma opened this issue Sep 4, 2020 · 6 comments

Comments

@AndyNetDuma
Copy link

Is your feature request related to a problem? Please describe.
I would like to take a unit and write it with the simplest unit available. I.E. 1000mb should become 1gb. Ideally with 1024 bit division support. Very sorry if this is already a feature and I missed it, seems hard to believe nobody needed this before

Describe the solution you'd like
An overload of tostring or perhaps another function that finds the best unit before conversion. For mb it would require a concept of bits and bytes as it some cases I wish to show 1 or the other.
Another option is to take a unit and to provide a function to set the optimal base unit

Describe alternatives you've considered
We currently use a custom conversion system which is messy, I am hoping UnitsNet will do much better however without this vital feature it's, unfortunately, effectively useless to us. So sticking with that is the current alternative.

Additional context
An example:

var a = new UnitsNet.Information(2000, InformationUnit.Megabit);
var b = a.ToString(); // Gives 2000mb
var c = a.ToOptimalUnit(InformationUnit.Bit); // Should give 2gb in bits
var c = a.ToOptimalUnit(InformationUnit.Bytes, 1); // Should give ~2gb in bytes. 1 is optional decimal points
@angularsen
Copy link
Owner

Hi and thanks for the suggestion.
This is not implemented today, but if you are interested in doing a pull request then I think this would be a welcome feature.

We already have the following convention x.ToUnit(whatUnitToConvertTo).ToString(paramsOnHowToPresentText) so building on your example, how about something like this:

Output:

2,100 Mb
2,100 MB
2,148 Mib
2,148 MiB

ToUnit:
2.1 Gb
2.1 GB
2.1 Gib
2.1 GiB

ToOptimalUnit (inferred binary multiple):
2.1 Gb
2.1 GB
2.1 Gib
2.1 GiB

ToOptimalUnit (explicit binary multiple):
2.1 Gb
2.1 GB
2.1 Gib
2.1 GiB

LINQPad example:

void Main()
{
	var a = Information.FromMegabits(2100).Dump();
	var b = Information.FromMegabytes(2100).Dump();
	var c = Information.FromMebibits(2148).Dump();
	var d = Information.FromMebibytes(2148).Dump();
	
	
	// From this
	Console.WriteLine("\nToUnit:");
	a.ToUnit(InformationUnit.Gigabit).ToString().Dump();
	b.ToUnit(InformationUnit.Gigabyte).ToString().Dump();
	c.ToUnit(InformationUnit.Gibibit).ToString().Dump();
	d.ToUnit(InformationUnit.Gibibyte).ToString().Dump();
	
	// To this
	Console.WriteLine("\nToOptimalUnit (inferred binary multiple):");
	a.ToOptimalUnit().ToString().Dump();
	b.ToOptimalUnit().ToString().Dump();
	c.ToOptimalUnit().ToString().Dump();
	d.ToOptimalUnit().ToString().Dump();
	
	// Or this
	Console.WriteLine("\nToOptimalUnit (explicit binary multiple):");
	a.ToOptimalUnit(BinaryMultiple.Bit1000).ToString().Dump();
	b.ToOptimalUnit(BinaryMultiple.Byte1000).ToString().Dump();
	c.ToOptimalUnit(BinaryMultiple.Bit1024).ToString().Dump();
	d.ToOptimalUnit(BinaryMultiple.Byte1024).ToString().Dump();
}

public enum BinaryMultiple
{
	Bit1000,
	Bit1024,
	Byte1000,
	Byte1024
}

// Define other methods and classes here
public static class MyExtensions
{

	public static Information ToOptimalUnit(this Information x, BinaryMultiple binaryMultiple)
	{
		InformationUnit GetOptimalUnit()
		{
			switch (binaryMultiple)
			{
				case BinaryMultiple.Bit1000:
				{
					var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
					if (log10 < 3) return InformationUnit.Bit;
					if (log10 < 6) return InformationUnit.Kilobit;
					if (log10 < 9) return InformationUnit.Megabit;
					if (log10 < 12) return InformationUnit.Gigabit;
					if (log10 < 15) return InformationUnit.Terabit;
					if (log10 < 18) return InformationUnit.Exabit;

					throw new ArgumentOutOfRangeException("Value", x.Value.ToString());
				}

				case BinaryMultiple.Byte1000:
				{
					var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Byte).Value));
					if (log10 < 3) return InformationUnit.Byte;
					if (log10 < 6) return InformationUnit.Kilobyte;
					if (log10 < 9) return InformationUnit.Megabyte;
					if (log10 < 12) return InformationUnit.Gigabyte;
					if (log10 < 15) return InformationUnit.Terabyte;
					if (log10 < 18) return InformationUnit.Exabyte;

					throw new ArgumentOutOfRangeException("Value", x.Value.ToString());
				}

				case BinaryMultiple.Bit1024:
				{
					var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
					if (log10 < 6) return InformationUnit.Kibibit;
					if (log10 < 9) return InformationUnit.Mebibit;
					if (log10 < 12) return InformationUnit.Gibibit;
					if (log10 < 15) return InformationUnit.Tebibit;
					if (log10 < 18) return InformationUnit.Exbibit;
					
					throw new ArgumentOutOfRangeException("Value", x.Value.ToString());
				}

				case BinaryMultiple.Byte1024:
				{
					var log10 = Math.Log10(Convert.ToDouble(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value)));
					if (log10 < 6) return InformationUnit.Kibibyte;
					if (log10 < 9) return InformationUnit.Mebibyte;
					if (log10 < 12) return InformationUnit.Gibibyte;
					if (log10 < 15) return InformationUnit.Tebibyte;
					if (log10 < 18) return InformationUnit.Exbibyte;
					
					throw new ArgumentOutOfRangeException("Value", x.Value.ToString());
				}
				
				default:
					throw new ArgumentOutOfRangeException("Unit", x.Unit.ToString());
			}
		}

		return x.ToUnit(GetOptimalUnit());
	}

	// Tries to infer the binary multiple based on the unit.
	public static Information ToOptimalUnit(this Information x)
	{
		BinaryMultiple GetInferredBinaryMultiple()
		{
			switch (x.Unit)
			{
				case InformationUnit.Bit:
				case InformationUnit.Kilobit:
				case InformationUnit.Megabit:
				case InformationUnit.Gigabit:
				case InformationUnit.Terabit:
				case InformationUnit.Exabit:
					return BinaryMultiple.Bit1000;

				case InformationUnit.Byte:
				case InformationUnit.Kilobyte:
				case InformationUnit.Megabyte:
				case InformationUnit.Gigabyte:
				case InformationUnit.Terabyte:
				case InformationUnit.Exabyte:
					return BinaryMultiple.Byte1000;

				case InformationUnit.Kibibit:
				case InformationUnit.Mebibit:
				case InformationUnit.Gibibit:
				case InformationUnit.Tebibit:
				case InformationUnit.Exbibit:
					return BinaryMultiple.Bit1024;

				case InformationUnit.Kibibyte:
				case InformationUnit.Mebibyte:
				case InformationUnit.Gibibyte:
				case InformationUnit.Tebibyte:
				case InformationUnit.Exbibyte:
					return BinaryMultiple.Byte1024;

				default:
					throw new ArgumentOutOfRangeException("Unit");
			}
		}

	
		var binaryMultiple = GetInferredBinaryMultiple();
		return ToOptimalUnit(x, binaryMultiple);
	}
}

@AndyNetDuma
Copy link
Author

I can take a look however it's unlikely I will have time to spin it into a proper PR. I am doing this for work and only have so much time in the day. If I can easily turn it into a PR I will otherwise I will just have to use a local extension method or library.

I also have other considerations to worry about, for example I will have to back prop it into the data struct to minimise recalculation as I will need to redo the calculation quite often.

@AndyNetDuma
Copy link
Author

Still not 100% sure if this lib is going to work out for me. After reading your code in more detail it was very close to what I needed. I cleaned it up for testing if the lib is good. Feel free to use it for a PR if you want, I couldn't even find the information code in the repo presumably because it's auto gen so I have no idea where it would ilve. Other concerns for a PR include unit testing and generalisation.

    public static class UnitsNetExtensionMethods
    {
        public enum BinaryMultiple
        {
            Bit1000  = 1000,
            Bit1024  = 1024,
            Byte1000 = 8000,
            Byte1024 = 8192,
        }

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        /// <returns>Modified version of the base information unit</returns>
        public static Information ToOptimalUnit(this Information x, BinaryMultiple binaryMultiple = BinaryMultiple.Byte1000) => x.ToUnit(GetOptimalUnit(x, binaryMultiple));

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit and result of the method</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        public static void ConvertToOptimalUnit(this ref Information x, BinaryMultiple binaryMultiple = BinaryMultiple.Byte1000) => x = x.ToUnit(GetOptimalUnit(x, binaryMultiple));

        private static InformationUnit GetOptimalUnit(Information x, BinaryMultiple binaryMultiple)
        {
            switch (binaryMultiple)
            {
                case BinaryMultiple.Bit1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
                    if (log10 < 3)  return InformationUnit.Bit;
                    if (log10 < 6)  return InformationUnit.Kilobit;
                    if (log10 < 9)  return InformationUnit.Megabit;
                    if (log10 < 12) return InformationUnit.Gigabit;
                    if (log10 < 15) return InformationUnit.Terabit;
                    return InformationUnit.Exabit;
                }

                case BinaryMultiple.Byte1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Byte).Value));
                    if (log10 < 3)  return InformationUnit.Byte;
                    if (log10 < 6)  return InformationUnit.Kilobyte;
                    if (log10 < 9)  return InformationUnit.Megabyte;
                    if (log10 < 12) return InformationUnit.Gigabyte;
                    if (log10 < 15) return InformationUnit.Terabyte;
                    return InformationUnit.Exabyte;
                }

                case BinaryMultiple.Bit1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
                    if (log10 < 6)  return InformationUnit.Kibibit;
                    if (log10 < 9)  return InformationUnit.Mebibit;
                    if (log10 < 12) return InformationUnit.Gibibit;
                    if (log10 < 15) return InformationUnit.Tebibit;
                    return InformationUnit.Exbibit;
                }

                case BinaryMultiple.Byte1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value)));
                    if (log10 < 6)  return InformationUnit.Kibibyte;
                    if (log10 < 9)  return InformationUnit.Mebibyte;
                    if (log10 < 12) return InformationUnit.Gibibyte;
                    if (log10 < 15) return InformationUnit.Tebibyte;
                    return InformationUnit.Exbibyte;
                }

                default:
                    throw new ArgumentOutOfRangeException($"Unsupported binary multiplier {(int)binaryMultiple}");
            }
        }
    }

Note I also added an extra method to modify the variable in place. Example usages:

var t = new Information(2000, InformationUnit.Megabyte).ToOptimalUnit();

t = new Information(2000, InformationUnit.Megabyte);
t.ConvertToOptimalUnit();

@AndyNetDuma
Copy link
Author

Added some handling for bitrates, close to copy/paste. It can almost certainly be written more efficiently but I don't have the time or need for anything better.

using System;
using UnitsNet;
using UnitsNet.Units;

namespace DumaMobile.Utils
{
    public static class UnitsNetExtensionMethods
    {
        public enum BinaryMultiple
        {
            Bit1000  = 1000,
            Bit1024  = 1024,
            Byte1000 = 8000,
            Byte1024 = 8192,
        }

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        /// <returns>Modified version of the base information unit</returns>
        public static Information ToOptimalUnit(this Information x, BinaryMultiple binaryMultiple = BinaryMultiple.Byte1000) => x.ToUnit(GetOptimalUnit(x, binaryMultiple));

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit and result of the method</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        public static void ConvertToOptimalUnit(this ref Information x, BinaryMultiple binaryMultiple = BinaryMultiple.Byte1000) => x = x.ToUnit(GetOptimalUnit(x, binaryMultiple));

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        /// <returns>Modified version of the base information unit</returns>
        public static BitRate ToOptimalUnit(this BitRate x, BinaryMultiple binaryMultiple = BinaryMultiple.Bit1000) => x.ToUnit(GetOptimalBitrateUnit(x, binaryMultiple));

        /// <summary>
        /// Converts a unit to the simplest representation, ie 2000mb -> 2gb
        /// When used with tostring it will display the best representation of the original unit.
        /// </summary>
        /// <param name="x">The base unit and result of the method</param>
        /// <param name="binaryMultiple">Final representation, bit/byte 1000/1024</param>
        public static void ConvertToOptimalUnit(this ref BitRate x, BinaryMultiple binaryMultiple = BinaryMultiple.Bit1000) => x = x.ToUnit(GetOptimalBitrateUnit(x, binaryMultiple));

        private static InformationUnit GetOptimalUnit(Information x, BinaryMultiple binaryMultiple)
        {
            switch (binaryMultiple)
            {
                case BinaryMultiple.Bit1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
                    if (log10 < 3)  return InformationUnit.Bit;
                    if (log10 < 6)  return InformationUnit.Kilobit;
                    if (log10 < 9)  return InformationUnit.Megabit;
                    if (log10 < 12) return InformationUnit.Gigabit;
                    if (log10 < 15) return InformationUnit.Terabit;
                    return InformationUnit.Exabit;
                }

                case BinaryMultiple.Byte1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Byte).Value));
                    if (log10 < 3)  return InformationUnit.Byte;
                    if (log10 < 6)  return InformationUnit.Kilobyte;
                    if (log10 < 9)  return InformationUnit.Megabyte;
                    if (log10 < 12) return InformationUnit.Gigabyte;
                    if (log10 < 15) return InformationUnit.Terabyte;
                    return InformationUnit.Exabyte;
                }

                case BinaryMultiple.Bit1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value));
                    if (log10 < 6)  return InformationUnit.Kibibit;
                    if (log10 < 9)  return InformationUnit.Mebibit;
                    if (log10 < 12) return InformationUnit.Gibibit;
                    if (log10 < 15) return InformationUnit.Tebibit;
                    return InformationUnit.Exbibit;
                }

                case BinaryMultiple.Byte1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(Convert.ToDouble(x.ToUnit(InformationUnit.Bit).Value)));
                    if (log10 < 6)  return InformationUnit.Kibibyte;
                    if (log10 < 9)  return InformationUnit.Mebibyte;
                    if (log10 < 12) return InformationUnit.Gibibyte;
                    if (log10 < 15) return InformationUnit.Tebibyte;
                    return InformationUnit.Exbibyte;
                }

                default:
                    throw new ArgumentOutOfRangeException($"Unsupported binary multiplier {(int)binaryMultiple}");
            }
        }

        private static BitRateUnit GetOptimalBitrateUnit(BitRate x, BinaryMultiple binaryMultiple)
        {
            switch (binaryMultiple)
            {
                case BinaryMultiple.Bit1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(BitRateUnit.BitPerSecond).Value));
                    if (log10 < 3)  return BitRateUnit.BitPerSecond;
                    if (log10 < 6)  return BitRateUnit.KilobitPerSecond;
                    if (log10 < 9)  return BitRateUnit.MegabitPerSecond;
                    if (log10 < 12) return BitRateUnit.GigabitPerSecond;
                    if (log10 < 15) return BitRateUnit.TerabitPerSecond;
                    return BitRateUnit.ExabitPerSecond;
                }

                case BinaryMultiple.Byte1000:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(BitRateUnit.BytePerSecond).Value));
                    if (log10 < 3)  return BitRateUnit.BytePerSecond;
                    if (log10 < 6)  return BitRateUnit.KilobytePerSecond;
                    if (log10 < 9)  return BitRateUnit.MegabytePerSecond;
                    if (log10 < 12) return BitRateUnit.GigabytePerSecond;
                    if (log10 < 15) return BitRateUnit.TerabytePerSecond;
                    return BitRateUnit.ExabytePerSecond;
                }

                case BinaryMultiple.Bit1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(x.ToUnit(BitRateUnit.BitPerSecond).Value));
                    if (log10 < 6)  return BitRateUnit.KibibitPerSecond;
                    if (log10 < 9)  return BitRateUnit.MebibitPerSecond;
                    if (log10 < 12) return BitRateUnit.GibibitPerSecond;
                    if (log10 < 15) return BitRateUnit.TebibitPerSecond;
                    return BitRateUnit.ExbibitPerSecond;
                }

                case BinaryMultiple.Byte1024:
                {
                    var log10 = Math.Log10(Convert.ToDouble(Convert.ToDouble(x.ToUnit(BitRateUnit.BitPerSecond).Value)));
                    if (log10 < 6)  return BitRateUnit.KibibytePerSecond;
                    if (log10 < 9)  return BitRateUnit.MebibytePerSecond;
                    if (log10 < 12) return BitRateUnit.GibibytePerSecond;
                    if (log10 < 15) return BitRateUnit.TebibytePerSecond;
                    return BitRateUnit.ExbibytePerSecond;
                }

                default:
                    throw new ArgumentOutOfRangeException($"Unsupported binary multiplier {(int)binaryMultiple}");
            }
        }
    }
}

Up to you @angularsen if you want to close this issue, I personally have what I need with the above code. Thank you for the help.

@angularsen
Copy link
Owner

Thanks for the update. I might merge this in at some point, it seems useful enough.

@stale
Copy link

stale bot commented Nov 7, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants