Truncate problem with conversion to less decimals

Posts   
 
    
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 24-Nov-2021 11:03:53   

Hi,

We noticed that the default for EntityCore<T>.ScaleOverflowCorrectionActionToUse the value Truncate is but SD.LLBLGen.Pro.ORMSupportClasses.FieldUtilities.CorrectScale does not handle the truncation correctly. It uses a ToString() whereas the Round does use a nice type check.

We have a double with value: -0.000000000307431677337711, which has a ToString() of -3.07431677337711E-10

The target scale is 6 and rounding will do so but the Truncate takes the first 6 digits after the decimal dot and ignores the "E-10" part completely.

Sounds like a bug to me, wouldn't it be better to do a type check as is done by Round but instead of Math.Round, use for example the Math.Floor combined with a multiplication + division by 10^scale?

Best, Dick

Attachments
Filename File size Added on Approval
truncate_problem.png 18,428 24-Nov-2021 11:04.29 Approved
Walaa avatar
Walaa
Support Team
Posts: 14983
Joined: 21-Aug-2005
# Posted on: 24-Nov-2021 20:09:58   

We will look into it, and get back to you. Thanks,

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 25-Nov-2021 11:42:57   

The problem with using Math.floor and then multiply/divide is that double and single values are IEEE encoded and not precise: doing multiply/divide could alter the resulting value and in some cases cause loss of value. In fact, single/double don't have a 'scale' set internally. The tostring route was chosen as it was a somewhat 'reliable' way to avoid any problems wrt multiply and divide and has been in the framework since the beginning. You're the first which found this edge case! smile

Looking at it again, I think it's a bit weird we do truncate for double/single values at all. The main issue is that you can't avoid having a repetitive number with infinite scale with a double or single. E.g. take this simple code:

double d = 2.1;
double e = 4.1;
double f = e - d;
Console.WriteLine(f);

It prints: 1,9999999999999996, which, if we set the scale to e.g. 5, results in a scale truncation but it shouldn't as it has, in theory, a fraction of 0. As doubles/singles are imprecise for this, scale is arbitrary and for string usage mostly, there's no overflow for scale in a database anyway (or better: databases supporting doubles will round).

I think at the time we thought it was a good idea, but looking at it again, it doesn't really make sense to truncate a double/single as it's imprecise; it should always round. There's also no solution that will work with all situations, not even a ToString("Fscale + 1"), which works with your value, but not with 0.199999999999, which will get rounded to 0.20000 for a scale of 5, and lead to an incorrect value (if you can call it that).

In any case, the truncate on single/double isn't useful, and we'll remove it for these types. But removing / changing features is something we don't like to do in existing versions. We will change it for v5.9. We can't change it for v5.7.x/v5.8.x, as it's a breaking change: should we ignore the scale overflow or do a rounding?

To work around this, assign an IValidator instance to an entity using dependency injection, and in the IValidator implementation simply ignore single/double value set actions if the scale overflowcorrection action to use is truncate. See: https://www.llblgen.com/Documentation/5.8/LLBLGen%20Pro%20RTF/Using%20the%20generated%20code/gencode_validation.htm#validation-logic-inside-validator-classes

Another workaround could be to pre-round the value when it's set in an override of the entitycore method: PreProcessValueToSet (do that in a partial class of CommonEntityBase for instance, or the particular entity class)

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 25-Nov-2021 12:04:45   

I am aware of the problems with such small numbers and using math.floor (incl mul/div), the error on our end with the current ToString way of doing the truncation is even more significant though... wink

Anyway, I agree the fact that truncating is not useful for double/single values so we will likely use the global setting (in the app.config) to change the default of scaleOverflowCorrectionActionToUse to 2 (rounding)...

Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 25-Nov-2021 12:18:39   

We still needed to find a solution to the problem for 5.9, and the main question is: to round or to leave it as-is. So doing the mul/div route on just the fraction will solve that.

We think we have found a solution, and will implement that on v5.7.x and 5.8.x as this isn't a breaking change

