Skip to content

Instantly share code, notes, and snippets.

@bboyle1234
Last active January 15, 2022 19:06
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bboyle1234/df47f661b531efd7386f0dbcdfbeee6f to your computer and use it in GitHub Desktop.
Save bboyle1234/df47f661b531efd7386f0dbcdfbeee6f to your computer and use it in GitHub Desktop.
Humanizes numbers. Makes them human-readable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static System.Math;
namespace Foo {
public static class DoubleExtensions {
public static double RoundToSignificantFigures(this double value, int numSignificantFigures) {
if (value == 0) return 0.0;
var scale = Pow(10, Floor(Log10(Abs(value))) + 1);
// Perform the last step using decimals to prevent double-arithmetic re-introducing tiny errors (and more figures to the result)
return (double)((decimal)scale * (decimal)Math.Round(value / scale, numSignificantFigures));
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static System.Math;
namespace Foo {
public static class HumanReadableDoubles {
// Created with thanks to http://stackoverflow.com/questions/16083666/make-big-and-small-numbers-human-readable/16091580#16091580
static readonly string[] humanReadableSuffixes = { "f", "a", "p", "n", "μ", "m", "", "k", "M", "G", "T", "P", "E" };
public static string ToHumanReadable(this double value, int numSignificantDigits) {
// Deal with special values
if (double.IsInfinity(value) || double.IsNaN(value) || value == 0 || numSignificantDigits <= 0)
return value.ToString();
// We deal only with positive values in the code below
var isNegative = Sign(value) < 0;
value = Abs(value);
// Calculate the exponent as a multiple of 3, ie -6, -3, 0, 3, 6, etc
var exponent = (int)Floor(Log10(value) / 3) * 3;
// Find the correct suffix for the exponent, or fall back to scientific notation
var indexOfSuffix = exponent / 3 + 6;
var suffix = indexOfSuffix >= 0 && indexOfSuffix < humanReadableSuffixes.Length
? humanReadableSuffixes[indexOfSuffix]
: "·10^" + exponent;
// Scale the value to the exponent, then format it to the correct number of significant digits and add the suffix
value = value * Pow(10, -exponent);
var numIntegerDigits = (int)Floor(Log(value, 10)) + 1;
var numFractionalDigits = Min(numSignificantDigits - numIntegerDigits, 15);
var format = $"{new string('0', numIntegerDigits)}.{new string('0', numFractionalDigits)}";
var result = value.ToString(format) + suffix;
// Handle negatives
if (isNegative)
result = "-" + result;
return result;
}
public static double ParseHumanReadableDouble(this string expression) {
var multiplier = 1.0;
if (expression.Contains("·10^")) {
var indexOfCaret = expression.LastIndexOf('^');
multiplier = Pow(10, int.Parse(expression.Substring(indexOfCaret + 1)));
expression = expression.Substring(0, indexOfCaret - 3);
} else {
var suffix = humanReadableSuffixes.SingleOrDefault(s => s.Length > 0 && expression.EndsWith(s, StringComparison.InvariantCulture)) ?? "";
var suffixIndex = humanReadableSuffixes.IndexOf(suffix);
multiplier = Pow(10, 3 * (suffixIndex - 6));
expression = expression.Replace(suffix, string.Empty);
}
return double.Parse(expression) * multiplier;
}
}
}
@ja72
Copy link

ja72 commented May 25, 2017

Hi. Author of the SO answer here. How about adding Parsing of the human readable value.

    public static double ParseNice(this string expression)
    {
        Contract.Requires(expression!=null);
        expression=expression.Trim();
        double exp=1;
        if(expression.Contains("·10^"))
        {
            int caret=expression.LastIndexOf('^');
            exp=Math.Pow(10, expression.Substring(caret+1).ParseDouble());
            expression=expression.Substring(0, caret-1).Trim();
        }
        else
        {
            for(int i=0; i<humanReadableSuffixes.Length; i++)
            {
                var pfx=humanReadableSuffixes[i];
                if(pfx.Length>0&&expression.EndsWith(pfx, StringComparison.InvariantCulture))
                {
                    exp=Math.Pow(10, 3*(i-6));
                    expression=expression.Substring(0, expression.Length-humanReadableSuffixes[i].Length).Trim();
                    break;
                }
            }
        }
        return expression.ParseDouble()*exp;
    }

@ja72
Copy link

ja72 commented May 25, 2017

I want to contribute one more function that rounds floating point numbers based on the number of significant digits.

    public static double RoundSig(this double d, int significant_figures)
    {
        Contract.Requires(significant_figures>=0&&significant_figures<=MaxSignificantFigures);
        // http://stackoverflow.com/a/374470
        if(d==0)
            return 0;
        double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d)))+1);
        return scale*Math.Round(d/scale, significant_figures);
    }

