Skip to content

Commit 7c8b200

Browse files
committed
gh-143132: Add math.sign() function
Implementation includes IEEE 754 support, duck-typing, and a test suite.
1 parent 5989095 commit 7c8b200

File tree

4 files changed

+234
-1
lines changed

4 files changed

+234
-1
lines changed

Doc/library/math.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ noted otherwise, all return values are floats.
3838
:func:`fmod(x, y) <fmod>` Remainder of division ``x / y``
3939
:func:`modf(x) <modf>` Fractional and integer parts of *x*
4040
:func:`remainder(x, y) <remainder>` Remainder of *x* with respect to *y*
41+
:func:`sign(x) <sign>` Sign of *x*, indicating whether *x* is negative, zero, or positive
4142
:func:`trunc(x) <trunc>` Integer part of *x*
4243

4344
**Floating point manipulation functions**
@@ -226,6 +227,21 @@ Floating point arithmetic
226227

227228
.. versionadded:: 3.7
228229

230+
.. function:: sign(x)
231+
232+
Return the sign of *x*: ``-1`` if *x < 0*, ``0`` if *x == 0*, and ``1`` if *x > 0*.
233+
234+
The function delegates to the object's rich comparison operators. This
235+
allows it to work with various numeric types including :class:`int`,
236+
:class:`float`, :class:`fractions.Fraction`, and :class:`decimal.Decimal`.
237+
It is platform-independent, and works with any existing or future scalar type
238+
that internally supports numeric comparisons.
239+
240+
For ``NaN`` inputs, the function returns a float ``NaN``. For other arguments
241+
and any scalar numeric type, the result is always an :class:`int`. For non-numeric
242+
or non-scalar types, the function raises a :exc:`TypeError`.
243+
244+
.. versionadded:: 3.15
229245

230246
.. function:: trunc(x)
231247

Lib/test/test_math.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,107 @@ class TestBadFloor:
553553
self.assertEqual(math.floor(FloatLike(+1.0)), +1.0)
554554
self.assertEqual(math.floor(FloatLike(-1.0)), -1.0)
555555

