Last active
March 16, 2017 00:22
-
-
Save erdomke/cf112c6a30624245c4bd7205d8a2fffa to your computer and use it in GitHub Desktop.
Formats a number to a specific number of significant digits
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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