example, scale of 6:

double a = 1234.123456789123456;
double b = Math.Round(a);
double c = Math.Abs(a) - Math.Abs(b);
double d = Math.Pow(10, 6);
double e = Math.Floor(c * d);
double f = e / d;
double result = (a < 0) ? b - f : b + f;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(d);
Console.WriteLine(e);
Console.WriteLine(f);
Console.WriteLine(result);

gives:

1234,1234567891236
1234
0,12345678912356561
1000000
123456
0,123456
1234,123456

This will work up to a scale of 15 as it will otherwise overflow. If the scale is > 15 we'll ignore it, as a fraction that small is bigger than the error you get with IEEE arithmetic anyway wink (Math.Round will fail with scales > 15)

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 25-Nov-2021 13:01:30   

Wouldn't the following also suffice?

int scale = 6; 

double a = -0.000000000307431677337711; // 1234.123456289123456;
double b = a - Math.Sign(a) * (.5 / Math.Pow(10, scale));
double c = Math.Round(b, scale, MidpointRounding.AwayFromZero);
double result = c;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(result);

Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 25-Nov-2021 15:31:17   

Sure, tho expressing it in a bit more steps makes it simpler to understand wink. The main advantage however of having more steps is that we can check if there's a fraction in the value multiplied by 10^scale. If not, no action should be taken. If there is, only then the action has to be performed, as that's the API's designed behavior.

We've uploaded hotfixes for v5.7 (v5.7.7) and v5.8 (v5.8.4) to your site and nuget which should correct this problem. they'll only check for scale overflow and only perform a round or truncate, they'll never throw an exception.

Rounding is done when specified and scale is < 16. if the scale is 16 or higher, no work is performed as it would lead an overflow in double anyway when performing a round, so it will fall through and do a truncation.

Hopefully this fixes the validation problems you had simple_smile

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 25-Nov-2021 15:56:59   

Thanks for the fix, we will update our sources and see if it works.

What I meant with my snippet of code is that instead of the Abs-es and Floor, you would also be able to use the Round if you first subtract 0.5/10^scale, this will be like doing a floor and not having to calculate with portions of the value and adding them up in the end.

Best, Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 26-Nov-2021 09:20:20   

Thanks yes that was clear, the only thing we ran into was, that we have to determine if we need to do anything at all. simple_smile For the truncation, that's fine, you can just truncate and if the fraction has less digits, no problem. But for the rounding, this is required and to determine if we needed to do anything, we have to determine if there are digits in a fraction after we've multiplied it with the 10^scale. It's not really a big thing, this code should be faster than the string based code we had in any way simple_smile

Let us know if the new code still gives problems or fails in another way

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 26-Nov-2021 12:27:27   

Hi!

We have tested the hotfix and it worked like a charm for our testcase. Thanks!

Dick

Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 30-Nov-2021 21:51:52   

Hi,

Our testcase was working fine but the new implementation does not respect the type of the value and always returns a double, for example:

var value = SD.LLBLGen.Pro.ORMSupportClasses.FieldUtilities.CheckPrecisionScaleSingleDouble(90f, ScaleOverflowCorrectionAction.Truncate, 2);

"value" will be of type double even though we started with a Single value. Resulting in an exception when the getter of the property is called:

        public virtual System.Single Percentage
        {
            get { return (System.Single)GetValue((int)MyEntityFieldIndex.Percentage, true); }
            set { SetValue((int)MyEntityFieldIndex.Percentage, value); }
        }

--> the GetValue returns a boxed double resulting in the cast "(System.Single)" to crash.

Would you be able to fix it in an update?

Best, Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 01-Dec-2021 09:14:14   

Hmm, that's indeed an oversight. As the functions required are only available for double, we used the same function for floats as well, but didn't implement a convert. We'll correct that asap.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 01-Dec-2021 10:16:09   

Fixed in the new hotfix builds for 5.7.7 and 5.8.4. Sorry for this inconvenience

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 01-Dec-2021 16:08:05   

