From 0c6322c62ab513e7d85b40747864179bd745a479 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Wed, 17 Dec 2025 11:39:36 +0530 Subject: [PATCH 1/9] initial stringdtype cast support --- quaddtype/numpy_quaddtype/src/casts.cpp | 261 +++++++++++++++++++++++- 1 file changed, 260 insertions(+), 1 deletion(-) diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index 659d6daf..579e296b 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -27,7 +27,7 @@ extern "C" { #include "dragon4.h" #include "ops.hpp" -#define NUM_CASTS 38 // 17 to_casts + 17 from_casts + 1 quad_to_quad + 1 void_to_quad +#define NUM_CASTS 40 // 17 to_casts + 17 from_casts + 1 quad_to_quad + 1 void_to_quad + 2 StringDType #define QUAD_STR_WIDTH 50 // 42 is enough for scientific notation float128, just keeping some buffer static NPY_CASTING @@ -605,6 +605,227 @@ quad_to_bytes_loop(PyArrayMethod_Context *context, char *const data[], return 0; } +// StringDType to QuadDType casting +static NPY_CASTING +stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], + PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) +{ + Py_INCREF(given_descrs[0]); + loop_descrs[0] = given_descrs[0]; + + if (given_descrs[1] == NULL) { + loop_descrs[1] = (PyArray_Descr *)new_quaddtype_instance(BACKEND_SLEEF); + if (loop_descrs[1] == nullptr) { + Py_DECREF(loop_descrs[0]); + return (NPY_CASTING)-1; + } + } + else { + Py_INCREF(given_descrs[1]); + loop_descrs[1] = given_descrs[1]; + } + + return NPY_UNSAFE_CASTING; +} + +static int +stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], + npy_intp const dimensions[], npy_intp const strides[], + void *NPY_UNUSED(auxdata)) +{ + npy_intp N = dimensions[0]; + char *in_ptr = data[0]; + char *out_ptr = data[1]; + npy_intp in_stride = strides[0]; + npy_intp out_stride = strides[1]; + + PyArray_Descr *const *descrs = context->descriptors; + PyArray_StringDTypeObject *str_descr = (PyArray_StringDTypeObject *)descrs[0]; + QuadPrecDTypeObject *descr_out = (QuadPrecDTypeObject *)descrs[1]; + QuadBackendType backend = descr_out->backend; + + npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr); + + while (N--) { + const npy_packed_static_string *ps = (npy_packed_static_string *)in_ptr; + npy_static_string s = {0, NULL}; + int is_null = NpyString_load(allocator, ps, &s); + + if (is_null == -1) { + NpyString_release_allocator(allocator); + PyErr_SetString(PyExc_MemoryError, "Failed to load string in StringDType to Quad cast"); + return -1; + } + else if (is_null) { + // Handle null string - use the default string if available, otherwise error + if (str_descr->has_string_na || str_descr->default_string.buf != NULL) { + s = str_descr->default_string; + } + else { + NpyString_release_allocator(allocator); + PyErr_SetString(PyExc_ValueError, "Cannot convert null string to QuadPrecision"); + return -1; + } + } + + // Create a null-terminated copy of the string + char *temp_str = (char *)malloc(s.size + 1); + if (temp_str == NULL) { + NpyString_release_allocator(allocator); + PyErr_NoMemory(); + return -1; + } + memcpy(temp_str, s.buf, s.size); + temp_str[s.size] = '\0'; + + char *endptr; + quad_value out_val; + int err = NumPyOS_ascii_strtoq(temp_str, backend, &out_val, &endptr); + + if (err < 0) { + PyErr_Format(PyExc_ValueError, + "could not convert string to QuadPrecision: '%s'", temp_str); + free(temp_str); + NpyString_release_allocator(allocator); + return -1; + } + + // Check that we parsed the entire string (skip trailing whitespace) + while (ascii_isspace(*endptr)) { + endptr++; + } + + if (*endptr != '\0') { + PyErr_Format(PyExc_ValueError, + "could not convert string to QuadPrecision: '%s'", temp_str); + free(temp_str); + NpyString_release_allocator(allocator); + return -1; + } + + free(temp_str); + + // Store the result - StringDType elements are always aligned + memcpy(out_ptr, &out_val, sizeof(quad_value)); + + in_ptr += in_stride; + out_ptr += out_stride; + } + + NpyString_release_allocator(allocator); + return 0; +} + +// QuadDType to StringDType casting +static NPY_CASTING +quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *dtypes[2], + PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], + npy_intp *view_offset) +{ + Py_INCREF(given_descrs[0]); + loop_descrs[0] = given_descrs[0]; + + if (given_descrs[1] == NULL) { + // Create a new StringDType instance with coercion enabled + PyObject *args = PyTuple_New(0); + if (args == NULL) { + Py_DECREF(loop_descrs[0]); + return (NPY_CASTING)-1; + } + PyObject *kwargs = PyDict_New(); + if (kwargs == NULL) { + Py_DECREF(args); + Py_DECREF(loop_descrs[0]); + return (NPY_CASTING)-1; + } + // Set coerce=True for the new instance + if (PyDict_SetItemString(kwargs, "coerce", Py_True) < 0) { + Py_DECREF(args); + Py_DECREF(kwargs); + Py_DECREF(loop_descrs[0]); + return (NPY_CASTING)-1; + } + + loop_descrs[1] = (PyArray_Descr *)PyObject_Call( + (PyObject *)&PyArray_StringDType, args, kwargs); + Py_DECREF(args); + Py_DECREF(kwargs); + + if (loop_descrs[1] == NULL) { + Py_DECREF(loop_descrs[0]); + return (NPY_CASTING)-1; + } + } + else { + Py_INCREF(given_descrs[1]); + loop_descrs[1] = given_descrs[1]; + } + + return NPY_SAFE_CASTING; +} + +static int +quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const data[], + npy_intp const dimensions[], npy_intp const strides[], + void *NPY_UNUSED(auxdata)) +{ + npy_intp N = dimensions[0]; + char *in_ptr = data[0]; + char *out_ptr = data[1]; + npy_intp in_stride = strides[0]; + npy_intp out_stride = strides[1]; + + PyArray_Descr *const *descrs = context->descriptors; + QuadPrecDTypeObject *descr_in = (QuadPrecDTypeObject *)descrs[0]; + PyArray_StringDTypeObject *str_descr = (PyArray_StringDTypeObject *)descrs[1]; + QuadBackendType backend = descr_in->backend; + + npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr); + + while (N--) { + // Load the quad value - StringDType elements are always aligned + quad_value in_val; + memcpy(&in_val, in_ptr, sizeof(quad_value)); + + // Convert to Sleef_quad for Dragon4 + Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend); + + // Get string representation with adaptive notation + // Use a large buffer size to allow for full precision + PyObject *py_str = quad_to_string_adaptive(&sleef_val, QUAD_STR_WIDTH); + if (py_str == NULL) { + NpyString_release_allocator(allocator); + return -1; + } + + Py_ssize_t str_size; + const char *str_buf = PyUnicode_AsUTF8AndSize(py_str, &str_size); + if (str_buf == NULL) { + Py_DECREF(py_str); + NpyString_release_allocator(allocator); + return -1; + } + + // Pack the string into the output + npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr; + if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) { + Py_DECREF(py_str); + NpyString_release_allocator(allocator); + PyErr_SetString(PyExc_MemoryError, "Failed to pack string in Quad to StringDType cast"); + return -1; + } + + Py_DECREF(py_str); + + in_ptr += in_stride; + out_ptr += out_stride; + } + + NpyString_release_allocator(allocator); + return 0; +} + // Tag dispatching to ensure npy_bool/npy_ubyte and npy_half/npy_ushort do not alias in templates // see e.g. https://stackoverflow.com/q/32522279 struct spec_npy_bool {}; @@ -1395,6 +1616,44 @@ init_casts_internal(void) }; add_spec(quad_to_bytes_spec); + // StringDType to QuadPrecision cast + PyArray_DTypeMeta **stringdtype_to_quad_dtypes = new PyArray_DTypeMeta *[2]{&PyArray_StringDType, &QuadPrecDType}; + PyType_Slot *stringdtype_to_quad_slots = new PyType_Slot[4]{ + {NPY_METH_resolve_descriptors, (void *)&stringdtype_to_quad_resolve_descriptors}, + {NPY_METH_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, + {0, nullptr}}; + + PyArrayMethod_Spec *stringdtype_to_quad_spec = new PyArrayMethod_Spec{ + .name = "cast_StringDType_to_QuadPrec", + .nin = 1, + .nout = 1, + .casting = NPY_UNSAFE_CASTING, + .flags = static_cast(NPY_METH_SUPPORTS_UNALIGNED | NPY_METH_REQUIRES_PYAPI), + .dtypes = stringdtype_to_quad_dtypes, + .slots = stringdtype_to_quad_slots, + }; + add_spec(stringdtype_to_quad_spec); + + // QuadPrecision to StringDType cast + PyArray_DTypeMeta **quad_to_stringdtype_dtypes = new PyArray_DTypeMeta *[2]{&QuadPrecDType, &PyArray_StringDType}; + PyType_Slot *quad_to_stringdtype_slots = new PyType_Slot[4]{ + {NPY_METH_resolve_descriptors, (void *)&quad_to_stringdtype_resolve_descriptors}, + {NPY_METH_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, + {0, nullptr}}; + + PyArrayMethod_Spec *quad_to_stringdtype_spec = new PyArrayMethod_Spec{ + .name = "cast_QuadPrec_to_StringDType", + .nin = 1, + .nout = 1, + .casting = NPY_SAFE_CASTING, + .flags = static_cast(NPY_METH_SUPPORTS_UNALIGNED | NPY_METH_REQUIRES_PYAPI), + .dtypes = quad_to_stringdtype_dtypes, + .slots = quad_to_stringdtype_slots, + }; + add_spec(quad_to_stringdtype_spec); + specs[spec_count] = nullptr; return specs; } From 970b62ca8960fdcba14657614a33ebfe59e34274 Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 19 Dec 2025 15:02:23 +0000 Subject: [PATCH 2/9] adding tests --- quaddtype/numpy_quaddtype/src/casts.cpp | 29 ++-- quaddtype/tests/test_quaddtype.py | 183 ++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 16 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index 579e296b..68710000 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -626,9 +626,13 @@ stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp loop_descrs[1] = given_descrs[1]; } + // no notion of fix length, so always unsafe return NPY_UNSAFE_CASTING; } +// Note: StringDType elements are always aligned, so Aligned template parameter +// is kept for API consistency but both versions use the same logic +template static int stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[], npy_intp const dimensions[], npy_intp const strides[], @@ -669,7 +673,6 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat } } - // Create a null-terminated copy of the string char *temp_str = (char *)malloc(s.size + 1); if (temp_str == NULL) { NpyString_release_allocator(allocator); @@ -691,7 +694,6 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat return -1; } - // Check that we parsed the entire string (skip trailing whitespace) while (ascii_isspace(*endptr)) { endptr++; } @@ -706,8 +708,7 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat free(temp_str); - // Store the result - StringDType elements are always aligned - memcpy(out_ptr, &out_val, sizeof(quad_value)); + store_quad(out_ptr, out_val, backend); in_ptr += in_stride; out_ptr += out_stride; @@ -727,7 +728,6 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp loop_descrs[0] = given_descrs[0]; if (given_descrs[1] == NULL) { - // Create a new StringDType instance with coercion enabled PyObject *args = PyTuple_New(0); if (args == NULL) { Py_DECREF(loop_descrs[0]); @@ -739,7 +739,6 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp Py_DECREF(loop_descrs[0]); return (NPY_CASTING)-1; } - // Set coerce=True for the new instance if (PyDict_SetItemString(kwargs, "coerce", Py_True) < 0) { Py_DECREF(args); Py_DECREF(kwargs); @@ -765,6 +764,9 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp return NPY_SAFE_CASTING; } +// Note: StringDType elements are always aligned, so Aligned template parameter +// is kept for API consistency but both versions use the same logic +template static int quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const data[], npy_intp const dimensions[], npy_intp const strides[], @@ -784,11 +786,7 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr); while (N--) { - // Load the quad value - StringDType elements are always aligned - quad_value in_val; - memcpy(&in_val, in_ptr, sizeof(quad_value)); - - // Convert to Sleef_quad for Dragon4 + quad_value in_val = load_quad(in_ptr, backend); Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend); // Get string representation with adaptive notation @@ -807,7 +805,6 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat return -1; } - // Pack the string into the output npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr; if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) { Py_DECREF(py_str); @@ -1620,8 +1617,8 @@ init_casts_internal(void) PyArray_DTypeMeta **stringdtype_to_quad_dtypes = new PyArray_DTypeMeta *[2]{&PyArray_StringDType, &QuadPrecDType}; PyType_Slot *stringdtype_to_quad_slots = new PyType_Slot[4]{ {NPY_METH_resolve_descriptors, (void *)&stringdtype_to_quad_resolve_descriptors}, - {NPY_METH_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, - {NPY_METH_unaligned_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, + {NPY_METH_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&stringdtype_to_quad_strided_loop}, {0, nullptr}}; PyArrayMethod_Spec *stringdtype_to_quad_spec = new PyArrayMethod_Spec{ @@ -1639,8 +1636,8 @@ init_casts_internal(void) PyArray_DTypeMeta **quad_to_stringdtype_dtypes = new PyArray_DTypeMeta *[2]{&QuadPrecDType, &PyArray_StringDType}; PyType_Slot *quad_to_stringdtype_slots = new PyType_Slot[4]{ {NPY_METH_resolve_descriptors, (void *)&quad_to_stringdtype_resolve_descriptors}, - {NPY_METH_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, - {NPY_METH_unaligned_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, + {NPY_METH_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, + {NPY_METH_unaligned_strided_loop, (void *)&quad_to_stringdtype_strided_loop}, {0, nullptr}}; PyArrayMethod_Spec *quad_to_stringdtype_spec = new PyArrayMethod_Spec{ diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 1585f6fd..8b225257 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -747,6 +747,189 @@ def test_empty_bytes_raises_error(self): with pytest.raises(ValueError): bytes_array.astype(QuadPrecDType()) + +class TestStringDTypeCasting: + @pytest.mark.parametrize("input_val", [ + "3.141592653589793238462643383279502884197", + "2.71828182845904523536028747135266249775", + "1.0", + "-1.0", + "0.0", + "-0.0", + "1e100", + "1e-100", + "1.23456789012345678901234567890123456789", + "-9.87654321098765432109876543210987654321", + ]) + def test_stringdtype_to_quad_basic(self, input_val): + """Test basic StringDType to QuadPrecision conversion""" + str_array = np.array([input_val], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + + assert quad_array.dtype.name == "QuadPrecDType128" + expected = np.array([input_val], dtype=QuadPrecDType()) + np.testing.assert_array_equal(quad_array, expected) + + @pytest.mark.parametrize("input_val", [ + "3.1415926535897932384626433832795028", # pi to quad precision + "2.7182818284590452353602874713526623", # e to quad precision + "1.0e+100", # scientific notation (normalized form) + "1.0e-100", # scientific notation (normalized form) + "0.0", + "-0.0", + "inf", + "-inf", + "nan", + "1.0", + "-1.0", + "123.456", + "-123.456", + ]) + def test_stringdtype_roundtrip(self, input_val): + str_array = np.array([input_val], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + result_str_array = quad_array.astype(np.dtypes.StringDType()) + + np.testing.assert_array_equal(result_str_array, str_array) + + @pytest.mark.parametrize("original", [ + QuadPrecision("0.417022004702574000667425480060047"), + QuadPrecision("1.23456789012345678901234567890123456789"), + pytest.param(numpy_quaddtype.pi, id="pi"), + pytest.param(numpy_quaddtype.e, id="e"), + QuadPrecision("1e-100"), + QuadPrecision("1e100"), + QuadPrecision("-3.14159265358979323846264338327950288419"), + QuadPrecision("0.0"), + QuadPrecision("-0.0"), + QuadPrecision("1.0"), + QuadPrecision("-1.0"), + ]) + def test_quad_to_stringdtype_roundtrip(self, original): + """Test QuadPrecision -> StringDType -> QuadPrecision preserves value""" + quad_array = np.array([original], dtype=QuadPrecDType()) + str_array = quad_array.astype(np.dtypes.StringDType()) + reconstructed = str_array.astype(QuadPrecDType()) + + if np.isnan(original): + assert np.isnan(reconstructed[0]) + else: + np.testing.assert_array_equal(reconstructed, quad_array) + + # ============ Special Values Tests ============ + + @pytest.mark.parametrize("input_str,check_func", [ + ("inf", lambda x: np.isinf(float(x)) and float(x) > 0), + ("-inf", lambda x: np.isinf(float(x)) and float(x) < 0), + ("+inf", lambda x: np.isinf(float(x)) and float(x) > 0), + ("Inf", lambda x: np.isinf(float(x)) and float(x) > 0), + ("Infinity", lambda x: np.isinf(float(x)) and float(x) > 0), + ("-Infinity", lambda x: np.isinf(float(x)) and float(x) < 0), + ("INF", lambda x: np.isinf(float(x)) and float(x) > 0), + ("INFINITY", lambda x: np.isinf(float(x)) and float(x) > 0), + ]) + def test_stringdtype_infinity_variants(self, input_str, check_func): + """Test various infinity representations in StringDType""" + str_array = np.array([input_str], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + + assert check_func(quad_array[0]), f"Failed for {input_str}" + + @pytest.mark.parametrize("input_str", [ + "nan", "NaN", "NAN", "+nan", "-nan", + "nan()", "nan(123)", "NaN(payload)", + ]) + def test_stringdtype_nan_variants(self, input_str): + """Test various NaN representations in StringDType""" + str_array = np.array([input_str], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + + assert np.isnan(float(quad_array[0])), f"Expected NaN for {input_str}" + + def test_stringdtype_negative_zero(self): + neg_zero = QuadPrecision("-0.0") + quad_array = np.array([neg_zero], dtype=QuadPrecDType()) + assert np.signbit(quad_array[0]), "Input should have negative zero signbit" + str_array = quad_array.astype(np.dtypes.StringDType()) + assert str_array[0] == "-0.0", f"Expected '-0.0', got '{str_array[0]}'" + roundtrip = str_array.astype(QuadPrecDType()) + assert np.signbit(roundtrip[0]), "Signbit should be preserved after round-trip" + assert float(roundtrip[0]) == 0.0, "Value should be zero" + + # ============ Whitespace Handling Tests ============ + + @pytest.mark.parametrize("input_str,expected", [ + (" 3.14", "3.14"), + ("3.14 ", "3.14"), + (" 3.14 ", "3.14"), + ("\t3.14\t", "3.14"), + ("\n3.14\n", "3.14"), + (" \t\n 3.14 \t\n ", "3.14"), + ]) + def test_stringdtype_whitespace_handling(self, input_str, expected): + """Test that StringDType handles whitespace correctly""" + str_array = np.array([input_str], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + expected_quad = QuadPrecision(expected) + + np.testing.assert_array_equal(quad_array, np.array([expected_quad], dtype=QuadPrecDType())) + + @pytest.mark.parametrize("invalid_str", [ + "", + "not_a_number", + "abc123", + "1.23.45", + "1e", + "++1.0", + "--1.0", + "+-1.0", + "1.0abc", + "abc1.0", + "3.14ñ", + "π", + ]) + def test_stringdtype_invalid_input(self, invalid_str): + """Test that invalid StringDType input raises ValueError""" + str_array = np.array([invalid_str], dtype=np.dtypes.StringDType()) + + with pytest.raises(ValueError): + str_array.astype(QuadPrecDType()) + + + @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) + @pytest.mark.parametrize("input_str", [ + "1.0", + "-1.0", + "3.141592653589793238462643383279502884197", + "1e100", + "1e-100", + "0.0", + ]) + def test_stringdtype_backend_consistency(self, backend, input_str): + """Test that StringDType parsing works consistently across backends""" + str_array = np.array([input_str], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType(backend=backend)) + scalar_val = QuadPrecision(input_str, backend=backend) + np.testing.assert_array_equal(quad_array, np.array([scalar_val], dtype=QuadPrecDType(backend=backend))) + + def test_stringdtype_empty_array(self): + """Test conversion of empty StringDType array""" + str_array = np.array([], dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + np.testing.assert_array_equal(quad_array, np.array([], dtype=QuadPrecDType())) + + @pytest.mark.parametrize("size", [500, 1000, 10000]) + def test_stringdtype_large_array(self, size): + """Test conversion of large StringDType array""" + str_values = [str(i * 0.001) for i in range(size)] + str_array = np.array(str_values, dtype=np.dtypes.StringDType()) + quad_array = str_array.astype(QuadPrecDType()) + + assert quad_array.shape == (size,) + np.testing.assert_array_equal(quad_array, np.array(str_values, dtype=QuadPrecDType())) + + + class TestStringParsingEdgeCases: """Test edge cases in NumPyOS_ascii_strtoq string parsing""" @pytest.mark.parametrize("input_str", ['3.14', '-2.71', '0.0', '1e10', '-1e-10']) From 07039211a82f90718b8be761de99038b17d0669a Mon Sep 17 00:00:00 2001 From: SwayamInSync Date: Fri, 19 Dec 2025 15:18:32 +0000 Subject: [PATCH 3/9] use the bytes_to_quad helper --- quaddtype/numpy_quaddtype/src/casts.cpp | 59 +++---------------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index 68710000..62fe1b73 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -27,7 +27,7 @@ extern "C" { #include "dragon4.h" #include "ops.hpp" -#define NUM_CASTS 40 // 17 to_casts + 17 from_casts + 1 quad_to_quad + 1 void_to_quad + 2 StringDType +#define NUM_CASTS 40 // 18 to_casts + 18 from_casts + 1 quad_to_quad + 1 void_to_quad #define QUAD_STR_WIDTH 50 // 42 is enough for scientific notation float128, just keeping some buffer static NPY_CASTING @@ -673,41 +673,12 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat } } - char *temp_str = (char *)malloc(s.size + 1); - if (temp_str == NULL) { - NpyString_release_allocator(allocator); - PyErr_NoMemory(); - return -1; - } - memcpy(temp_str, s.buf, s.size); - temp_str[s.size] = '\0'; - - char *endptr; quad_value out_val; - int err = NumPyOS_ascii_strtoq(temp_str, backend, &out_val, &endptr); - - if (err < 0) { - PyErr_Format(PyExc_ValueError, - "could not convert string to QuadPrecision: '%s'", temp_str); - free(temp_str); + if (bytes_to_quad_convert(s.buf, s.size, backend, &out_val) < 0) { NpyString_release_allocator(allocator); return -1; } - while (ascii_isspace(*endptr)) { - endptr++; - } - - if (*endptr != '\0') { - PyErr_Format(PyExc_ValueError, - "could not convert string to QuadPrecision: '%s'", temp_str); - free(temp_str); - NpyString_release_allocator(allocator); - return -1; - } - - free(temp_str); - store_quad(out_ptr, out_val, backend); in_ptr += in_stride; @@ -728,29 +699,9 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp loop_descrs[0] = given_descrs[0]; if (given_descrs[1] == NULL) { - PyObject *args = PyTuple_New(0); - if (args == NULL) { - Py_DECREF(loop_descrs[0]); - return (NPY_CASTING)-1; - } - PyObject *kwargs = PyDict_New(); - if (kwargs == NULL) { - Py_DECREF(args); - Py_DECREF(loop_descrs[0]); - return (NPY_CASTING)-1; - } - if (PyDict_SetItemString(kwargs, "coerce", Py_True) < 0) { - Py_DECREF(args); - Py_DECREF(kwargs); - Py_DECREF(loop_descrs[0]); - return (NPY_CASTING)-1; - } - - loop_descrs[1] = (PyArray_Descr *)PyObject_Call( - (PyObject *)&PyArray_StringDType, args, kwargs); - Py_DECREF(args); - Py_DECREF(kwargs); - + // Default StringDType() already has coerce=True + loop_descrs[1] = (PyArray_Descr *)PyObject_CallNoArgs( + (PyObject *)&PyArray_StringDType); if (loop_descrs[1] == NULL) { Py_DECREF(loop_descrs[0]); return (NPY_CASTING)-1; From a3d326abea24ea778b387f404d7da97c3a9a937c Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 02:27:56 +0530 Subject: [PATCH 4/9] removed comment, fix given_descrs[0] check order --- quaddtype/numpy_quaddtype/src/casts.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index 62fe1b73..cceee650 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -611,13 +611,9 @@ stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], npy_intp *view_offset) { - Py_INCREF(given_descrs[0]); - loop_descrs[0] = given_descrs[0]; - if (given_descrs[1] == NULL) { loop_descrs[1] = (PyArray_Descr *)new_quaddtype_instance(BACKEND_SLEEF); if (loop_descrs[1] == nullptr) { - Py_DECREF(loop_descrs[0]); return (NPY_CASTING)-1; } } @@ -626,7 +622,9 @@ stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp loop_descrs[1] = given_descrs[1]; } - // no notion of fix length, so always unsafe + Py_INCREF(given_descrs[0]); + loop_descrs[0] = given_descrs[0]; + return NPY_UNSAFE_CASTING; } @@ -695,15 +693,11 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], npy_intp *view_offset) { - Py_INCREF(given_descrs[0]); - loop_descrs[0] = given_descrs[0]; - if (given_descrs[1] == NULL) { // Default StringDType() already has coerce=True loop_descrs[1] = (PyArray_Descr *)PyObject_CallNoArgs( (PyObject *)&PyArray_StringDType); if (loop_descrs[1] == NULL) { - Py_DECREF(loop_descrs[0]); return (NPY_CASTING)-1; } } @@ -712,6 +706,9 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp loop_descrs[1] = given_descrs[1]; } + Py_INCREF(given_descrs[0]); + loop_descrs[0] = given_descrs[0]; + return NPY_SAFE_CASTING; } From 4d143227cba81e4e3704871d8f59ccb372a3c566 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 02:57:33 +0530 Subject: [PATCH 5/9] consolidated tests, removed some not needed --- quaddtype/tests/test_quaddtype.py | 191 ++++-------------------------- 1 file changed, 25 insertions(+), 166 deletions(-) diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 8b225257..f4476b23 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -554,7 +554,7 @@ def test_unsupported_astype(dtype): np.array(QuadPrecision(1)).astype(dtype, casting="unsafe") class TestArrayCastStringBytes: - @pytest.mark.parametrize("strtype", [np.str_, str]) + @pytest.mark.parametrize("strtype", [np.str_, str, np.dtypes.StringDType()]) @pytest.mark.parametrize("input_val", [ "3.141592653589793238462643383279502884197", "2.71828182845904523536028747135266249775", @@ -747,187 +747,46 @@ def test_empty_bytes_raises_error(self): with pytest.raises(ValueError): bytes_array.astype(QuadPrecDType()) - -class TestStringDTypeCasting: - @pytest.mark.parametrize("input_val", [ - "3.141592653589793238462643383279502884197", - "2.71828182845904523536028747135266249775", - "1.0", - "-1.0", - "0.0", - "-0.0", - "1e100", - "1e-100", - "1.23456789012345678901234567890123456789", - "-9.87654321098765432109876543210987654321", - ]) - def test_stringdtype_to_quad_basic(self, input_val): - """Test basic StringDType to QuadPrecision conversion""" - str_array = np.array([input_val], dtype=np.dtypes.StringDType()) - quad_array = str_array.astype(QuadPrecDType()) - - assert quad_array.dtype.name == "QuadPrecDType128" - expected = np.array([input_val], dtype=QuadPrecDType()) - np.testing.assert_array_equal(quad_array, expected) - - @pytest.mark.parametrize("input_val", [ - "3.1415926535897932384626433832795028", # pi to quad precision - "2.7182818284590452353602874713526623", # e to quad precision - "1.0e+100", # scientific notation (normalized form) - "1.0e-100", # scientific notation (normalized form) - "0.0", - "-0.0", - "inf", - "-inf", - "nan", - "1.0", - "-1.0", - "123.456", - "-123.456", - ]) - def test_stringdtype_roundtrip(self, input_val): - str_array = np.array([input_val], dtype=np.dtypes.StringDType()) - quad_array = str_array.astype(QuadPrecDType()) - result_str_array = quad_array.astype(np.dtypes.StringDType()) - - np.testing.assert_array_equal(result_str_array, str_array) - - @pytest.mark.parametrize("original", [ - QuadPrecision("0.417022004702574000667425480060047"), - QuadPrecision("1.23456789012345678901234567890123456789"), - pytest.param(numpy_quaddtype.pi, id="pi"), - pytest.param(numpy_quaddtype.e, id="e"), - QuadPrecision("1e-100"), - QuadPrecision("1e100"), - QuadPrecision("-3.14159265358979323846264338327950288419"), - QuadPrecision("0.0"), - QuadPrecision("-0.0"), - QuadPrecision("1.0"), - QuadPrecision("-1.0"), - ]) - def test_quad_to_stringdtype_roundtrip(self, original): - """Test QuadPrecision -> StringDType -> QuadPrecision preserves value""" - quad_array = np.array([original], dtype=QuadPrecDType()) - str_array = quad_array.astype(np.dtypes.StringDType()) - reconstructed = str_array.astype(QuadPrecDType()) - - if np.isnan(original): - assert np.isnan(reconstructed[0]) - else: - np.testing.assert_array_equal(reconstructed, quad_array) - - # ============ Special Values Tests ============ - - @pytest.mark.parametrize("input_str,check_func", [ - ("inf", lambda x: np.isinf(float(x)) and float(x) > 0), - ("-inf", lambda x: np.isinf(float(x)) and float(x) < 0), - ("+inf", lambda x: np.isinf(float(x)) and float(x) > 0), - ("Inf", lambda x: np.isinf(float(x)) and float(x) > 0), - ("Infinity", lambda x: np.isinf(float(x)) and float(x) > 0), - ("-Infinity", lambda x: np.isinf(float(x)) and float(x) < 0), - ("INF", lambda x: np.isinf(float(x)) and float(x) > 0), - ("INFINITY", lambda x: np.isinf(float(x)) and float(x) > 0), - ]) - def test_stringdtype_infinity_variants(self, input_str, check_func): - """Test various infinity representations in StringDType""" - str_array = np.array([input_str], dtype=np.dtypes.StringDType()) - quad_array = str_array.astype(QuadPrecDType()) - - assert check_func(quad_array[0]), f"Failed for {input_str}" - - @pytest.mark.parametrize("input_str", [ - "nan", "NaN", "NAN", "+nan", "-nan", - "nan()", "nan(123)", "NaN(payload)", - ]) - def test_stringdtype_nan_variants(self, input_str): - """Test various NaN representations in StringDType""" - str_array = np.array([input_str], dtype=np.dtypes.StringDType()) - quad_array = str_array.astype(QuadPrecDType()) - - assert np.isnan(float(quad_array[0])), f"Expected NaN for {input_str}" - - def test_stringdtype_negative_zero(self): + # Tests for all string types (fixed-length Unicode and variable-length StringDType) + @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) + def test_negative_zero_roundtrip(self, strtype): + """Test that negative zero sign is preserved through string roundtrip""" neg_zero = QuadPrecision("-0.0") quad_array = np.array([neg_zero], dtype=QuadPrecDType()) assert np.signbit(quad_array[0]), "Input should have negative zero signbit" - str_array = quad_array.astype(np.dtypes.StringDType()) + + str_array = quad_array.astype(strtype) assert str_array[0] == "-0.0", f"Expected '-0.0', got '{str_array[0]}'" + roundtrip = str_array.astype(QuadPrecDType()) assert np.signbit(roundtrip[0]), "Signbit should be preserved after round-trip" - assert float(roundtrip[0]) == 0.0, "Value should be zero" - - # ============ Whitespace Handling Tests ============ - - @pytest.mark.parametrize("input_str,expected", [ - (" 3.14", "3.14"), - ("3.14 ", "3.14"), - (" 3.14 ", "3.14"), - ("\t3.14\t", "3.14"), - ("\n3.14\n", "3.14"), - (" \t\n 3.14 \t\n ", "3.14"), - ]) - def test_stringdtype_whitespace_handling(self, input_str, expected): - """Test that StringDType handles whitespace correctly""" - str_array = np.array([input_str], dtype=np.dtypes.StringDType()) - quad_array = str_array.astype(QuadPrecDType()) - expected_quad = QuadPrecision(expected) - - np.testing.assert_array_equal(quad_array, np.array([expected_quad], dtype=QuadPrecDType())) - - @pytest.mark.parametrize("invalid_str", [ - "", - "not_a_number", - "abc123", - "1.23.45", - "1e", - "++1.0", - "--1.0", - "+-1.0", - "1.0abc", - "abc1.0", - "3.14ñ", - "π", - ]) - def test_stringdtype_invalid_input(self, invalid_str): - """Test that invalid StringDType input raises ValueError""" - str_array = np.array([invalid_str], dtype=np.dtypes.StringDType()) - - with pytest.raises(ValueError): - str_array.astype(QuadPrecDType()) - - + + @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) - @pytest.mark.parametrize("input_str", [ - "1.0", - "-1.0", - "3.141592653589793238462643383279502884197", - "1e100", - "1e-100", - "0.0", - ]) - def test_stringdtype_backend_consistency(self, backend, input_str): - """Test that StringDType parsing works consistently across backends""" - str_array = np.array([input_str], dtype=np.dtypes.StringDType()) + def test_string_backend_consistency(self, strtype, backend): + """Test that string parsing works consistently across backends""" + input_str = "3.141592653589793238462643383279502884197" + str_array = np.array([input_str], dtype=strtype) quad_array = str_array.astype(QuadPrecDType(backend=backend)) scalar_val = QuadPrecision(input_str, backend=backend) np.testing.assert_array_equal(quad_array, np.array([scalar_val], dtype=QuadPrecDType(backend=backend))) - - def test_stringdtype_empty_array(self): - """Test conversion of empty StringDType array""" - str_array = np.array([], dtype=np.dtypes.StringDType()) + + @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) + def test_string_empty_array(self, strtype): + """Test conversion of empty string array""" + str_array = np.array([], dtype=strtype) quad_array = str_array.astype(QuadPrecDType()) np.testing.assert_array_equal(quad_array, np.array([], dtype=QuadPrecDType())) - @pytest.mark.parametrize("size", [500, 1000, 10000]) - def test_stringdtype_large_array(self, size): - """Test conversion of large StringDType array""" - str_values = [str(i * 0.001) for i in range(size)] - str_array = np.array(str_values, dtype=np.dtypes.StringDType()) + @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) + def test_string_large_array(self, strtype): + """Test conversion of large string array""" + str_values = [str(i * 0.001) for i in range(1000)] + str_array = np.array(str_values, dtype=strtype) quad_array = str_array.astype(QuadPrecDType()) - assert quad_array.shape == (size,) + assert quad_array.shape == (1000,) np.testing.assert_array_equal(quad_array, np.array(str_values, dtype=QuadPrecDType())) - class TestStringParsingEdgeCases: From f0e6b1fa0cf8176a179a1621c83e66b686369e10 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 03:09:22 +0530 Subject: [PATCH 6/9] StringDtype bypass PyUnicode creation --- quaddtype/numpy_quaddtype/src/casts.cpp | 48 +++++++++++++++----- quaddtype/numpy_quaddtype/src/dragon4.c | 59 +++++++++++++++++++++++++ quaddtype/numpy_quaddtype/src/dragon4.h | 8 ++++ 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index cceee650..2107be23 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -369,6 +369,39 @@ quad_to_string_adaptive(Sleef_quad *sleef_val, npy_intp unicode_size_chars) } } +static inline const char * +quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars) +{ + // Try positional format first to see if it would fit + const char* positional_str = Dragon4_Positional_QuadDType_CStr( + sleef_val, DigitMode_Unique, CutoffMode_TotalLength, SLEEF_QUAD_DECIMAL_DIG, 0, 1, + TrimMode_LeaveOneZero, 1, 0); + + if (positional_str == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Float formatting failed"); + return NULL; + } + + // no need to scan full, only checking if its longer + npy_intp pos_len = strnlen(positional_str, unicode_size_chars + 1); + + // If positional format fits, use it; otherwise use scientific notation + if (pos_len <= unicode_size_chars) { + return positional_str; // Keep the positional string + } + else { + // Use scientific notation with full precision + const char *scientific_str = Dragon4_Scientific_QuadDType_CStr(sleef_val, DigitMode_Unique, + SLEEF_QUAD_DECIMAL_DIG, 0, 1, + TrimMode_LeaveOneZero, 1, 2); + if (scientific_str == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Float formatting failed"); + return NULL; + } + return scientific_str; + } +} + template static int quad_to_unicode_loop(PyArrayMethod_Context *context, char *const data[], @@ -739,30 +772,21 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat // Get string representation with adaptive notation // Use a large buffer size to allow for full precision - PyObject *py_str = quad_to_string_adaptive(&sleef_val, QUAD_STR_WIDTH); - if (py_str == NULL) { - NpyString_release_allocator(allocator); - return -1; - } - - Py_ssize_t str_size; - const char *str_buf = PyUnicode_AsUTF8AndSize(py_str, &str_size); + const char *str_buf = quad_to_string_adaptive_cstr(&sleef_val, QUAD_STR_WIDTH); if (str_buf == NULL) { - Py_DECREF(py_str); NpyString_release_allocator(allocator); return -1; } + Py_ssize_t str_size = strnlen(str_buf, QUAD_STR_WIDTH); + npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr; if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) { - Py_DECREF(py_str); NpyString_release_allocator(allocator); PyErr_SetString(PyExc_MemoryError, "Failed to pack string in Quad to StringDType cast"); return -1; } - Py_DECREF(py_str); - in_ptr += in_stride; out_ptr += out_stride; } diff --git a/quaddtype/numpy_quaddtype/src/dragon4.c b/quaddtype/numpy_quaddtype/src/dragon4.c index b9a896c8..b47e292f 100644 --- a/quaddtype/numpy_quaddtype/src/dragon4.c +++ b/quaddtype/numpy_quaddtype/src/dragon4.c @@ -1954,6 +1954,15 @@ Dragon4_Positional_QuadDType_opt(Sleef_quad *val, Dragon4_Options *opt) return ret; } +const char * +Dragon4_Positional_QuadDType_opt_cstr(Sleef_quad *val, Dragon4_Options *opt) +{; + if (Dragon4_PrintFloat_Sleef_quad(val, opt) < 0) { + return NULL; + } + return _bigint_static.repr; +} + PyObject * Dragon4_Positional_QuadDType(Sleef_quad *val, DigitMode digit_mode, CutoffMode cutoff_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, @@ -1975,6 +1984,27 @@ Dragon4_Positional_QuadDType(Sleef_quad *val, DigitMode digit_mode, CutoffMode c return Dragon4_Positional_QuadDType_opt(val, &opt); } +const char * +Dragon4_Positional_QuadDType_CStr(Sleef_quad *val, DigitMode digit_mode, CutoffMode cutoff_mode, + int precision, int min_digits, int sign, TrimMode trim, int pad_left, + int pad_right) +{ + Dragon4_Options opt; + + opt.scientific = 0; + opt.digit_mode = digit_mode; + opt.cutoff_mode = cutoff_mode; + opt.precision = precision; + opt.min_digits = min_digits; + opt.sign = sign; + opt.trim_mode = trim; + opt.digits_left = pad_left; + opt.digits_right = pad_right; + opt.exp_digits = -1; + + return Dragon4_Positional_QuadDType_opt_cstr(val, &opt); +} + PyObject * Dragon4_Scientific_QuadDType_opt(Sleef_quad *val, Dragon4_Options *opt) { @@ -1986,6 +2016,15 @@ Dragon4_Scientific_QuadDType_opt(Sleef_quad *val, Dragon4_Options *opt) return ret; } +const char * +Dragon4_Scientific_QuadDType_opt_cstr(Sleef_quad *val, Dragon4_Options *opt) +{ + if (Dragon4_PrintFloat_Sleef_quad(val, opt) < 0) { + return NULL; + } + return _bigint_static.repr; +} + PyObject * Dragon4_Scientific_QuadDType(Sleef_quad *val, DigitMode digit_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, int exp_digits) @@ -2006,6 +2045,26 @@ Dragon4_Scientific_QuadDType(Sleef_quad *val, DigitMode digit_mode, int precisio return Dragon4_Scientific_QuadDType_opt(val, &opt); } +const char * +Dragon4_Scientific_QuadDType_CStr(Sleef_quad *val, DigitMode digit_mode, int precision, int min_digits, + int sign, TrimMode trim, int pad_left, int exp_digits) +{ + Dragon4_Options opt; + + opt.scientific = 1; + opt.digit_mode = digit_mode; + opt.cutoff_mode = CutoffMode_TotalLength; + opt.precision = precision; + opt.min_digits = min_digits; + opt.sign = sign; + opt.trim_mode = trim; + opt.digits_left = pad_left; + opt.digits_right = -1; + opt.exp_digits = exp_digits; + + return Dragon4_Scientific_QuadDType_opt_cstr(val, &opt); +} + PyObject * Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, int pad_right) diff --git a/quaddtype/numpy_quaddtype/src/dragon4.h b/quaddtype/numpy_quaddtype/src/dragon4.h index 1977595e..8e7753d8 100644 --- a/quaddtype/numpy_quaddtype/src/dragon4.h +++ b/quaddtype/numpy_quaddtype/src/dragon4.h @@ -51,10 +51,18 @@ PyObject *Dragon4_Positional_QuadDType(Sleef_quad *val, DigitMode digit_mode, CutoffMode cutoff_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, int pad_right); +const char *Dragon4_Positional_QuadDType_CStr(Sleef_quad *val, DigitMode digit_mode, + CutoffMode cutoff_mode, int precision, int min_digits, + int sign, TrimMode trim, int pad_left, int pad_right); + PyObject *Dragon4_Scientific_QuadDType(Sleef_quad *val, DigitMode digit_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, int exp_digits); +const char *Dragon4_Scientific_QuadDType_CStr(Sleef_quad *val, DigitMode digit_mode, + int precision, int min_digits, int sign, TrimMode trim, + int pad_left, int exp_digits); + PyObject *Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, int precision, int min_digits, int sign, TrimMode trim, int pad_left, int pad_right); From 87fd7dcdcd7441daecee6fb10bdb6e7d4edd262a Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 03:17:46 +0530 Subject: [PATCH 7/9] cover edges --- quaddtype/tests/test_quaddtype.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index f4476b23..36d4c1c8 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -825,9 +825,10 @@ def test_numeric_string_parsing(self, input_str, byte_order): ("+INFINITY", 1), ("-INFINITY", -1), ]) - def test_infinity_sign_preservation(self, input_str, expected_sign): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_infinity_sign_preservation(self, input_str, expected_sign, strtype): """Test that +/- signs are correctly applied to infinity values""" - arr = np.array([input_str], dtype='U20') + arr = np.array([input_str], dtype=strtype) result = arr.astype(QuadPrecDType()) assert np.isinf(float(str(result[0]))), f"Expected inf for '{input_str}'" @@ -842,9 +843,10 @@ def test_infinity_sign_preservation(self, input_str, expected_sign): "NAN", "+NAN", "-NAN", "nan()", "nan(123)", "nan(abc_)", "NAN(XYZ)", ]) - def test_nan_case_insensitive(self, input_str): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_nan_case_insensitive(self, input_str, strtype): """Test case-insensitive NaN parsing with optional payloads""" - arr = np.array([input_str], dtype='U20') + arr = np.array([input_str], dtype=strtype) result = arr.astype(QuadPrecDType()) assert np.isnan(float(str(result[0]))), f"Expected NaN for '{input_str}'" @@ -890,9 +892,10 @@ def test_numeric_sign_handling(self, input_str, expected_val): "\t-inf\t", " nan ", ]) - def test_whitespace_handling(self, input_str): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_whitespace_handling(self, input_str, strtype): """Test that leading/trailing whitespace is handled correctly""" - arr = np.array([input_str], dtype='U20') + arr = np.array([input_str], dtype=strtype) result = arr.astype(QuadPrecDType()) # Should not raise an error @@ -912,9 +915,10 @@ def test_whitespace_handling(self, input_str): "na", # Incomplete nan "infinit", # Incomplete infinity ]) - def test_invalid_strings_raise_error(self, invalid_str): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_invalid_strings_raise_error(self, invalid_str, strtype): """Test that invalid strings raise ValueError""" - arr = np.array([invalid_str], dtype='U20') + arr = np.array([invalid_str], dtype=strtype) with pytest.raises(ValueError): arr.astype(QuadPrecDType()) @@ -925,9 +929,10 @@ def test_invalid_strings_raise_error(self, invalid_str): "3.1€4", # Mid non-ASCII "π", # Greek pi ]) - def test_non_ascii_raises_error(self, input_str): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_non_ascii_raises_error(self, input_str, strtype): """Test that non-ASCII characters raise ValueError""" - arr = np.array([input_str], dtype='U20') + arr = np.array([input_str], dtype=strtype) with pytest.raises(ValueError): arr.astype(QuadPrecDType()) From 407d57ab05d4764a2905625e2479cff8f25ea1f8 Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 03:27:20 +0530 Subject: [PATCH 8/9] replace -0 with already present --- quaddtype/tests/test_quaddtype.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 36d4c1c8..3ff9d3fb 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -747,20 +747,6 @@ def test_empty_bytes_raises_error(self): with pytest.raises(ValueError): bytes_array.astype(QuadPrecDType()) - # Tests for all string types (fixed-length Unicode and variable-length StringDType) - @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) - def test_negative_zero_roundtrip(self, strtype): - """Test that negative zero sign is preserved through string roundtrip""" - neg_zero = QuadPrecision("-0.0") - quad_array = np.array([neg_zero], dtype=QuadPrecDType()) - assert np.signbit(quad_array[0]), "Input should have negative zero signbit" - - str_array = quad_array.astype(strtype) - assert str_array[0] == "-0.0", f"Expected '-0.0', got '{str_array[0]}'" - - roundtrip = str_array.astype(QuadPrecDType()) - assert np.signbit(roundtrip[0]), "Signbit should be preserved after round-trip" - @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) @pytest.mark.parametrize("backend", ["sleef", "longdouble"]) def test_string_backend_consistency(self, strtype, backend): @@ -865,9 +851,10 @@ def test_nan_case_insensitive(self, input_str, strtype): ("+1.23e-45", 1.23e-45), ("-1.23e-45", -1.23e-45), ]) - def test_numeric_sign_handling(self, input_str, expected_val): + @pytest.mark.parametrize("strtype", ['U20', np.dtypes.StringDType()]) + def test_numeric_sign_handling(self, input_str, expected_val, strtype): """Test that +/- signs are correctly handled for numeric values""" - arr = np.array([input_str], dtype='U20') + arr = np.array([input_str], dtype=strtype) result = arr.astype(QuadPrecDType()) result_val = float(str(result[0])) From fc4c7fa302d1511ae2a4e2ce97f08687bfa2782e Mon Sep 17 00:00:00 2001 From: swayaminsync Date: Sat, 20 Dec 2025 03:28:26 +0530 Subject: [PATCH 9/9] remove empty array test --- quaddtype/tests/test_quaddtype.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index 3ff9d3fb..0ef1fd39 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -757,13 +757,6 @@ def test_string_backend_consistency(self, strtype, backend): scalar_val = QuadPrecision(input_str, backend=backend) np.testing.assert_array_equal(quad_array, np.array([scalar_val], dtype=QuadPrecDType(backend=backend))) - @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) - def test_string_empty_array(self, strtype): - """Test conversion of empty string array""" - str_array = np.array([], dtype=strtype) - quad_array = str_array.astype(QuadPrecDType()) - np.testing.assert_array_equal(quad_array, np.array([], dtype=QuadPrecDType())) - @pytest.mark.parametrize("strtype", [np.str_, np.dtypes.StringDType()]) def test_string_large_array(self, strtype): """Test conversion of large string array"""