Skip to content

Instantly share code, notes, and snippets.

@erdomke
Last active March 16, 2017 00:22
Show Gist options
  • Save erdomke/cf112c6a30624245c4bd7205d8a2fffa to your computer and use it in GitHub Desktop.
Save erdomke/cf112c6a30624245c4bd7205d8a2fffa to your computer and use it in GitHub Desktop.
Formats a number to a specific number of significant digits
using System;
using System.Globalization;
public static class FormatExtensions
{
public static string ToPrecision(this int value, int digits, string format = "")
{
return SigFigFormatProvider.Instance.Format("s" + digits.ToString() + format, value, null);
}
public static string ToPrecision(this double value, int digits, string format = "")
{
return SigFigFormatProvider.Instance.Format("s" + digits.ToString() + format, value, null);
}
}
public class SigFigFormatProvider : IFormatProvider, ICustomFormatter
{
private enum DecimalState : byte
{
NoDecimal,
DecimalFound,
DecimalWritten
}
private static SigFigFormatProvider _instance = new SigFigFormatProvider();
public static SigFigFormatProvider Instance { get { return _instance; } }
private const string DoubleFixedPoint = "0.###################################################################################################################################################################################################################################################################################################################################################";
private readonly string CultureDecimal = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
private SigFigFormatProvider() { }
public string Format(string format, object arg, IFormatProvider formatProvider)
{
var value = default(string);
var sigFigs = -1;
// Parse the number of significant figures from the format specifier
if (format.StartsWith("s", StringComparison.OrdinalIgnoreCase)
&& (arg is double || arg is Single || arg is decimal
|| arg is int || arg is short || arg is long
|| arg is uint || arg is ushort || arg is ulong
|| arg is byte))
{
var i = 1;
while (i < format.Length && char.IsDigit(format[i]))
i++;
if (i > 1)
sigFigs = int.Parse(format.Substring(1, i - 1));
format = format.Substring(i);
if (format.Length == 1)
{
switch (format[0])
{
case 'C':
case 'c':
case 'F':
case 'f':
case 'N':
case 'n':
case 'P':
case 'p':
format += "99";
break;
case 'R':
case 'r':
break;
case 'D':
case 'd':
case 'G':
case 'g':
case 'E':
case 'e':
case 'X':
case 'x':
throw new FormatException("Invalid format string");
}
}
if (string.IsNullOrEmpty(format) && (arg is double || arg is Single || arg is decimal))
format = DoubleFixedPoint;
}
if (arg is IFormattable)
value = ((IFormattable)arg).ToString(format, CultureInfo.CurrentCulture);
else if (arg != null)
value = arg.ToString();
if (sigFigs > 0)
{
var output = new char[value.Length + sigFigs];
var decimalChar = CultureDecimal[0];
var o = 0;
var digits = 0;
var decimalState = DecimalState.NoDecimal;
var digitFound = false;
for (var i = 0; i < value.Length; i++)
{
if (char.IsDigit(value[i]))
{
// Write the decimal
if (decimalState == DecimalState.DecimalFound && digits <= sigFigs)
{
output[o++] = '.';
decimalState = DecimalState.DecimalWritten;
}
// This digit is significant
if (digitFound || value[i] != '0')
{
digitFound = true;
digits++;
// The last significant digit
if (digits == sigFigs)
{
var j = i + 1;
while (j < value.Length && !char.IsDigit(value[j]))
j++;
if (j < value.Length)
{
var rounded = Math.Round(value[i] - '0' + (value[j] - '0') / 10.0).ToString("0");
rounded.CopyTo(0, output, o, rounded.Length);
o += rounded.Length;
}
else
{
output[o++] = value[i];
}
}
// This digit is after the significant digits. Only write if prior to the decimal
else if (digits > sigFigs)
{
if (decimalState < DecimalState.DecimalFound)
output[o++] = '0';
}
// A general significant digit
else
{
output[o++] = value[i];
}
}
// A digit prior to the significant digits
else
{
output[o++] = value[i];
}
}
// The decimal character
else if (value[i] == decimalChar)
{
decimalState = DecimalState.DecimalFound;
}
// A general character
else
{
output[o++] = value[i];
}
}
// Any decimal digits require to pad to the number of significant figures
if (digits < sigFigs && decimalState != DecimalState.DecimalWritten)
{
CultureDecimal.CopyTo(0, output, o, CultureDecimal.Length);
o += CultureDecimal.Length;
}
while (digits < sigFigs)
{
output[o++] = '0';
digits++;
}
return new string(output, 0, o);
}
return value;
}
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment