In C#, the result of Math.Round(2.5) is 2.
It is supposed to be 3, isn't it? Why is it 2 instead in C#?
Firstly, this wouldn't be a C# bug anyway - it would be a .NET bug. C# is the language - it doesn't decide how Math.Round is implemented.
And secondly, no - if you read the docs, you'll see that the default rounding is "round to even" (banker's rounding):
Return ValueType: System.DoubleThe integer nearest a. If the
fractional component of a is halfway
between two integers, one of which is
even and the other odd, then the even
number is returned. Note that this
method returns a Double instead of an
integral type.
RemarksThe behavior of this method follows IEEE Standard 754,
section 4. This kind of rounding is
sometimes called rounding to nearest,
or banker's rounding. It minimizes
rounding errors that result from
consistently rounding a midpoint value
in a single direction.
You can specify how Math.Round should round mid-points using an overload which takes a MidpointRounding value. There's one overload with a MidpointRounding corresponding to each of the overloads which doesn't have one:
Round(Decimal) / Round(Decimal, MidpointRounding)
Round(Double) / Round(Double, MidpointRounding)
Round(Decimal, Int32) / Round(Decimal, Int32, MidpointRounding)
Round(Double, Int32) / Round(Double, Int32, MidpointRounding)
Whether this default was well chosen or not is a different matter. (MidpointRounding was only introduced in .NET 2.0. Before then I'm not sure there was any easy way of implementing the desired behaviour without doing it yourself.) In particular, history has shown that it's not the expected behaviour - and in most cases that's a cardinal sin in API design. I can see why Banker's Rounding is useful... but it's still a surprise to many.
You may be interested to take a look at the nearest Java equivalent enum (RoundingMode) which offers even more options. (It doesn't just deal with midpoints.)
That's called rounding to even (or banker's rounding), which is a valid rounding strategy for minimizing accrued errors in sums (MidpointRounding.ToEven). The theory is that, if you always round a 0.5 number in the same direction, the errors will accrue faster (round-to-even is supposed to minimize that) (a).
Follow these links for the MSDN descriptions of:
Math.Floor, which rounds down towards negative infinity.
Math.Ceiling, which rounds up towards positive infinity.
Math.Truncate, which rounds up or down towards zero.
Math.Round, which rounds to the nearest integer or specified number of decimal places. You can specify the behavior if it's exactly equidistant between two possibilities, such as rounding so that the final digit is even ("Round(2.5,MidpointRounding.ToEven)" becoming 2) or so that it's further away from zero ("Round(2.5,MidpointRounding.AwayFromZero)" becoming 3).
The following diagram and table may help:
-3 -2 -1 0 1 2 3
+--|------+---------+----|----+--|------+----|----+-------|-+
a b c d e
a=-2.7 b=-0.5 c=0.3 d=1.5 e=2.8
====== ====== ===== ===== =====
Floor -3 -1 0 1 2
Ceiling -2 0 1 2 3
Truncate -2 0 0 1 2
Round(ToEven) -3 0 0 2 3
Round(AwayFromZero) -3 -1 0 2 3
Note that Round is a lot more powerful than it seems, simply because it can round to a specific number of decimal places. All the others round to zero decimals always. For example:
n = 3.145;
a = System.Math.Round (n, 2, MidpointRounding.ToEven); // 3.14
b = System.Math.Round (n, 2, MidpointRounding.AwayFromZero); // 3.15
With the other functions, you have to use multiply/divide trickery to achieve the same effect:
c = System.Math.Truncate (n * 100) / 100; // 3.14
d = System.Math.Ceiling (n * 100) / 100; // 3.15
(a) Of course, that theory depends on the fact that your data has an fairly even spread of values across the even halves (0.5, 2.5, 4.5, ...) and odd halves (1.5, 3.5, ...).
If all the "half-values" are evens (for example), the errors will accumulate just as fast as if you always rounded up.
You should check MSDN for Math.Round:
The behavior of this method follows IEEE Standard 754, section 4. This kind of rounding is sometimes called rounding to nearest, or banker's rounding.
You can specify the behavior of Math.Round using an overload:
Math.Round(2.5, 0, MidpointRounding.AwayFromZero); // gives 3
Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2
From MSDN, Math.Round(double a) returns:
The integer nearest a. If the
fractional component of a is halfway
between two integers, one of which is
even and the other odd, then the even
number is returned.
... and so 2.5, being halfway between 2 and 3, is rounded down to the even number (2). this is called Banker's Rounding (or round-to-even), and is a commonly-used rounding standard.
Same MSDN article:
The behavior of this method follows
IEEE Standard 754, section 4. This
kind of rounding is sometimes called
rounding to nearest, or banker's
rounding. It minimizes rounding errors
that result from consistently rounding
a midpoint value in a single
direction.
You can specify a different rounding behavior by calling the overloads of Math.Round that take a MidpointRounding mode.
The nature of rounding
Consider the task of rounding a number that contains a fraction to, say, a whole number. The process of rounding in this circumstance is to determine which whole number best represents the number you are rounding.
In common, or 'arithmetic' rounding, it is clear that 2.1, 2.2, 2.3 and 2.4 round to 2.0; and 2.6, 2.7, 2.8 and 2.9 to 3.0.
That leaves 2.5, which is no nearer to 2.0 than it is to 3.0. It is up to you to choose between 2.0 and 3.0, either would be equally valid.
For minus numbers, -2.1, -2.2, -2.3 and -2.4, would become -2.0; and -2.6, 2.7, 2.8 and 2.9 would become -3.0 under arithmetic rounding.
For -2.5 a choice is needed between -2.0 and -3.0.
Other forms of rounding
'Rounding up' takes any number with decimal places and makes it the next 'whole' number. Thus not only do 2.5 and 2.6 round to 3.0, but so do 2.1 and 2.2.
Rounding up moves both positive and negative numbers away from zero. Eg. 2.5 to 3.0 and -2.5 to -3.0.
'Rounding down' truncates numbers by chopping off unwanted digits. This has the effect of moving numbers towards zero. Eg. 2.5 to 2.0 and -2.5 to -2.0
In "banker's rounding" - in its most common form - the .5 to be rounded is rounded either up or down so that the result of the rounding is always an even number. Thus 2.5 rounds to 2.0, 3.5 to 4.0, 4.5 to 4.0, 5.5 to 6.0, and so on.
'Alternate rounding' alternates the process for any .5 between rounding down and rounding up.
'Random rounding' rounds a .5 up or down on an entirely random basis.
Symmetry and asymmetry
A rounding function is said to be 'symmetric' if it either rounds all numbers away from zero or rounds all numbers towards zero.
A function is 'asymmetric' if rounds positive numbers towards zero and negative numbers away from zero.. Eg. 2.5 to 2.0; and -2.5 to -3.0.
Also asymmetric is a function that rounds positive numbers away from zero and negative numbers towards zero. Eg. 2.5 to 3.0; and -2.5 to -2.0.
Most of time people think of symmetric rounding, where -2.5 will be rounded towards -3.0 and 3.5 will be rounded towards 4.0. (in C# Round(AwayFromZero))
The default MidpointRounding.ToEven, or Bankers' rounding (2.5 become 2, 4.5 becomes 4 and so on) has stung me before with writing reports for accounting, so I'll write a few words of what I found out, previously and from looking into it for this post.
Who are these bankers that are rounding down on even numbers (British bankers perhaps!)?
From wikipedia
The origin of the term bankers'
rounding remains more obscure. If this
rounding method was ever a standard in
banking, the evidence has proved
extremely difficult to find. To the
contrary, section 2 of the European
Commission report The Introduction of
the Euro and the Rounding of Currency
Amounts suggests that there had
previously been no standard approach
to rounding in banking; and it
specifies that "half-way" amounts
should be rounded up.
It seems a very strange way of rounding particularly for banking, unless of course banks use to receive lots of deposits of even amounts. Deposit £2.4m, but we'll call it £2m sir.
The IEEE Standard 754 dates back to 1985 and gives both ways of rounding, but with banker's as the recommended by the standard. This wikipedia article has a long list of how languages implement rounding (correct me if any of the below are wrong) and most don't use Bankers' but the rounding you're taught at school:
C/C++ round() from math.h rounds away from zero (not banker's rounding)
Java Math.Round rounds away from zero (it floors the result, adds 0.5, casts to an integer). There's an alternative in BigDecimal
Perl uses a similar way to C
Javascript is the same as Java's Math.Round.
From MSDN:
By default, Math.Round uses
MidpointRounding.ToEven. Most people
are not familiar with "rounding to
even" as the alternative, "rounding
away from zero" is more commonly
taught in school. .NET defaults to
"Rounding to even" as it is
statistically superior because it
doesn't share the tendency of
"rounding away from zero" to round up
slightly more often than it rounds
down (assuming the numbers being
rounded tend to be positive.)
http://msdn.microsoft.com/en-us/library/system.math.round.aspx
Since Silverlight doesn't support the MidpointRounding option you have to write your own. Something like:
public double RoundCorrect(double d, int decimals)
{
double multiplier = Math.Pow(10, decimals);
if (d < 0)
multiplier *= -1;
return Math.Floor((d * multiplier) + 0.5) / multiplier;
}
For the examples including how to use this as an extension see the post: .NET and Silverlight Rounding
I had this problem where my SQL server rounds up 0.5 to 1 while my C# application didn't. So you would see two different results.
Here's an implementation with int/long. This is how Java rounds.
int roundedNumber = (int)Math.Floor(d + 0.5);
It's probably the most efficient method you could think of as well.
If you want to keep it a double and use decimal precision , then it's really just a matter of using exponents of 10 based on how many decimal places.
public double getRounding(double number, int decimalPoints)
{
double decimalPowerOfTen = Math.Pow(10, decimalPoints);
return Math.Floor(number * decimalPowerOfTen + 0.5)/ decimalPowerOfTen;
}
You can input a negative decimal for decimal points and it's word fine as well.
getRounding(239, -2) = 200
Silverlight doesn't support the MidpointRounding option.
Here's an extension method for Silverlight that adds the MidpointRounding enum:
public enum MidpointRounding
{
ToEven,
AwayFromZero
}
public static class DecimalExtensions
{
public static decimal Round(this decimal d, MidpointRounding mode)
{
return d.Round(0, mode);
}
/// <summary>
/// Rounds using arithmetic (5 rounds up) symmetrical (up is away from zero) rounding
/// </summary>
/// <param name="d">A Decimal number to be rounded.</param>
/// <param name="decimals">The number of significant fractional digits (precision) in the return value.</param>
/// <returns>The number nearest d with precision equal to decimals. If d is halfway between two numbers, then the nearest whole number away from zero is returned.</returns>
public static decimal Round(this decimal d, int decimals, MidpointRounding mode)
{
if ( mode == MidpointRounding.ToEven )
{
return decimal.Round(d, decimals);
}
else
{
decimal factor = Convert.ToDecimal(Math.Pow(10, decimals));
int sign = Math.Sign(d);
return Decimal.Truncate(d * factor + 0.5m * sign) / factor;
}
}
}
Source: http://anderly.com/2009/08/08/silverlight-midpoint-rounding-solution/
Simple way is:
Math.Ceiling(decimal.Parse(yourNumber + ""));
Rounding numbers with .NET has the answer you are looking for.
Basically this is what it says:
Return Value
The number nearest value with precision equal to digits. If value is halfway between two numbers, one of which is even and the other odd, then the even number is returned. If the precision of value is less than digits, then value is returned unchanged.
The behavior of this method follows IEEE Standard 754, section 4. This kind of rounding is sometimes called rounding to nearest, or banker's rounding. If digits is zero, this kind of rounding is sometimes called rounding toward zero.
using a custom rounding
public int Round(double value)
{
double decimalpoints = Math.Abs(value - Math.Floor(value));
if (decimalpoints > 0.5)
return (int)Math.Round(value);
else
return (int)Math.Floor(value);
}
Here's the way i had to work it around :
Public Function Round(number As Double, dec As Integer) As Double
Dim decimalPowerOfTen = Math.Pow(10, dec)
If CInt(number * decimalPowerOfTen) = Math.Round(number * decimalPowerOfTen, 2) Then
Return Math.Round(number, 2, MidpointRounding.AwayFromZero)
Else
Return CInt(number * decimalPowerOfTen + 0.5) / 100
End If
End Function
Trying with 1.905 with 2 decimals will give 1.91 as expected but Math.Round(1.905,2,MidpointRounding.AwayFromZero) gives 1.90! Math.Round method is absolutely inconsistent and unusable for most of the basics problems programmers may encounter. I have to check if (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2) cause i don not want to round up what should be round down.
This is ugly as all hell, but always produces correct arithmetic rounding.
public double ArithRound(double number,int places){
string numberFormat = "###.";
numberFormat = numberFormat.PadRight(numberFormat.Length + places, '#');
return double.Parse(number.ToString(numberFormat));
}
Related
For example,
double a = 2.55;
double b = Math.Round(a, 1); // expected result is 2.5
Console.WriteLine(b); // 2.6
The reason we expect 2.5 there is that the closest 64-bit IEEE 754 float to 2.55 is exactly 2.54999999999999982236431605997495353221893310546875, so whether we're using MidpointRounding.ToEven or MidpointRounding.AwayFromZero the value should round down to 2.5.
On the other hand, the "F" format specifier seems to handle the rounding correctly.
double a = 2.55;
Console.WriteLine($"{a:F1}"); // 2.5
Edit: It looks like the .NET team is tracking basically the same issue with Math.Round here. According to this, the issue might be addressed in the upcoming .NET 7 but it's not certain.
The answer to your actual question "Does Math.Round in .NET round certain values incorrectly?" is: Yes. (Well, Microsoft would probably argue that this behaviour is defined, and is therefore correct.)
The reason for this is described in the documentation for Math.Round():
Because of the loss of precision that can result from representing
decimal values as floating-point numbers or performing arithmetic
operations on floating-point values, in some cases the Round(Double,
Int32, MidpointRounding) method may not appear to round midpoint
values as specified by the mode parameter. This is illustrated in the
following example, where 2.135 is rounded to 2.13 instead of 2.14.
This occurs because internally the method multiplies value by
10^digits, and the multiplication operation in this case suffers from a
loss of precision.
We can test this:
double a = 2.55;
double c = a * Math.Pow(10, 1); // "a * 10.0" gives the same result.
Console.WriteLine(a.ToString("f16"));
Console.WriteLine(c.ToString("f16"));
The output is:
2.5499999999999998
25.5000000000000000
You can see that the value after multiplication by 10^1 is 25.5, which will be rounded up in the next step of the rounding algorithm.
You can look at the actual implementation here.
It's a bit fiddly, but the answer is really "something something rounding something" ;)
The default implementation for rounding in .NET is "Round half to even" which is "Bankers Rounding". This means that mid-point values are rounded towards the nearest even number.
Math.Round(value,decimal) works as expected. You define your value as 2.55 and thought you are rounding 2.55 aka 2.54999999... in IEEE 754 but this is false. Rounding with decimal apply a power of 10 to the rounding. So your 2.55 with 1 decimal is apply a single power of 10 so it become 25.5 which is perfectly represented as 25.5 in IEEE 754. Then a rounding become 26.0 then it divide by the factor back to 2.6 hence your results.
Because 2.55 is a midway value, it uses the convention when rounding. Default this is "To Even". So in this case it rounds to the closest even, 2.6.
If you use AwayFromZero it will do the same in this case, if you rounded 2.65 it would give different results.
Strange, I get the same result with the string formatter... ? Maybe it's a cultural setting. See also Convert Double to String: Culture specific
I have a bug with Math.Round with no explanation.
When I make
Math.Round(81.725, 2, MidpointRounding.AwayFromZero)
The result is 81,72 but when I make the same with Decimal.Round
Decimal.Round(81.725M, 2, MidpointRounding.AwayFromZero)
The result is 81,73
I d'ont understand why, have you an solution to use Math.Round systematically ?
You shouldn't be talking about bugs before understanding how double works, and the differences with decimal that explain the behavior you are seeing.
A double is the best approximation of a real number with the following structure:
number = sign * mantissa * 2 ^ exponent
Therefore, the number 81.725, when represented as a double, is really:
1 * 2875442808959795 * 2^-45 = 81,724999999999994315658113919199
Now you should understand why Math.Round(81.725, 2) resolves to 81.72 and not 81.73.
This doesn't happen with decimal because decimal, contrary to double, can exactly represent 81.725. This is due to the fact that the scaling factor in decimal is a power of 10.
This added precision obviously comes at a cost, in speed, space and range. When to choose one type or the other is well explained in a link to another SO question provided in comments.
The M suffix on second assignment below determines data type:
Math.Round(81.725, 2, MidpointRounding.AwayFromZero); // double, returns 81.72
Math.Round(81.725M, 2, MidpointRounding.AwayFromZero); // decimal, returns 81.73
Decimal.Round(81.725M, 2, MidpointRounding.AwayFromZero); // decimal, returns 81.73
By using that suffix, the given data type on second assignment treated as decimal instead of double, makes the rounding more precise.
The MSDN documentation of Math.Round already mention this difference:
Notes to Callers:
Because of the loss of precision that can result from representing
decimal values as floating-point numbers or performing arithmetic
operations on floating-point values, in some cases the Round(Double)
method may not appear to round midpoint values to the nearest even
integer.
And there is further explanation for Decimal.Round:
The behavior of this method follows IEEE Standard 754, section 4. This
kind of rounding is sometimes called round half to even or banker's
rounding. It minimizes rounding errors that result from consistently
rounding a midpoint value in a single direction. It is equivalent to
calling the Round(Decimal, MidpointRounding) method with a mode
argument of MidpointRounding.ToEven.
In short, decimal uses IEEE-754 which uses consistent rounding for midpoint value, while double (and float) uses floating-point which has no finite binary representation.
Other reference:
Declaration suffix for decimal type
I am trying to found a float using math round
I found the following
0.5 --> 0
1.5 --> 2
2.5 --> 2
3.5 --> 4
and so on.
I believe this is due to floating point error, but not quite sure how.
How can I get around this so even numbers round properly?
From documentation;
The integer nearest a. If the fractional component of a is halfway
between two integers, one of which is even and the other odd, then the
even number is returned. Note that this method returns a Double
instead of an integral type.
Math.Round method has some overloads that takes MidpointRounding as a parameter which you can specify the rounding value if it is midway between two numbers.
AwayFromZero
When a number is halfway between two others, it is rounded toward the
nearest number that is away from zero.
ToEven
When a number is halfway between two others, it is rounded toward the
nearest even number.
You could use this one, to overcome that you have stated:
Math.Round(value, MidpointRounding.AwayFromZero);
Using the above:
When a number is halfway between two others, it is rounded toward the
nearest number that is away from zero.
For further documentation about the MidpointRounding enumeration, please have a look here.
You may try like this
Math.Round(value, MidpointRounding.AwayFromZero);
From MSDN
If the fractional component of a is halfway between two integers, one
of which is even and the other odd, then the even number is returned.
Also to mention one important point which I think is good to mention is that Microsoft has followed the IEEE 754 standard. This is also mentioned in MSDN for Math.Round under Remarks which says:
Round to nearest, ties to even – rounds to the nearest value; if the number falls midway it is rounded to the nearest value with an
even (zero) least significant bit, which occurs 50% of the time; this
is the default for binary floating-point and the recommended default
for decimal.
Round to nearest, ties away from zero – rounds to the nearest value; if the number falls midway it is rounded to the nearest value
above (for positive numbers) or below (for negative numbers); this is
intended as an option for decimal floating point.
This is known as bankers rounding (round to even). You can read more about it here. This is a .NET Framework feature and is working as designed.
In testing as to why my program is not working as intended, I tried typing the calculations that seem to be failing into the immediate window.
Math.Floor(1.0f)
1.0 - correct
However:
200f * 0.005f
1.0
Math.Floor(200f * 0.005f)
0.0 - incorrect
Furthermore:
(float)(200f * 0.005f)
1.0
Math.Floor((float)(200f * 0.005f))
0.0 - incorrect
Probably some float loss is occuring, 0.99963 ≠ 1.00127 for example.
I wouldn't mind storing less pricise values, but in a non lossy way, for example if there were a numeric type that stored values as integers do, but to only three decimal places, if it could be made performant.
I think probably there is a better way of calculating (n * 0.005f) in regards to such errors.
edit:
TY, a solution:
Math.Floor(200m * 0.005m)
Also, as I understand it, this would work if I didn't mind changing the 1/200 into 1/256:
Math.Floor(200f * 0.00390625f)
The solution I'm using. It's the closest I can get in my program and seems to work ok:
float x = ...;
UInt16 n = 200;
decimal d = 1m / n;
... = Math.Floor((decimal)x * d)
Floats represent numbers as fractions with powers of two in the denominator. That is, you can exactly represent 1/2, or 3/4, or 19/256. Since .005 is 1/200, and 200 is not a power of two, instead what you get for 0.005f is the closest fraction that has a power of two on the bottom that can fit into a 32 bit float.
Decimals represent numbers as fractions with powers of ten in the denominator. Like floats, they introduce errors when you try to represent numbers that do not fit that pattern. 1m/333m for example, will give you the closest number to 1/333 that has a power of ten as the denominator and 29 or fewer significant digits. Since 0.005 is 5/1000, and that is a power of ten, 0.005m will give you an exact representation. The price you pay is that decimals are much larger and slower than floats.
You should always always always use decimals for financial calculations, never floats.
The problem is that 0.005f is actually 0.004999999888241291046142578125... so less than 0.005. That's the closest float value to 0.005. When you multiply that by 200, you end up with something less than 1.
If you use decimal instead - all the time, not converting from float - you should be fine in this particular scenario. So:
decimal x = 0.005m;
decimal y = 200m;
decimal z = x * y;
Console.WriteLine(z == 1m); // True
However, don't assume that this means decimal has "infinite precision". It's still a floating point type with limited precision - it's just a floating decimal point type, so 0.005 is exactly representable.
If you cannot tolerate any floating point precision issues, use decimal.
http://msdn.microsoft.com/en-us/library/364x0z75.aspx
Ultimately even decimal has precision issues (it allows for 28-29 significant digits). If you are working in it's supported range ((-7.9 x 10^28 to 7.9 x 10^28) / (100^28)), you are quite unlikely to be impacted by them.
So I'm trying to figure out why the modulo operator is returning such a large unusual value.
If I have the code:
double result = 1.0d % 0.1d;
it will give a result of 0.09999999999999995. I would expect a value of 0
Note this problem doesn't exist using the dividing operator -
double result = 1.0d / 0.1d;
will give a result of 10.0, meaning that the remainder should be 0.
Let me be clear: I'm not surprised that an error exists, I'm surprised that the error is so darn large compared to the numbers at play. 0.0999 ~= 0.1 and 0.1 is on the same order of magnitude as 0.1d and only one order of magnitude away from 1.0d. Its not like you can compare it to a double.epsilon, or say "its equal if its < 0.00001 difference".
I've read up on this topic on StackOverflow, in the following posts one two three, amongst others.
Can anyone suggest explain why this error is so large? Any any suggestions to avoid running into the problems in the future (I know I could use decimal instead but I'm concerned about the performance of that).
Edit: I should specifically point out that I know that 0.1 is an infinitely repeating series of numbers in binary - does that have anything to do with it?
The error comes about because a double can't exactly represent 0.1 -- the closest it can represent is something like 0.100000000000000005551115123126. Now when you divide 1.0 by that it gives you a number slightly less than 10, but again a double can't exactly represent it, so it ends up rounding up to 10. But when you do the mod, it can give you that slightly less than 0.1 remainder.
since 0 = 0.1 mod 0.1, the actual error in the mod is 0.1 - 0.09999999... -- very small.
If you add the result of the % operator to 9 * 0.1, it will give you 1.0 again.
Edit
A bit more detail on the rounding stuff -- particularly as this problem is a good example of the perils of mixed precision.
The way a % b for floating point numbers is usually computed is as a - (b * floor(a/b)). The problem is that it may be done all at once with more internal precision than you'd get with those operations (and rounding the result to a fp number at each stage), so it might give you a different result. One example that a lot of people see is with the Intel x86/x87 hardware is using 80-bit precision for intermediate computations and only 64-bit precision for values in memory. So the value in b in the equation above is coming from memory and is thus a 64-bit fp number that's not quite 0.1 (thank dan04 for the exact value), so when it computes 1.0/0.1 it gets 9.99999999999999944488848768742172978818416595458984375 (rounded to 80 bits). Now if you round that to 64 bits, it would be 10.0, but if you keep the 80 bit internal and do the floor on it, it truncates to 9.0 and thus gets .0999999999999999500399638918679556809365749359130859375 as the final answer.
So in this case, you're seeing a large apparent error because you're using a noncontinuous step function (floor) which means that a very tiny difference in an internal value can push you over the step. But since mod is itself a noncontinuous step function thats to be expected and the real error here is 0.1-0.0999... as 0.1 is the discontinuous point in the range of the mod function.
The fact that 0.1 cannot be represented exactly in binary has everything to do with it.
If 0.1 could be represented as a double, you would be getting the representable double nearest (assuming "nearest" rounding mode) to the actual result of the operation you want to compute.
Since it can't, you are getting the representable double nearest to an operation that is entirely different from the one you were trying to compute.
Also note that / is a mostly continuous function (a small difference on the arguments generally means a small difference on the result, and while the derivative can be large near but on a same side of zero, at least additional precision for the arguments helps). On the other hand % is not continuous: whatever the precision you choose, there will always be arguments for which an arbitrarily small representation error on the first argument means a large error on the result.
The way IEEE 754 is specified, you only get guarantees on the approximation of the result of one floating-point operation assuming the arguments are exactly what you want. If the arguments are not exactly what you want, you need to switch to other solutions, such as interval arithmetic or analysis of the well-conditionedness of your program (if it uses % on floating-point numbers, it is likely not to be well-conditioned).
It's not precisely an "error" in the calculation but the fact that you never really had 0.1 to start with.
The problem is that 1.0 can be represented exactly in binary floating point but 0.1 cannot, because it can't be constructed exactly from negative powers of two. (It's 1/16 + 1/32 + ...)
So you aren't really getting 1.0 % 0.1, the machine is left to compute 1.0 % 0.1 +- 0.00... and then it reports honestly what it got as a result...
In order to have a large remainder, I suppose the second operand of % must have been slightly over 0.1, preventing the final division, and resulting in almost the entire 0.1 being the result of the operation.
The error you see is small; it only looks large at first glance.
Your result (after rounding for display) was
0.09999999999999995 == (0.1 - 5e-17) when you expected 0 from a
% 0.1 operation.
But remember that this is almost 0.1, and 0.1 % 0.1 == 0.
So your actual error here is -5e-17. I would call this small.
Depending on what you need the number for, it might be better to write:
double result = 1.0 % 0.1;
result = result >= 0.1/2 ? result - 0.1 : result;