-1

I have a Console Application with .NET 5 and IronPython 2.7.11 which uses Python 2.7.

This is my code from C#:

Program.cs:

using System;

namespace IronPythonTest2
{
    class Program
    {
        static void Main(string[] args)
        {
            Microsoft.Scripting.Hosting.ScriptRuntime scriptRuntime = null;
            try
            {
                scriptRuntime = IronPython.Hosting.Python.CreateRuntime();
                dynamic scriptModule = scriptRuntime.UseFile("test.py");
                decimal decimalNumber = 1m / 3m;
                scriptModule.print_decimal(decimalNumber);
            }
            catch (Exception exc)
            {
                Console.WriteLine(exc.Message);
            }
            finally
            {
                scriptRuntime.Shutdown();
            }
        }
    }
}

test.py:

import locale
locale.setlocale(locale.LC_NUMERIC, 'en_US')
from decimal import *

def print_decimal(system_decimal):
    print(convert_to_decimal(system_decimal))
    print(convert_to_decimal_locale(system_decimal))

def convert_to_decimal(system_decimal):
    return Decimal(str(system_decimal))

def convert_to_decimal_locale(system_decimal):
    return Decimal(locale.format_string('%.28f', system_decimal))

This is my Project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IronPython" Version="2.7.11" />
    <PackageReference Include="IronPython.StdLib" Version="2.7.11" />
  </ItemGroup>

</Project>

output:

0.3333333333333333333333333333
0.3333333333333333703407674875

Question 1: Why they are different outputs?

The reason I'm using locale is that the convert_to_decimal function will throw the exception: "Invalid literal for Decimal" if that runs in a computer with a different culture which uses a comma "," instead of a dot "." for decimals.

I also noticed that the python Decimal does NOT accept comma ",", so by using locale I managed to fix it, however I want to keep all decimal places when using locale.format_string.

Question 2: Instead of passing a hardcoded string format like %.28f how to pass a specific format to display all decimal places something like e.g. '%d.%d' so it wouldn't need to specify the .28f part. How to do that?

EDIT

As @UlrichEckhardt has mentioned on his comment, I'm providing a minimal reproducible example for python users.

import locale
from decimal import *

def print_decimal(system_decimal):
    print(convert_to_decimal(system_decimal))
    print(convert_to_decimal_locale(system_decimal))

def convert_to_decimal(system_decimal):
    return Decimal(str(system_decimal))

def convert_to_decimal_locale(system_decimal):
    return Decimal(locale.format_string('%.28f', system_decimal))

def main():
    print_decimal(Decimal('1') / Decimal('3'))

if __name__ == "__main__":
    main()

output is slightly different from the Original Post though:

0.3333333333333333333333333333
0.3333333333333333148296162562

EDIT 2

As @tripleee answer suggests, the locale.str() seems to partially solve the problem since setting the locale to 'en_US' makes it replace the comma "," by the dot "." in a computer with such a culture, so python Decimal accepted it. However getcontext().prec = 28 didn't seem to work, it prints 12 decimal places. I'm not sure if I applied it correctly in my code, so here's it:

import locale
locale.setlocale(locale.LC_NUMERIC, 'en_US')
from decimal import *
getcontext().prec = 28

def print_decimal(system_decimal):
    print(convert_to_decimal(system_decimal))
    print(convert_to_decimal_locale(system_decimal))

def convert_to_decimal(system_decimal):
    return Decimal(str(system_decimal))

def convert_to_decimal_locale(system_decimal):
    return Decimal(locale.str(system_decimal))

Just a reminder that I'm testing it on 2 computers, one with en_US culture and another with pt_BR culture. The convert_to_decimal_locale function is intended to be used when pt_BR culture is present otherwise it would get the Invalid literal for Decimal exception since python Decimal does NOT support commas for decimals. So this output is from a computer with en_US culture, if you're going to use that code in another computer with culture e.g. which uses a comma "," for decimals so comment the #print(convert_to_decimal(system_decimal)) line otherwise you're gonna get the mentioned exception.

output:

0.3333333333333333333333333333
0.333333333333
Simple
  • 827
  • 1
  • 9
  • 21
  • [the rounding error may come from this](https://docs.python.org/3/tutorial/floatingpoint.html) – Saatvik Ramani Jul 17 '21 at 05:43
  • @SaatvikRamani Thanks but in my question I'm expecting a `Decimal` not a float. – Simple Jul 17 '21 at 06:01
  • I don't see any output statement by the Python code at all. Is this specific to Ironpython? Can you reduce (think [mcve]) that environment from your example? – Ulrich Eckhardt Jul 17 '21 at 06:30
  • @UlrichEckhardt I'm not sure whether it's a bug from `Python 2.7` or `IronPython` so that's why I'm using the python tags in the question. If you're familiar with C# I already gave everything out to run a minimal reproducible test. So users that know both should be able to reproduce it. – Simple Jul 17 '21 at 06:45
  • @UlrichEckhardt Question has been updated in the EDIT section, a minimal reproducible example has been added. – Simple Jul 17 '21 at 07:37
  • It's still not particularly minimal. I'm guessing it boils down to "how can I perform a calculation on a pair of numbers represented as strings in my locale's format and get the result back in the same format?" It would be useful to get example input numbers, and the locale you are using. – tripleee Jul 17 '21 at 14:47
  • @tripleee I'm testing it on 2 computers, one with `en_US` culture and another with `pt_BR` culture. The `convert_to_decimal_locale` function is intended to be used when `pt_BR` culture is present otherwise it would get the ***Invalid literal for Decimal*** exception since python `Decimal` does NOT support commas for decimals. – Simple Jul 17 '21 at 20:59
  • BTW: Don't add multiple "EDIT" sections. You should aim for a single, concise question. If anyone is interested in how it evolved, SO keeps a history which they can browse. – Ulrich Eckhardt Jul 18 '21 at 07:06
  • @UlrichEckhardt I partially disagree, depending on the context you might need edit sections to explicitly expose some relevant changes. I've seen some of Jon Skeet answers with a pair of Edit sections. – Simple Jul 18 '21 at 08:03

2 Answers2

1

The %f format specifier forces the number to be converted into a float, which by definition is imprecise. See also Is floating point math broken?

Because your example number 1/3 is not representable as a finite sequence of digits, there is no way to display "all the digits". As per the documentation, you can use

getcontext().prec = 28

to set the print format to 28 decimal digits.

The decimal module supports the mathematical concept of significant digits. Thus, the number 1.0 will still display as 1.0 after this, because that's the available precision for this number. If you want to enter it with 28 significant decimal digits, you have to specify them explicitly (Decimal('1.' + 28 * '0') might be more readable than Decimal('1.0000000000000000000000000000').)

The package does not appear to support locale-based number representations. There is a machanism to generate objects from a tuple which you could build from your own number parser. But a simpler solution is to convert the localized numeric string into Python's internal format by using locale.delocalize().

def from_locale_str(string):
    return Decimal(locale.delocalize(string))

However, neither of these mechanisms are available in Python 2. If you are stuck with that, I can see no other recourse than a simple string replacement.

def from_locale_str(string):
    conv = locale.localeconv()
    sep = conv['thousands_separator']
    dec = conv['decimal_point']
    return Decimal(string.replace(sep, "").replace(dec, "."))

For going in the other direction, locale.str() is available even in Python 2.

>>> locale.setlocale(locale.LC_NUMERIC, 'de_DE')
>>> d = Decimal('2.000.000,23'.replace('.', '').replace(',', '.'))
>>> locale.str(d)
'2000000,23'

Unfortunately, locale.str() hard-codes return _format("%.12g", val); you can use locale.format_string() to provide a different format string, but again, this bumps into floating-point imprecisions. So, again, perhaps a simple and stupid replace is the way to go.

def to_locale_str(num):
    conv = locale.localeconv()
    dec = conv['decimal_point']
    return str(num).replace(".", dec)
tripleee
  • 175,061
  • 34
  • 275
  • 318
  • So for what should I replace the `%f` for? Can you answer the second question too? Can you specify it in your answer, so people that might have the same issue will see it in this context rather than another question. – Simple Jul 17 '21 at 07:48
  • I mean how am I going to solve the ***Invalid literal for Decimal*** exception if I cannot use the `locale.format_string`, what is your workaround for that? Can you reformulate your answer with the solution applied to the actual context? – Simple Jul 17 '21 at 07:58
  • Simply using the replace method seems a bad idea, I mean what if you have to take into account the different thousands separators? Configuring locales seems more reliable. `getcontext().prec` only sets the format for the print function? I wish I could get a string itself straight from a return so I could return it back to my C# end. – Simple Jul 17 '21 at 08:17
  • The `replace` hack is just that, a hack. You can generalize it by using `locale.delocalize()` ... Actually I'll update the answer to suggest that instead. – tripleee Jul 17 '21 at 08:52
  • The context affects all string conversions, so `str(x)` will return `x` with however many digits the context of `x` dictates. Notice that you can create an instance-specific context so you can affect the display of individual objects without any need to modify the global context. – tripleee Jul 17 '21 at 09:13
  • It seems we're going in the right direction now! I did update my question in the EDIT 2 section, check it out. – Simple Jul 17 '21 at 20:05
  • Your update is _again_ not entirely minimal, but I think I can guess what's wrong. I'll add a minor update to this answer. – tripleee Jul 18 '21 at 06:38
  • Still not clear, why `str()` works with 28 digits and `locale.str()` did NOT, also u did NOT explain why `getcontext().prec = 28` did NOT work either. – Simple Jul 18 '21 at 07:45
  • I hadn't noticed that `locale.str()` provided fewer digits. Hmmm. I think I just explained why `prec = 28` won't add more digits than the available precision permits; do you have an emample where this doesn't explain it? Again, ***please*** review the guidance for providing a [mre] so I don't have to keep on guessing what you actually mean. – tripleee Jul 18 '21 at 08:07
  • See final update now. If you need more help, I seriously suggest you break this down into smaller individual, well-defined questions with *explicit* examples of what you want, ideally without the C# detour. Regardless of whether you are willing to accept this answer, an upvote just to reward the effort I have spent here would be much appreciated. – tripleee Jul 18 '21 at 08:17
  • 1
    Well the simple and stupid replace is the way I go, thanks +1 upvoted. – Simple Jul 18 '21 at 08:23