Related
The string should look like this "[1,3,5,7]" I tried different versions of this code but I always either have too many commas or not enough. For example for this array "[2,4,7,8,10]" i get "[7,]".
static string ZwrocNieparzyste1(int[] tab, int i = 0)
{
if (tab.Length == 1)
if (tab[i] % 2 == 1)
return string.Format("[{0}]", tab[i]);
else
return "[]";
if (tab.Length == 0)
return "[]";
if (i == 0)
if (tab[i] % 2 == 1)
return string.Format("[{0},{1}", tab[i], ZwrocNieparzyste1(tab, i + 1));
else
return string.Format("[{0}", ZwrocNieparzyste1(tab, i + 1));
if (i < tab.Length - 1)
if (tab[i] % 2 == 1)
return string.Format("{0},{1}", tab[i], ZwrocNieparzyste1(tab, i + 1));
else
return string.Format("{0}", ZwrocNieparzyste1(tab, i + 1));
else
if (tab[i] % 2 == 1)
return string.Format("{0}]", tab[i]);
else
return "]";
}
Your code is too much convoluted... To much difficult to comprehend. An example about how it could be simplified:
public static string PrintRecursive(int[] nums)
{
if (nums.Length == 0)
{
return "[]";
}
return PrintRecursiveImpl(nums, 0, false);
}
/// <summary>
///
/// </summary>
/// <param name="nums">The array of numbers</param>
/// <param name="ix">The ix that perhaps will be printed</param>
/// <param name="alreadyPrintedNumber">Do we need to prepend a comma?</param>
/// <returns></returns>
private static string PrintRecursiveImpl(int[] nums, int ix, bool alreadyPrintedNumber)
{
string ret = string.Empty;
if (ix == 0)
{
ret = "[";
}
if (nums[ix] % 2 == 1)
{
if (alreadyPrintedNumber)
{
ret += ", ";
}
ret += nums[ix];
alreadyPrintedNumber = true;
}
if (ix + 1 < nums.Length)
{
ret += PrintRecursiveImpl(nums, ix + 1, alreadyPrintedNumber);
}
else
{
ret += "]";
}
return ret;
}
Note that I've splitted the "special case" nums.Length == 0 from the other cases. So there are two methods, a "public" method to make the printing start and a private recursive method that "prints" the numbers.
I need to support languages that can use non ascii letters in input text, so i need to implement StringComparison.CurrentCultureIgnoreCase for FirstUnmatchedIndex. Ignoring casing isn't so bad, but i don't know how to convert combined symbols to a standard representation and then compare. So here are a few cases where the function should return -1 but returns something else instead....
encyclopædia = encyclopaedia
Archæology = Archaeology
ARCHÆOLOGY = archaeology
Archæology = archaeology
Weißbier = WEISSBIER
How to know if a char needs to be expanded and convert each char to the expanded form when needed ?
/// <summary>
/// Gets a first different char occurence index
/// </summary>
/// <param name="a">First string</param>
/// <param name="b">Second string</param>
/// <param name="compareSmallest">
/// If true, Returns the first difference found or -1 if the end of a string is reached without finding a difference.
/// IE, Return -1 if the smallest string is contained in the other.
/// Otherwise returns -1 only if both string are really the same and will return the position where the smallest string ends if no difference is found.
/// </param>
/// <returns>
/// Returns first difference index or -1 if no difference is found
/// </returns>
public static int FirstUnmatchedIndex(this string a, string b, bool compareSmallest = false, StringComparison comparisonType = StringComparison.CurrentCulture)
{
//Treat null as empty
if (String.IsNullOrEmpty(a)) {
if (String.IsNullOrEmpty(b)) {
//Equal, both empty.
return -1;
} else {
//If compareSmallest, empty is always found in longest.
//Otherwise, difference at pos 0.
return compareSmallest ? -1 : 0;
}
}
if (object.ReferenceEquals(a, b)) {
//Same Ref.
return -1;
}
//Convert strings before compare.
switch (comparisonType) {
case StringComparison.CurrentCulture:
//FIXME
break;
case StringComparison.CurrentCultureIgnoreCase:
//FIXME
var currentCulture = System.Globalization.CultureInfo.CurrentCulture;
a = a.ToLower(currentCulture);
b = b.ToLower(currentCulture);
break;
case StringComparison.InvariantCulture:
//FIXME
break;
case StringComparison.InvariantCultureIgnoreCase:
//FIXME
a = a.ToLowerInvariant();
b = b.ToLowerInvariant();
break;
case StringComparison.OrdinalIgnoreCase:
a = a.ToLower();
b = b.ToLower();
break;
case StringComparison.Ordinal:
//Ordinal(Binary) comprare, nothing special to do.
default:
break;
}
string longStr = a.Length > b.Length ? a : b;
string shortStr = a.Length > b.Length ? b : a;
int count = shortStr.Length;
for (int idx = 0; idx < count; idx++) {
//FIXME Check if char needs to be expanded ?
if (shortStr[idx] != longStr[idx]) {
return idx;
}
}
return compareSmallest || longStr.Length == count ? -1 : count;
}
I'm not sure if I understood your question correctly, but you can use "dictionary + regular expression" combination. The idea is to create dictionary with the characters you want to expand and find them with the help of regex. The following code shows example how to do it.
Explanation of regular expression:
(?i) - This enables case insensitive search (same as
RegexOptions.IgnoreCase, but in-line)
[^\p{IsBasicLatin}]+ -
This will search for all characters that don't fit basic Latin
characters set (from \u0000 through \u007F).
The code uses ToLower method which avoids adding upper-case non-Latin characters to dictionary. You can, of course, not to do it, if you want to be explicit (i.e. adding all both lower-case and upper-case characters into dictionary and removing ToLower).
var dic = new Dictionary<string, string>
{
["æ"] = "ae",
["ß"] = "ss"
};
var words = new[] { "encyclopædia", "Archæology", "ARCHÆOLOGY", "Archæology", "Weißbier" };
var pattern = #"(?i)[^\p{IsBasicLatin}]+";
int x = -1;
foreach(var word in words)
{
// Each match (m.Value) is passed to dictionary
words[++x] = Regex.Replace(word, pattern, m => dic[m.Value.ToLower()]);
}
words.ToList().ForEach(WriteLine);
/*
Output:
encyclopaedia
Archaeology
ARCHaeOLOGY
Archaeology
Weissbier
*/
All string.Split methods seems to return an array of strings (string[]).
I'm wondering if there is a lazy variant that returns an IEnumerable<string> such that one for large strings (or an infinite length IEnumerable<char>), when one is only interested in a first subsequences, one saves computational effort as well as memory. It could also be useful if the string is constructed by a device/program (network, terminal, pipes) and the entire strings is thus not necessary immediately fully available. Such that one can already process the first occurences.
Is there such method in the .NET framework?
You could easily write one:
public static class StringExtensions
{
public static IEnumerable<string> Split(this string toSplit, params char[] splits)
{
if (string.IsNullOrEmpty(toSplit))
yield break;
StringBuilder sb = new StringBuilder();
foreach (var c in toSplit)
{
if (splits.Contains(c))
{
yield return sb.ToString();
sb.Clear();
}
else
{
sb.Append(c);
}
}
if (sb.Length > 0)
yield return sb.ToString();
}
}
Clearly, I haven't tested it for parity with string.split, but I believe it should work just about the same.
As Servy notes, this doesn't split on strings. That's not as simple, and not as efficient, but it's basically the same pattern.
public static IEnumerable<string> Split(this string toSplit, string[] separators)
{
if (string.IsNullOrEmpty(toSplit))
yield break;
StringBuilder sb = new StringBuilder();
foreach (var c in toSplit)
{
var s = sb.ToString();
var sep = separators.FirstOrDefault(i => s.Contains(i));
if (sep != null)
{
yield return s.Replace(sep, string.Empty);
sb.Clear();
}
else
{
sb.Append(c);
}
}
if (sb.Length > 0)
yield return sb.ToString();
}
There is no such thing built-in. Regex.Matches is lazy if I interpret the decompiled code correctly. Maybe you can make use of that.
Or, you simply write your own split function.
Actually, you could image most string functions generalized to arbitrary sequences. Often, even sequences of T, not just char. The BCL does not emphasize that at generalization all. There is no Enumerable.Subsequence for example.
Nothing built-in, but feel free to rip my Tokenize method:
/// <summary>
/// Splits a string into tokens.
/// </summary>
/// <param name="s">The string to split.</param>
/// <param name="isSeparator">
/// A function testing if a code point at a position
/// in the input string is a separator.
/// </param>
/// <returns>A sequence of tokens.</returns>
IEnumerable<string> Tokenize(string s, Func<string, int, bool> isSeparator = null)
{
if (isSeparator == null) isSeparator = (str, i) => !char.IsLetterOrDigit(str, i);
int startPos = -1;
for (int i = 0; i < s.Length; i += char.IsSurrogatePair(s, i) ? 2 : 1)
{
if (!isSeparator(s, i))
{
if (startPos == -1) startPos = i;
}
else if (startPos != -1)
{
yield return s.Substring(startPos, i - startPos);
startPos = -1;
}
}
if (startPos != -1)
{
yield return s.Substring(startPos);
}
}
There is no built-in method to do this as far as I'm know. But it doesn't mean you can't write one. Here is a sample to give you an idea:
public static IEnumerable<string> SplitLazy(this string str, params char[] separators)
{
List<char> temp = new List<char>();
foreach (var c in str)
{
if (separators.Contains(c) && temp.Any())
{
yield return new string(temp.ToArray());
temp.Clear();
}
else
{
temp.Add(c);
}
}
if(temp.Any()) { yield return new string(temp.ToArray()); }
}
Ofcourse this doesn't handle all cases and can be improved.
I wrote this variant which supports also SplitOptions and count.
It behaves same like string.Split in all test cases I tried.
The nameof operator is C# 6 sepcific and can be replaced by "count".
public static class StringExtensions
{
/// <summary>
/// Splits a string into substrings that are based on the characters in an array.
/// </summary>
/// <param name="value">The string to split.</param>
/// <param name="options"><see cref="StringSplitOptions.RemoveEmptyEntries"/> to omit empty array elements from the array returned; or <see cref="StringSplitOptions.None"/> to include empty array elements in the array returned.</param>
/// <param name="count">The maximum number of substrings to return.</param>
/// <param name="separator">A character array that delimits the substrings in this string, an empty array that contains no delimiters, or null. </param>
/// <returns></returns>
/// <remarks>
/// Delimiter characters are not included in the elements of the returned array.
/// If this instance does not contain any of the characters in separator the returned sequence consists of a single element that contains this instance.
/// If the separator parameter is null or contains no characters, white-space characters are assumed to be the delimiters. White-space characters are defined by the Unicode standard and return true if they are passed to the <see cref="Char.IsWhiteSpace"/> method.
/// </remarks>
public static IEnumerable<string> SplitLazy(this string value, int count = int.MaxValue, StringSplitOptions options = StringSplitOptions.None, params char[] separator)
{
if (count <= 0)
{
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be less than zero.");
yield break;
}
Func<char, bool> predicate = char.IsWhiteSpace;
if (separator != null && separator.Length != 0)
predicate = (c) => separator.Contains(c);
if (string.IsNullOrEmpty(value) || count == 1 || !value.Any(predicate))
{
yield return value;
yield break;
}
bool removeEmptyEntries = (options & StringSplitOptions.RemoveEmptyEntries) != 0;
int ct = 0;
var sb = new StringBuilder();
for (int i = 0; i < value.Length; ++i)
{
char c = value[i];
if (!predicate(c))
{
sb.Append(c);
}
else
{
if (sb.Length != 0)
{
yield return sb.ToString();
sb.Clear();
}
else
{
if (removeEmptyEntries)
continue;
yield return string.Empty;
}
if (++ct >= count - 1)
{
if (removeEmptyEntries)
while (++i < value.Length && predicate(value[i]));
else
++i;
if (i < value.Length - 1)
{
sb.Append(value, i, value.Length - i);
yield return sb.ToString();
}
yield break;
}
}
}
if (sb.Length > 0)
yield return sb.ToString();
else if (!removeEmptyEntries && predicate(value[value.Length - 1]))
yield return string.Empty;
}
public static IEnumerable<string> SplitLazy(this string value, params char[] separator)
{
return value.SplitLazy(int.MaxValue, StringSplitOptions.None, separator);
}
public static IEnumerable<string> SplitLazy(this string value, StringSplitOptions options, params char[] separator)
{
return value.SplitLazy(int.MaxValue, options, separator);
}
public static IEnumerable<string> SplitLazy(this string value, int count, params char[] separator)
{
return value.SplitLazy(count, StringSplitOptions.None, separator);
}
}
I wanted the functionality of Regex.Split, but in a lazily evaluated form. The code below just runs through all Matches in the input string, and produces the same results as Regex.Split:
public static IEnumerable<string> Split(string input, string pattern, RegexOptions options = RegexOptions.None)
{
// Always compile - we expect many executions
var regex = new Regex(pattern, options | RegexOptions.Compiled);
int currentSplitStart = 0;
var match = regex.Match(input);
while (match.Success)
{
yield return input.Substring(currentSplitStart, match.Index - currentSplitStart);
currentSplitStart = match.Index + match.Length;
match = match.NextMatch();
}
yield return input.Substring(currentSplitStart);
}
Note that using this with the pattern parameter #"\s" will give you the same results as string.Split().
Lazy split without create tempory string.
Chunk of string copied using system coll mscorlib String.SubString.
public static IEnumerable<string> LazySplit(this string source, StringSplitOptions stringSplitOptions, params string[] separators)
{
var sourceLen = source.Length;
bool IsSeparator(int index, string separator)
{
var separatorLen = separator.Length;
if (sourceLen < index + separatorLen)
{
return false;
}
for (var i = 0; i < separatorLen; i++)
{
if (source[index + i] != separator[i])
{
return false;
}
}
return true;
}
var indexOfStartChunk = 0;
for (var i = 0; i < source.Length; i++)
{
foreach (var separator in separators)
{
if (IsSeparator(i, separator))
{
if (indexOfStartChunk == i && stringSplitOptions != StringSplitOptions.RemoveEmptyEntries)
{
yield return string.Empty;
}
else
{
yield return source.Substring(indexOfStartChunk, i - indexOfStartChunk);
}
i += separator.Length;
indexOfStartChunk = i--;
break;
}
}
}
if (indexOfStartChunk != 0)
{
yield return source.Substring(indexOfStartChunk, sourceLen - indexOfStartChunk);
}
}
I have the following code: given a string with length m, add '-' to any possible position (including between different chars of original string) to extend it to length n. For example, given "ABC", extend it to 6. It will be "---ABC", "--A-BC", "--AB-C", "--ABC-", ......, "AB---C", "ABC---".
class Program
{
// <summary>
/// Grow a string to desired length with time limitation
/// </summary>
/// <param name="s"></param>
/// <param name="length"></param>
/// <param name="pad"></param>
/// <param name="Padded"></param>
public static bool ExtendToLen(string s, int length, char pad, ref List<string> Padded, ref Stopwatch timer, int timeOut)
{
if (s.Length == length)
{
Padded.Add(s);
return true;
}
else if (s.Length > length)
{
return true;
}
else
{
List<int> pos = GetExceptPos(s, pad.ToString());
pos.Sort();
int count = -1;
foreach (int p in pos)
{
if (timer.Elapsed.TotalSeconds > timeOut)
{
return false;
}
//Debug.WriteLine(string.Format("pos:{0}", p), "PadToLength");
count++;
// Pad left
string leftPadStr = s.Substring(0, p) + pad + s.Substring(p);
//Debug.WriteLine(string.Format("\tLeftPadStr:{0}", leftPadStr));
bool go = ExtendToLen(leftPadStr, length, pad, ref Padded, ref timer, timeOut);
if (go == false) { return false; }
// Pad right at the last pos
if (count == pos.Count - 1)
{
string rightPadStr = s + pad;
go = ExtendToLen(rightPadStr, length, pad, ref Padded, ref timer, timeOut);
if (go == false) { return false; }
}
}
return true;
}
}
/// <summary>
/// Find indexes of elements different from target str
/// </summary>
/// <param name="str"></param>
/// <param name="excludeStr"></param>
/// <returns></returns>
private static List<int> GetExceptPos(string str, string excludeStr)
{
List<int> allIndexes = new List<int>();
for (int i = 0; i < str.Length; i++)
{
allIndexes.Add(i);
}
return allIndexes.Except(str.IndexesOf(excludeStr)).ToList();
}
static void Main(string[] args)
{
string old = "ACGUA";
List<string> newList = new List<string>();
Stopwatch timer = new Stopwatch();
timer.Start();
bool isGood = ExtendToLen(old, 12, '-', ref newList, ref timer, 100);
timer.Stop();
foreach (string s in newList)
{
Console.WriteLine(s);
}
Console.WriteLine("Elapsed time: {0}", timer.Elapsed);
Console.ReadLine();
}
}
public static class ExtensionMethods
{
/// <summary>
/// Return all indeces
/// </summary>
/// <param name="haystack"></param>
/// <param name="needle"></param>
/// <returns></returns>
public static IEnumerable<int> IndexesOf(this string haystack, string needle)
{
int lastIndex = 0;
while (true)
{
int index = haystack.IndexOf(needle, lastIndex);
if (index == -1)
{
yield break;
}
yield return index;
lastIndex = index + needle.Length;
}
}
}
It runs slowly, for example, if I want to extend a string (len = 5) to 20, it runs long long long time. And the result seems to be redundant.
So the question is how to speed it up and remove those redundancy.
Any suggestion will be appreciated.
This is a very rough hack and should get you started.
It basically moves the string backwards through your padding one character at a time.
static unsafe void performPaddedBubbleSort(string s, char c, int padCount, IList<string> list) {
s = new string(c, padCount) + s;
bool complete = false;
int index = 0;
int count = 0;
fixed (char* p = s) {
while (count < s.Length && *p == c) {
while (index < s.Length) {
if (*(p + index) != c) {
// flip them
char tempChar = *(p + index);
if (index != 0)
*((p + index) - 1) = tempChar;
*(p + index) = c;
list.Add(new string(p));
}
index++;
}
index = 0;
count++;
}
}
}
Input of "ABC" with padding of 3 (to make it 6 chars wide) is:
--A-BC
--AB-C
--ABC-
-A-BC-
-AB-C-
-ABC--
A-BC--
AB-C--
ABC---
Elapsed time: 00:00:00.0008663
Not sure if that's exactly what you were after.. but its a start I guess. As you can see it executes in a fraction of a second.
I have some decimal data that I am pushing into a SharePoint list where it is to be viewed. I'd like to restrict the number of significant figures displayed in the result data based on my knowledge of the specific calculation. Sometimes it'll be 3, so 12345 will become 12300 and 0.012345 will become 0.0123. Occasionally it will be 4 or 5. Is there any convenient way to handle this?
See: RoundToSignificantFigures by "P Daddy".
I've combined his method with another one I liked.
Rounding to significant figures is a lot easier in TSQL where the rounding method is based on rounding position, not number of decimal places - which is the case with .Net math.round. You could round a number in TSQL to negative places, which would round at whole numbers - so the scaling isn't needed.
Also see this other thread. Pyrolistical's method is good.
The trailing zeros part of the problem seems like more of a string operation to me, so I included a ToString() extension method which will pad zeros if necessary.
using System;
using System.Globalization;
public static class Precision
{
// 2^-24
public const float FLOAT_EPSILON = 0.0000000596046448f;
// 2^-53
public const double DOUBLE_EPSILON = 0.00000000000000011102230246251565d;
public static bool AlmostEquals(this double a, double b, double epsilon = DOUBLE_EPSILON)
{
// ReSharper disable CompareOfFloatsByEqualityOperator
if (a == b)
{
return true;
}
// ReSharper restore CompareOfFloatsByEqualityOperator
return (System.Math.Abs(a - b) < epsilon);
}
public static bool AlmostEquals(this float a, float b, float epsilon = FLOAT_EPSILON)
{
// ReSharper disable CompareOfFloatsByEqualityOperator
if (a == b)
{
return true;
}
// ReSharper restore CompareOfFloatsByEqualityOperator
return (System.Math.Abs(a - b) < epsilon);
}
}
public static class SignificantDigits
{
public static double Round(this double value, int significantDigits)
{
int unneededRoundingPosition;
return RoundSignificantDigits(value, significantDigits, out unneededRoundingPosition);
}
public static string ToString(this double value, int significantDigits)
{
// this method will round and then append zeros if needed.
// i.e. if you round .002 to two significant figures, the resulting number should be .0020.
var currentInfo = CultureInfo.CurrentCulture.NumberFormat;
if (double.IsNaN(value))
{
return currentInfo.NaNSymbol;
}
if (double.IsPositiveInfinity(value))
{
return currentInfo.PositiveInfinitySymbol;
}
if (double.IsNegativeInfinity(value))
{
return currentInfo.NegativeInfinitySymbol;
}
int roundingPosition;
var roundedValue = RoundSignificantDigits(value, significantDigits, out roundingPosition);
// when rounding causes a cascading round affecting digits of greater significance,
// need to re-round to get a correct rounding position afterwards
// this fixes a bug where rounding 9.96 to 2 figures yeilds 10.0 instead of 10
RoundSignificantDigits(roundedValue, significantDigits, out roundingPosition);
if (Math.Abs(roundingPosition) > 9)
{
// use exponential notation format
// ReSharper disable FormatStringProblem
return string.Format(currentInfo, "{0:E" + (significantDigits - 1) + "}", roundedValue);
// ReSharper restore FormatStringProblem
}
// string.format is only needed with decimal numbers (whole numbers won't need to be padded with zeros to the right.)
// ReSharper disable FormatStringProblem
return roundingPosition > 0 ? string.Format(currentInfo, "{0:F" + roundingPosition + "}", roundedValue) : roundedValue.ToString(currentInfo);
// ReSharper restore FormatStringProblem
}
private static double RoundSignificantDigits(double value, int significantDigits, out int roundingPosition)
{
// this method will return a rounded double value at a number of signifigant figures.
// the sigFigures parameter must be between 0 and 15, exclusive.
roundingPosition = 0;
if (value.AlmostEquals(0d))
{
roundingPosition = significantDigits - 1;
return 0d;
}
if (double.IsNaN(value))
{
return double.NaN;
}
if (double.IsPositiveInfinity(value))
{
return double.PositiveInfinity;
}
if (double.IsNegativeInfinity(value))
{
return double.NegativeInfinity;
}
if (significantDigits < 1 || significantDigits > 15)
{
throw new ArgumentOutOfRangeException("significantDigits", value, "The significantDigits argument must be between 1 and 15.");
}
// The resulting rounding position will be negative for rounding at whole numbers, and positive for decimal places.
roundingPosition = significantDigits - 1 - (int)(Math.Floor(Math.Log10(Math.Abs(value))));
// try to use a rounding position directly, if no scale is needed.
// this is because the scale mutliplication after the rounding can introduce error, although
// this only happens when you're dealing with really tiny numbers, i.e 9.9e-14.
if (roundingPosition > 0 && roundingPosition < 16)
{
return Math.Round(value, roundingPosition, MidpointRounding.AwayFromZero);
}
// Shouldn't get here unless we need to scale it.
// Set the scaling value, for rounding whole numbers or decimals past 15 places
var scale = Math.Pow(10, Math.Ceiling(Math.Log10(Math.Abs(value))));
return Math.Round(value / scale, significantDigits, MidpointRounding.AwayFromZero) * scale;
}
}
This might do the trick:
double Input1 = 1234567;
string Result1 = Convert.ToDouble(String.Format("{0:G3}",Input1)).ToString("R0");
double Input2 = 0.012345;
string Result2 = Convert.ToDouble(String.Format("{0:G3}", Input2)).ToString("R6");
Changing the G3 to G4 produces the oddest result though.
It appears to round up the significant digits?
I ended up snagging some code from http://ostermiller.org/utils/SignificantFigures.java.html. It was in java, so I did a quick search/replace and some resharper reformatting to make the C# build. It seems to work nicely for my significant figure needs. FWIW, I removed his javadoc comments to make it more concise here, but the original code is documented quite nicely.
/*
* Copyright (C) 2002-2007 Stephen Ostermiller
* http://ostermiller.org/contact.pl?regarding=Java+Utilities
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* See COPYING.TXT for details.
*/
public class SignificantFigures
{
private String original;
private StringBuilder _digits;
private int mantissa = -1;
private bool sign = true;
private bool isZero = false;
private bool useScientificNotation = true;
public SignificantFigures(String number)
{
original = number;
Parse(original);
}
public SignificantFigures(double number)
{
original = Convert.ToString(number);
try
{
Parse(original);
}
catch (Exception nfe)
{
_digits = null;
}
}
public bool UseScientificNotation
{
get { return useScientificNotation; }
set { useScientificNotation = value; }
}
public int GetNumberSignificantFigures()
{
if (_digits == null) return 0;
return _digits.Length;
}
public SignificantFigures SetLSD(int place)
{
SetLMSD(place, Int32.MinValue);
return this;
}
public SignificantFigures SetLMSD(int leastPlace, int mostPlace)
{
if (_digits != null && leastPlace != Int32.MinValue)
{
int significantFigures = _digits.Length;
int current = mantissa - significantFigures + 1;
int newLength = significantFigures - leastPlace + current;
if (newLength <= 0)
{
if (mostPlace == Int32.MinValue)
{
original = "NaN";
_digits = null;
}
else
{
newLength = mostPlace - leastPlace + 1;
_digits.Length = newLength;
mantissa = leastPlace;
for (int i = 0; i < newLength; i++)
{
_digits[i] = '0';
}
isZero = true;
sign = true;
}
}
else
{
_digits.Length = newLength;
for (int i = significantFigures; i < newLength; i++)
{
_digits[i] = '0';
}
}
}
return this;
}
public int GetLSD()
{
if (_digits == null) return Int32.MinValue;
return mantissa - _digits.Length + 1;
}
public int GetMSD()
{
if (_digits == null) return Int32.MinValue;
return mantissa + 1;
}
public override String ToString()
{
if (_digits == null) return original;
StringBuilder digits = new StringBuilder(this._digits.ToString());
int length = digits.Length;
if ((mantissa <= -4 || mantissa >= 7 ||
(mantissa >= length &&
digits[digits.Length - 1] == '0') ||
(isZero && mantissa != 0)) && useScientificNotation)
{
// use scientific notation.
if (length > 1)
{
digits.Insert(1, '.');
}
if (mantissa != 0)
{
digits.Append("E" + mantissa);
}
}
else if (mantissa <= -1)
{
digits.Insert(0, "0.");
for (int i = mantissa; i < -1; i++)
{
digits.Insert(2, '0');
}
}
else if (mantissa + 1 == length)
{
if (length > 1 && digits[digits.Length - 1] == '0')
{
digits.Append('.');
}
}
else if (mantissa < length)
{
digits.Insert(mantissa + 1, '.');
}
else
{
for (int i = length; i <= mantissa; i++)
{
digits.Append('0');
}
}
if (!sign)
{
digits.Insert(0, '-');
}
return digits.ToString();
}
public String ToScientificNotation()
{
if (_digits == null) return original;
StringBuilder digits = new StringBuilder(this._digits.ToString());
int length = digits.Length;
if (length > 1)
{
digits.Insert(1, '.');
}
if (mantissa != 0)
{
digits.Append("E" + mantissa);
}
if (!sign)
{
digits.Insert(0, '-');
}
return digits.ToString();
}
private const int INITIAL = 0;
private const int LEADZEROS = 1;
private const int MIDZEROS = 2;
private const int DIGITS = 3;
private const int LEADZEROSDOT = 4;
private const int DIGITSDOT = 5;
private const int MANTISSA = 6;
private const int MANTISSADIGIT = 7;
private void Parse(String number)
{
int length = number.Length;
_digits = new StringBuilder(length);
int state = INITIAL;
int mantissaStart = -1;
bool foundMantissaDigit = false;
// sometimes we don't know if a zero will be
// significant or not when it is encountered.
// keep track of the number of them so that
// the all can be made significant if we find
// out that they are.
int zeroCount = 0;
int leadZeroCount = 0;
for (int i = 0; i < length; i++)
{
char c = number[i];
switch (c)
{
case '.':
{
switch (state)
{
case INITIAL:
case LEADZEROS:
{
state = LEADZEROSDOT;
}
break;
case MIDZEROS:
{
// we now know that these zeros
// are more than just trailing place holders.
for (int j = 0; j < zeroCount; j++)
{
_digits.Append('0');
}
zeroCount = 0;
state = DIGITSDOT;
}
break;
case DIGITS:
{
state = DIGITSDOT;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
case '+':
{
switch (state)
{
case INITIAL:
{
sign = true;
state = LEADZEROS;
}
break;
case MANTISSA:
{
state = MANTISSADIGIT;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
case '-':
{
switch (state)
{
case INITIAL:
{
sign = false;
state = LEADZEROS;
}
break;
case MANTISSA:
{
state = MANTISSADIGIT;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
case '0':
{
switch (state)
{
case INITIAL:
case LEADZEROS:
{
// only significant if number
// is all zeros.
zeroCount++;
leadZeroCount++;
state = LEADZEROS;
}
break;
case MIDZEROS:
case DIGITS:
{
// only significant if followed
// by a decimal point or nonzero digit.
mantissa++;
zeroCount++;
state = MIDZEROS;
}
break;
case LEADZEROSDOT:
{
// only significant if number
// is all zeros.
mantissa--;
zeroCount++;
state = LEADZEROSDOT;
}
break;
case DIGITSDOT:
{
// non-leading zeros after
// a decimal point are always
// significant.
_digits.Append(c);
}
break;
case MANTISSA:
case MANTISSADIGIT:
{
foundMantissaDigit = true;
state = MANTISSADIGIT;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
{
switch (state)
{
case INITIAL:
case LEADZEROS:
case DIGITS:
{
zeroCount = 0;
_digits.Append(c);
mantissa++;
state = DIGITS;
}
break;
case MIDZEROS:
{
// we now know that these zeros
// are more than just trailing place holders.
for (int j = 0; j < zeroCount; j++)
{
_digits.Append('0');
}
zeroCount = 0;
_digits.Append(c);
mantissa++;
state = DIGITS;
}
break;
case LEADZEROSDOT:
case DIGITSDOT:
{
zeroCount = 0;
_digits.Append(c);
state = DIGITSDOT;
}
break;
case MANTISSA:
case MANTISSADIGIT:
{
state = MANTISSADIGIT;
foundMantissaDigit = true;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
case 'E':
case 'e':
{
switch (state)
{
case INITIAL:
case LEADZEROS:
case DIGITS:
case LEADZEROSDOT:
case DIGITSDOT:
{
// record the starting point of the mantissa
// so we can do a substring to get it back later
mantissaStart = i + 1;
state = MANTISSA;
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
break;
default:
{
throw new Exception(
"Unexpected character '" + c + "' at position " + i
);
}
}
}
if (mantissaStart != -1)
{
// if we had found an 'E'
if (!foundMantissaDigit)
{
// we didn't actually find a mantissa to go with.
throw new Exception(
"No digits in mantissa."
);
}
// parse the mantissa.
mantissa += Convert.ToInt32(number.Substring(mantissaStart));
}
if (_digits.Length == 0)
{
if (zeroCount > 0)
{
// if nothing but zeros all zeros are significant.
for (int j = 0; j < zeroCount; j++)
{
_digits.Append('0');
}
mantissa += leadZeroCount;
isZero = true;
sign = true;
}
else
{
// a hack to catch some cases that we could catch
// by adding a ton of extra states. Things like:
// "e2" "+e2" "+." "." "+" etc.
throw new Exception(
"No digits in number."
);
}
}
}
public SignificantFigures SetNumberSignificantFigures(int significantFigures)
{
if (significantFigures <= 0)
throw new ArgumentException("Desired number of significant figures must be positive.");
if (_digits != null)
{
int length = _digits.Length;
if (length < significantFigures)
{
// number is not long enough, pad it with zeros.
for (int i = length; i < significantFigures; i++)
{
_digits.Append('0');
}
}
else if (length > significantFigures)
{
// number is too long chop some of it off with rounding.
bool addOne; // we need to round up if true.
char firstInSig = _digits[significantFigures];
if (firstInSig < '5')
{
// first non-significant digit less than five, round down.
addOne = false;
}
else if (firstInSig == '5')
{
// first non-significant digit equal to five
addOne = false;
for (int i = significantFigures + 1; !addOne && i < length; i++)
{
// if its followed by any non-zero digits, round up.
if (_digits[i] != '0')
{
addOne = true;
}
}
if (!addOne)
{
// if it was not followed by non-zero digits
// if the last significant digit is odd round up
// if the last significant digit is even round down
addOne = (_digits[significantFigures - 1] & 1) == 1;
}
}
else
{
// first non-significant digit greater than five, round up.
addOne = true;
}
// loop to add one (and carry a one if added to a nine)
// to the last significant digit
for (int i = significantFigures - 1; addOne && i >= 0; i--)
{
char digit = _digits[i];
if (digit < '9')
{
_digits[i] = (char) (digit + 1);
addOne = false;
}
else
{
_digits[i] = '0';
}
}
if (addOne)
{
// if the number was all nines
_digits.Insert(0, '1');
mantissa++;
}
// chop it to the correct number of figures.
_digits.Length = significantFigures;
}
}
return this;
}
public double ToDouble()
{
return Convert.ToDouble(original);
}
public static String Format(double number, int significantFigures)
{
SignificantFigures sf = new SignificantFigures(number);
sf.SetNumberSignificantFigures(significantFigures);
return sf.ToString();
}
}
I have a shorted answer to calculating significant figures of a number. Here is the code & the test results...
using System;
using System.Collections.Generic;
namespace ConsoleApplicationRound
{
class Program
{
static void Main(string[] args)
{
//char cDecimal = '.'; // for English cultures
char cDecimal = ','; // for German cultures
List<double> l_dValue = new List<double>();
ushort usSignificants = 5;
l_dValue.Add(0);
l_dValue.Add(0.000640589);
l_dValue.Add(-0.000640589);
l_dValue.Add(-123.405009);
l_dValue.Add(123.405009);
l_dValue.Add(-540);
l_dValue.Add(540);
l_dValue.Add(-540911);
l_dValue.Add(540911);
l_dValue.Add(-118.2);
l_dValue.Add(118.2);
l_dValue.Add(-118.18);
l_dValue.Add(118.18);
l_dValue.Add(-118.188);
l_dValue.Add(118.188);
foreach (double d in l_dValue)
{
Console.WriteLine("d = Maths.Round('" +
cDecimal + "', " + d + ", " + usSignificants +
") = " + Maths.Round(
cDecimal, d, usSignificants));
}
Console.Read();
}
}
}
The Maths class used is as follows:
using System;
using System.Text;
namespace ConsoleApplicationRound
{
class Maths
{
/// <summary>
/// The word "Window"
/// </summary>
private static String m_strZeros = "000000000000000000000000000000000";
/// <summary>
/// The minus sign
/// </summary>
public const char m_cDASH = '-';
/// <summary>
/// Determines the number of digits before the decimal point
/// </summary>
/// <param name="cDecimal">
/// Language-specific decimal separator
/// </param>
/// <param name="strValue">
/// Value to be scrutinised
/// </param>
/// <returns>
/// Nr. of digits before the decimal point
/// </returns>
private static ushort NrOfDigitsBeforeDecimal(char cDecimal, String strValue)
{
short sDecimalPosition = (short)strValue.IndexOf(cDecimal);
ushort usSignificantDigits = 0;
if (sDecimalPosition >= 0)
{
strValue = strValue.Substring(0, sDecimalPosition + 1);
}
for (ushort us = 0; us < strValue.Length; us++)
{
if (strValue[us] != m_cDASH) usSignificantDigits++;
if (strValue[us] == cDecimal)
{
usSignificantDigits--;
break;
}
}
return usSignificantDigits;
}
/// <summary>
/// Rounds to a fixed number of significant digits
/// </summary>
/// <param name="d">
/// Number to be rounded
/// </param>
/// <param name="usSignificants">
/// Requested significant digits
/// </param>
/// <returns>
/// The rounded number
/// </returns>
public static String Round(char cDecimal,
double d,
ushort usSignificants)
{
StringBuilder value = new StringBuilder(Convert.ToString(d));
short sDecimalPosition = (short)value.ToString().IndexOf(cDecimal);
ushort usAfterDecimal = 0;
ushort usDigitsBeforeDecimalPoint =
NrOfDigitsBeforeDecimal(cDecimal, value.ToString());
if (usDigitsBeforeDecimalPoint == 1)
{
usAfterDecimal = (d == 0)
? usSignificants
: (ushort)(value.Length - sDecimalPosition - 2);
}
else
{
if (usSignificants >= usDigitsBeforeDecimalPoint)
{
usAfterDecimal =
(ushort)(usSignificants - usDigitsBeforeDecimalPoint);
}
else
{
double dPower = Math.Pow(10,
usDigitsBeforeDecimalPoint - usSignificants);
d = dPower*(long)(d/dPower);
}
}
double dRounded = Math.Round(d, usAfterDecimal);
StringBuilder result = new StringBuilder();
result.Append(dRounded);
ushort usDigits = (ushort)result.ToString().Replace(
Convert.ToString(cDecimal), "").Replace(
Convert.ToString(m_cDASH), "").Length;
// Add lagging zeros, if necessary:
if (usDigits < usSignificants)
{
if (usAfterDecimal != 0)
{
if (result.ToString().IndexOf(cDecimal) == -1)
{
result.Append(cDecimal);
}
int i = (d == 0) ? 0 : Math.Min(0, usDigits - usSignificants);
result.Append(m_strZeros.Substring(0, usAfterDecimal + i));
}
}
return result.ToString();
}
}
}
Any answer with a shorter code?
You can get an elegant bit perfect rounding by using the GetBits method on Decimal and leveraging BigInteger to perform masking.
Some utils
public static int CountDigits
(BigInteger number) => ((int)BigInteger.Log10(number))+1;
private static readonly BigInteger[] BigPowers10
= Enumerable.Range(0, 100)
.Select(v => BigInteger.Pow(10, v))
.ToArray();
The main function
public static decimal RoundToSignificantDigits
(this decimal num,
short n)
{
var bits = decimal.GetBits(num);
var u0 = unchecked((uint)bits[0]);
var u1 = unchecked((uint)bits[1]);
var u2 = unchecked((uint)bits[2]);
var i = new BigInteger(u0)
+ (new BigInteger(u1) << 32)
+ (new BigInteger(u2) << 64);
var d = CountDigits(i);
var delta = d - n;
if (delta < 0)
return num;
var scale = BigPowers10[delta];
var div = i/scale;
var rem = i%scale;
var up = rem > scale/2;
if (up)
div += 1;
var shifted = div*scale;
bits[0] =unchecked((int)(uint) (shifted & BigUnitMask));
bits[1] =unchecked((int)(uint) (shifted>>32 & BigUnitMask));
bits[2] =unchecked((int)(uint) (shifted>>64 & BigUnitMask));
return new decimal(bits);
}
test case 0
public void RoundToSignificantDigits()
{
WMath.RoundToSignificantDigits(0.0012345m, 2).Should().Be(0.0012m);
WMath.RoundToSignificantDigits(0.0012645m, 2).Should().Be(0.0013m);
WMath.RoundToSignificantDigits(0.040000000000000008, 6).Should().Be(0.04);
WMath.RoundToSignificantDigits(0.040000010000000008, 6).Should().Be(0.04);
WMath.RoundToSignificantDigits(0.040000100000000008, 6).Should().Be(0.0400001);
WMath.RoundToSignificantDigits(0.040000110000000008, 6).Should().Be(0.0400001);
WMath.RoundToSignificantDigits(0.20000000000000004, 6).Should().Be(0.2);
WMath.RoundToSignificantDigits(0.10000000000000002, 6).Should().Be(0.1);
WMath.RoundToSignificantDigits(0.0, 6).Should().Be(0.0);
}
test case 1
public void RoundToSigFigShouldWork()
{
1.2m.RoundToSignificantDigits(1).Should().Be(1m);
0.01235668m.RoundToSignificantDigits(3).Should().Be(0.0124m);
0.01m.RoundToSignificantDigits(3).Should().Be(0.01m);
1.23456789123456789123456789m.RoundToSignificantDigits(4)
.Should().Be(1.235m);
1.23456789123456789123456789m.RoundToSignificantDigits(16)
.Should().Be(1.234567891234568m);
1.23456789123456789123456789m.RoundToSignificantDigits(24)
.Should().Be(1.23456789123456789123457m);
1.23456789123456789123456789m.RoundToSignificantDigits(27)
.Should().Be(1.23456789123456789123456789m);
}
I found this article doing a quick search on it. Basically this one converts to a string and goes by the characters in that array one at a time, till it reached the max. significance. Will this work?
The following code doesn't quite meet the spec, since it doesn't try to round anything to the left of the decimal point. But it's simpler than anything else presented here (so far). I was quite surprised that C# doesn't have a built-in method to handle this.
static public string SignificantDigits(double d, int digits=10)
{
int magnitude = (d == 0.0) ? 0 : (int)Math.Floor(Math.Log10(Math.Abs(d))) + 1;
digits -= magnitude;
if (digits < 0)
digits = 0;
string fmt = "f" + digits.ToString();
return d.ToString(fmt);
}
This method is dead simple and works with any number, positive or negative, and only uses a single transcendental function (Log10). The only difference (which may/may-not matter) is that it will not round the integer component. This is perfect however for currency processing where you know the limits are within certain bounds, because you can use doubles for much faster processing than the dreadfully slow Decimal type.
public static double ToDecimal( this double x, int significantFigures = 15 ) {
// determine # of digits before & after the decimal
int digitsBeforeDecimal = (int)x.Abs().Log10().Ceil().Max( 0 ),
digitsAfterDecimal = (significantFigures - digitsBeforeDecimal).Max( 0 );
// round it off
return x.Round( digitsAfterDecimal );
}
As I remember it "significant figures" means the number of digits after the dot separator so 3 significant digits for 0.012345 would be 0.012 and not 0.0123, but that really doesnt matter for the solution.
I also understand that you want to "nullify" the last digits to a certain degree if the number is > 1. You write that 12345 would become 12300 but im not sure whether you want 123456 to become 1230000 or 123400 ? My solution does the last. Instead of calculating the factor you could ofcourse make a small initialized array if you only have a couple of variations.
private static string FormatToSignificantFigures(decimal number, int amount)
{
if (number > 1)
{
int factor = Factor(amount);
return ((int)(number/factor)*factor).ToString();
}
NumberFormatInfo nfi = new CultureInfo("en-US", false).NumberFormat;
nfi.NumberDecimalDigits = amount;
return(number.ToString("F", nfi));
}
private static int Factor(int x)
{
return DoCalcFactor(10, x-1);
}
private static int DoCalcFactor(int x, int y)
{
if (y == 1) return x;
return 10*DoCalcFactor(x, y - 1);
}
Kind regards
Carsten