Merge pull request #926 from hgiesel/ratedextension
Introduce "prop:rated" and "prop:resched"
This commit is contained in:
commit
4c30f5506a
@ -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.
|
||||||
|
@ -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);
|
||||||
|
@ -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!(
|
||||||
|
@ -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),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user