From c7cff2d9a7baab43b853da543ff07bf18ca35f9f Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:24:32 +0200 Subject: [PATCH] Fix smart album gte/lte rules missing equal f-number and focal length Found while testing crates/catalog/src/rules.rs:673 (num_op), reached via Rule::matches at :956-957 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/catalog/src/rules.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/catalog/src/rules.rs b/crates/catalog/src/rules.rs index 667509d6f..bdf4b8c88 100644 --- a/crates/catalog/src/rules.rs +++ b/crates/catalog/src/rules.rs @@ -670,6 +670,11 @@ fn number(v: &Value) -> Option { } } +/// Widens an `f32` to the `f64` nearest its shortest decimal form (1.4f32 -> 1.4), so it compares equal to the value a user types. +fn f32_as_decimal(v: f32) -> f64 { + v.to_string().parse().unwrap_or(v as f64) +} + fn num_op(op: &str, have: Option, value: &Value) -> bool { if op == "between" { let (a, b) = match value { @@ -953,8 +958,8 @@ impl Rule { "editDate" => date_op(op, p.edited.as_deref(), value), "shutterSpeed" => num_op(op, crate::parse_shutter_seconds(&m.shutter), &shutter_value(value)), "iso" => num_op(op, m.iso.map(|v| v as f64), value), - "aperture" => num_op(op, m.aperture.map(|v| v as f64), value), - "focalLength" => num_op(op, m.focal_mm.map(|v| v as f64), value), + "aperture" => num_op(op, m.aperture.map(f32_as_decimal), value), + "focalLength" => num_op(op, m.focal_mm.map(f32_as_decimal), value), "longEdge" | "shortEdge" => { let (w, h) = p.shown_size(); num_op(op, Some(if field == "longEdge" { w.max(h) } else { w.min(h) }.round()), value) @@ -1129,6 +1134,25 @@ mod tests { serde_json::from_value(v).unwrap() } + #[test] + fn f32_boundary_values_match_gte_lte() { + let cat = Catalog::new(); + let matches = + |p: &Photo, field: &str, op: &str, value: f64| rs(json!({"rules": [{"field": field, "op": op, "value": value}]})).matches(p, &cat); + for v in [1.4f32, 1.8, 2.8, 5.6, 0.95] { + let mut p = photo(1); + p.meta.aperture = Some(v); + p.meta.focal_mm = Some(v); + let w = v.to_string().parse::().unwrap(); + for field in ["aperture", "focalLength"] { + assert!(matches(&p, field, "gte", w), "{field} gte {w}"); + assert!(matches(&p, field, "lte", w), "{field} lte {w}"); + assert!(!matches(&p, field, "gt", w), "{field} gt {w}"); + assert!(!matches(&p, field, "lt", w), "{field} lt {w}"); + } + } + } + #[test] fn fields_ops_and_groups() { let cat = Catalog::new();