Tested and seems to be working. Thanks!

Dick

Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 22-Dec-2021 14:27:21   

Otis wrote:

Fixed in the new hotfix builds for 5.7.7 and 5.8.4. Sorry for this inconvenience

Hi,

This issue is hunting us, so it seems... we have a double property with scale 4 and want to assign 24.11 (can be stored in a double) to it... it results in the property having the value 24.1099 due to the fact that 24.11 - 24 (calculation of the fraction variable) results in 0.10999..9943.

We are using the default ScaleOverflowCorrectionAction.Truncate.

So on our end, the variable is still fine and just having 2 decimals but during the assignment, significance gets lost. If we would use ScaleOverflowCorrectionAction.Round, it works for this case but that might change other calculations.

How to solve this?

Best, Dick

Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 22-Dec-2021 15:11:11   

To get back to the original issue, maybe the string formatting was the easiest but you should use something like:

            var small = -0.000000000307431677337711;
            var smallstr = String.Format("{0:F20}", small);
            Console.WriteLine(smallstr);

It will result in the string without the exponent...

Best, Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 23-Dec-2021 09:59:25   

Aren't you running into the limitations of IEEE encoded floating point values? If you want a value to always have a fraction with 2 decimals, you should use a Decimal, not a double, as that will lead to problems one way or the other (which is also the reason, as you might know, money related values aren't stored in floating point types)

I tried the string route, but couldn't find a formatting that would lead to something that wouldn't bug at some point. In the end the double/float route for rounding is one way or the other, arbitrary, simply because the result of the calculation/rounding might be exact, the storage of it might not, as a double with scale '4' actually doesn't mean anything.