556+
@requires_IEEE_754
557+
def testSign(self):
558+
# For compactness
559+
sign = math.sign
560+
isnan = math.isnan
561+
inf = math.inf
562+
Frac = fractions.Fraction
563+
Dec = decimal.Decimal
564+
565+
# --- int
566+
self.assertEqual(sign(-5), -1)
567+
self.assertEqual(sign(-1), -1)
568+
self.assertEqual(sign(0), 0)
569+
self.assertEqual(sign(1), 1)
570+
self.assertEqual(sign(5), 1)
571+
# ------ bool
572+
self.assertEqual(sign(True), 1)
573+
self.assertEqual(sign(False), 0)
574+
# ------ big numbers
575+
self.assertEqual(sign(10**1000), 1)
576+
self.assertEqual(sign(-10**1000), -1)
577+
self.assertEqual(sign(10**1000-10**1000), 0)
578+
579+
# --- float
580+
self.assertEqual(sign(-5.0), -1)
581+
self.assertEqual(sign(-1.0), -1)
582+
self.assertEqual(sign(0.0), 0)
583+
self.assertEqual(sign(1.0), 1)
584+
self.assertEqual(sign(5.0), 1)
585+
# ------ -0.0 and +0.0
586+
self.assertEqual(sign(float('-0.0')), 0)
587+
self.assertEqual(sign(float('+0.0')), 0)
588+
# ------ -inf and inf
589+
self.assertEqual(sign(NINF), -1)
590+
self.assertEqual(sign(INF), 1)
591+
# ------ -nan (the same as nan), nan
592+
self.assertTrue(isnan(sign(NNAN)))
593+
self.assertTrue(isnan(sign(NAN)))
594+
self.assertTrue(isnan(sign(0.0*NAN)))
595+
596+
# --- Fraction
597+
self.assertEqual(sign(Frac(-5, 2)), -1)
598+
self.assertEqual(sign(Frac(-1, 2)), -1)
599+
self.assertEqual(sign(Frac(0, 2)), 0)
600+
self.assertEqual(sign(Frac(1, 2)), 1)
601+
self.assertEqual(sign(Frac(5, 2)), 1)
602+
603+
# --- Decimal
604+
self.assertEqual(sign(Dec(-5.5)), -1)
605+
self.assertEqual(sign(Dec(-1.5)), -1)
606+
self.assertEqual(sign(Dec(0.0)), 0)
607+
self.assertEqual(sign(Dec(1.5)), 1)
608+
self.assertEqual(sign(Dec(5.5)), 1)
609+
# ------ Decimal NaN
610+
self.assertTrue(isnan(sign(Dec('NaN'))))
611+
612+
# --- New custom class (testing possible future extentions)
613+
# This class has no __float__ that tests one subtle branch in the CPython sign
614+
class MyNumber:
615+
def __init__(self, value):
616+
self.value = value
617+
def __gt__(self, other):
618+
return self.value > other
619+
def __lt__(self, other):
620+
return self.value < other
621+
def __eq__(self, other):
622+
return self.value == other
623+
def __repr__(self):
624+
return f'MyNumber({self.value})'
625+
626+
self.assertEqual(sign(MyNumber(-5)), -1)
627+
self.assertEqual(sign(MyNumber(-1)), -1)
628+
self.assertEqual(sign(MyNumber(0)), 0)
629+
self.assertEqual(sign(MyNumber(1)), 1)
630+
self.assertEqual(sign(MyNumber(5)), 1)
631+
with self.assertRaisesRegex(TypeError, r'math\.sign: invalid argument `MyNumber\(nan\)`'):
632+
sign(MyNumber(NAN))
633+
634+
# Testing inappropriate arguments and types (non-scalar, non-comparable, etc.)
635+
# --- No arguments and three arguments
636+
with self.assertRaisesRegex(TypeError, r'math\.sign\(\) takes exactly one argument \(0 given\)'):
637+
sign()
638+
with self.assertRaisesRegex(TypeError, r'math\.sign\(\) takes exactly one argument \(3 given\)'):
639+
sign(-1, 0, 1)
640+
641+
# --- None, str, list, complex, set
642+
tests = [(r"`None`", None),
643+
(r"`'5\.0'`", '5.0'),
644+
(r"`'nan'`", 'nan'),
645+
(r"`'number 5'`", 'number 5'),
646+
(r"`\[-8\.75\]`", [-8.75]),
647+
(r"`\(-1\+1j\)`", -1+1j),
648+
(r"`\{-3\.14\}`", {-3.14}),
649+
]
650+
651+
for msg, obj in tests:
652+
with self.subTest(obj=obj):
653+
with self.assertRaisesRegex(TypeError,
654+
r'math\.sign: invalid argument ' + msg):
655+
sign(obj)
656+
556657
def testFmod(self):
557658
self.assertRaises(TypeError, math.fmod)
558659
self.ftest('fmod(10, 1)', math.fmod(10, 1), 0.0)

Modules/clinic/mathmodule.c.h

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/mathmodule.c

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,110 @@ math_floor(PyObject *module, PyObject *number)
10811081
return PyLong_FromDouble(floor(x));
10821082
}
10831083

