Skip to content

Commit 970b62c

Browse files
committed
adding tests
1 parent 14dac3d commit 970b62c

File tree

2 files changed

+196
-16
lines changed

2 files changed

+196
-16
lines changed

quaddtype/numpy_quaddtype/src/casts.cpp

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,13 @@ stringdtype_to_quad_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp
626626
loop_descrs[1] = given_descrs[1];
627627
}
628628

629+
// no notion of fix length, so always unsafe
629630
return NPY_UNSAFE_CASTING;
630631
}
631632

633+
// Note: StringDType elements are always aligned, so Aligned template parameter
634+
// is kept for API consistency but both versions use the same logic
635+
template <bool Aligned>
632636
static int
633637
stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const data[],
634638
npy_intp const dimensions[], npy_intp const strides[],
@@ -669,7 +673,6 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat
669673
}
670674
}
671675

672-
// Create a null-terminated copy of the string
673676
char *temp_str = (char *)malloc(s.size + 1);
674677
if (temp_str == NULL) {
675678
NpyString_release_allocator(allocator);
@@ -691,7 +694,6 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat
691694
return -1;
692695
}
693696

694-
// Check that we parsed the entire string (skip trailing whitespace)
695697
while (ascii_isspace(*endptr)) {
696698
endptr++;
697699
}
@@ -706,8 +708,7 @@ stringdtype_to_quad_strided_loop(PyArrayMethod_Context *context, char *const dat
706708

707709
free(temp_str);
708710

709-
// Store the result - StringDType elements are always aligned
710-
memcpy(out_ptr, &out_val, sizeof(quad_value));
711+
store_quad<Aligned>(out_ptr, out_val, backend);
711712

712713
in_ptr += in_stride;
713714
out_ptr += out_stride;
@@ -727,7 +728,6 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp
727728
loop_descrs[0] = given_descrs[0];
728729

729730
if (given_descrs[1] == NULL) {
730-
// Create a new StringDType instance with coercion enabled
731731
PyObject *args = PyTuple_New(0);
732732
if (args == NULL) {
733733
Py_DECREF(loop_descrs[0]);
@@ -739,7 +739,6 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp
739739
Py_DECREF(loop_descrs[0]);
740740
return (NPY_CASTING)-1;
741741
}
742-
// Set coerce=True for the new instance
743742
if (PyDict_SetItemString(kwargs, "coerce", Py_True) < 0) {
744743
Py_DECREF(args);
745744
Py_DECREF(kwargs);
@@ -765,6 +764,9 @@ quad_to_stringdtype_resolve_descriptors(PyObject *NPY_UNUSED(self), PyArray_DTyp
765764
return NPY_SAFE_CASTING;
766765
}
767766

767+
// Note: StringDType elements are always aligned, so Aligned template parameter
768+
// is kept for API consistency but both versions use the same logic
769+
template <bool Aligned>
768770
static int
769771
quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const data[],
770772
npy_intp const dimensions[], npy_intp const strides[],
@@ -784,11 +786,7 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat
784786
npy_string_allocator *allocator = NpyString_acquire_allocator(str_descr);
785787

786788
while (N--) {
787-
// Load the quad value - StringDType elements are always aligned
788-
quad_value in_val;
789-
memcpy(&in_val, in_ptr, sizeof(quad_value));
790-
791-
// Convert to Sleef_quad for Dragon4
789+
quad_value in_val = load_quad<Aligned>(in_ptr, backend);
792790
Sleef_quad sleef_val = quad_to_sleef_quad(&in_val, backend);
793791

794792
// Get string representation with adaptive notation
@@ -807,7 +805,6 @@ quad_to_stringdtype_strided_loop(PyArrayMethod_Context *context, char *const dat
807805
return -1;
808806
}
809807

810-
// Pack the string into the output
811808
npy_packed_static_string *out_ps = (npy_packed_static_string *)out_ptr;
812809
if (NpyString_pack(allocator, out_ps, str_buf, (size_t)str_size) < 0) {
813810
Py_DECREF(py_str);
@@ -1620,8 +1617,8 @@ init_casts_internal(void)
16201617
PyArray_DTypeMeta **stringdtype_to_quad_dtypes = new PyArray_DTypeMeta *[2]{&PyArray_StringDType, &QuadPrecDType};
16211618
PyType_Slot *stringdtype_to_quad_slots = new PyType_Slot[4]{
16221619
{NPY_METH_resolve_descriptors, (void *)&stringdtype_to_quad_resolve_descriptors},
1623-
{NPY_METH_strided_loop, (void *)&stringdtype_to_quad_strided_loop},
1624-
{NPY_METH_unaligned_strided_loop, (void *)&stringdtype_to_quad_strided_loop},
1620+
{NPY_METH_strided_loop, (void *)&stringdtype_to_quad_strided_loop<true>},
1621+
{NPY_METH_unaligned_strided_loop, (void *)&stringdtype_to_quad_strided_loop<false>},
16251622
{0, nullptr}};
16261623

16271624
PyArrayMethod_Spec *stringdtype_to_quad_spec = new PyArrayMethod_Spec{
@@ -1639,8 +1636,8 @@ init_casts_internal(void)
16391636
PyArray_DTypeMeta **quad_to_stringdtype_dtypes = new PyArray_DTypeMeta *[2]{&QuadPrecDType, &PyArray_StringDType};
16401637
PyType_Slot *quad_to_stringdtype_slots = new PyType_Slot[4]{
16411638
{NPY_METH_resolve_descriptors, (void *)&quad_to_stringdtype_resolve_descriptors},
1642-
{NPY_METH_strided_loop, (void *)&quad_to_stringdtype_strided_loop},
1643-
{NPY_METH_unaligned_strided_loop, (void *)&quad_to_stringdtype_strided_loop},
1639+
{NPY_METH_strided_loop, (void *)&quad_to_stringdtype_strided_loop<true>},
1640+
{NPY_METH_unaligned_strided_loop, (void *)&quad_to_stringdtype_strided_loop<false>},
16441641
{0, nullptr}};
16451642

16461643
PyArrayMethod_Spec *quad_to_stringdtype_spec = new PyArrayMethod_Spec{

quaddtype/tests/test_quaddtype.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,189 @@ def test_empty_bytes_raises_error(self):
747747
with pytest.raises(ValueError):
748748
bytes_array.astype(QuadPrecDType())
749749

750+
751+
class TestStringDTypeCasting:
752+
@pytest.mark.parametrize("input_val", [
753+
"3.141592653589793238462643383279502884197",
754+
"2.71828182845904523536028747135266249775",
755+
"1.0",
756+
"-1.0",
757+
"0.0",
758+
"-0.0",
759+
"1e100",
760+
"1e-100",
761+
"1.23456789012345678901234567890123456789",
762+
"-9.87654321098765432109876543210987654321",
763+
])
764+
def test_stringdtype_to_quad_basic(self, input_val):
765+
"""Test basic StringDType to QuadPrecision conversion"""
766+
str_array = np.array([input_val], dtype=np.dtypes.StringDType())
767+
quad_array = str_array.astype(QuadPrecDType())
768+
769+
assert quad_array.dtype.name == "QuadPrecDType128"
770+
expected = np.array([input_val], dtype=QuadPrecDType())
771+
np.testing.assert_array_equal(quad_array, expected)
772+
773+
@pytest.mark.parametrize("input_val", [
774+
"3.1415926535897932384626433832795028", # pi to quad precision
775+
"2.7182818284590452353602874713526623", # e to quad precision
776+
"1.0e+100", # scientific notation (normalized form)
777+
"1.0e-100", # scientific notation (normalized form)
778+
"0.0",
779+
"-0.0",
780+
"inf",
781+
"-inf",
782+
"nan",
783+
"1.0",
784+
"-1.0",
785+
"123.456",
786+
"-123.456",
787+
])
788+
def test_stringdtype_roundtrip(self, input_val):
789+
str_array = np.array([input_val], dtype=np.dtypes.StringDType())
790+
quad_array = str_array.astype(QuadPrecDType())
791+
result_str_array = quad_array.astype(np.dtypes.StringDType())
792+
793+
np.testing.assert_array_equal(result_str_array, str_array)
794+
795+
@pytest.mark.parametrize("original", [
796+
QuadPrecision("0.417022004702574000667425480060047"),
797+
QuadPrecision("1.23456789012345678901234567890123456789"),
798+
pytest.param(numpy_quaddtype.pi, id="pi"),
799+
pytest.param(numpy_quaddtype.e, id="e"),
800+
QuadPrecision("1e-100"),
801+
QuadPrecision("1e100"),
802+
QuadPrecision("-3.14159265358979323846264338327950288419"),
803+
QuadPrecision("0.0"),
804+
QuadPrecision("-0.0"),
805+
QuadPrecision("1.0"),
806+
QuadPrecision("-1.0"),
807+
])
808+
def test_quad_to_stringdtype_roundtrip(self, original):
809+
"""Test QuadPrecision -> StringDType -> QuadPrecision preserves value"""
810+
quad_array = np.array([original], dtype=QuadPrecDType())
811+
str_array = quad_array.astype(np.dtypes.StringDType())
812+
reconstructed = str_array.astype(QuadPrecDType())
813+
814+
if np.isnan(original):
815+
assert np.isnan(reconstructed[0])
816+
else:
817+
np.testing.assert_array_equal(reconstructed, quad_array)
818+
819+
# ============ Special Values Tests ============
820+
821+
@pytest.mark.parametrize("input_str,check_func", [
822+
("inf", lambda x: np.isinf(float(x)) and float(x) > 0),
823+
("-inf", lambda x: np.isinf(float(x)) and float(x) < 0),
824+
("+inf", lambda x: np.isinf(float(x)) and float(x) > 0),
825+
("Inf", lambda x: np.isinf(float(x)) and float(x) > 0),
826+
("Infinity", lambda x: np.isinf(float(x)) and float(x) > 0),
827+
("-Infinity", lambda x: np.isinf(float(x)) and float(x) < 0),
828+
("INF", lambda x: np.isinf(float(x)) and float(x) > 0),
829+
("INFINITY", lambda x: np.isinf(float(x)) and float(x) > 0),
830+
])
831+
def test_stringdtype_infinity_variants(self, input_str, check_func):
832+
"""Test various infinity representations in StringDType"""
833+
str_array = np.array([input_str], dtype=np.dtypes.StringDType())
834+
quad_array = str_array.astype(QuadPrecDType())
835+
836+
assert check_func(quad_array[0]), f"Failed for {input_str}"
837+
838+
@pytest.mark.parametrize("input_str", [
839+
"nan", "NaN", "NAN", "+nan", "-nan",
840+
"nan()", "nan(123)", "NaN(payload)",
841+
])
842+
def test_stringdtype_nan_variants(self, input_str):
843+
"""Test various NaN representations in StringDType"""
844+
str_array = np.array([input_str], dtype=np.dtypes.StringDType())
845+
quad_array = str_array.astype(QuadPrecDType())
846+
847+
assert np.isnan(float(quad_array[0])), f"Expected NaN for {input_str}"
848+
849+
def test_stringdtype_negative_zero(self):
850+
neg_zero = QuadPrecision("-0.0")
851+
quad_array = np.array([neg_zero], dtype=QuadPrecDType())
852+
assert np.signbit(quad_array[0]), "Input should have negative zero signbit"
853+
str_array = quad_array.astype(np.dtypes.StringDType())
854+
assert str_array[0] == "-0.0", f"Expected '-0.0', got '{str_array[0]}'"
855+
roundtrip = str_array.astype(QuadPrecDType())
856+
assert np.signbit(roundtrip[0]), "Signbit should be preserved after round-trip"
857+
assert float(roundtrip[0]) == 0.0, "Value should be zero"
858+
859+
# ============ Whitespace Handling Tests ============
860+
861+
@pytest.mark.parametrize("input_str,expected", [
862+
(" 3.14", "3.14"),
863+
("3.14 ", "3.14"),
864+
(" 3.14 ", "3.14"),
865+
("\t3.14\t", "3.14"),
866+
("\n3.14\n", "3.14"),
867+
(" \t\n 3.14 \t\n ", "3.14"),
868+
])
869+
def test_stringdtype_whitespace_handling(self, input_str, expected):
870+
"""Test that StringDType handles whitespace correctly"""
871+
str_array = np.array([input_str], dtype=np.dtypes.StringDType())
872+
quad_array = str_array.astype(QuadPrecDType())
873+
expected_quad = QuadPrecision(expected)
874+
875+
np.testing.assert_array_equal(quad_array, np.array([expected_quad], dtype=QuadPrecDType()))
876+
877+
@pytest.mark.parametrize("invalid_str", [
878+
"",
879+
"not_a_number",
880+
"abc123",
881+
"1.23.45",
882+
"1e",
883+
"++1.0",
884+
"--1.0",
885+
"+-1.0",
886+
"1.0abc",
887+
"abc1.0",
888+
"3.14ñ",
889+
"π",
890+
])
891+
def test_stringdtype_invalid_input(self, invalid_str):
892+
"""Test that invalid StringDType input raises ValueError"""
893+
str_array = np.array([invalid_str], dtype=np.dtypes.StringDType())
894+
895+
with pytest.raises(ValueError):
896+
str_array.astype(QuadPrecDType())
897+
898+
899+
@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
900+
@pytest.mark.parametrize("input_str", [
901+
"1.0",
902+
"-1.0",
903+
"3.141592653589793238462643383279502884197",
904+
"1e100",
905+
"1e-100",
906+
"0.0",
907+
])
908+
def test_stringdtype_backend_consistency(self, backend, input_str):
909+
"""Test that StringDType parsing works consistently across backends"""
910+
str_array = np.array([input_str], dtype=np.dtypes.StringDType())
911+
quad_array = str_array.astype(QuadPrecDType(backend=backend))
912+
scalar_val = QuadPrecision(input_str, backend=backend)
913+
np.testing.assert_array_equal(quad_array, np.array([scalar_val], dtype=QuadPrecDType(backend=backend)))
914+
915+
def test_stringdtype_empty_array(self):
916+
"""Test conversion of empty StringDType array"""
917+
str_array = np.array([], dtype=np.dtypes.StringDType())
918+
quad_array = str_array.astype(QuadPrecDType())
919+
np.testing.assert_array_equal(quad_array, np.array([], dtype=QuadPrecDType()))
920+
921+
@pytest.mark.parametrize("size", [500, 1000, 10000])
922+
def test_stringdtype_large_array(self, size):
923+
"""Test conversion of large StringDType array"""
924+
str_values = [str(i * 0.001) for i in range(size)]
925+
str_array = np.array(str_values, dtype=np.dtypes.StringDType())
926+
quad_array = str_array.astype(QuadPrecDType())
927+
928+
assert quad_array.shape == (size,)
929+
np.testing.assert_array_equal(quad_array, np.array(str_values, dtype=QuadPrecDType()))
930+
931+
932+
750933
class TestStringParsingEdgeCases:
751934
"""Test edge cases in NumPyOS_ascii_strtoq string parsing"""
752935
@pytest.mark.parametrize("input_str", ['3.14', '-2.71', '0.0', '1e10', '-1e-10'])

0 commit comments

Comments
 (0)