I suspect you're using Oracle? ODP.NET has the sad tendency to convert some NUMBER(x, y) variants to System.Double / Single instead of always using System.Decimal. ref This is still the case to this day in v21.x I don't see a way around this as the oracle / ODP.NET datareader e.g. returns a value from a column with type NUMBER(1, 12) as a double. The lame thing is that their Entity Framework Core variant of ODP.NET does return it as a decimal in all cases. (which isn't ideal either in some cases, but alas).

So, to recap: what exactly is the problem and what exactly are the specs you have to work with as both are unclear to me.

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 23-Dec-2021 10:32:00   

Hi Otis,

Agree that decimal should be prefered for currencies but some things can't be changed... wink And yes, we are using Oracle.

In what case does the string.format not give the required results to go back to the initial method of truncating via string?

I am sure it is a limitation of IEEE we see now but it is because of the calculations on your end. I would say that if the value is already in range (24.11 fits a scale of 4 already), no additional calculations are required and the value should be stored as-is.

To recap: I would like 24.11 to be stored as 24.11 and not a value very close to that.

Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 24-Dec-2021 09:07:38   

You make the mistake that you assume 24.11 is really 24.11 and therefore can be truncated at all. Maybe it's 24.110006 and stays that way. (It is)

I did this test:

[Test]
public void ScaleOverflowTest2()
{
    try
    {
        EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.Truncate;
        var e = new TestdefaultEntity();
        var value = 24.11f;
        e.FloatField= value;
        var fieldValue = e.FloatField;
        Assert.AreEqual(value, fieldValue);
        Assert.AreEqual(24.11f, fieldValue);
    }
    finally
    {
        EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.None;
    }
}

This test succeeds. FloatField is a NUMBER(5, 2). Not sure why it fails on your side tho. To be clear, value after the assignment is 24.110006 even though I specified a constant in the code: 24.11f and fieldValue is also 24.110006, also after truncation. That is because truncating float/double is imprecise.

I do the same calculations one could do in this case:

public static object CheckPrecisionScaleSingleDouble(object valueAsObject, ScaleOverflowCorrectionAction overflowActionToUse, int scale)
{
    if(valueAsObject == null)
    {
        return null;
    }
    bool isFloat = valueAsObject is float;
    double value = Convert.ToDouble(valueAsObject);
    double valueWithoutFraction = Math.Truncate(value);
    double fraction = Math.Abs(value) - Math.Abs(valueWithoutFraction);

    double scalePower = Math.Pow(10, scale);
    double fractionMultipliedWithPower = scalePower * fraction;
    // The digits outside the scale are in the fraction of the fractionMultipliedWithPower
    double fractionMultipliedWithPowerTruncated = Math.Truncate(fractionMultipliedWithPower);
    double fractionOfFractionMultipliedWithPower = fractionMultipliedWithPower - fractionMultipliedWithPowerTruncated;
    double valueToReturn = value;
    if(fractionOfFractionMultipliedWithPower > 0)
    {
        switch(overflowActionToUse)
        {
            // there's an overflow in the scale. 
            // we'll get an overflow otherwise, so we'll fall through and truncate. 
            case ScaleOverflowCorrectionAction.Round when scale < 16:
                // simply round it. no exception check needed. 
                valueToReturn = Math.Round(value, scale);
                break;
            case ScaleOverflowCorrectionAction.None:
                // no action is performed
                valueToReturn = value;
                break;
            default:
                double truncatedFraction = fractionMultipliedWithPowerTruncated / scalePower;
                // construct the value to return. Make sure to add the fraction back the right way in the case the original value is negative
                valueToReturn = (value < 0) ? valueWithoutFraction - truncatedFraction : valueWithoutFraction + truncatedFraction;
                break;
        }
    }
    // return the value we received, nothing has to be done
    if(isFloat)
    {
        float toReturn = Convert.ToSingle(valueToReturn);
        return toReturn;
    }
    return valueToReturn;
}

When using the string route we did run into problems with your original test values IIRC (but I didn't keep them, so I don't know exactly which ones failed. I do know the double with the large fraction above did fail). There was no string construct that could make it do a 100% roundtrip that worked in all cases. As the .net framework uses the same code, multiply with a power, e.g. in the double.Round() route (See Double.InternalRound() where the value ends up), we decided to do the same thing as we now have control over when not to apply it.

To recap: I would like 24.11 to be stored as 24.11 and not a value very close to that.

I don't see how you can not store that as my test makes the entity value stay 24.11f, but it's as always an approximation. You order a truncate, there's a tiny fraction in the .11 that justifies a truncate so the code will do that and constructs a value back, which will always have that fraction. There are no 'wrong' calculations, as there are no better calculations to my knowledge that work in all cases, and make a float/double be precise. You want 24.11 to be precisely 24.11. That's not possible with a float/double, no matter what calculations you do. Not sure why you insist my calculations are wrong, but if you have better ones which work in all cases, sure, show me.

Yes it sucks, it sucked since 2003 when Oracle decided to use float/double values for NUMBER(x,y) where scale is < 8 or < 16. I wished I could change that, but the values coming from the data-reader are what they are. A type converter isn't suitable either, as it works on the value coming from the datareader.

To be honest truncate should be changed to round to 0 (but that too will still be imprecise). Not sure what Oracle does internally when the constant 24.110006 is persisted in a NUMBER(5.2) field: will it truncate or round? If the problem for you is to calculate with 24.110006 in-memory, you could consider a type converter on the field to convert it to a Decimal field. This type converter isn't built-in but is easy to build. I've placed one below, not tested, but it's straight forward:

using System;
using System.ComponentModel;

namespace SD.LLBLGen.Pro.TypeConverters
{
    public class DecimalDoubleConverter : TypeConverter
    {
        public DecimalDoubleConverter()
        {
        }

        /// <summary>
        /// Returns whether this converter can convert an object of the given type to the type of this converter (Decimal).
        /// </summary>
        /// <param name="context">Ignored</param>
        /// <param name="sourceType">A <see cref="T:System.Type"/> that represents the type you want to convert from.</param>
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType.FullName=="System.Double";
        }

        /// <summary>
        /// Returns whether this converter can convert the object to the specified type.
        /// </summary>
        /// <param name="context">Ignored</param>
        /// <param name="destinationType">A <see cref="T:System.Type"/> that represents the type you want to convert to.</param>
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return sourceType.FullName=="System.Double";
        }

        /// <summary>
        /// Converts the given object to the type of this converter (Decimal).
        /// </summary>
        /// <param name="context">Ignored</param>
        /// <param name="culture">Ignored</param>
        /// <param name="value">The <see cref="T:System.Object"/> to convert.</param>
        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            return Convert.ToDecimal((double)value);
        }

        /// <summary>
        /// Converts the given value object to the specified type
        /// </summary>
        /// <param name="context">Ignored</param>
        /// <param name="culture">Ignored</param>
        /// <param name="value">The <see cref="T:System.Object"/> to convert.</param>
        /// <param name="destinationType">The <see cref="T:System.Type"/> to convert the <paramref name="value"/> parameter to.</param>
        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if(value==null)
            {
                throw new ArgumentNullException("value", "Value can't be null");
            }

            if(! (value is Double))
            {
                throw new ArgumentException("Value isn't of type Double", "value");
            }
            
            return (double)((decimal)value);
        }


        /// <summary>
        /// Creates an instance of the Type that this <see cref="T:System.ComponentModel.TypeConverter"/> is associated with (decimal)
        /// </summary>
        /// <param name="context">ignored.</param>
        /// <param name="propertyValues">ignored.</param>
        public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues)
        {
            return 0.0M;
        }
    }
}

