Merge pull request #926 from hgiesel/ratedextension

Introduce "prop:rated" and "prop:resched"
This commit is contained in:
Damien Elmes 2021-01-18 09:09:53 +10:00 committed by GitHub
commit 4c30f5506a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 39 deletions

View File

@ -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-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-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-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-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-float = `prop:{ $val }` must be followed by a decimal number.
search-invalid-prop-integer = `prop:{ $val }` must be followed by a whole number. search-invalid-prop-integer = `prop:{ $val }` must be followed by a whole number.

View File

@ -87,6 +87,7 @@ pub enum PropertyKind {
Lapses(u32), Lapses(u32),
Ease(f32), Ease(f32),
Position(u32), Position(u32),
Rated(i32, EaseKind),
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -350,9 +351,9 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
/// eg resched:3 /// eg resched:3
fn parse_resched(s: &str) -> ParseResult<SearchNode> { fn parse_resched(s: &str) -> ParseResult<SearchNode> {
if let Ok(d) = s.parse::<u32>() { if let Ok(days) = s.parse::<u32>() {
Ok(SearchNode::Rated { Ok(SearchNode::Rated {
days: d.max(1).min(365), days,
ease: EaseKind::ManualReschedule, ease: EaseKind::ManualReschedule,
}) })
} else { } else {
@ -369,6 +370,8 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
tag("lapses"), tag("lapses"),
tag("ease"), tag("ease"),
tag("pos"), tag("pos"),
tag("rated"),
tag("resched"),
))(s) ))(s)
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty(s.into())))?; .map_err(|_| parse_failure(s, FailKind::InvalidPropProperty(s.into())))?;
@ -400,6 +403,54 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
FailKind::InvalidPropInteger(format!("{}{}", prop, operator)), 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::<i32>() {
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::<u8>() {
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::<i32>() {
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::<u32>() { } else if let Ok(u) = num.parse::<u32>() {
match prop { match prop {
"ivl" => PropertyKind::Interval(u), "ivl" => PropertyKind::Interval(u),
@ -443,8 +494,8 @@ fn parse_edited(s: &str) -> ParseResult<SearchNode> {
/// second arg must be between 1-4 /// second arg must be between 1-4
fn parse_rated(s: &str) -> ParseResult<SearchNode> { fn parse_rated(s: &str) -> ParseResult<SearchNode> {
let mut it = s.splitn(2, ':'); let mut it = s.splitn(2, ':');
if let Ok(d) = it.next().unwrap().parse::<u32>() { if let Ok(days) = it.next().unwrap().parse::<u32>() {
let days = d.max(1).min(365); let days = days.max(1);
let ease = if let Some(tail) = it.next() { let ease = if let Some(tail) = it.next() {
if let Ok(u) = tail.parse::<u8>() { if let Ok(u) = tail.parse::<u8>() {
if u > 0 && u < 5 { if u > 0 && u < 5 {
@ -452,13 +503,13 @@ fn parse_rated(s: &str) -> ParseResult<SearchNode> {
} else { } else {
return Err(parse_failure( return Err(parse_failure(
s, s,
FailKind::InvalidRatedEase(days.to_string()), FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())),
)); ));
} }
} else { } else {
return Err(parse_failure( return Err(parse_failure(
s, s,
FailKind::InvalidRatedEase(days.to_string()), FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())),
)); ));
} }
} else { } else {
@ -872,10 +923,10 @@ mod test {
assert_err_kind("rated:", InvalidRatedDays); assert_err_kind("rated:", InvalidRatedDays);
assert_err_kind("rated:foo", InvalidRatedDays); assert_err_kind("rated:foo", InvalidRatedDays);
assert_err_kind("rated:1:", InvalidRatedEase("1".to_string())); assert_err_kind("rated:1:", InvalidRatedEase("rated:1".to_string()));
assert_err_kind("rated:2:-1", InvalidRatedEase("2".to_string())); assert_err_kind("rated:2:-1", InvalidRatedEase("rated:2".to_string()));
assert_err_kind("rated:3:1.1", InvalidRatedEase("3".to_string())); assert_err_kind("rated:3:1.1", InvalidRatedEase("rated:3".to_string()));
assert_err_kind("rated:0:foo", InvalidRatedEase("1".to_string())); assert_err_kind("rated:0:foo", InvalidRatedEase("rated:1".to_string()));
assert_err_kind("resched:", FailKind::InvalidResched); assert_err_kind("resched:", FailKind::InvalidResched);
assert_err_kind("resched:-1", FailKind::InvalidResched); assert_err_kind("resched:-1", FailKind::InvalidResched);

View File

@ -144,7 +144,7 @@ impl SqlWriter<'_> {
write!(self.sql, "c.did = {}", did).unwrap(); write!(self.sql, "c.did = {}", did).unwrap();
} }
SearchNode::NoteType(notetype) => self.write_note_type(&norm(notetype))?, 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::Tag(tag) => self.write_tag(&norm(tag))?,
SearchNode::State(state) => self.write_state(state)?, SearchNode::State(state) => self.write_state(state)?,
@ -214,14 +214,32 @@ impl SqlWriter<'_> {
Ok(()) 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 today_cutoff = self.col.timing_today()?.next_day_at;
let target_cutoff_ms = (today_cutoff - 86_400 * i64::from(days)) * 1_000; let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000;
write!( 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, self.sql,
"c.id in (select cid from revlog where id>{}", " between {} and {}",
target_cutoff_ms, 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(); .unwrap();
match ease { match ease {
@ -255,25 +273,25 @@ impl SqlWriter<'_> {
previewrepeat = CardQueue::PreviewRepeat as i8, previewrepeat = CardQueue::PreviewRepeat as i8,
cutoff = timing.next_day_at, cutoff = timing.next_day_at,
days = days days = days
) ).unwrap()
} }
PropertyKind::Position(pos) => { PropertyKind::Position(pos) => write!(
write!(
self.sql, self.sql,
"(c.type = {t} and due {op} {pos})", "(c.type = {t} and due {op} {pos})",
t = CardType::New as u8, t = CardType::New as u8,
op = op, op = op,
pos = pos pos = pos
) )
} .unwrap(),
PropertyKind::Interval(ivl) => write!(self.sql, "ivl {} {}", op, ivl), PropertyKind::Interval(ivl) => write!(self.sql, "ivl {} {}", op, ivl).unwrap(),
PropertyKind::Reps(reps) => write!(self.sql, "reps {} {}", op, reps), PropertyKind::Reps(reps) => write!(self.sql, "reps {} {}", op, reps).unwrap(),
PropertyKind::Lapses(days) => write!(self.sql, "lapses {} {}", op, days), PropertyKind::Lapses(days) => write!(self.sql, "lapses {} {}", op, days).unwrap(),
PropertyKind::Ease(ease) => { 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(()) Ok(())
} }
@ -719,15 +737,15 @@ mod test {
assert_eq!( assert_eq!(
s(ctx, "rated:2").0, s(ctx, "rated:2").0,
format!( 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 (timing.next_day_at - (86_400 * 2)) * 1_000
) )
); );
assert_eq!( assert_eq!(
s(ctx, "rated:400:1").0, s(ctx, "rated:400:1").0,
format!( format!(
"(c.id in (select cid from revlog where id>{} and ease = 1))", "(c.id in (select cid from revlog where id >= {} and ease = 1))",
(timing.next_day_at - (86_400 * 365)) * 1_000 (timing.next_day_at - (86_400 * 400)) * 1_000
) )
); );
assert_eq!(s(ctx, "rated:0").0, s(ctx, "rated:1").0); assert_eq!(s(ctx, "rated:0").0, s(ctx, "rated:1").0);
@ -736,8 +754,8 @@ mod test {
assert_eq!( assert_eq!(
s(ctx, "resched:400").0, s(ctx, "resched:400").0,
format!( 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 * 365)) * 1_000 (timing.next_day_at - (86_400 * 400)) * 1_000
) )
); );
@ -752,6 +770,7 @@ mod test {
cutoff = timing.next_day_at cutoff = timing.next_day_at
) )
); );
assert_eq!(s(ctx, "prop:rated>-5:3").0, s(ctx, "rated:5:3").0);
// note types by name // note types by name
assert_eq!( assert_eq!(

View File

@ -195,6 +195,11 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String {
Lapses(u) => format!("\"prop:lapses{}{}\"", operator, u), Lapses(u) => format!("\"prop:lapses{}{}\"", operator, u),
Ease(f) => format!("\"prop:ease{}{}\"", operator, f), Ease(f) => format!("\"prop:ease{}{}\"", operator, f),
Position(u) => format!("\"prop:pos{}{}\"", operator, u), 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),
},
} }
} }