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
59 changes: 59 additions & 0 deletions src/zarr/core/dtype/npy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
IntishFloat = NewType("IntishFloat", float)
"""A type for floats that represent integers, like 1.0 (but not 1.1)."""

IntishStr = NewType("IntishStr", str)
"""A type for strings that represent integers, like "0" or "42"."""

FloatishStr = NewType("FloatishStr", str)
"""A type for strings that represent floats, like "3.14" or "-2.5"."""

NumpyEndiannessStr = Literal[">", "<", "="]
NUMPY_ENDIANNESS_STR: Final = ">", "<", "="

Expand Down Expand Up @@ -488,6 +494,59 @@ def check_json_intish_float(data: JSON) -> TypeGuard[IntishFloat]:
return isinstance(data, float) and data.is_integer()


def check_json_intish_str(data: JSON) -> TypeGuard[IntishStr]:
"""
Check if a JSON value is a string that represents an integer, like "0", "42", or "-5".

Parameters
----------
data : JSON
The JSON value to check.

Returns
-------
bool
True if the data is a string representing an integer, False otherwise.
"""
if not isinstance(data, str):
return False

try:
int(data)
except ValueError:
return False
else:
return True


def check_json_floatish_str(data: JSON) -> TypeGuard[FloatishStr]:
"""
Check if a JSON value is a string that represents a float, like "3.14", "-2.5", or "0.0".

Note: This function is intended to be used AFTER check_json_float_v2/v3, so it only
handles regular string representations that those functions don't cover.

Parameters
----------
data : JSON
The JSON value to check.

Returns
-------
bool
True if the data is a string representing a regular float, False otherwise.
"""
if not isinstance(data, str):
return False

try:
float(data)
except ValueError:
return False
else:
return True


def check_json_str(data: JSON) -> TypeGuard[str]:
"""
Check if a JSON value is a string.
Expand Down
5 changes: 5 additions & 0 deletions src/zarr/core/dtype/npy/float.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
TFloatScalar_co,
check_json_float_v2,
check_json_float_v3,
check_json_floatish_str,
endianness_to_numpy_str,
float_from_json_v2,
float_from_json_v3,
Expand Down Expand Up @@ -270,13 +271,17 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> TFloatScal
if zarr_format == 2:
if check_json_float_v2(data):
return self._cast_scalar_unchecked(float_from_json_v2(data))
elif check_json_floatish_str(data):
return self._cast_scalar_unchecked(float(data))
else:
raise TypeError(
f"Invalid type: {data}. Expected a float or a special string encoding of a float."
)
elif zarr_format == 3:
if check_json_float_v3(data):
return self._cast_scalar_unchecked(float_from_json_v3(data))
elif check_json_floatish_str(data):
return self._cast_scalar_unchecked(float(data))
else:
raise TypeError(
f"Invalid type: {data}. Expected a float or a special string encoding of a float."
Expand Down
5 changes: 5 additions & 0 deletions src/zarr/core/dtype/npy/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from zarr.core.dtype.npy.common import (
check_json_int,
check_json_intish_float,
check_json_intish_str,
endianness_to_numpy_str,
get_endianness_from_numpy_dtype,
)
Expand Down Expand Up @@ -209,6 +210,10 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> TIntScalar
return self._cast_scalar_unchecked(data)
if check_json_intish_float(data):
return self._cast_scalar_unchecked(int(data))

if check_json_intish_str(data):
return self._cast_scalar_unchecked(int(data))

raise TypeError(f"Invalid type: {data}. Expected an integer.")

def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_dtype/test_npy/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,47 @@ class TestFloat64(_BaseTestFloat):
("0x3ff0000000000000", 1.0),
)
item_size_params = (Float64(),)


def test_check_json_floatish_str() -> None:
"""Test the check_json_floatish_str function."""
from zarr.core.dtype.npy.common import check_json_floatish_str

# Test valid string floats
assert check_json_floatish_str("3.14")
assert check_json_floatish_str("0.0")
assert check_json_floatish_str("-2.5")
assert check_json_floatish_str("1.0")

# Test invalid cases
assert not check_json_floatish_str("not_a_number")
assert not check_json_floatish_str("")
assert not check_json_floatish_str(3.14) # actual float, not string
assert not check_json_floatish_str(42) # int
assert not check_json_floatish_str(None)

# Test that special cases still work via float() conversion
# (these will be handled by existing functions first in practice)
assert check_json_floatish_str("NaN")
assert check_json_floatish_str("Infinity")
assert check_json_floatish_str("-Infinity")


def test_string_float_from_json_scalar() -> None:
"""Test that string representations of floats can be parsed by from_json_scalar."""
# Test with Float32
dtype_instance = Float32()
result = dtype_instance.from_json_scalar("3.14", zarr_format=3)
assert abs(result - np.float32(3.14)) < 1e-6
assert isinstance(result, np.float32)

# Test other cases
result = dtype_instance.from_json_scalar("0.0", zarr_format=3)
assert result == np.float32(0.0)

result = dtype_instance.from_json_scalar("-2.5", zarr_format=3)
assert result == np.float32(-2.5)

# Test that it works for v2 format too
result = dtype_instance.from_json_scalar("1.5", zarr_format=2)
assert result == np.float32(1.5)
39 changes: 39 additions & 0 deletions tests/test_dtype/test_npy/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,42 @@ class TestUInt64(BaseTestZDType):
)
invalid_scalar_params = ((UInt64(), {"set!"}), (UInt64(), ("tuple",)))
item_size_params = (UInt64(),)


def test_check_json_intish_str() -> None:
"""Test the check_json_intish_str function."""
from zarr.core.dtype.npy.common import check_json_intish_str

# Test valid string integers
assert check_json_intish_str("0")
assert check_json_intish_str("42")
assert check_json_intish_str("-5")
assert check_json_intish_str("123")

# Test invalid cases
assert not check_json_intish_str("3.14")
assert not check_json_intish_str("not_a_number")
assert not check_json_intish_str("")
assert not check_json_intish_str(42) # actual int, not string
assert not check_json_intish_str(3.14) # float
assert not check_json_intish_str(None)


def test_string_integer_from_json_scalar() -> None:
"""Test that string representations of integers can be parsed by from_json_scalar."""
# Test the specific reproducer case
dtype_instance = Int32()
result = dtype_instance.from_json_scalar("0", zarr_format=3)
assert result == np.int32(0)
assert isinstance(result, np.int32)

# Test other cases
result = dtype_instance.from_json_scalar("42", zarr_format=3)
assert result == np.int32(42)

result = dtype_instance.from_json_scalar("-5", zarr_format=3)
assert result == np.int32(-5)

# Test that it works for v2 format too
result = dtype_instance.from_json_scalar("123", zarr_format=2)
assert result == np.int32(123)
Loading