Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/core/IronPython/Runtime/Operations/FloatOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,17 +1132,17 @@ public static object __new__(CodeContext/*!*/ context, PythonType cls, object x)
return (float)(double)DoubleOps.NewFloat(context, TypeCache.Single, x);
}

public static string __str__(CodeContext/*!*/ context, float x) {
// Python does not natively support System.Single. However, we try to provide
// formatting consistent with System.Double.
return StringFormatter.Format(context, "%.6g", x, trailingZeroAfterWholeFloat: true);
}
public static string __str__(CodeContext/*!*/ context, float x)
=> Schubfach.ftoa(x);

public static string __repr__(CodeContext/*!*/ context, float self) {
return __str__(context, self);
}

public static string __format__(CodeContext/*!*/ context, float self, [NotNone] string/*!*/ formatSpec) {
// format(x, '') must be equal to str(x), which is not the same as str((double)x)
if (formatSpec.Length == 0) return __str__(context, self);

return DoubleOps.__format__(context, self, formatSpec);
}

Expand Down
52 changes: 43 additions & 9 deletions src/core/IronPython/Runtime/Schubfach.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#nullable enable

using System;
using System.Runtime.CompilerServices;

namespace IronPython.Runtime {
internal static partial class Schubfach {
Expand Down Expand Up @@ -95,11 +96,11 @@ private static void write8digits(Span<char> buffer, ref int pos, uint n) {
];

// Rewrites the digits in buffer[start..pos] in fixed notation if Python's
// repr would, i.e. for -4 < dec_point <= 16.
private static bool try_write_fixed(Span<char> buffer, ref int pos, int start, int dec_exp, bool add_dot_0) {
// repr would, i.e. for -4 < dec_point <= max_dec_point (16 for double).
private static bool try_write_fixed(Span<char> buffer, ref int pos, int start, int dec_exp, int max_dec_point, bool add_dot_0) {
int num_digits = pos - start;
int dec_point = dec_exp + 1;
if (dec_point <= -4 || dec_point > 16) return false;
if (dec_point <= -4 || dec_point > max_dec_point) return false;

if (dec_point <= 0) {
// 0.000ddd
Expand Down Expand Up @@ -127,7 +128,7 @@ private static bool try_write_fixed(Span<char> buffer, ref int pos, int start, i
}

// Writes the decimal FP number dec_sig * 10**dec_exp to buffer.
private static string write(bool is_negative, ulong dec_sig, int dec_exp, bool add_dot_0) {
private static string write(bool is_negative, ulong dec_sig, int dec_exp, int max_dec_point, bool add_dot_0) {
Span<char> buffer = stackalloc char[buffer_size];
int pos = 0;
if (is_negative) buffer[pos++] = '-';
Expand All @@ -149,7 +150,7 @@ private static string write(bool is_negative, ulong dec_sig, int dec_exp, bool a
// Remove trailing zeros.
while (buffer[pos - 1] == '0') --pos;

if (try_write_fixed(buffer, ref pos, start, dec_exp, add_dot_0)) {
if (try_write_fixed(buffer, ref pos, start, dec_exp, max_dec_point, add_dot_0)) {
return buffer.Slice(0, pos).ToString();
}

Expand Down Expand Up @@ -202,11 +203,44 @@ public static string dtoa(double value, bool add_dot_0 = true) {
bin_sig ^= implicit_bit;
bin_exp -= num_sig_bits + 1023; // Remove the exponent bias.

return to_decimal(is_negative, bin_sig, bin_exp, regular, num_sig_bits, 16, add_dot_0);
}

/// Returns the shortest decimal representation of `value` that round-trips as a float,
/// formatted like Python's repr but switching to exponent notation from 1e6 like numpy.
public static string ftoa(float value, bool add_dot_0 = true) {
int bitsInt = Unsafe.As<float, int>(ref value);
bool is_negative = bitsInt < 0;
ulong bits = unchecked((uint)bitsInt);

const int num_sig_bits = 24 - 1; // 24 = std::numeric_limits<float>::digits
const int exp_mask = 0xff;
int bin_exp = (int)(bits >> num_sig_bits) & exp_mask;

const ulong implicit_bit = (ulong)1 << num_sig_bits;
ulong bin_sig = bits & (implicit_bit - 1); // binary significand

bool regular = bin_sig != 0;
if (((bin_exp + 1) & exp_mask) <= 1) {
if (bin_exp != 0) return bin_sig != 0 ? "nan" : is_negative ? "-inf" : "inf";
if (bin_sig == 0) return is_negative ? (add_dot_0 ? "-0.0" : "-0") : (add_dot_0 ? "0.0" : "0");
// Handle subnormals.
bin_sig ^= implicit_bit;
++bin_exp;
regular = true;
}
bin_sig ^= implicit_bit;
bin_exp -= num_sig_bits + 127; // Remove the exponent bias.

return to_decimal(is_negative, bin_sig, bin_exp, regular, num_sig_bits, 6, add_dot_0);
}

private static string to_decimal(bool is_negative, ulong bin_sig, int bin_exp, bool regular, int num_sig_bits, int max_dec_point, bool add_dot_0) {
// Handle small integers.
if ((bin_exp < 0) & (bin_exp >= -num_sig_bits)) {
ulong f = bin_sig >> -bin_exp;
if (f << -bin_exp == bin_sig) {
return write(is_negative, f, 0, add_dot_0);
return write(is_negative, f, 0, max_dec_point, add_dot_0);
}
}

Expand Down Expand Up @@ -260,7 +294,7 @@ public static string dtoa(double value, bool add_dot_0 = true) {
bool under_in2 = lower + bin_sig_lsb <= (dec_sig_under2 << 2);
bool over_in2 = (dec_sig_over2 << 2) + bin_sig_lsb <= upper;
if (under_in2 != over_in2) {
return write(is_negative, under_in2 ? dec_sig_under2 : dec_sig_over2, dec_exp, add_dot_0);
return write(is_negative, under_in2 ? dec_sig_under2 : dec_sig_over2, dec_exp, max_dec_point, add_dot_0);
}
}

Expand All @@ -269,13 +303,13 @@ public static string dtoa(double value, bool add_dot_0 = true) {
bool over_in = (dec_sig_over << 2) + bin_sig_lsb <= upper;
if (under_in != over_in) {
// Only one of dec_sig_under or dec_sig_over are in the rounding interval.
return write(is_negative, under_in ? dec_sig_under : dec_sig_over, dec_exp, add_dot_0);
return write(is_negative, under_in ? dec_sig_under : dec_sig_over, dec_exp, max_dec_point, add_dot_0);
}

// Both dec_sig_under and dec_sig_over are in the interval - pick the closest.
int cmp = unchecked((int)(scaled_sig - ((dec_sig_under + dec_sig_over) << 1)));
bool under_closer = cmp < 0 || cmp == 0 && (dec_sig_under & 1) == 0;
return write(is_negative, under_closer ? dec_sig_under : dec_sig_over, dec_exp, add_dot_0);
return write(is_negative, under_closer ? dec_sig_under : dec_sig_over, dec_exp, max_dec_point, add_dot_0);
}

// std::countl_zero
Expand Down
11 changes: 2 additions & 9 deletions src/core/IronPython/Runtime/StringFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ internal class StringFormatter {

// The options for formatting the current formatting specifier in the format string
private FormatSettings _opts;
// Should ddd.0 be displayed as "ddd" or "ddd.0". "'%g' % ddd.0" needs "ddd", but str(ddd.0) needs "ddd.0"
private bool _trailingZeroAfterWholeFloat;

private bool _asBytes;

Expand Down Expand Up @@ -75,8 +73,8 @@ private StringFormatter(CodeContext/*!*/ context, string str, object? data) {

#region Public API Surface

public static string Format(CodeContext/*!*/ context, string str, object? data, bool trailingZeroAfterWholeFloat = false)
=> new StringFormatter(context, str, data) { _trailingZeroAfterWholeFloat = trailingZeroAfterWholeFloat }.Format();
public static string Format(CodeContext/*!*/ context, string str, object? data)
=> new StringFormatter(context, str, data).Format();

internal static byte[] FormatBytes(CodeContext/*!*/ context, ReadOnlySpan<byte> str, object? data)
=> new StringFormatter(context, str.MakeString(), data) { _asBytes = true }.Format().MakeByteArray();
Expand Down Expand Up @@ -544,11 +542,6 @@ private void AppendFloat(char format) {
var fPos = DoubleOps.Sign(val) >= 0 || double.IsNaN(val);
var str = FloatFormatter.Format(val, format, _opts.Precision, _opts.AltForm);

// str(System.Single) uses "%.6g" but needs "ddd.0" for whole numbers
if (_trailingZeroAfterWholeFloat && !double.IsNaN(val) && !double.IsInfinity(val) && str.IndexOfAny(['.', 'e', 'E']) == -1) {
str += ".0";
}

if (!fPos) {
str = "-" + str;
} else if (_opts.SignChar) {
Expand Down
8 changes: 6 additions & 2 deletions tests/suite/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,14 @@ def test_single(self):
self.assertEqual(str(f), "1.1")
self.assertEqual("%g" % f, "1.1")
f = IronPythonTest.DoubleToFloat.ToFloat(1.2345678)
self.assertEqual(str(f), "1.23457")
self.assertEqual(str(f), "1.2345678")
self.assertEqual(format(f), "1.2345678")
self.assertEqual("%g" % f, "1.23457")
f = IronPythonTest.DoubleToFloat.ToFloat(123456.0)
self.assertEqual(str(f), "123456.0")
self.assertEqual("%g" % f, "123456")
f = IronPythonTest.DoubleToFloat.ToFloat(1234567.8)
self.assertEqual(str(f), "1.23457e+06")
self.assertEqual(str(f), "1.2345678e+06")
self.assertEqual("%g" % f, "1.23457e+06")

def test_long(self):
Expand Down
Loading