1#ifndef UZUKI2_PARSE_HPP
2#define UZUKI2_PARSE_HPP
11#include <unordered_set>
15#include "ritsuko/ritsuko.hpp"
20#include "ExternalTracker.hpp"
60inline void validate_numeric_missing_placeholder(
const H5::Attribute& attr,
const H5::DataSet& data,
const Version& version) {
61 if (attr.getSpace().getSimpleExtentNdims() != 0) {
62 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a scalar");
64 if (version.lt(1, 2)) {
65 if (attr.getDataType().getClass() != data.getDataType().getClass()) {
66 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to have the same type class as its dataset");
69 if (attr.getDataType() != data.getDataType()) {
70 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to have the same type as its dataset");
75inline void validate_string_missing_placeholder(
const H5::Attribute& attr) {
76 if (attr.getSpace().getSimpleExtentNdims() != 0) {
77 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a scalar");
79 if (!ritsuko::hdf5::is_utf8_string(attr)) {
80 throw std::runtime_error(
"expected the '" + ritsuko::hdf5::get_name(attr) +
"' attribute to be a UTF-8 string");
84template<
typename Type_,
class Stream_,
class Action_>
85void iterate_stream(Stream_& stream, Action_ action) {
88 auto available = stream.load(buffer.data());
92 for (I<
decltype(available)> i = 0; i < available; ++i) {
93 action(i + stream.start(), std::move(buffer[i]));
98template<
class Host_,
class Function_>
99void parse_integer_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version,
const Options& options)
try {
100 if (ritsuko::hdf5::exceeds_integer_limit(handle, 32,
true)) {
101 throw std::runtime_error(
"dataset cannot be represented by 32-bit signed integers");
104 bool has_missing =
false;
105 std::int32_t missing_value = -2147483648;
106 if (version.equals(1, 0)) {
109 const char* placeholder_name =
"missing-value-placeholder";
110 has_missing = handle.attrExists(placeholder_name);
112 auto attr = handle.openAttribute(placeholder_name);
113 validate_numeric_missing_placeholder(attr, handle, version);
114 attr.read(H5::PredType::NATIVE_INT32, &missing_value);
118 auto set = [&](hsize_t i, std::int32_t x) ->
void {
119 if (has_missing && x == missing_value) {
129 handle.read(&value, H5::PredType::NATIVE_INT32);
132 ritsuko::hdf5::Stream1dNumericDataset<std::int32_t> stream(
134 static_cast<hsize_t
>(ptr->size()),
136 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
137 opt.contiguous_chunk_size = options.buffer_size;
141 iterate_stream<std::int32_t>(stream, set);
144}
catch (std::exception& e) {
145 throw std::runtime_error(
"failed to load integer dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
148template<
class Host_,
class Function_>
149void parse_string_like(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Options& options)
try {
150 if (!ritsuko::hdf5::is_utf8_string(handle)) {
151 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string");
154 std::optional<std::string> missingness;
155 const char* placeholder_name =
"missing-value-placeholder";
156 if (handle.attrExists(placeholder_name)) {
157 auto attr = handle.openAttribute(placeholder_name);
158 validate_string_missing_placeholder(attr);
159 missingness = ritsuko::hdf5::read_scalar_string(attr);
162 auto set = [&](hsize_t i, std::string x) ->
void {
163 if (missingness.has_value() && x == *missingness) {
167 ptr->set(i, std::move(x));
172 auto x = ritsuko::hdf5::read_scalar_string(handle);
173 set(0, std::move(x));
175 ritsuko::hdf5::Stream1dStringDataset stream(
177 static_cast<hsize_t
>(ptr->size()),
179 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
180 opt.contiguous_chunk_size = options.buffer_size;
184 iterate_stream<std::string>(stream, set);
187}
catch (std::exception& e) {
188 throw std::runtime_error(
"failed to load string dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
191inline double r_missing_value() {
192 std::uint32_t tmp_value = 1;
193 auto tmp_ptr =
reinterpret_cast<unsigned char*
>(&tmp_value);
197 double missing_value = 0;
198 auto missing_ptr =
reinterpret_cast<unsigned char*
>(&missing_value);
201 if (tmp_ptr[0] == 1) {
202 missing_ptr +=
sizeof(double) - 1;
207 *(missing_ptr += step) = 0xf0;
208 *(missing_ptr += step) = 0x00;
209 *(missing_ptr += step) = 0x00;
210 *(missing_ptr += step) = 0x00;
211 *(missing_ptr += step) = 0x00;
212 *(missing_ptr += step) = 0x07;
213 *(missing_ptr += step) = 0xa2;
215 return missing_value;
218template<
class Host_,
class Function_>
219void parse_numbers(
const H5::DataSet& handle, Host_* ptr,
bool is_scalar, Function_ check,
const Version& version,
const Options& options)
try {
220 if (version.lt(1, 3)) {
221 if (handle.getTypeClass() != H5T_FLOAT) {
222 throw std::runtime_error(
"expected a floating-point dataset");
225 if (ritsuko::hdf5::exceeds_float_limit(handle, 64)) {
226 throw std::runtime_error(
"dataset cannot be represented by 64-bit floats");
231 static_assert(std::numeric_limits<double>::is_iec559);
233 bool has_missing =
false;
234 double missing_value = 0;
235 if (version.equals(1, 0)) {
237 missing_value = r_missing_value();
239 const char* placeholder_name =
"missing-value-placeholder";
240 has_missing = handle.attrExists(placeholder_name);
242 auto attr = handle.openAttribute(placeholder_name);
243 validate_numeric_missing_placeholder(attr, handle, version);
244 attr.read(H5::PredType::NATIVE_DOUBLE, &missing_value);
248 bool should_compare_nan = version.lt(1, 3);
249 bool is_placeholder_nan = std::isnan(missing_value);
250 auto is_missing_value = [&](
double val) ->
bool {
251 if (should_compare_nan) {
252 auto xptr =
reinterpret_cast<const unsigned char*
>(&missing_value);
253 auto yptr =
reinterpret_cast<const unsigned char*
>(&val);
254 return std::memcmp(xptr, yptr,
sizeof(
double)) == 0;
255 }
else if (is_placeholder_nan) {
256 return std::isnan(val);
258 return val == missing_value;
262 auto set = [&](hsize_t i,
double x) ->
void {
263 if (has_missing && is_missing_value(x)) {
273 handle.read(&val, H5::PredType::NATIVE_DOUBLE);
276 ritsuko::hdf5::Stream1dNumericDataset<double> stream(
278 static_cast<hsize_t
>(ptr->size()),
280 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
281 opt.contiguous_chunk_size = options.buffer_size;
285 iterate_stream<double>(stream, set);
288}
catch (std::exception& e) {
289 throw std::runtime_error(
"failed to load floating-point dataset at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
293void extract_names(
const H5::Group& handle, Host_* ptr,
const Options& options)
try {
294 auto nhandle = handle.openDataSet(
"names");
295 if (!ritsuko::hdf5::is_utf8_string(nhandle)) {
296 throw std::runtime_error(
"expected 'names' to use a datatype that can be represented by a UTF-8 string");
299 const auto space = nhandle.getSpace();
300 if (space.getSimpleExtentNdims() != 1) {
301 throw std::runtime_error(
"expected 'names' to be a 1-dimensional dataset");
304 space.getSimpleExtentDims(&nlen);
306 throw std::runtime_error(
"number of names should be equal to the object length");
309 ritsuko::hdf5::Stream1dStringDataset stream(
313 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
314 opt.contiguous_chunk_size = options.buffer_size;
318 iterate_stream<std::string>(
320 [&](hsize_t pos, std::string val) ->
void {
321 ptr->set_name(pos, std::move(val));
325}
catch (std::exception& e) {
326 throw std::runtime_error(
"failed to load names at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
329inline std::string read_uzuki_attr(
const H5::Group& handle,
const char* name) {
330 const auto attr = handle.openAttribute(name);
331 if (attr.getSpace().getSimpleExtentNdims() != 0) {
332 throw std::runtime_error(
"'" + std::string(name) +
"' should be a scalar attribute in '" + ritsuko::hdf5::get_name(handle) +
"'");
334 if (!ritsuko::hdf5::is_utf8_string(attr)) {
335 throw std::runtime_error(
"'" + std::string(name) +
"' should be stored as a UTF-8 string in '" + ritsuko::hdf5::get_name(handle) +
"'");
337 return ritsuko::hdf5::read_scalar_string(attr);
340template<
class Provisioner_,
class Externals_>
341std::shared_ptr<Base> parse_inner(
const H5::Group& handle, Externals_& ext,
const Version& version,
const Options& options)
try {
342 auto object_type = read_uzuki_attr(handle,
"uzuki_object");
343 std::shared_ptr<Base> output;
345 if (object_type ==
"list") {
346 auto dhandle = handle.openGroup(
"data");
347 const auto len = dhandle.getNumObjs();
349 bool named = handle.exists(
"names");
353 for (I<
decltype(len)> i = 0; i < len; ++i) {
354 const auto istr = std::to_string(i);
356 auto lhandle = dhandle.openGroup(istr);
357 lptr->set(i, parse_inner<Provisioner_>(lhandle, ext, version, options));
358 }
catch (std::exception& e) {
359 throw std::runtime_error(
"failed to parse list element " + istr +
"; " + std::string(e.what()));
364 extract_names(handle, lptr, options);
367 }
else if (object_type ==
"vector") {
368 auto dhandle = handle.openDataSet(
"data");
369 const auto dspace = dhandle.getSpace();
370 const auto ndims = dspace.getSimpleExtentNdims();
373 bool is_scalar =
false;
376 }
else if (ndims == 1) {
377 dspace.getSimpleExtentDims(&len);
379 throw std::runtime_error(
"expected a scalar or 1-dimensional dataset in 'data'");
382 const bool named = handle.exists(
"names");
383 auto vector_type = read_uzuki_attr(handle,
"uzuki_type");
384 if (vector_type ==
"integer") {
391 [](std::int32_t) ->
void {},
396 }
else if (vector_type ==
"boolean") {
403 [&](std::int32_t x) ->
void {
404 if (x != 0 && x != 1) {
405 throw std::runtime_error(
"boolean values should be 0 or 1");
412 }
else if (vector_type ==
"factor" || (version.equals(1, 0) && vector_type ==
"ordered")) {
413 auto levhandle = handle.openDataSet(
"levels");
414 if (!ritsuko::hdf5::is_utf8_string(levhandle)) {
415 throw std::runtime_error(
"expected a datatype that can be represented by a UTF-8 string for 'levels'");
419 auto lspace = levhandle.getSpace();
420 if (lspace.getSimpleExtentNdims() != 1) {
421 throw std::runtime_error(
"expected a 1-dimensional dataset for 'levels'");
423 lspace.getSimpleExtentDims(&levlen);
425 bool ordered =
false;
426 if (vector_type ==
"ordered") {
428 }
else if (handle.exists(
"ordered")) {
429 auto ohandle = handle.openDataSet(
"ordered");
430 if (ohandle.getSpace().getSimpleExtentNdims() != 0) {
431 throw std::runtime_error(
"expected 'ordered' to be a scalar dataset");
433 if (ritsuko::hdf5::exceeds_integer_limit(ohandle, 32,
true)) {
434 throw std::runtime_error(
"'ordered' value cannot be represented by a 32-bit integer");
436 std::int32_t tmp_ordered = 0;
437 ohandle.read(&tmp_ordered, H5::PredType::NATIVE_INT32);
438 ordered = tmp_ordered > 0;
447 [&](std::int32_t x) ->
void {
449 throw std::runtime_error(
"factor codes should be non-negative");
451 throw std::runtime_error(
"factor codes should be less than the number of levels");
458 std::unordered_set<std::string> present;
459 ritsuko::hdf5::Stream1dStringDataset stream(
463 ritsuko::hdf5::Stream1dStringDatasetOptions opt;
464 opt.contiguous_chunk_size = options.buffer_size;
468 iterate_stream<std::string>(
470 [&](hsize_t pos, std::string val) ->
void {
471 if (present.find(val) != present.end()) {
472 throw std::runtime_error(
"levels should be unique (multiple occurrences of '" + val +
"')");
474 fptr->set_level(pos, val);
475 present.insert(std::move(val));
479 }
else if (vector_type ==
"vls" && !version.lt(1, 4)) {
480 constexpr auto precision = std::numeric_limits<std::uint64_t>::digits;
481 ritsuko::cvls::validate_pointer_datatype(dhandle, precision, precision);
482 auto hhandle = handle.openDataSet(
"heap");
483 auto hlen = ritsuko::cvls::validate_heap(hhandle);
485 const char* placeholder_name =
"missing-value-placeholder";
486 std::optional<std::string> missingness;
487 if (dhandle.attrExists(placeholder_name)) {
488 auto attr = dhandle.openAttribute(placeholder_name);
489 validate_string_missing_placeholder(attr);
490 missingness = ritsuko::hdf5::read_scalar_string(attr);
496 auto set = [&](hsize_t i, std::string x) ->
void {
497 if (missingness.has_value() && x == *missingness) {
500 ptr->set(i, std::move(x));
505 ritsuko::cvls::Pointer<std::uint64_t, std::uint64_t> vlsptr;
506 dhandle.read(&vlsptr, ritsuko::cvls::define_pointer_datatype<std::uint64_t, std::uint64_t>());
507 if (ritsuko::cvls::is_Pointer_out_of_range(vlsptr, hlen)) {
508 throw std::runtime_error(
"compressed VLS pointer in '" + ritsuko::hdf5::get_name(dhandle) +
"' is out of range of the heap");
511 H5::DataSpace dspace(1, &hlen);
512 const hsize_t len = vlsptr.length;
513 const hsize_t offset = vlsptr.offset;
514 dspace.selectHyperslab(H5S_SELECT_SET, &len, &offset);
515 H5::DataSpace mspace(1, &len);
517 std::vector<std::uint8_t> buffer(vlsptr.length);
518 hhandle.read(buffer.data(), H5::PredType::NATIVE_UINT8, mspace, dspace);
519 auto cptr =
reinterpret_cast<const char*
>(buffer.data());
520 set(0, std::string(cptr, cptr + ritsuko::hdf5::strnlen(cptr, vlsptr.length)));
523 ritsuko::cvls::Stream1dArray<std::uint64_t, std::uint64_t> stream(
529 ritsuko::cvls::Stream1dArrayOptions opt;
530 opt.contiguous_chunk_size = options.buffer_size;
534 iterate_stream<std::string>(stream, set);
537 }
else if (vector_type ==
"string" || (version.equals(1, 0) && (vector_type ==
"date" || vector_type ==
"date-time"))) {
539 if (version.equals(1, 0)) {
540 if (vector_type ==
"date") {
541 format = StringVector::DATE;
542 }
else if (vector_type ==
"date-time") {
543 format = StringVector::DATETIME;
546 }
else if (handle.exists(
"format")) {
547 auto fhandle = handle.openDataSet(
"format");
548 if (fhandle.getSpace().getSimpleExtentNdims() != 0) {
549 throw std::runtime_error(
"expected 'format' to be a scalar dataset");
551 if (!ritsuko::hdf5::is_utf8_string(fhandle)) {
552 throw std::runtime_error(
"expected 'format' to use a datatype that can be represented by a UTF-8 encoded string");
554 auto x = ritsuko::hdf5::read_scalar_string(fhandle);
556 format = StringVector::DATE;
557 }
else if (x ==
"date-time") {
558 format = StringVector::DATETIME;
560 throw std::runtime_error(
"unsupported format '" + x +
"'");
566 if (format == StringVector::NONE) {
571 [](
const std::string&) ->
void {},
575 }
else if (format == StringVector::DATE) {
580 [&](
const std::string& x) ->
void {
581 if (!ritsuko::is_date(x.c_str(), x.size())) {
582 throw std::runtime_error(
"dates should follow YYYY-MM-DD formatting");
588 }
else if (format == StringVector::DATETIME) {
593 [&](
const std::string& x) ->
void {
594 if (!ritsuko::is_rfc3339(x.c_str(), x.size())) {
595 throw std::runtime_error(
"date-times should follow the Internet Date/Time format");
602 }
else if (vector_type ==
"number") {
609 [](
double) ->
void {},
615 throw std::runtime_error(
"unknown vector type '" + vector_type +
"'");
619 auto vptr =
static_cast<Vector*
>(output.get());
620 extract_names(handle, vptr, options);
623 }
else if (object_type ==
"nothing") {
624 output.reset(Provisioner_::new_Nothing());
626 }
else if (object_type ==
"external") {
627 auto ihandle = handle.openDataSet(
"index");
628 if (ritsuko::hdf5::exceeds_integer_limit(ihandle, 32,
true)) {
629 throw std::runtime_error(
"external index at 'index' cannot be represented by a 32-bit signed integer");
632 if (ihandle.getSpace().getSimpleExtentNdims() != 0) {
633 throw std::runtime_error(
"expected scalar dataset at 'index'");
637 ihandle.read(&idx, H5::PredType::NATIVE_INT32);
639 throw std::runtime_error(
"external index at 'index' should be non-negative");
641 throw std::runtime_error(
"external index at 'index' is out of range");
644 output.reset(Provisioner_::new_External(ext.get(idx)));
647 throw std::runtime_error(
"unknown uzuki2 object type '" + object_type +
"'");
651}
catch (std::exception& e) {
652 throw std::runtime_error(
"failed to load object at '" + ritsuko::hdf5::get_name(handle) +
"'; " + std::string(e.what()));
704template<
class Provisioner_,
class Externals_>
707 if (group.attrExists(
"uzuki_version")) {
708 auto ver_str = read_uzuki_attr(group,
"uzuki_version");
709 auto vraw = ritsuko::parse_version_string(ver_str.c_str(), ver_str.size(),
true);
710 version.major = vraw.major;
711 version.minor = vraw.minor;
714 ExternalTracker etrack(std::move(ext));
715 auto ptr = parse_inner<Provisioner_>(group, etrack, version, options);
718 throw std::runtime_error(
"top-level object should represent an R list");
722 return ParsedList(std::move(ptr), std::move(version));
741template<
class Provisioner_,
class Externals_>
743 H5::H5File fhandle(file, H5F_ACC_RDONLY);
755inline void validate(
const H5::Group& group,
int num_external,
const Options& options) {
768inline void validate(
const std::string& file,
const std::string& name,
int num_external,
const Options& options) {
Dummy classes for parsing without storing the results.
Class to hold the parsed list.
Dummy class satisfying the Externals_ interface of hdf5::parse().
Definition Dummy.hpp:131
Format
Definition interfaces.hpp:159
Defines the interfaces to use in HDF5 parsing.
Container_ create(Value_ x, Args_ &&... args)
constexpr bool is_equal(Left_ left, Right_ right)
constexpr bool is_greater_than_or_equal(Left_ left, Right_ right)
constexpr Dest_ cap(Value_ x)
constexpr Dest_ cast(Value_ x)
ParsedList parse(const H5::Group &group, Externals_ ext, const Options &options)
Definition parse_hdf5.hpp:705
void validate(const H5::Group &group, int num_external, const Options &options)
Definition parse_hdf5.hpp:755
Parse an R list from a HDF5 or JSON file.
Definition parse_json.hpp:31
Results of parsing a list from file.
Definition ParsedList.hpp:19
Options for HDF5 file parsing.
Definition parse_hdf5.hpp:44
hsize_t buffer_size
Definition parse_hdf5.hpp:49
bool strict_list
Definition parse_hdf5.hpp:54