Numbers form an integral part of many applications. Although seemingly a simple topic, when you consider how the world’s many cultures represent numbers, and even how computers can represent numbers, the topic is not so simple after all. This chapter covers a lot of tips that are useful in many applications.
Float
, Double
, and Decimal
Solution: Deciding which floating-point type to use is application dependent, but here are some things to think about:
• How big do the numbers need to be? Are they on the extreme, either positive or negative?
• How much precision do you need? Is seven digits enough? Sixteen? Need more?
• Do you have memory requirements that influence which you choose?
• Are the numbers coming from a database that already specifies the size it’s using? This should match what you choose so you don’t lose data.
Given these questions, Table 5.1 demonstrates the differences.
Largest exact integer means the largest integer value that can be represented without loss of precision. These values should be kept in mind when you’re converting between integer and floating-point types.
For any calculation involving money or finances, the Decimal
type should always be used. Only this type has the appropriate precision to avoid critical rounding errors.
BigInteger
)Solution: .NET 4 ships with the BigInteger
class, located in the System.Numerics
namespace (you will need to add a reference to this assembly). With it, you can do arbitrarily large integer math, such as the following:
BigInteger a = UInt64.MaxValue;
BigInteger b = UInt64.MaxValue;
//this is a really, really big number
BigInteger c = a * b;
BigInteger
is immutable, which means that calling
BigInteger a = 1;
a++;
will result in the creation of a second BigInteger
object, assigning it back to a
. Although this is transparent to you in practice, you should be aware that it has potential performance implications.
BigInteger
has many static methods, such as the typical Parse()
, TryParse()
, and ToString()
methods (which support hex format as well) found in other integer types, as well as mathematical helpers such as Pow()
, Log()
, Log10()
, and others.
BigInteger
’s ToString()
method accepts the usual format specifiers ("C"
, "N0"
, and so on), plus one more: "R"
. With all except "R"
, only 50 digits of integer precision are output, with the rest replaced by zeroes. With "R"
, all digits are preserved, but you can’t apply other formatting (such as digit separators). The difference is shown here:
Output:
Using the output of the "R"
format, you could of course perform your own manual formatting.
BigInteger
also includes some instance helper properties, such as IsPowerOfTwo
and IsEven
, to simplify some tasks.
Solution: Use the System.Numerics.Complex
class to represent imaginary numbers. Complex
wraps both the real and imaginary parts using doubles as the underlying format.
This has the output
While (a, b) is a valid format for imaginary numbers, it is common to see it written a+bi instead. This is easily accomplished with a custom formatter (as you learned in Chapter 2, “Creating Versatile Types”).
This gives the output:
Sqrt(c) = 0.00 + 1.00i
Solution: The .NET Framework provides a wealth of number formatting options. There are so many options that it can be confusing at first. This section highlights a few of the more interesting options, including a table summarizing many examples.
If you call ToString()
with no arguments, the current thread’s culture will be assumed. It is often in your best interest to explicitly specify the culture.
Culture defines how the formatted number looks. Depending on the culture, some or all of these variables could change:
• Decimal separator—1.5 or 1,5?
• Digit separator—1,000,000 or 1.000.000?
• Digit grouping—In some cultures (such as India), digits are not grouped by threes.
• Currency symbol—U.S. Dollar ($), British Pound (£), Euro (€), Japanese Yen (¥).
• Default number of decimal places for certain formats
CultureInfo
implements IFormatProvider
, so any method that takes it can accept a culture. Here are some examples:
This code produces the following output:
1 234 567,89
12,34,567.89
Use the invariant culture whenever you need to store data for the application to consume. This is vitally important because it is impossible to accurately parse numbers when the format is not known beforehand. Alternatively, use binary formats because culture information is not relevant.
Note that the “0x” that is typically prepended to hexadecimal numbers must be added by you. You can also specify how many digits you want shown (they will be zero-padded).
The output is as follows:
3039
0x00003039
To group digits, use the "N"
format option, as shown here:
This produces the following output:
12,345
You can print leading zeros with the D
and X
format strings. The number specifies the total number of digits to output. If this number is greater than the number of digits in the number, the output is padded with zeros; otherwise, the number is output normally. Here’s an example:
And here’s the output:
00012345
You can specify the number of decimal places with the C
, E
, F
, G
, N
, and P
format strings:
The output is as follows (note the rounding):
12345.679
Solution: Use custom format specifiers.
00012345.68
00,012,345.68
0,00,12,345.68
(00012345.679)
nothing!
Table 5.2 provides a summary of number format strings.
Solution: You can choose between Parse
, which throws an exception if anything goes wrong, or TryParse
, which is guaranteed not to throw an exception. I think you’ll find that TryParse
makes sense in most situations because of the performance implications when using exceptions.
Note that goodStr
has digit separators, decimal points, spaces, and a negative sign. By default, TryParse
will accept all of these. If you know the format of the number, you can restrict the format using the NumberStyles
enumeration.
If you don’t specify a culture when you parse, the current thread’s culture is assumed. To specify another culture, do this:
To parse hexadecimal number strings, you must remove any “0x” that precedes the number before calling TryParse()
.
The following output is produced:
Parsed 0x3039 to value 12345
Solution: You can’t convert numbers, per se, to different bases. They are, after all, always represented as binary in the computer. You are merely showing a representation of that number—in other words, a string. How to do it depends on whether you want the typical “computer” bases or an arbitrary one.
Thankfully, if you want a standard base such as 2, 8, or 16, the functionality is already built into the Framework:
destStr
will contain the value 64, the hexadecimal equivalent of 100 in base-10.
What if you need a really strange number base, such as 5 or 99? For that, the following code will help:
Note that although this can accept any destination base, you will need to define any characters for digit values up to destBase - 1
.
The following code converts a string representation of a number in an arbitrary base (up to 16 in this example) to base-10:
If you need to convert between two arbitrary bases, it is easier to first convert to base-10, then convert to the other base using a combination of the methods given.
Solution: The handy BitConverter
class will help you.
Int32 num = 13;
byte[] bytes = BitConverter.GetBytes(13);
Int32 num = BitConverter.ToInt32(bytes);
BitConverter
can work on all number types except, oddly enough, Decimal
. For that, you need this code:
Whenever you are converting numbers to bytes (and vice versa), you need to be concerned about endianness, which is merely the order of the bytes in memory. Numbers are said to be little endian if they start with the least-significant byte first. Big endian values start with the most-significant byte. Most personal computers these days are little endian, but the BitConverter
class provides a handy static property called IsLittleEndian
that can tell you about the current hardware.
Solution: An integer is even if the least-significant bit is 0.
Solution: The solution relies on the fact that integers are twos-complement encoded and that a power of 2 has only a single 1 bit.
The check for 0 could be omitted if the function needed to be called a lot and you could ensure that 0 will never be passed to it.
Solution: A prime number is divisible only by 1 and itself. A popular solution is this example:
See Chapter 23, “Threads and Asynchronous Programming,” for an example of splitting up the work IsPrime()
is doing across multiple processors.
Solution: There are many solutions to this problem, but here’s my favorite:
This runs in time proportion to the number of 1 bits set in number
.
Solution: This code is common in many types of graphical applications, where degrees make sense to humans, but many graphics APIs handle radians. If your application uses these a lot, you might consider making them extension methods on double
.
See the RadiansAndDegrees sample project to see this code in action in conjunction with the mouse.
Pi, as well as other useful constants and mathematical functions, is in the Math
class.
Solution: The Math
class provides a handy function that performs rounding for you. You can specify the decimal precision as well as the type of midpoint rounding (the behavior when on a middle value such as .5, .05, and so on). If no rounding type is specified, the default is MidpointRounding.ToEven
, also known as banker’s rounding.
Here are some examples (the sample output is from the RoundNumbers
example in the chapter’s source code).
Table 5.4 summarizes the results from calling with Math.Round() with various arguments.
Scenario/Problem: In many graphical editors, a common feature is to restrict mouse positions to gridlines. This makes the user’s job easier because they don’t have to make ultra-precise mouse movements when editing shapes. For example, if the mouse were at (104, 96) and you wanted the values snapped to a 5-by-5 pixel grid, you could change them to (105, 95).
Solution: This is easily accomplished with the following code:
Table 5.5 demonstrates this algorithm when rounding/snapping values to the nearest multiple of ten.
See the SnapToGrid project in the code for this chapter for an example of snapping actual mouse input to a grid.
If you want to round to a floating-point value (say, the nearest 0.5), you need to massage the inputs and outputs of the preceding function slightly, as shown here:
The precision argument specifies how many decimal points your input values can contain. This is important because the multiple needs to be scaled to an integer so that the math can work correctly. Table 5.6 uses a precision of two.
Solution: The standard way to generate random numbers is with the System.Random
class, which produces numbers that appear random, based on a seed value (often the current time). That usage is shown here:
Random rand = new Random();
Int32 randomNumber = rand.Next();
If you just need an arbitrary number for noncryptographic purposes, this is perfectly fine. However, if you are doing anything security related, you should never use Random
. Instead, use System.Security.Cryptography.RNGCryptoServiceProvider
. Here’s an example:
Both methods of generating random numbers are actually pseudorandom (that is, not truly random). Pseudorandom numbers are based on some seed, or known data. System.Random
uses a seed value (often the current time), and RNGCryptoServiceProvider
uses a combination of processor info, operating system counters, cycle counters, time, and other information to generate cryptographically secure bytes. True randomness can generally be achieved only through more complicated phenomena (static noise, mouse movement, network traffic patterns, and so on).
You may not always want or need cryptographically secure random numbers. For instance, if you just need an arbitrary number and there are no security considerations, using System.Random
might be preferable for easier testing because you can reproduce the desired numbers at will by giving a known seed value.
Solution: Use the System.Guid
class to generate 128 bytes of data that has a very high probability of being unique across all computers and all networks, for all time.
Guid g = Guid.NewGuid();
Console.WriteLine("GUID: {0}", g);
produces the output:
GUID: ea8b716c-892a-4bed-b918-0d454c1b8474
GUIDs are used all over in databases and operating systems to uniquely identify records and components.
GUIDs are generated from a combination of hardware information and the current time, but the generation is one way; that is, you cannot infer any information about the hardware from a given GUID.
The Guid
class provides Parse()
and TryParse()
methods to convert strings to GUID objects. There are a few common string representations of GUIDs, so there are also ParseExact()
and TryParseExact()
methods.