Compile into a .net framework dll for the designer and a .net standard dll for the runtime (multitargeted csproj).

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 27-Dec-2021 10:45:35   

Hi Otis,

Would not surprise you if I did my own test, right?! simple_smile

Anyway, consider the following:

            var amount = 24.11d;
            Console.WriteLine("Initial value: " + amount);

            var ent = new MyEntity();

            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.Truncate;
            ent.AmountBalance = amount;
            Console.WriteLine("Using Truncate: " + ent.AmountBalance);

            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.Round;
            ent.AmountBalance = amount;
            Console.WriteLine("Using Round: " + ent.AmountBalance);


            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.None;
            ent.AmountBalance = amount;
            Console.WriteLine("Using None: " + ent.AmountBalance);

            Console.ReadKey();

Where AmountBalance is a double and has a scale of 4 (number(12,4)). By changing the ScaleOverflowCorrectionActionToUse on the EntityBase2 we get the following results:

Initial value: 24.11 Using Truncate: 24.1099 Using Round: 24.11 Using None: 24.11

Your calculation starts with a float constant whereas we are having a double containing that value and assign it to a property of type double.

Guess it will be an arbitrary choice between Truncate and Round as both might have their own implications. From a usage perspective, I would suspect the Trunctate not to alter the value if the number of decimal is already less or equal to the scale.

Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 27-Dec-2021 13:38:38   

Sena wrote:

Hi Otis,

Would not surprise you if I did my own test, right?! simple_smile

Anyway, consider the following:

            var amount = 24.11d;
            Console.WriteLine("Initial value: " + amount);

            var ent = new MyEntity();

            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.Truncate;
            ent.AmountBalance = amount;
            Console.WriteLine("Using Truncate: " + ent.AmountBalance);

            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.Round;
            ent.AmountBalance = amount;
            Console.WriteLine("Using Round: " + ent.AmountBalance);


            EntityBase2.ScaleOverflowCorrectionActionToUse = ScaleOverflowCorrectionAction.None;
            ent.AmountBalance = amount;
            Console.WriteLine("Using None: " + ent.AmountBalance);

            Console.ReadKey();

Where AmountBalance is a double and has a scale of 4 (number(12,4)). By changing the ScaleOverflowCorrectionActionToUse on the EntityBase2 we get the following results:

Initial value: 24.11 Using Truncate: 24.1099 Using Round: 24.11 Using None: 24.11

