diff --git a/ftl/core/search.ftl b/ftl/core/search.ftl index 1869741bc..fcae80877 100644 --- a/ftl/core/search.ftl +++ b/ftl/core/search.ftl @@ -17,7 +17,7 @@ search-invalid-argument = `{ $term }` was given an invalid argument '`{ $argumen search-invalid-flag = `flag:` must be followed by a valid flag number: `1` (red), `2` (orange), `3` (green), `4` (blue) or `0` (no flag). search-invalid-followed-by-positive-days = `{ $term }` must be followed by a positive number of days. search-invalid-rated-days = `rated:` must be followed by a positive number of days. -search-invalid-rated-ease = `rated:{ $val }:` must be followed by `1` (again), `2` (hard), `3` (good) or `4` (easy). +search-invalid-rated-ease = `{ $val }:` must be followed by `1` (again), `2` (hard), `3` (good) or `4` (easy). search-invalid-prop-operator = `prop:{ $val }` must be followed by one of the comparison operators: `=`, `!=`, `<`, `>`, `<=` or `>=`. search-invalid-prop-float = `prop:{ $val }` must be followed by a decimal number. search-invalid-prop-integer = `prop:{ $val }` must be followed by a whole number. diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index ff7f1ab84..6fdff4d8b 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -87,6 +87,7 @@ pub enum PropertyKind { Lapses(u32), Ease(f32), Position(u32), + Rated(i32, EaseKind), } #[derive(Debug, PartialEq, Clone)] @@ -350,9 +351,9 @@ fn parse_flag(s: &str) -> ParseResult { /// eg resched:3 fn parse_resched(s: &str) -> ParseResult { - if let Ok(d) = s.parse::() { + if let Ok(days) = s.parse::() { Ok(SearchNode::Rated { - days: d.max(1).min(365), + days, ease: EaseKind::ManualReschedule, }) } else { @@ -369,6 +370,8 @@ fn parse_prop(s: &str) -> ParseResult { tag("lapses"), tag("ease"), tag("pos"), + tag("rated"), + tag("resched"), ))(s) .map_err(|_| parse_failure(s, FailKind::InvalidPropProperty(s.into())))?; @@ -400,6 +403,54 @@ fn parse_prop(s: &str) -> ParseResult { FailKind::InvalidPropInteger(format!("{}{}", prop, operator)), )); } + } else if prop == "rated" { + let mut it = num.splitn(2, ':'); + + let days: i32 = if let Ok(i) = it.next().unwrap().parse::() { + i.min(0) + } else { + return Err(parse_failure( + s, + FailKind::InvalidPropInteger(format!("{}{}", prop, operator)), + )); + }; + + let ease = match it.next() { + Some(v) => { + if let Ok(u) = v.parse::() { + if (1..5).contains(&u) { + EaseKind::AnswerButton(u) + } else { + return Err(parse_failure( + s, + FailKind::InvalidRatedEase(format!( + "prop:{}{}{}", + prop, + operator, + days.to_string() + )), + )); + } + } else { + return Err(parse_failure( + s, + FailKind::InvalidPropInteger(format!("{}{}", prop, operator)), + )); + } + } + None => EaseKind::AnyAnswerButton, + }; + + PropertyKind::Rated(days, ease) + } else if prop == "resched" { + if let Ok(days) = num.parse::() { + PropertyKind::Rated(days.min(0), EaseKind::ManualReschedule) + } else { + return Err(parse_failure( + s, + FailKind::InvalidPropInteger(format!("{}{}", prop, operator)), + )); + } } else if let Ok(u) = num.parse::() { match prop { "ivl" => PropertyKind::Interval(u), @@ -443,8 +494,8 @@ fn parse_edited(s: &str) -> ParseResult { /// second arg must be between 1-4 fn parse_rated(s: &str) -> ParseResult { let mut it = s.splitn(2, ':'); - if let Ok(d) = it.next().unwrap().parse::() { - let days = d.max(1).min(365); + if let Ok(days) = it.next().unwrap().parse::() { + let days = days.max(1); let ease = if let Some(tail) = it.next() { if let Ok(u) = tail.parse::() { if u > 0 && u < 5 { @@ -452,13 +503,13 @@ fn parse_rated(s: &str) -> ParseResult { } else { return Err(parse_failure( s, - FailKind::InvalidRatedEase(days.to_string()), + FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())), )); } } else { return Err(parse_failure( s, - FailKind::InvalidRatedEase(days.to_string()), + FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())), )); } } else { @@ -872,10 +923,10 @@ mod test { assert_err_kind("rated:", InvalidRatedDays); assert_err_kind("rated:foo", InvalidRatedDays); - assert_err_kind("rated:1:", InvalidRatedEase("1".to_string())); - assert_err_kind("rated:2:-1", InvalidRatedEase("2".to_string())); - assert_err_kind("rated:3:1.1", InvalidRatedEase("3".to_string())); - assert_err_kind("rated:0:foo", InvalidRatedEase("1".to_string())); + assert_err_kind("rated:1:", InvalidRatedEase("rated:1".to_string())); + assert_err_kind("rated:2:-1", InvalidRatedEase("rated:2".to_string())); + assert_err_kind("rated:3:1.1", InvalidRatedEase("rated:3".to_string())); + assert_err_kind("rated:0:foo", InvalidRatedEase("rated:1".to_string())); assert_err_kind("resched:", FailKind::InvalidResched); assert_err_kind("resched:-1", FailKind::InvalidResched); diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 4e2faf076..e64b7f6df 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -144,7 +144,7 @@ impl SqlWriter<'_> { write!(self.sql, "c.did = {}", did).unwrap(); } SearchNode::NoteType(notetype) => self.write_note_type(&norm(notetype))?, - SearchNode::Rated { days, ease } => self.write_rated(*days, ease)?, + SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?, SearchNode::Tag(tag) => self.write_tag(&norm(tag))?, SearchNode::State(state) => self.write_state(state)?, @@ -214,14 +214,32 @@ impl SqlWriter<'_> { Ok(()) } - fn write_rated(&mut self, days: u32, ease: &EaseKind) -> Result<()> { + fn write_rated(&mut self, op: &str, days: i64, ease: &EaseKind) -> Result<()> { let today_cutoff = self.col.timing_today()?.next_day_at; - let target_cutoff_ms = (today_cutoff - 86_400 * i64::from(days)) * 1_000; - write!( - self.sql, - "c.id in (select cid from revlog where id>{}", - target_cutoff_ms, - ) + let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000; + let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000; + + write!(self.sql, "c.id in (select cid from revlog where id").unwrap(); + + match op { + ">" => write!(self.sql, " >= {}", target_cutoff_ms), + ">=" => write!(self.sql, " >= {}", day_before_cutoff_ms), + "<" => write!(self.sql, " < {}", day_before_cutoff_ms), + "<=" => write!(self.sql, " < {}", target_cutoff_ms), + "=" => write!( + self.sql, + " between {} and {}", + day_before_cutoff_ms, + target_cutoff_ms - 1 + ), + "!=" => write!( + self.sql, + " not between {} and {}", + day_before_cutoff_ms, + target_cutoff_ms - 1 + ), + _ => unreachable!("unexpected op"), + } .unwrap(); match ease { @@ -255,25 +273,25 @@ impl SqlWriter<'_> { previewrepeat = CardQueue::PreviewRepeat as i8, cutoff = timing.next_day_at, days = days - ) + ).unwrap() } - PropertyKind::Position(pos) => { - write!( - self.sql, - "(c.type = {t} and due {op} {pos})", - t = CardType::New as u8, - op = op, - pos = pos - ) - } - PropertyKind::Interval(ivl) => write!(self.sql, "ivl {} {}", op, ivl), - PropertyKind::Reps(reps) => write!(self.sql, "reps {} {}", op, reps), - PropertyKind::Lapses(days) => write!(self.sql, "lapses {} {}", op, days), + PropertyKind::Position(pos) => write!( + self.sql, + "(c.type = {t} and due {op} {pos})", + t = CardType::New as u8, + op = op, + pos = pos + ) + .unwrap(), + PropertyKind::Interval(ivl) => write!(self.sql, "ivl {} {}", op, ivl).unwrap(), + PropertyKind::Reps(reps) => write!(self.sql, "reps {} {}", op, reps).unwrap(), + PropertyKind::Lapses(days) => write!(self.sql, "lapses {} {}", op, days).unwrap(), PropertyKind::Ease(ease) => { - write!(self.sql, "factor {} {}", op, (ease * 1000.0) as u32) + write!(self.sql, "factor {} {}", op, (ease * 1000.0) as u32).unwrap() } + PropertyKind::Rated(days, ease) => self.write_rated(op, i64::from(*days), ease)?, } - .unwrap(); + Ok(()) } @@ -719,15 +737,15 @@ mod test { assert_eq!( s(ctx, "rated:2").0, format!( - "(c.id in (select cid from revlog where id>{} and ease > 0))", + "(c.id in (select cid from revlog where id >= {} and ease > 0))", (timing.next_day_at - (86_400 * 2)) * 1_000 ) ); assert_eq!( s(ctx, "rated:400:1").0, format!( - "(c.id in (select cid from revlog where id>{} and ease = 1))", - (timing.next_day_at - (86_400 * 365)) * 1_000 + "(c.id in (select cid from revlog where id >= {} and ease = 1))", + (timing.next_day_at - (86_400 * 400)) * 1_000 ) ); assert_eq!(s(ctx, "rated:0").0, s(ctx, "rated:1").0); @@ -736,8 +754,8 @@ mod test { assert_eq!( s(ctx, "resched:400").0, format!( - "(c.id in (select cid from revlog where id>{} and ease = 0))", - (timing.next_day_at - (86_400 * 365)) * 1_000 + "(c.id in (select cid from revlog where id >= {} and ease = 0))", + (timing.next_day_at - (86_400 * 400)) * 1_000 ) ); @@ -752,6 +770,7 @@ mod test { cutoff = timing.next_day_at ) ); + assert_eq!(s(ctx, "prop:rated>-5:3").0, s(ctx, "rated:5:3").0); // note types by name assert_eq!( diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 94215697e..1715c11e3 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -195,6 +195,11 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String { Lapses(u) => format!("\"prop:lapses{}{}\"", operator, u), Ease(f) => format!("\"prop:ease{}{}\"", operator, f), Position(u) => format!("\"prop:pos{}{}\"", operator, u), + Rated(u, ease) => match ease { + EaseKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val), + EaseKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u), + EaseKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u), + }, } }