1084+
/*[clinic input]
1085+
math.sign
1086+
1087+
x: object
1088+
/
1089+
1090+
Return the sign of x: -1 if x < 0, 0 if x == 0, 1 if x > 0.
1091+
1092+
For NaN inputs, return a float NaN.
1093+
[clinic start generated code]*/
1094+
1095+
static PyObject *
1096+
math_sign(PyObject *module, PyObject *x)
1097+
/*[clinic end generated code: output=83b2f9bfbf48c222 input=b07f2b4af6d01e62]*/
1098+
{
1099+
/* Check for numeric NaN */
1100+
double d = PyFloat_AsDouble(x);
1101+
if (Py_IS_NAN(d)) return PyFloat_FromDouble(Py_NAN);
1102+
/* If it is something special, we will try comparisons */
1103+
if (PyErr_Occurred()) PyErr_Clear();
1104+
1105+
PyObject* zero = _PyLong_GetZero();
1106+
1107+
int gt = PyObject_RichCompareBool(x, zero, Py_GT) + 1; /* 2: True; 1: False; 0: Error */
1108+
int lt = PyObject_RichCompareBool(x, zero, Py_LT) + 1;
1109+
int res = gt - lt; /* Result, if nothing special */
1110+
int eq = PyObject_RichCompareBool(x, zero, Py_EQ) + 1;
1111+
1112+
/* gt, lt, eq can be 0, 1, 2; let them be digits in the ternary number system */
1113+
/* code = 9*gt+3*lt+eq is the value of the number written in the ternary system
1114+
with these digits */
1115+
/* We also use 9 = 8+1, 3 = 4-1, and replace multiplication by 8 and 4 with shift */
1116+
int code = (gt << 3) + (lt << 2) + eq + res;
1117+
/* (gt<<3) = 8*gt; (lt<<2) = 4*gt; code = 8*gt + 4*lt + eq + (gt-lt) = 9*gt+3*lt+eq */
1118+
1119+
switch (code) {
1120+
case 13: { /* 111₃ -> 8+4+1+0: possible NaN (False, False, False) */
1121+
int self_eq = PyObject_RichCompareBool(x, x, Py_EQ);
1122+
if (self_eq == 0) return PyFloat_FromDouble(Py_NAN); /* NaN: not equal to itself */
1123+
if (self_eq == -1) return NULL; /* Error in __eq__, we keep Python error */
1124+
goto error; /* Not a NaN, but not comparable to 0: go to TypeError */
1125+
}
1126+
case 14: /* 112₃ -> 8+4+2+0: x == 0 (res= 0) (False, False, True ) */
1127+
case 16: /* 121₃ -> 8+8+1-1: x < 0 (res=-1) (False, True, False) */
1128+
case 22: /* 211₃ -> 16+4+1+1: x > 0 (res= 1) (True, False, False) */
1129+
return PyLong_FromLong((long)res);
1130+
default: /* No more valid cases */
1131+
goto error;
1132+
}
1133+
1134+
error:
1135+
if (PyErr_Occurred()) {
1136+
PyObject *type, *value, *traceback;
1137+
/* Extarct the current error (the error flow became empty) */
1138+
PyErr_Fetch(&type, &value, &traceback);
1139+
PyErr_NormalizeException(&type, &value, &traceback);
1140+
1141+
/* Prepare the argument details */
1142+
PyObject *repr = PyObject_Repr(x);
1143+
const char *type_name = Py_TYPE(x)->tp_name;
1144+
1145+
/* Prepare the old error as string */
1146+
PyObject *old_msg = PyObject_Str(value);
1147+
const char *old_msg_str = old_msg ? PyUnicode_AsUTF8(old_msg) : "unknown error";
1148+
1149+
/* Form the new message
1150+
PyErr_Format will clean footprints of errors from PyObject_Str if any */
1151+
PyErr_Format(PyExc_TypeError,
1152+
"math.sign: invalid argument `%.160s` (type '%.80s'). "
1153+
"Inner error: %.320s",
1154+
repr ? PyUnicode_AsUTF8(repr) : "???",
1155+
type_name,
1156+
old_msg_str);
1157+
1158+
/* Clean memory */
1159+
Py_XDECREF(repr);
1160+
Py_XDECREF(old_msg);
1161+
Py_XDECREF(type);
1162+
Py_XDECREF(value);
1163+
Py_XDECREF(traceback);
1164+
}
1165+
else {
1166+
PyObject *repr = PyObject_Repr(x);
1167+
const char *type_name = Py_TYPE(x)->tp_name;
1168+
1169+
if (repr) {
1170+
PyErr_Format(PyExc_TypeError,
1171+
"math.sign: invalid argument `%.160s`. "
1172+
"Type '%.80s' does not support order comparisons (>, <, ==) "
1173+
"or NaN detection.",
1174+
PyUnicode_AsUTF8(repr),
1175+
type_name);
1176+
Py_DECREF(repr);
1177+
}
1178+
else {
1179+
PyErr_Format(PyExc_TypeError,
1180+
"math.sign: invalid argument of type '%.80s', "
1181+
"which does not support order comparisons (>, <, ==) and printing.",
1182+
type_name);
1183+
}
1184+
}
1185+
return NULL;
1186+
}
1187+
10841188
/*[clinic input]
10851189
math.fmax -> double
10861190
@@ -3088,6 +3192,7 @@ static PyMethodDef math_methods[] = {
30883192
MATH_POW_METHODDEF
30893193
MATH_RADIANS_METHODDEF
30903194
{"remainder", _PyCFunction_CAST(math_remainder), METH_FASTCALL, math_remainder_doc},
3195+
MATH_SIGN_METHODDEF
30913196
MATH_SIGNBIT_METHODDEF
30923197
{"sin", math_sin, METH_O, math_sin_doc},
30933198
{"sinh", math_sinh, METH_O, math_sinh_doc},

0 commit comments

Comments
 (0)