@bboyle1234
Copy link
Author

bboyle1234 commented May 26, 2017

ja72, I've updated the gist. Thank you. I wasn't sure of whether the decimal thing had to be added to the rounding method. Experience made me drop it in, but I didn't spend time testing to determine whether it's truly necessary.

@ja72
Copy link

ja72 commented May 26, 2017

I just saw it. It is an interesting approach. Do you have any references that describe in more detail this trick? I can see if the number is decomposed into this form X 10^n that we can use decimal for X which has more bits than the mantissa of double.

@ja72
Copy link

ja72 commented May 26, 2017

Sorry to dump more code at you, but how about these numeric manipulation functions:

    /// <summary>
    /// Return 1 only when values in between min and max, 0 otherwise.
    /// </summary>
    /// <param name="value">The value to evaluate</param>
    /// <param name="min_value">The min value</param>
    /// <param name="max_value">The max value</param>
    /// <returns></returns>
    [Pure]
    public static double Chi(this double value, double min_value, double max_value)
    {
        double dx = Math.Abs(max_value-min_value);
        min_value=Math.Min(min_value, max_value);
        max_value=min_value+dx;
        return value<min_value ? 0 : (value>max_value ? 0 : 1);
    }
    /// <summary>
    /// Return a saw-tooth value between a minimum and a maxmimum value.
    /// <remarks>Sorts the min/max values from lowest to highest</remarks>
    /// </summary>
    /// <param name="value">The value to wrap</param>
    /// <param name="min_value">The lower limit</param>
    /// <param name="max_value">The upper limit</param>
    /// <returns>A scalar value</returns>
    [Pure]
    public static double WrapAround(this double value, double min_value, double max_value)
    {
        double dx = Math.Abs(max_value-min_value);
        min_value=Math.Min(min_value, max_value);
        return value-dx*Math.Floor((value-min_value)/dx);
    }
    /// <summary>
    /// Return a saw-tooth value between zero an a maximum value.
    /// </summary>
    /// <param name="x">The value to use</param>
    /// <param name="x_high">The maximum value allowed</param>
    /// <returns>A scalar value</returns>
    [Pure]
    public static double WrapAround(this double value, double max_value)
    {
        return WrapAround(value, 0, max_value);
    }
    /// <summary>
    /// Return x when between min and max value, otherwise clamp at limits
    /// </summary>
    /// <example>    
    ///     ClapMinMax(-0.33, 0.0, 1.0) = 0.00
    ///     ClapMinMax( 0.33, 0.0, 1.0) = 0.33
    ///     ClapMinMax( 1.33, 0.0, 1.0) = 1.00
    /// </example>
    /// <param name="x">The value to clamp</param>
    /// <param name="min_value">The minimum value to use</param>
    /// <param name="max_value">The maximum value to use</param>
    /// <returns>A scalar value</returns>
    [Pure]
    public static double ClampMinMax(this double value, double min_value, double max_value)
    {
        return value>max_value ? max_value : value<min_value ? min_value : value;
    }
    /// <summary>
    /// Return x when more than min, otherwise return min
    /// </summary>
    /// <param name="x">The value to clamp</param>
    /// <param name="min_value">The minimum value to use</param>
    /// <returns>A scalar value</returns>
    [Pure]
    public static double ClampMin(this double value, double min_value)
    {
        return value<min_value ? min_value : value;
    }
    /// <summary>
    /// Return x when less than max, otherwise return max
    /// </summary>
    /// <param name="x">The value to clamp</param>
    /// <param name="max_value">The maximum value to use</param>
    /// <returns>A scalar value</returns>
    [Pure]
    public static double ClampMax(this double value, double max_value)
    {
        return value>max_value ? max_value : value;
    }

Fore me WrapAround and ClampMinMax are used all the time. Chi not so often. Examples would be getting angle results between -180 and +180. or 0 to 360 for usage in Sin() or Cos() in order to maintain precision. Try to see that Sin(1) != Sin(1+2*Math.PI). They differ by some 10^-16. But as the angles go up that error accumulates rapidly.

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