+use std::str::FromStr;
+
use nom::{
branch::alt,
bytes::complete::{tag, take_until},
- character::complete::{alpha1, multispace0},
- combinator::{map, value},
+ character::complete::{alpha1, multispace0, one_of},
+ combinator::{map, map_res, recognize, value},
error::ParseError,
- multi::{many0_count, separated_list0, separated_list1},
+ multi::{many0_count, many1, separated_list0, separated_list1},
sequence::{delimited, preceded, tuple},
IResult,
};
use super::{GameSettings, GameSummary};
+use crate::util::timestamp::Timestamp;
+
pub trait Filter {
- fn matches(&self, summary: &GameSummary) -> bool;
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool;
}
fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
Format(String),
Title(String),
Completed(bool),
+ CreatedInLast(i64),
Parens(Box<Or>),
}
impl Filter for Field {
- fn matches(&self, summary: &GameSummary) -> bool {
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool {
match self {
Field::Format(format) => match summary.settings {
GameSettings::Chatroom(_) => format.eq_ignore_ascii_case("Chatroom"),
},
Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()),
Field::Completed(completed) => summary.status.completed == *completed,
- Field::Parens(filter) => filter.matches(summary),
+ Field::CreatedInLast(millis) => now.millis_since(summary.status.created) <= *millis,
+ Field::Parens(filter) => filter.matches(summary, now),
}
}
}
+fn parse_millis(input: &str) -> IResult<&str, i64> {
+ let int = || map_res(recognize(many1(one_of("0123456789"))), i64::from_str);
+ let seconds = map(int(), |secs| secs * 1000);
+ let minutes_seconds = map(
+ tuple((
+ int(),
+ tag(":"),
+ map_res(recognize(one_of("012345")), i64::from_str),
+ map_res(recognize(one_of("0123456789")), i64::from_str)
+ )),
+ |(mins, _, tens, secs)| (mins * 60 + tens * 10 + secs) * 1000
+ );
+ let hours_minutes_seconds = map(
+ tuple((
+ int(),
+ tag(":"),
+ map_res(recognize(one_of("012345")), i64::from_str),
+ map_res(recognize(one_of("0123456789")), i64::from_str),
+ tag(":"),
+ map_res(recognize(one_of("012345")), i64::from_str),
+ map_res(recognize(one_of("0123456789")), i64::from_str)
+ )),
+ |(hours, _, tenmins, mins, _, tens, secs)| (hours * 60 * 60 + tenmins * 10 * 60 + mins * 60 + tens * 10 + secs) * 1000
+ );
+ alt((hours_minutes_seconds, minutes_seconds, seconds))(input)
+}
+
fn parse_field(input: &str) -> IResult<&str, Field> {
let string = || map(alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))), String::from);
let boolean = || alt((value(true, tag("true")), value(false, tag("false"))));
let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format);
let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title);
let completed = map(preceded(tuple((tag("completed"), ws(tag(":")))), boolean()), Field::Completed);
+ let created = map(preceded(tuple((tag("created_in_last"), ws(tag(":")))), parse_millis), Field::CreatedInLast);
let parens = delimited(tag("("), map(map(ws(parse_or), Box::new), Field::Parens), tag(")"));
- alt((format, title, completed, parens))(input)
+ alt((format, title, completed, created, parens))(input)
}
#[derive(Clone, Debug, PartialEq, Eq)]
}
impl Filter for Not {
- fn matches(&self, summary: &GameSummary) -> bool {
- self.negate ^ self.field.matches(summary)
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool {
+ self.negate ^ self.field.matches(summary, now)
}
}
struct And(Vec<Not>);
impl Filter for And {
- fn matches(&self, summary: &GameSummary) -> bool {
- self.0.iter().all(|filter| filter.matches(summary))
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool {
+ self.0.iter().all(|filter| filter.matches(summary, now))
}
}
struct Or(Vec<And>);
impl Filter for Or {
- fn matches(&self, summary: &GameSummary) -> bool {
- self.0.iter().any(|filter| filter.matches(summary))
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool {
+ self.0.iter().any(|filter| filter.matches(summary, now))
}
}
}
impl Filter for ParsedFilter {
- fn matches(&self, summary: &GameSummary) -> bool {
- self.0.matches(summary)
+ fn matches(&self, summary: &GameSummary, now: Timestamp) -> bool {
+ self.0.matches(summary, now)
}
}
mod tests {
use super::*;
+ use serde::Deserialize;
+ use serde::de::value::{Error, I64Deserializer};
+
+ fn now() -> Timestamp {
+ Timestamp::deserialize(I64Deserializer::<Error>::new(1685204312000)).unwrap()
+ }
+
#[test]
fn field_parse() {
assert_eq!(Field::Format(String::from("KnockOutWhist")), parse_field("format: KnockOutWhist").unwrap().1);
]"#,
)
.unwrap();
- assert!(filter.matches(&games[0]));
- assert!(filter.matches(&games[1]));
- assert!(!filter.matches(&games[2]));
- assert!(!filter.matches(&games[3]));
- assert!(filter.matches(&games[4]));
+ assert!(filter.matches(&games[0], now()));
+ assert!(filter.matches(&games[1], now()));
+ assert!(!filter.matches(&games[2], now()));
+ assert!(!filter.matches(&games[3], now()));
+ assert!(filter.matches(&games[4], now()));
}
#[test]
]"#,
)
.unwrap();
- assert!(filter.matches(&games[0]));
- assert!(filter.matches(&games[1]));
- assert!(filter.matches(&games[2]));
- assert!(filter.matches(&games[3]));
- assert!(filter.matches(&games[4]));
+ assert!(filter.matches(&games[0], now()));
+ assert!(filter.matches(&games[1], now()));
+ assert!(filter.matches(&games[2], now()));
+ assert!(filter.matches(&games[3], now()));
+ assert!(filter.matches(&games[4], now()));
+ }
+
+ #[test]
+ fn created_in_last_ten_hours() {
+ let filter1 = parse_filter("created_in_last: 10:00:00").unwrap();
+ let filter2 = parse_filter("created_in_last: 36000").unwrap();
+ let filter3 = parse_filter("created_in_last: 600:00").unwrap();
+ let games: Vec<GameSummary> = serde_json::from_str(
+ r#"[
+ {"id": 0, "status":{"created":1685168302000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
+ {"id": 1, "status":{"created":1685168311999,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
+ {"id": 2, "status":{"created":1685168312000,"completed":false,"players":[]}, "settings": {"format":"Chatroom","title":"Game 2"}},
+ {"id": 3, "status":{"created":1685204312000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
+ {"id": 4, "status":{"created":1685204312000,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
+ ]"#,
+ )
+ .unwrap();
+ assert!(!filter1.matches(&games[0], now()));
+ assert!(!filter1.matches(&games[1], now()));
+ assert!(filter1.matches(&games[2], now()));
+ assert!(filter1.matches(&games[3], now()));
+ assert!(filter1.matches(&games[4], now()));
+
+ assert!(!filter2.matches(&games[0], now()));
+ assert!(!filter2.matches(&games[1], now()));
+ assert!(filter2.matches(&games[2], now()));
+ assert!(filter2.matches(&games[3], now()));
+ assert!(filter2.matches(&games[4], now()));
+
+ assert!(!filter3.matches(&games[0], now()));
+ assert!(!filter3.matches(&games[1], now()));
+ assert!(filter3.matches(&games[2], now()));
+ assert!(filter3.matches(&games[3], now()));
+ assert!(filter3.matches(&games[4], now()));
}
}