diff --git a/src/core/IronPython/Runtime/Binding/PythonBinder.cs b/src/core/IronPython/Runtime/Binding/PythonBinder.cs
index 5b91b531d..2b83913a4 100644
--- a/src/core/IronPython/Runtime/Binding/PythonBinder.cs
+++ b/src/core/IronPython/Runtime/Binding/PythonBinder.cs
@@ -760,6 +760,9 @@ internal static void AssertNotExtensionType(Type t) {
res[typeof(char)] = new Type[] { typeof(CharOps) };
res[typeof(decimal)] = new Type[] { typeof(DecimalOps) };
res[typeof(float)] = new Type[] { typeof(SingleOps) };
+#if NET6_0_OR_GREATER
+ res[typeof(Half)] = new Type[] { typeof(HalfOps) };
+#endif
res[typeof(DateTime)] = new Type[] { typeof(DateTimeOps) };
return res;
diff --git a/src/core/IronPython/Runtime/Converter.cs b/src/core/IronPython/Runtime/Converter.cs
index 9dc49999e..5ed8baa49 100644
--- a/src/core/IronPython/Runtime/Converter.cs
+++ b/src/core/IronPython/Runtime/Converter.cs
@@ -751,9 +751,9 @@ private static bool HasNarrowingConversion(Type fromType, Type toType, Narrowing
if (IsPythonFloatingPoint(fromType) && !IsPythonFloatingPoint(toType)) return false;
return true;
}
- if (toType == Int32Type && HasPythonProtocol(fromType, "__int__")) return true;
+ if (toType == Int32Type && !IsHalf(fromType) && HasPythonProtocol(fromType, "__int__")) return true;
if (toType == DoubleType && HasPythonProtocol(fromType, "__float__")) return true;
- if (toType == BigIntegerType && HasPythonProtocol(fromType, "__int__")) return true;
+ if (toType == BigIntegerType && !IsHalf(fromType) && HasPythonProtocol(fromType, "__int__")) return true;
}
if (allowNarrowing >= PythonNarrowing.IndexOperator) {
@@ -929,6 +929,14 @@ internal static bool IsExtensibleNumeric(Type t) {
|| ExtensibleComplexType.IsAssignableFrom(t);
}
+ private static bool IsHalf(Type t) {
+#if NET6_0_OR_GREATER
+ return t == typeof(Half);
+#else
+ return false;
+#endif
+ }
+
internal static bool IsPythonNumeric(Type t)
=> IsNumeric(t) || IsExtensibleNumeric(t);
diff --git a/src/core/IronPython/Runtime/Operations/HalfOps.cs b/src/core/IronPython/Runtime/Operations/HalfOps.cs
new file mode 100644
index 000000000..3f87b32a9
--- /dev/null
+++ b/src/core/IronPython/Runtime/Operations/HalfOps.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+#if NET6_0_OR_GREATER
+
+using System;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+using Microsoft.Scripting.Runtime;
+
+using IronPython.Runtime.Types;
+
+namespace IronPython.Runtime.Operations {
+ public static class HalfOps {
+
+ [StaticExtensionMethod]
+ public static object __new__(PythonType cls) {
+ CheckHalfType(cls);
+ return default(Half);
+ }
+
+ [StaticExtensionMethod]
+ public static object __new__(CodeContext/*!*/ context, PythonType cls, object x) {
+ CheckHalfType(cls);
+ if (x is Half h) return h;
+ if (Converter.TryConvertToDouble(x, out double doubleVal)) return (Half)doubleVal;
+
+ return (Half)(double)DoubleOps.NewFloat(context, TypeCache.Double, x);
+ }
+
+ public static bool __bool__(Half self) => self != Half.Zero;
+
+ public static PythonTuple __getnewargs__(Half self) => PythonTuple.MakeTuple((double)self);
+
+ [PropertyMethod, SpecialName]
+ public static Half Getreal(Half self) => self;
+
+ [PropertyMethod, SpecialName]
+ public static Half Getimag(Half self) => default;
+
+ public static Half conjugate(Half self) => self;
+
+ public static string __format__(CodeContext/*!*/ context, Half 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__(self);
+
+ return DoubleOps.__format__(context, (double)self, formatSpec);
+ }
+
+ public static string __str__(Half self) => __repr__(self);
+
+ ///
+ /// Returns the shortest decimal representation that round-trips to the same value.
+ ///
+ ///
+ /// The layout follows NumPy's str(numpy.float16): fixed notation for zero and for
+ /// magnitudes in [1e-4, 1e3), scientific notation otherwise, e.g. 0.1, 999.5,
+ /// 1e+03, 6.55e+04, 6e-08, -0.0, inf, nan.
+ ///
+ /// Note: repr(Half.MaxValue) == '6.55e+04' and Half('6.55e+04') == Half.MaxValue
+ /// although float(Half.MaxValue) == 65504.
+ ///
+ public static string __repr__(Half self) {
+ if (Half.IsNaN(self)) return "nan";
+ if (Half.IsPositiveInfinity(self)) return "inf";
+ if (Half.IsNegativeInfinity(self)) return "-inf";
+
+ // .NET produces the shortest round-trippable digits, in the form [-]ddd[.ddd][E-dd]
+ string r = self.ToString("R", CultureInfo.InvariantCulture);
+ bool isNegative = r[0] == '-';
+ if (isNegative) r = r.Substring(1);
+
+ int exp = 0;
+ int ePos = r.IndexOf('E');
+ if (ePos >= 0) {
+ exp = int.Parse(r.Substring(ePos + 1), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
+ r = r.Substring(0, ePos);
+ }
+ int dotPos = r.IndexOf('.');
+ if (dotPos < 0) dotPos = r.Length;
+
+ // value is 0.digits * 10**decPoint
+ string digits = r.Remove(dotPos, Math.Min(1, r.Length - dotPos));
+ int decPoint = dotPos + exp;
+ int numLeadingZeros = digits.Length - digits.TrimStart('0').Length;
+ digits = digits.Trim('0');
+ decPoint -= numLeadingZeros;
+
+ var sb = new StringBuilder(16);
+ if (isNegative) sb.Append('-');
+
+ double abs = Math.Abs((double)self);
+ if (digits.Length == 0) {
+ sb.Append("0.0");
+ } else if (1e-4 <= abs && abs < 1e3) {
+ if (decPoint <= 0) {
+ sb.Append("0.").Append('0', -decPoint).Append(digits);
+ } else if (decPoint >= digits.Length) {
+ sb.Append(digits).Append('0', decPoint - digits.Length).Append(".0");
+ } else {
+ sb.Append(digits, 0, decPoint).Append('.').Append(digits, decPoint, digits.Length - decPoint);
+ }
+ } else {
+ sb.Append(digits[0]);
+ if (digits.Length > 1) sb.Append('.').Append(digits, 1, digits.Length - 1);
+ int sciExp = decPoint - 1;
+ sb.Append(sciExp < 0 ? "e-" : "e+").Append(Math.Abs(sciExp).ToString("00", CultureInfo.InvariantCulture));
+ }
+ return sb.ToString();
+ }
+
+ private static void CheckHalfType(PythonType cls) {
+ if (cls != DynamicHelpers.GetPythonTypeFromType(typeof(Half))) {
+ throw PythonOps.TypeError("Half.__new__: first argument must be Half type.");
+ }
+ }
+ }
+}
+
+#endif
diff --git a/tests/suite/test_number.py b/tests/suite/test_number.py
index 193a46a17..d046fe259 100644
--- a/tests/suite/test_number.py
+++ b/tests/suite/test_number.py
@@ -5,7 +5,7 @@
import sys
import unittest
-from iptest import IronPythonTestCase, is_cli, big, run_test, skipUnlessIronPython
+from iptest import IronPythonTestCase, is_cli, is_netcoreapp, is_netstandard, big, run_test, skipUnlessIronPython
def get_builtins_dict():
if type(__builtins__) is type(sys):
@@ -248,6 +248,204 @@ def __pos__(self):
self.assertEqual(int(Single(3.14159)), 3)
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_new(self):
+ import clr
+ from System import Half, Single, Int32
+
+ self.assertIsInstance(Half(), Half)
+ self.assertEqual(Half(), clr.Convert(0.0, Half))
+ self.assertIsInstance(Half(1.5), Half)
+ self.assertEqual(Half(1.5), clr.Convert(1.5, Half))
+ self.assertEqual(Half(3), clr.Convert(3.0, Half))
+ self.assertEqual(Half(big(3)), clr.Convert(3.0, Half))
+ self.assertEqual(Half(Int32(3)), clr.Convert(3.0, Half))
+ self.assertEqual(Half(Single(2.5)), clr.Convert(2.5, Half))
+ self.assertEqual(Half(clr.Convert(2.5, Half)), clr.Convert(2.5, Half))
+ self.assertEqual(Half("2.5"), clr.Convert(2.5, Half))
+ self.assertEqual(Half(1e10), Half.PositiveInfinity)
+ self.assertTrue(Half.IsNaN(Half("nan")))
+
+ self.assertRaises(TypeError, Half, 1.0, 2.0)
+ self.assertRaises(TypeError, Half, None)
+ self.assertRaises(ValueError, Half, "abc")
+
+ self.assertRaises(TypeError, Half.__new__, float)
+ self.assertRaises(TypeError, Half.__new__, float, 1.0)
+ self.assertRaises(TypeError, Half.__new__, Single, 1.0)
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_bool(self):
+ from System import Half
+
+ # like float: false only when equal to zero, so NaN is true
+ for h in [Half(0), Half(-0.0)]:
+ self.assertIs(bool(h), False)
+ self.assertIs(not h, True)
+ self.assertEqual(h or 'alt', 'alt')
+ self.assertIs(h and 'alt', h)
+
+ for h in [Half(1), Half(-1.5), Half.Epsilon, Half.MaxValue, Half.MinValue,
+ Half.PositiveInfinity, Half.NegativeInfinity, Half.NaN]:
+ self.assertIs(bool(h), True)
+ self.assertIs(not h, False)
+ self.assertIs(h or 'alt', h)
+ self.assertEqual(h and 'alt', 'alt')
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_copy_pickle(self):
+ import copy, math, pickle
+ from System import Half
+
+ values = [Half(0), Half(-0.0), Half(1.5), Half(-2.25), Half(0.1), Half.MaxValue, Half.MinValue, Half.Epsilon,
+ Half.PositiveInfinity, Half.NegativeInfinity]
+
+ self.assertEqual(Half(1.5).__getnewargs__(), (1.5,))
+ self.assertIs(type(Half(1.5).__getnewargs__()[0]), float)
+
+ for h in values:
+ clones = [copy.copy(h), copy.deepcopy(h)]
+ # like other CLR numeric types, Half cannot be pickled with protocols 0 and 1
+ clones += [pickle.loads(pickle.dumps(h, protocol)) for protocol in range(2, pickle.HIGHEST_PROTOCOL + 1)]
+ for clone in clones:
+ self.assertIs(type(clone), Half)
+ self.assertEqual(clone, h)
+ self.assertEqual(math.copysign(1, clone), math.copysign(1, h))
+
+ for clone in [copy.copy(Half.NaN), pickle.loads(pickle.dumps(Half.NaN))]:
+ self.assertIs(type(clone), Half)
+ self.assertTrue(Half.IsNaN(clone))
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_format(self):
+ from System import Half
+
+ # Half is formatted like float with the same (exact) value
+ values = [Half(0.1), Half(-0.0), Half(1.5), Half(-2.25), Half(1/3), Half.MaxValue, Half.MinValue, Half.Epsilon,
+ Half.PositiveInfinity, Half.NegativeInfinity, Half.NaN]
+ specs = ['.2f', '10.3f', '<10', '^12.4e', '+g', '.3g', 'G', '.1%', ',.1f', 'e', '010.2f', ' f']
+ for h in values:
+ for spec in specs:
+ self.assertEqual(format(h, spec), format(float(h), spec))
+
+ self.assertEqual(format(Half(0.1), '.2f'), '0.10')
+ self.assertEqual(format(Half(0.1), '.6f'), '0.099976')
+ self.assertEqual(format(Half(-1.5), '+.3e'), '-1.500e+00')
+ self.assertEqual(format(Half(65504), ',.1f'), '65,504.0')
+ self.assertEqual(format(Half(2.5), '*^9.1f'), '***2.5***')
+ self.assertEqual('{:.1f}'.format(Half(2.375)), '2.4')
+ self.assertEqual(f'{Half(2.25):>6.2f}', ' 2.25')
+
+ # empty format spec is the same as str()
+ for h in values:
+ self.assertEqual(format(h, ''), str(h))
+ self.assertEqual(f'{h}', str(h))
+
+ self.assertRaises(ValueError, format, Half(1.5), 's')
+ self.assertRaises(ValueError, format, Half(1.5), 'x')
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_str_repr(self):
+ from System import Half, BitConverter, Int16
+ from System.Globalization import CultureInfo
+ from System.Threading import Thread
+
+ # expected values are the same as str(numpy.float16(value))
+ cases = [
+ (0.0, '0.0'),
+ (-0.0, '-0.0'),
+ (1.0, '1.0'),
+ (-1.5, '-1.5'),
+ (0.1, '0.1'),
+ (1/3, '0.3333'),
+ (3.0, '3.0'),
+ (100.0, '100.0'),
+ (999.5, '999.5'),
+ (1000.0, '1e+03'),
+ (2048.0, '2.048e+03'),
+ (2049.0, '2.048e+03'),
+ (12345.0, '1.2344e+04'),
+ (65504.0, '6.55e+04'),
+ (-65504.0, '-6.55e+04'),
+ (0.0001, '0.0001'),
+ (0.00011, '0.00011'),
+ (9.99e-05, '9.99e-05'),
+ (6.103515625e-05, '6.104e-05'),
+ (5.960464477539063e-08, '6e-08'),
+ (1.0009765625, '1.001'),
+ (float('inf'), 'inf'),
+ (float('-inf'), '-inf'),
+ (float('nan'), 'nan'),
+ ]
+ culture = Thread.CurrentThread.CurrentCulture
+ try:
+ for c in [culture, CultureInfo.InvariantCulture, CultureInfo('de-DE')]:
+ Thread.CurrentThread.CurrentCulture = c
+ for value, expected in cases:
+ self.assertEqual(str(Half(value)), expected)
+ self.assertEqual(repr(Half(value)), expected)
+ finally:
+ Thread.CurrentThread.CurrentCulture = culture
+
+ self.assertEqual(str(-Half.NaN), 'nan')
+ self.assertEqual(str([Half(0.5), Half(-2)]), '[0.5, -2.0]')
+
+ # every value round-trips through str, and with no more digits than necessary
+ for bits in range(-0x8000, 0x8000):
+ h = BitConverter.Int16BitsToHalf(Int16(bits))
+ if not Half.IsFinite(h): continue
+ s = str(h)
+ self.assertEqual(Half(s), h)
+ mantissa = s.lstrip('-').split('e')[0]
+ digits = mantissa.replace('.', '').strip('0')
+ if len(digits) > 1:
+ shorter = '{:.{}e}'.format(float(h), len(digits) - 2)
+ self.assertNotEqual(Half(shorter), h, (s, shorter))
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_math_abs(self):
+ import math
+ from System import Half, Math
+
+ # Math has no Abs(Half); the lossless Abs(Double) must be selected, not Abs(Int32)
+ for value, expected in [(1.5, 1.5), (-2.75, 2.75), (-0.0, 0.0), (65504, 65504.0), (-1e5, math.inf)]:
+ res = Math.Abs(Half(value))
+ self.assertIs(type(res), float)
+ self.assertEqual(res, expected)
+
+ res = Half.Abs(Half(value))
+ self.assertIs(type(res), Half)
+ self.assertEqual(res, Half(expected))
+
+ self.assertTrue(math.isnan(Math.Abs(Half('nan'))))
+
+ @unittest.skipUnless(is_netcoreapp and not is_netstandard, "System.Half requires .NET 5+")
+ def test_half_real_imag_conjugate(self):
+ import math
+ from System import Half
+
+ for value in [1.5, -2.25, 0.0, -0.0, 65504, math.inf, -math.inf]:
+ h = Half(value)
+ for res in (h.real, h.conjugate()):
+ self.assertIs(type(res), Half)
+ self.assertEqual(res, h)
+ self.assertEqual(math.copysign(1, res), math.copysign(1, h))
+ self.assertIs(type(h.imag), Half)
+ self.assertEqual(h.imag, Half(0))
+ self.assertEqual(math.copysign(1, h.imag), 1)
+ self.assertEqual(complex(h.real, h.imag), complex(float(h)))
+
+ nan = Half('nan')
+ self.assertTrue(Half.IsNaN(nan.real))
+ self.assertTrue(Half.IsNaN(nan.conjugate()))
+ self.assertEqual(nan.imag, Half(0))
+
+ # real and imag are read-only
+ with self.assertRaises(AttributeError):
+ Half(1).real = Half(2)
+ with self.assertRaises(AttributeError):
+ Half(1).imag = Half(2)
+
#TODO: @skip("interpreted") #Too slow
def test_operators(self):
def operator_add(a, b) :