-
-
Save bboyle1234/df47f661b531efd7386f0dbcdfbeee6f to your computer and use it in GitHub Desktop.
| 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, 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.
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.
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.
I want to contribute one more function that rounds floating point numbers based on the number of significant digits.