Try to step through it with a debugger and then look at what amount is. Here in my debugger, Rider latest version, it's 24.1099999999999. Interestingly, in VS 2022 it says it's 24.11 but we'll see below that's a lie smile.

You still assume 24.11 is expressible as an exact value with a Double. It's not. It's close, but not quite there. It's the base of the issue at hand: you can't determine if the double value has a scale of 2. You have to calculate it by doing a series of calculations. During these calculations the not exact double values will result in a non-exact end result. So the value you pass in, 24.109999999999, has to be broken down to be able to determine what to do with it: does it have a fraction of 0, 2, 3, 10 digits? The only way to determine that is by multiplying it by a number, then round it down, then divide it by that number again to get the fraction back. But that's not a precise calculation, even if it looks like it is; the double is an approximation. So to truncate a double or float, the calculations with the multiply/divide has to take place. there's no other way to determine reliably how big the fraction is and where to truncate.

Let's break down what happens during that calculation if the fraction fits or not. I'll use vs 2022 here as it had the tendency to report the initial value was 24.11. I've copied the code and have specified what the result is of the statement:

bool isFloat = valueAsObject is float;                  // False
double value = Convert.ToDouble(valueAsObject);         // 24.11
double valueWithoutFraction = Math.Truncate(value);     // 24
double fraction = Math.Abs(value) - Math.Abs(valueWithoutFraction); // 0.10999999999999943

double scalePower = Math.Pow(10, scale);                // 10000
double fractionMultipliedWithPower = scalePower * fraction;     // 1099.9999999999943

As you can see, what looked like a perfect 24.11d turns out to be not so perfect. Here there's little else we can do: we can't use the string route (which is just as imprecise if I might add), we can only use perfectly legitimate calculations which should result in values like 0.11, but turn out to be slightly different. These then turn out to have a big influence at the end result.

Your calculation starts with a float constant whereas we are having a double containing that value and assign it to a property of type double.

That really doesn't matter. The 24.11d is still not exactly 24.11 like one would get with a System.Decimal. Some values are expressible in double/float values exactly as you'd expect, but most aren't. That it prints as 24.11 is nice, but not the reality.

Guess it will be an arbitrary choice between Truncate and Round as both might have their own implications. From a usage perspective, I would suspect the Trunctate not to alter the value if the number of decimal is already less or equal to the scale.

Of course one would, but that's the theory. In practice the double/float values aren't as precise as expected and therefore the end result isn't either. I truly wish I was able to determine the fraction of a double reliably and deterministically (or better: that Oracle gets their act together and treats Number(10, 4 like a decimal), but all we have today is this.

With a validator object you can skip validation for e.g. this field btw, but there's no 'turn key solution' that works for all possible double fields.

Frans Bouma | Lead developer LLBLGen Pro
Sena
User
Posts: 22
Joined: 21-Jul-2021
# Posted on: 27-Dec-2021 14:55:52   

Haha, using VS2022 here indeed... Agree that 24.11 is not exactly represented by the double but clearly VS2022 is hiding that from me.

Since we can't migrate to decimal types (for now), we will have to accept some (rounding) differences.

Tx, Dick

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 27-Dec-2021 15:55:45   

Sena wrote:

Haha, using VS2022 here indeed... Agree that 24.11 is not exactly represented by the double but clearly VS2022 is hiding that from me.

Since we can't migrate to decimal types (for now), we will have to accept some (rounding) differences.

Tx, Dick

If it's really unacceptable, you can fine tune the validation btw. If this is one of the fields you really don't need any built-in truncation on, you can set the entity's Validator object to an instance of a class that implements IValidator. You then get the call for validation in that object instead of the built-in validation method. it's a bit cumbersome tho. When you can migrate to a decimal, use the typeconverter route as that's the easiest. When converting back to the db type (double) simply return the decimal value instead so the parameter gets set to the exact decimal value in the entity field. But it's a workaround for what oracle throws at us, sadly... rage

Frans Bouma | Lead developer LLBLGen Pro