///|
/// Immutable byte storage; shape accessors return defensive copies.
pub struct ArrayValue {
priv dtype : DataType
priv space : Dataspace
priv raw : Bytes
} derive(Debug)
///|
pub extend ArrayValue with @debug.Debug::{to_repr}
///|
/// Data bytes use the datatype's declared byte order, not host endianness.
pub fn ArrayValue::new(
dtype : DataType,
space : Dataspace,
raw : Bytes,
limits~ : Limits = Limits::default(),
) -> ArrayValue raise H5Error {
let size = checked_mul(space.elements(limits~), dtype.width())
if size > limits.max_file_bytes {
raise Limit("array byte size")
}
if raw.length() != size {
raise Invalid("array byte length differs from extent", 0)
}
{ dtype, space: space.copy(), raw, }
}
///|
pub fn ArrayValue::datatype(self : ArrayValue) -> DataType {
self.dtype
}
///|
pub fn ArrayValue::dataspace(self : ArrayValue) -> Dataspace {
self.space.copy()
}
///|
pub fn ArrayValue::bytes(self : ArrayValue) -> Bytes {
self.raw
}
///|
pub fn ArrayValue::length(self : ArrayValue) -> Int32 raise H5Error {
self.raw.length() / self.dtype.width()
}
///|
fn value_bits(
data : Bytes,
start : Int32,
width : Int32,
order : ByteOrder,
) -> UInt64 raise H5Error {
let bytes = bytes_at(data, start, width)
let mut bits = 0UL
for i = 0; i < width; i = i + 1 {
let at = if order == Little { i } else { width - 1 - i }
bits = bits | (bytes[at].to_uint64() << (8 * i))
}
bits
}
///|
fn ArrayValue::offset(self : ArrayValue, index : Int32) -> Int32 raise H5Error {
if index < 0 || index >= self.length() {
raise Invalid("array index outside extent", index)
}
checked_mul(index, self.dtype.width())
}
///|
/// No conversion through Double; 64-bit integers retain every bit.
pub fn ArrayValue::signed_at(
self : ArrayValue,
index : Int32,
) -> Int64 raise H5Error {
let offset = self.offset(index)
guard self.dtype is Signed(width, order) else {
raise Invalid("not signed integer data", index)
}
let bits = value_bits(self.raw, offset, width, order)
let signed = bits.reinterpret_as_int64()
if width == 8 {
signed
} else {
signed << (64 - width * 8) >> (64 - width * 8)
}
}
///|
pub fn ArrayValue::unsigned_at(
self : ArrayValue,
index : Int32,
) -> UInt64 raise H5Error {
let offset = self.offset(index)
guard self.dtype is Unsigned(width, order) else {
raise Invalid("not unsigned integer data", index)
}
value_bits(self.raw, offset, width, order)
}
///|
pub fn ArrayValue::float_at(
self : ArrayValue,
index : Int32,
) -> Double raise H5Error {
let offset = self.offset(index)
guard self.dtype is Float(width, order) else {
raise Invalid("not floating data", index)
}
let bits = value_bits(self.raw, offset, width, order)
if width == 8 {
bits.reinterpret_as_double()
} else {
Float::reinterpret_from_uint(bits.to_uint()).to_double()
}
}
///|
pub fn ArrayValue::string_at(
self : ArrayValue,
index : Int32,
) -> String raise H5Error {
let offset = self.offset(index)
guard self.dtype is FixedString(width, padding, unicode) else {
raise Invalid("not fixed string data", index)
}
let bytes = bytes_at(self.raw, offset, width)
let mut end = width
match padding {
NullTerminated =>
for i = 0; i < width; i = i + 1 {
if bytes[i] == 0 {
end = i
break
}
}
NullPadded =>
while end > 0 && bytes[end - 1] == 0 {
end -= 1
}
SpacePadded =>
while end > 0 && bytes[end - 1] == 32 {
end -= 1
}
}
let trimmed = bytes[:end].to_owned()
if !unicode {
for x in trimmed {
if x > 127 {
raise Invalid("non-ASCII fixed string", offset)
}
}
}
@utf8.decode(trimmed) catch {
_ => raise Invalid("invalid UTF-8 string", offset)
}
}
///|
pub fn ArrayValue::from_signed(
space : Dataspace,
values : Array[Int64],
limits~ : Limits = Limits::default(),
) -> ArrayValue raise H5Error {
if values.length() != space.elements(limits~) {
raise Invalid("integer count differs from extent", 0)
}
if checked_mul(values.length(), 8) > limits.max_file_bytes {
raise Limit("array bytes")
}
let b = buffer()
for v in values {
b.uint(v.reinterpret_as_uint64(), 8)
}
ArrayValue::new(Signed(8, Little), space, b.finish(), limits~)
}
///|
pub fn ArrayValue::from_unsigned(
space : Dataspace,
values : Array[UInt64],
limits~ : Limits = Limits::default(),
) -> ArrayValue raise H5Error {
if values.length() != space.elements(limits~) {
raise Invalid("unsigned count differs from extent", 0)
}
if checked_mul(values.length(), 8) > limits.max_file_bytes {
raise Limit("array bytes")
}
let b = buffer()
for v in values {
b.uint(v, 8)
}
ArrayValue::new(Unsigned(8, Little), space, b.finish(), limits~)
}
///|
pub fn ArrayValue::from_floats(
space : Dataspace,
values : Array[Double],
limits~ : Limits = Limits::default(),
) -> ArrayValue raise H5Error {
if values.length() != space.elements(limits~) {
raise Invalid("float count differs from extent", 0)
}
if checked_mul(values.length(), 8) > limits.max_file_bytes {
raise Limit("array bytes")
}
let b = buffer()
for v in values {
b.uint(v.reinterpret_as_uint64(), 8)
}
ArrayValue::new(Float(8, Little), space, b.finish(), limits~)
}
///|
pub fn ArrayValue::from_strings(
space : Dataspace,
values : Array[String],
width : Int32,
limits~ : Limits = Limits::default(),
) -> ArrayValue raise H5Error {
let dtype = FixedString(width, NullPadded, true)
ignore(dtype.width())
if values.length() != space.elements(limits~) {
raise Invalid("string count differs from extent", 0)
}
if checked_mul(values.length(), width) > limits.max_file_bytes {
raise Limit("array bytes")
}
let b = buffer()
for v in values {
let bytes = @utf8.encode(v)
if bytes.length() > width {
raise Invalid("string does not fit fixed width", 0)
}
b.bytes(bytes)
b.zeros(width - bytes.length())
}
ArrayValue::new(dtype, space, b.finish(), limits~)
}
///|
test "typed arrays preserve int64 extremes and floating payloads" {
let v = ArrayValue::from_signed(Simple([3]), [
-9223372036854775808L, 0L, 9223372036854775807L,
])
assert_eq(v.signed_at(0), -9223372036854775808L)
assert_eq(v.signed_at(2), 9223372036854775807L)
let u = ArrayValue::from_unsigned(Scalar, [0xffffffffffffffffUL])
assert_eq(u.unsigned_at(0), 0xffffffffffffffffUL)
let f = ArrayValue::from_floats(Simple([2]), [-0.0, 1.25])
assert_eq(f.float_at(0).reinterpret_as_uint64(), 0x8000000000000000UL)
assert_eq(f.float_at(1), 1.25)
assert_true(
try {
ignore(v.signed_at(3))
false
} catch {
_ => true
},
)
assert_true(
try {
ignore(v.unsigned_at(0))
false
} catch {
_ => true
},
)
}
///|
test "fixed strings support UTF8 and reject byte truncation" {
let v = ArrayValue::from_strings(Simple([2]), ["温度", ""], 6)
assert_eq(v.string_at(0), "温度")
assert_eq(v.string_at(1), "")
assert_true(
try {
ignore(ArrayValue::from_strings(Scalar, ["温度"], 5))
false
} catch {
_ => true
},
)
let shape = [2]
let a = ArrayValue::from_signed(Simple(shape), [1L, 2L])
shape[0] = 99
assert_eq(a.length(), 2)
}