From 8fe7198dbdac3d90ee0f6bd15f6150e5b4649611 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 21:20:42 +0000 Subject: [PATCH] LR-FILT-SEARCH-META: >= and <= in fielded search tokens `rating:>=3`, `iso:<=400`, `f:>=2.8` read the `=` as part of the number, the parse failed, and the token silently matched no photo. Parse the two-character operators first and compare inclusively; `>`, `<`, `=` and bare numbers are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqKgSkHi5MfTQFrZvDKBjm --- crates/catalog/src/query.rs | 24 +++++++++++++----------- crates/catalog/src/tests.rs | 36 ++++++++++++++++++++++++++++++++++++ docs/parity-checklist.md | 4 ++-- docs/whats-new.md | 6 ++++++ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/crates/catalog/src/query.rs b/crates/catalog/src/query.rs index ff94268a8..7dea42e3f 100644 --- a/crates/catalog/src/query.rs +++ b/crates/catalog/src/query.rs @@ -20,7 +20,7 @@ pub enum RatingOp { #[serde(default, rename_all = "camelCase")] pub struct Filter { /// Free-text search: every token must match filename, title, caption, keywords, camera, lens, - /// location or format. Tokens like `rating:3`, `flag:pick`, `iso:>800`, `camera:x2` are fielded. + /// location or format. Tokens like `rating:3`, `rating:>=3`, `flag:pick`, `iso:>800`, `camera:x2` are fielded. pub text: String, pub rating: u8, pub rating_op: RatingOp, @@ -179,23 +179,25 @@ fn token_matches(p: &Photo, tok: &str) -> bool { // a token that starts with a quote is a phrase, even with a colon in it if let Some((field, quoted)) = t.split_once(':').filter(|_| !t.starts_with('"')) { let val = unquote(quoted); - let num = |s: &str| -> Option<(char, f64)> { - let (op, rest) = match s.chars().next()? { - c @ ('>' | '<' | '=') => (c, &s[1..]), - _ => ('=', s), - }; - rest.parse().ok().map(|v| (op, v)) + // `>=` / `<=` before `>` / `<`: otherwise `rating:>=3` reads `=3` as the number and matches nothing + let num = |s: &str| -> Option<(&str, f64)> { + let op = [">=", "<=", ">", "<", "="].into_iter().find(|op| s.starts_with(op)).unwrap_or(""); + s.get(op.len()..)?.parse().ok().map(|v| (op, v)) }; let cmp = |x: f64, s: &str| match num(s) { - Some(('>', v)) => x > v, - Some(('<', v)) => x < v, + Some((">", v)) => x > v, + Some(("<", v)) => x < v, + Some((">=", v)) => x > v || (x - v).abs() < 1e-9, + Some(("<=", v)) => x < v || (x - v).abs() < 1e-9, Some((_, v)) => (x - v).abs() < 1e-9, None => false, }; // Fields stored as `f32` compare at f32 precision: 2.8f32 widened to f64 is not 2.8. let cmp32 = |x: f32, s: &str| match num(s) { - Some(('>', v)) => x > v as f32, - Some(('<', v)) => x < v as f32, + Some((">", v)) => x > v as f32, + Some(("<", v)) => x < v as f32, + Some((">=", v)) => x >= v as f32, + Some(("<=", v)) => x <= v as f32, Some((_, v)) => x == v as f32, None => false, }; diff --git a/crates/catalog/src/tests.rs b/crates/catalog/src/tests.rs index 579175086..c27d1514b 100644 --- a/crates/catalog/src/tests.rs +++ b/crates/catalog/src/tests.rs @@ -163,6 +163,42 @@ fn search_tokens_match_f32_aperture_and_focal() { assert!(q("f:>2.8").is_empty(), "f:>2.8"); assert!(q("focal:<5.6").is_empty(), "focal:<5.6"); assert_eq!(q("f:>2.7"), vec![a], "f:>2.7"); + assert_eq!(q("f:>=2.8"), vec![a], "f:>=2.8 counts the photo's own value"); + assert_eq!(q("focal:<=5.6"), vec![a], "focal:<=5.6"); + assert!(q("f:>=2.9").is_empty(), "f:>=2.9"); + assert!(q("focal:<=5.5").is_empty(), "focal:<=5.5"); +} + +/// `>=` and `<=` in a fielded token are the inclusive comparisons. They used to read the `=` as +/// part of the number, so `rating:>=3` or `iso:<=400` silently matched no photo at all. +#[test] +fn search_tokens_compare_inclusively() { + let mut c = Catalog::new(); + let (two, three, four) = ( + photo(&mut c, "two.jpg", "2026-05-01T10:00:00"), + photo(&mut c, "three.jpg", "2026-05-02T10:00:00"), + photo(&mut c, "four.jpg", "2026-05-03T10:00:00"), + ); + for (id, rating, iso) in [(two, 2, 200), (three, 3, 400), (four, 4, 800)] { + c.apply(Op::SetRating { id, rating }).unwrap(); + let mut m = c.photo(id).unwrap().meta.clone(); + m.iso = Some(iso); + c.apply(Op::SetMeta { id, meta: Box::new(m) }).unwrap(); + } + let q = |t: &str| { + let mut ids = c.query(&Filter { text: t.into(), ..Default::default() }, &Sort::default()); + ids.sort(); + ids + }; + assert_eq!(q("rating:>=3"), vec![three, four]); + assert_eq!(q("stars:<=3"), vec![two, three]); + assert_eq!(q("iso:>=400"), vec![three, four]); + assert_eq!(q("iso:<=400"), vec![two, three]); + assert_eq!(q("rating:>3"), vec![four], "strict comparisons are unchanged"); + assert_eq!(q("rating:=3"), vec![three]); + assert_eq!(q("rating:3"), vec![three]); + assert!(q("rating:>=").is_empty(), "an operator without a number matches nothing"); + assert!(q("rating:=>3").is_empty(), "not an operator"); } /// A `captured` string that is not ASCII (the Lightroom importer copies `captureTime` into an diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 080680b9f..90cc314f7 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -146,7 +146,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | Id | Feature | Tier | Status | Evidence | Missing / notes | |---|---|---|---|---|---| -| LR-FILT-SEARCH-META | Text search | P0 | ✅ | `cmd:library.filter` (`text`), `crates/ui-egui/src/panels/topbar.rs`, `crates/catalog/src/query.rs` | fielded tokens (`rating:3`, `iso:>800`, `camera:…`); plain words also match the people named on faces; double quotes keep several words in one token (`person:"Jane Doe"`, `"jane doe"`, issue #1449); no suggestions dropdown | +| LR-FILT-SEARCH-META | Text search | P0 | ✅ | `cmd:library.filter` (`text`), `crates/ui-egui/src/panels/topbar.rs`, `crates/catalog/src/query.rs` | fielded tokens (`rating:3`, `iso:>800`, `rating:>=3` / `iso:<=400`, `camera:…`); plain words also match the people named on faces; double quotes keep several words in one token (`person:"Jane Doe"`, `"jane doe"`, issue #1449); no suggestions dropdown | | LR-FILT-SEARCH-AI | Natural-language search | P2 | ⬜ | | | | LR-FILT-RATING | Rating filter | P0 | ✅ | `cmd:library.filter` (`rating`, `ratingOp`), `crates/ui-egui/src/panels/filterbar.rs` | ≥ / = / ≤ stars in the filter bar (`cmd:view.filterBar`) | | LR-FILT-FLAG | Flag filter | P0 | ✅ | `cmd:library.filter` (`flag`), `crates/ui-egui/src/panels/filterbar.rs` | picked / rejected / unflagged (one at a time) | diff --git a/docs/whats-new.md b/docs/whats-new.md index c6c259200..cacd78b3d 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,11 @@ # What's new in LightCraft +## 2026-10-25 + +### `>=` and `<=` work in the search box +- Fielded search tokens now take inclusive comparisons: `rating:>=3`, `iso:<=400`, `f:>=2.8`, `focal:<=50`. Before, + the `=` was read as part of the number, so such a search quietly showed no photos at all. + ## 2026-10-24 ### Relative-date filters and smart albums follow the clock