feat(tools): h5rs, pure-Rust HDF5 tools (ls, dump, stat, diff, check)
New workspace crate clawhdf5-tools with one binary, h5rs, built only on the clawhdf5 facade and clawhdf5-format (no libhdf5, no C): - ls [-r] [-v] FILE[/path]: h5ls's listing (same text in its first two columns) plus the datatype; -v adds address, link count, layout and chunk index, chunk size, storage, filters, datatype and attributes. - dump [--json] [-A] [-p] [-d PATH] FILE: h5dump DDL (byte-identical to h5dump 1.14.6 on the test files) or hdf5-json. - stat FILE: h5stat's object/link/rank/layout/filter/attribute counts, raw data and total size. - diff [-r] [-q] [-d D] [-p R] A B [OBJ1 [OBJ2]]: structural and value differences, exit 0/1/2 like h5diff. - check [--data] FILE: walks every object, parses every message, verifies the checksums of every v2+ structure (including the fractal heap blocks the library never checks), checks chunk indexes against their datasets and raw data for out-of-file or overlapping extents; every problem with its address. Values over --max-bytes are reported, not read; dense-storage heaps are verified before objects are read from them; panics are caught (exit 3). Tests compare with h5ls, h5stat, h5dump and h5diff and with h5py's values, and flip the checksum of every checksummed structure in a v1.14-format file. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
//! `h5rs diff`: compare two files (or two objects) like h5diff.
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use clawhdf5_format::attribute::AttributeMessage;
|
||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||
use clawhdf5_format::datatype::Datatype;
|
||||
use clawhdf5_format::object_header::ObjectHeader;
|
||||
|
||||
use crate::cli::{Args, Out};
|
||||
use crate::h5::{H5, Kind, LinkKind};
|
||||
use crate::value::{self, Decoder, Value};
|
||||
|
||||
pub const USAGE: &str = "\
|
||||
usage: h5rs diff [options] FILE1 FILE2 [OBJ1 [OBJ2]]
|
||||
|
||||
Compare FILE1 and FILE2 (or OBJ1 in FILE1 with OBJ2 in FILE2, and everything
|
||||
below them): the objects present, their kinds, datatypes, shapes, attribute
|
||||
sets and values, and soft/external link targets.
|
||||
|
||||
-r, --report list every differing element (position, values, difference)
|
||||
-q, --quiet print nothing; only the exit status
|
||||
-d, --delta D numbers differ only when |a - b| > D
|
||||
-p, --relative R numbers differ only when |a - b| / |a| > R
|
||||
-c, --count N list at most N differing elements per object with -r
|
||||
--max-bytes N largest dataset read (default 1 GiB); a larger one is an error
|
||||
|
||||
Two NaNs compare equal. Unlike h5diff, objects that cannot be compared
|
||||
(different kinds, datatype classes or shapes) count as a difference.
|
||||
|
||||
Exit status: 0 no differences, 1 differences found, 2 error (a file or
|
||||
object could not be opened or read).";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Tol {
|
||||
Exact,
|
||||
Delta(f64),
|
||||
Relative(f64),
|
||||
}
|
||||
|
||||
struct Opts {
|
||||
report: bool,
|
||||
quiet: bool,
|
||||
tol: Tol,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
/// What a relative path names in one file.
|
||||
#[derive(Clone)]
|
||||
enum Entry {
|
||||
Obj(u64, Kind),
|
||||
Soft(String),
|
||||
External(String, String),
|
||||
UserDefined(u8),
|
||||
Broken(String),
|
||||
}
|
||||
|
||||
struct Side<'a> {
|
||||
h5: &'a H5,
|
||||
label: String,
|
||||
base: String,
|
||||
/// Relative path -> what it is.
|
||||
entries: BTreeMap<String, Entry>,
|
||||
/// Address -> relative path, for comparing references.
|
||||
rel_of: OnceCell<HashMap<u64, String>>,
|
||||
}
|
||||
|
||||
impl Side<'_> {
|
||||
fn full(&self, rel: &str) -> String {
|
||||
if rel.is_empty() {
|
||||
if self.base.is_empty() {
|
||||
"/".into()
|
||||
} else {
|
||||
self.base.clone()
|
||||
}
|
||||
} else {
|
||||
format!("{}{rel}", self.base)
|
||||
}
|
||||
}
|
||||
|
||||
fn rel_paths(&self) -> &HashMap<u64, String> {
|
||||
self.rel_of.get_or_init(|| {
|
||||
let mut m = HashMap::new();
|
||||
let _ = self.h5.walk(|it| {
|
||||
if let (Some(a), None) = (it.addr, it.first_path) {
|
||||
m.entry(a).or_insert_with(|| it.path.to_string());
|
||||
}
|
||||
});
|
||||
m
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Diff {
|
||||
opts: Opts,
|
||||
diffs: u64,
|
||||
/// Differences already reported under an object heading.
|
||||
per_object: u64,
|
||||
errors: u64,
|
||||
}
|
||||
|
||||
pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
||||
let mut opts = Opts {
|
||||
report: false,
|
||||
quiet: false,
|
||||
tol: Tol::Exact,
|
||||
count: usize::MAX,
|
||||
};
|
||||
let mut max_bytes = None;
|
||||
let mut pos = Vec::new();
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"-r" | "--report" => opts.report = true,
|
||||
"-q" | "--quiet" => opts.quiet = true,
|
||||
"-d" | "--delta" => match args.number::<f64>() {
|
||||
Some(d) if d >= 0.0 => opts.tol = Tol::Delta(d),
|
||||
_ => return args.usage_error(out, "--delta needs a number >= 0", USAGE),
|
||||
},
|
||||
"-p" | "--relative" => match args.number::<f64>() {
|
||||
Some(r) if r >= 0.0 => opts.tol = Tol::Relative(r),
|
||||
_ => return args.usage_error(out, "--relative needs a number >= 0", USAGE),
|
||||
},
|
||||
"-c" | "--count" => match args.number() {
|
||||
Some(n) => opts.count = n,
|
||||
None => return args.usage_error(out, "--count needs a number", USAGE),
|
||||
},
|
||||
"--max-bytes" => match args.number() {
|
||||
Some(n) => max_bytes = Some(n),
|
||||
None => return args.usage_error(out, "--max-bytes needs a number", USAGE),
|
||||
},
|
||||
"-h" | "--help" => {
|
||||
writeln!(out.o, "{USAGE}")?;
|
||||
return Ok(0);
|
||||
}
|
||||
s if s.starts_with('-') && s.len() > 1 => {
|
||||
return args.usage_error(out, &format!("unknown option {a}"), USAGE);
|
||||
}
|
||||
_ => pos.push(a),
|
||||
}
|
||||
}
|
||||
if pos.len() < 2 || pos.len() > 4 {
|
||||
return args.usage_error(out, "expected FILE1 FILE2 [OBJ1 [OBJ2]]", USAGE);
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
for f in &pos[..2] {
|
||||
match H5::open(std::path::Path::new(f)) {
|
||||
Ok(mut h) => {
|
||||
if let Some(m) = max_bytes {
|
||||
h.max_bytes = m;
|
||||
}
|
||||
files.push(h);
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(out.e, "h5rs diff: {e}")?;
|
||||
return Ok(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
let obj1 = pos.get(2).cloned().unwrap_or_else(|| "/".into());
|
||||
let obj2 = pos.get(3).cloned().unwrap_or_else(|| obj1.clone());
|
||||
let mut sides = Vec::new();
|
||||
for (h5, (obj, f)) in files.iter().zip([(&obj1, &pos[0]), (&obj2, &pos[1])]) {
|
||||
let addr = match h5.resolve(obj) {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
writeln!(
|
||||
out.e,
|
||||
"h5rs diff: object <{obj}> could not be found in <{f}>"
|
||||
)?;
|
||||
return Ok(2);
|
||||
}
|
||||
};
|
||||
let base = obj.trim_end_matches('/').to_string();
|
||||
let base = if base.is_empty() || base.starts_with('/') {
|
||||
base
|
||||
} else {
|
||||
format!("/{base}")
|
||||
};
|
||||
let mut entries = BTreeMap::new();
|
||||
let walk = h5.walk_from(addr, "", &mut |it| {
|
||||
let e = match (it.link.map(|l| &l.kind), it.header) {
|
||||
(Some(LinkKind::Soft(t)), _) => Entry::Soft(t.clone()),
|
||||
(Some(LinkKind::External { file, path }), _) => {
|
||||
Entry::External(file.clone(), path.clone())
|
||||
}
|
||||
(Some(LinkKind::UserDefined(t)), _) => Entry::UserDefined(*t),
|
||||
(_, Some(Err(e))) => Entry::Broken(e.to_string()),
|
||||
(_, Some(Ok(h))) => Entry::Obj(it.addr.unwrap_or(0), Kind::of(h)),
|
||||
// A second hard link to an object already compared.
|
||||
(_, None) => return,
|
||||
};
|
||||
entries.insert(it.path.to_string(), e);
|
||||
});
|
||||
if let Err(e) = walk {
|
||||
writeln!(out.e, "h5rs diff: {f}: {e}")?;
|
||||
return Ok(2);
|
||||
}
|
||||
sides.push(Side {
|
||||
h5,
|
||||
label: f.clone(),
|
||||
base,
|
||||
entries,
|
||||
rel_of: OnceCell::new(),
|
||||
});
|
||||
}
|
||||
let (a, b) = (&sides[0], &sides[1]);
|
||||
let mut d = Diff {
|
||||
opts,
|
||||
diffs: 0,
|
||||
per_object: 0,
|
||||
errors: 0,
|
||||
};
|
||||
let mut names: Vec<&String> = a.entries.keys().chain(b.entries.keys()).collect();
|
||||
names.sort();
|
||||
names.dedup();
|
||||
for rel in names {
|
||||
match (a.entries.get(rel), b.entries.get(rel)) {
|
||||
(Some(_), None) => {
|
||||
d.diffs += 1;
|
||||
d.say(
|
||||
out,
|
||||
&format!("<{}> exists only in <{}>", a.full(rel), a.label),
|
||||
)?;
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
d.diffs += 1;
|
||||
d.say(
|
||||
out,
|
||||
&format!("<{}> exists only in <{}>", b.full(rel), b.label),
|
||||
)?;
|
||||
}
|
||||
(Some(ea), Some(eb)) => d.entry(out, a, b, rel, ea, eb)?,
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
// Per-object counts were printed with each object; add a total when
|
||||
// they do not already tell the whole story.
|
||||
if !d.opts.quiet && d.diffs > 0 && d.diffs != d.per_object {
|
||||
writeln!(out.o, "{} difference(s) found in total", d.diffs)?;
|
||||
}
|
||||
Ok(if d.errors > 0 {
|
||||
2
|
||||
} else if d.diffs > 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
})
|
||||
}
|
||||
|
||||
fn kind_word(k: Kind) -> &'static str {
|
||||
match k {
|
||||
Kind::Group => "group",
|
||||
Kind::Dataset => "dataset",
|
||||
Kind::Datatype => "datatype",
|
||||
Kind::Unknown => "object",
|
||||
}
|
||||
}
|
||||
|
||||
impl Diff {
|
||||
fn say(&self, out: &mut Out, msg: &str) -> std::io::Result<()> {
|
||||
if self.opts.quiet {
|
||||
Ok(())
|
||||
} else {
|
||||
writeln!(out.o, "{msg}")
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&mut self, out: &mut Out, msg: &str) -> std::io::Result<()> {
|
||||
self.errors += 1;
|
||||
writeln!(out.e, "h5rs diff: {msg}")
|
||||
}
|
||||
|
||||
fn entry(
|
||||
&mut self,
|
||||
out: &mut Out,
|
||||
a: &Side,
|
||||
b: &Side,
|
||||
rel: &str,
|
||||
ea: &Entry,
|
||||
eb: &Entry,
|
||||
) -> std::io::Result<()> {
|
||||
let (pa, pb) = (a.full(rel), b.full(rel));
|
||||
match (ea, eb) {
|
||||
(Entry::Broken(e), _) => self.error(out, &format!("<{pa}> in <{}>: {e}", a.label)),
|
||||
(_, Entry::Broken(e)) => self.error(out, &format!("<{pb}> in <{}>: {e}", b.label)),
|
||||
(Entry::Soft(x), Entry::Soft(y)) => {
|
||||
if x != y {
|
||||
self.diffs += 1;
|
||||
self.say(out, &format!("soft link: <{pa}> -> {x} and <{pb}> -> {y}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Entry::External(f1, p1), Entry::External(f2, p2)) => {
|
||||
if (f1, p1) != (f2, p2) {
|
||||
self.diffs += 1;
|
||||
self.say(
|
||||
out,
|
||||
&format!("external link: <{pa}> -> {f1}:{p1} and <{pb}> -> {f2}:{p2}"),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Entry::UserDefined(x), Entry::UserDefined(y)) => {
|
||||
if x != y {
|
||||
self.diffs += 1;
|
||||
self.say(out, &format!("user-defined link: <{pa}> and <{pb}> differ"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Entry::Obj(aa, ka), Entry::Obj(ab, kb)) if ka == kb => {
|
||||
let (ha, hb) = match (a.h5.header(*aa), b.h5.header(*ab)) {
|
||||
(Ok(x), Ok(y)) => (x, y),
|
||||
(Err(e), _) | (_, Err(e)) => return self.error(out, &format!("<{pa}>: {e}")),
|
||||
};
|
||||
let before = self.diffs;
|
||||
let mut rows = Vec::new();
|
||||
match ka {
|
||||
Kind::Dataset => self.dataset(out, a, b, &pa, &pb, &ha, &hb, &mut rows)?,
|
||||
Kind::Datatype => match (a.h5.datatype(&ha), b.h5.datatype(&hb)) {
|
||||
(Ok(x), Ok(y)) => {
|
||||
if x != y {
|
||||
self.diffs += 1;
|
||||
rows.push("datatypes differ".to_string());
|
||||
}
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => self.error(out, &format!("<{pa}>: {e}"))?,
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.attributes(out, a, b, &pa, &ha, &hb, &mut rows)?;
|
||||
let n = self.diffs - before;
|
||||
if n > 0 && !self.opts.quiet {
|
||||
writeln!(out.o, "{}: <{pa}> and <{pb}>", kind_word(*ka))?;
|
||||
for r in &rows {
|
||||
writeln!(out.o, "{r}")?;
|
||||
}
|
||||
writeln!(out.o, "{n} difference(s) found")?;
|
||||
self.per_object += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
self.diffs += 1;
|
||||
self.say(
|
||||
out,
|
||||
&format!(
|
||||
"Not comparable: <{pa}> is a {} and <{pb}> is a {}",
|
||||
entry_word(ea),
|
||||
entry_word(eb)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dataset(
|
||||
&mut self,
|
||||
out: &mut Out,
|
||||
a: &Side,
|
||||
b: &Side,
|
||||
pa: &str,
|
||||
pb: &str,
|
||||
ha: &ObjectHeader,
|
||||
hb: &ObjectHeader,
|
||||
rows: &mut Vec<String>,
|
||||
) -> std::io::Result<()> {
|
||||
let got = (|| -> crate::h5::Result<_> {
|
||||
Ok((
|
||||
a.h5.datatype(ha)?,
|
||||
a.h5.resolved_dataspace(pa, ha)?,
|
||||
b.h5.datatype(hb)?,
|
||||
b.h5.resolved_dataspace(pb, hb)?,
|
||||
))
|
||||
})();
|
||||
let (dta, dsa, dtb, dsb) = match got {
|
||||
Ok(x) => x,
|
||||
Err(e) => return self.error(out, &format!("<{pa}>: {e}")),
|
||||
};
|
||||
if let Some(why) = not_comparable(&dta, &dsa, &dtb, &dsb) {
|
||||
self.diffs += 1;
|
||||
rows.push(format!("Not comparable: {why}"));
|
||||
return Ok(());
|
||||
}
|
||||
let raw =
|
||||
a.h5.read_dataset(pa, &dta, &dsa)
|
||||
.and_then(|x| Ok((x, b.h5.read_dataset(pb, &dtb, &dsb)?)));
|
||||
let (ra, rb) = match raw {
|
||||
Ok(x) => x,
|
||||
Err(e) => return self.error(out, &format!("<{pa}>: {e}")),
|
||||
};
|
||||
self.values(out, a, b, pa, (&dta, &ra), (&dtb, &rb), &dsa, rows)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn values(
|
||||
&mut self,
|
||||
out: &mut Out,
|
||||
a: &Side,
|
||||
b: &Side,
|
||||
what: &str,
|
||||
(dta, ra): (&Datatype, &[u8]),
|
||||
(dtb, rb): (&Datatype, &[u8]),
|
||||
ds: &Dataspace,
|
||||
rows: &mut Vec<String>,
|
||||
) -> std::io::Result<()> {
|
||||
let n = crate::h5::num_elements(ds).unwrap_or(0) as usize;
|
||||
let (da, db) = (Decoder::new(a.h5), Decoder::new(b.h5));
|
||||
let dims: Vec<u64> = match ds.space_type {
|
||||
DataspaceType::Simple => ds.dimensions.clone(),
|
||||
_ => vec![1],
|
||||
};
|
||||
let mut found = 0u64;
|
||||
let mut header = false;
|
||||
for i in 0..n {
|
||||
let va = da.element(dta, ra, i);
|
||||
let vb = db.element(dtb, rb, i);
|
||||
if let (Value::Error(e), _) | (_, Value::Error(e)) = (&va, &vb) {
|
||||
return self.error(out, &format!("<{what}> element {i}: {e}"));
|
||||
}
|
||||
if self.equal(a, b, &va, &vb) {
|
||||
continue;
|
||||
}
|
||||
found += 1;
|
||||
if self.opts.report && (found as usize) <= self.opts.count {
|
||||
if !header {
|
||||
header = true;
|
||||
rows.push(format!(
|
||||
"{:<24}{:<24}{:<24}{}",
|
||||
"position", "value 1", "value 2", "difference"
|
||||
));
|
||||
rows.push("-".repeat(80));
|
||||
}
|
||||
let pos = format!("[ {} ]", index(i as u64, &dims));
|
||||
let ta = value::text(&va, &|_| None);
|
||||
let tb = value::text(&vb, &|_| None);
|
||||
let dif = match (number(&va), number(&vb)) {
|
||||
(Some(x), Some(y)) => value::fmt_float((x - y).abs(), 64),
|
||||
_ => String::new(),
|
||||
};
|
||||
rows.push(format!("{pos:<24}{ta:<24}{tb:<24}{dif}"));
|
||||
}
|
||||
}
|
||||
self.diffs += found;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn attributes(
|
||||
&mut self,
|
||||
out: &mut Out,
|
||||
a: &Side,
|
||||
b: &Side,
|
||||
path: &str,
|
||||
ha: &ObjectHeader,
|
||||
hb: &ObjectHeader,
|
||||
rows: &mut Vec<String>,
|
||||
) -> std::io::Result<()> {
|
||||
let (la, lb) = match (a.h5.attributes(ha), b.h5.attributes(hb)) {
|
||||
(Ok(x), Ok(y)) => (x, y),
|
||||
(Err(e), _) | (_, Err(e)) => return self.error(out, &format!("<{path}>: {e}")),
|
||||
};
|
||||
for e in la.1.iter().chain(lb.1.iter()) {
|
||||
self.error(out, &format!("<{path}>: attribute: {e}"))?;
|
||||
}
|
||||
let ma: BTreeMap<&str, &AttributeMessage> =
|
||||
la.0.iter().map(|x| (x.name.as_str(), x)).collect();
|
||||
let mb: BTreeMap<&str, &AttributeMessage> =
|
||||
lb.0.iter().map(|x| (x.name.as_str(), x)).collect();
|
||||
let mut names: Vec<&str> = ma.keys().chain(mb.keys()).copied().collect();
|
||||
names.sort_unstable();
|
||||
names.dedup();
|
||||
for n in names {
|
||||
match (ma.get(n), mb.get(n)) {
|
||||
(Some(x), Some(y)) => {
|
||||
if let Some(why) =
|
||||
not_comparable(&x.datatype, &x.dataspace, &y.datatype, &y.dataspace)
|
||||
{
|
||||
self.diffs += 1;
|
||||
rows.push(format!("attribute \"{n}\": not comparable: {why}"));
|
||||
continue;
|
||||
}
|
||||
for (m, h5) in [(x, a.h5), (y, b.h5)] {
|
||||
let need =
|
||||
crate::h5::byte_len(&m.dataspace, &m.datatype).unwrap_or(u64::MAX);
|
||||
if need > h5.max_bytes || (m.raw_data.len() as u64) < need {
|
||||
return self.error(
|
||||
out,
|
||||
&format!(
|
||||
"<{path}> attribute \"{n}\": value is truncated or too large"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
let before = rows.len();
|
||||
let what = format!("{path}\" attribute \"{n}");
|
||||
self.values(
|
||||
out,
|
||||
a,
|
||||
b,
|
||||
&what,
|
||||
(&x.datatype, &x.raw_data),
|
||||
(&y.datatype, &y.raw_data),
|
||||
&x.dataspace,
|
||||
rows,
|
||||
)?;
|
||||
if rows.len() > before {
|
||||
rows.insert(before, format!("attribute \"{n}\":"));
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
self.diffs += 1;
|
||||
rows.push(format!("attribute \"{n}\" exists only in <{}>", a.label));
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
self.diffs += 1;
|
||||
rows.push(format!("attribute \"{n}\" exists only in <{}>", b.label));
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn equal(&self, a: &Side, b: &Side, x: &Value, y: &Value) -> bool {
|
||||
if let (Some(p), Some(q)) = (number(x), number(y)) {
|
||||
let exact_ints = matches!((x, y), (Value::Int(_), Value::Int(_)))
|
||||
|| matches!((x, y), (Value::Enum(..), Value::Enum(..)));
|
||||
if exact_ints && matches!(self.opts.tol, Tol::Exact) {
|
||||
return int_of(x) == int_of(y);
|
||||
}
|
||||
return close(p, q, self.opts.tol);
|
||||
}
|
||||
match (x, y) {
|
||||
(Value::Str(p), Value::Str(q)) => p == q,
|
||||
(Value::Bytes(p), Value::Bytes(q)) | (Value::OtherRef(p), Value::OtherRef(q)) => p == q,
|
||||
(Value::Compound(p), Value::Compound(q)) => {
|
||||
p.len() == q.len()
|
||||
&& p.iter()
|
||||
.zip(q)
|
||||
.all(|((_, u), (_, v))| self.equal(a, b, u, v))
|
||||
}
|
||||
(Value::Array(p), Value::Array(q)) | (Value::Seq(p), Value::Seq(q)) => {
|
||||
p.len() == q.len() && p.iter().zip(q).all(|(u, v)| self.equal(a, b, u, v))
|
||||
}
|
||||
(Value::Ref(None), Value::Ref(None)) => true,
|
||||
(Value::Ref(Some(p)), Value::Ref(Some(q))) => {
|
||||
// Addresses mean nothing across files: compare the paths the
|
||||
// references lead to.
|
||||
let (pp, qq) = (a.rel_paths().get(p), b.rel_paths().get(q));
|
||||
pp.is_some() && pp == qq
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn int_of(v: &Value) -> Option<i128> {
|
||||
match v {
|
||||
Value::Int(i) | Value::Enum(_, i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn number(v: &Value) -> Option<f64> {
|
||||
match v {
|
||||
Value::Int(i) | Value::Enum(_, i) => Some(*i as f64),
|
||||
Value::Float(f, _) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn close(a: f64, b: f64, tol: Tol) -> bool {
|
||||
if a.is_nan() || b.is_nan() {
|
||||
return a.is_nan() && b.is_nan();
|
||||
}
|
||||
if a == b {
|
||||
return true;
|
||||
}
|
||||
let d = (a - b).abs();
|
||||
match tol {
|
||||
Tol::Exact => false,
|
||||
Tol::Delta(t) => d <= t,
|
||||
Tol::Relative(r) => a != 0.0 && d / a.abs() <= r,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_word(e: &Entry) -> &'static str {
|
||||
match e {
|
||||
Entry::Obj(_, k) => kind_word(*k),
|
||||
Entry::Soft(_) => "soft link",
|
||||
Entry::External(..) => "external link",
|
||||
Entry::UserDefined(_) => "user-defined link",
|
||||
Entry::Broken(_) => "unreadable object",
|
||||
}
|
||||
}
|
||||
|
||||
/// Why two datasets/attributes cannot be compared element by element.
|
||||
fn not_comparable(
|
||||
dta: &Datatype,
|
||||
dsa: &Dataspace,
|
||||
dtb: &Datatype,
|
||||
dsb: &Dataspace,
|
||||
) -> Option<String> {
|
||||
let rank = |d: &Dataspace| match d.space_type {
|
||||
DataspaceType::Simple => Some(d.dimensions.clone()),
|
||||
DataspaceType::Scalar => Some(Vec::new()),
|
||||
DataspaceType::Null => None,
|
||||
};
|
||||
let (sa, sb) = (rank(dsa), rank(dsb));
|
||||
if sa != sb {
|
||||
let show = |s: &Option<Vec<u64>>| match s {
|
||||
None => "null".to_string(),
|
||||
Some(d) if d.is_empty() => "scalar".to_string(),
|
||||
Some(d) => format!("{d:?}"),
|
||||
};
|
||||
return Some(format!("shapes differ: {} and {}", show(&sa), show(&sb)));
|
||||
}
|
||||
if !types_comparable(dta, dtb) {
|
||||
return Some(format!(
|
||||
"datatypes differ: {} and {}",
|
||||
crate::dtype::short(dta),
|
||||
crate::dtype::short(dtb)
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn types_comparable(a: &Datatype, b: &Datatype) -> bool {
|
||||
use crate::dtype::class;
|
||||
let numeric = |t: &Datatype| matches!(class(t), "integer" | "float");
|
||||
if numeric(a) && numeric(b) {
|
||||
return class(a) == class(b);
|
||||
}
|
||||
if class(a) != class(b) {
|
||||
return false;
|
||||
}
|
||||
match (a, b) {
|
||||
(Datatype::Compound { members: ma, .. }, Datatype::Compound { members: mb, .. }) => {
|
||||
ma.len() == mb.len()
|
||||
&& ma
|
||||
.iter()
|
||||
.zip(mb)
|
||||
.all(|(x, y)| x.name == y.name && types_comparable(&x.datatype, &y.datatype))
|
||||
}
|
||||
(
|
||||
Datatype::Array {
|
||||
base_type: x,
|
||||
dimensions: dx,
|
||||
},
|
||||
Datatype::Array {
|
||||
base_type: y,
|
||||
dimensions: dy,
|
||||
},
|
||||
) => dx == dy && types_comparable(x, y),
|
||||
(
|
||||
Datatype::VariableLength { base_type: x, .. },
|
||||
Datatype::VariableLength { base_type: y, .. },
|
||||
) => class(a) == "string" || types_comparable(x, y),
|
||||
(
|
||||
Datatype::Enumeration { base_type: x, .. },
|
||||
Datatype::Enumeration { base_type: y, .. },
|
||||
) => types_comparable(x, y),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn index(mut i: u64, dims: &[u64]) -> String {
|
||||
let mut idx = vec![0u64; dims.len()];
|
||||
for (k, &d) in dims.iter().enumerate().rev() {
|
||||
let d = d.max(1);
|
||||
idx[k] = i % d;
|
||||
i /= d;
|
||||
}
|
||||
idx.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tolerances() {
|
||||
assert!(close(1.0, 1.0, Tol::Exact));
|
||||
assert!(!close(1.0, 1.001, Tol::Exact));
|
||||
assert!(close(1.0, 1.001, Tol::Delta(0.01)));
|
||||
assert!(!close(1.0, 1.1, Tol::Delta(0.01)));
|
||||
assert!(close(100.0, 101.0, Tol::Relative(0.02)));
|
||||
assert!(!close(0.0, 1e-9, Tol::Relative(0.5)));
|
||||
assert!(close(f64::NAN, f64::NAN, Tol::Exact));
|
||||
assert!(!close(f64::NAN, 1.0, Tol::Delta(1e9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
assert_eq!(index(5, &[3, 4]), "1 1");
|
||||
assert_eq!(index(0, &[1]), "0");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user