diff options
| author | Nathan Perry <np@nathanperry.dev> | 2024-05-08 14:30:21 -0400 |
|---|---|---|
| committer | Nathan Perry <np@nathanperry.dev> | 2024-05-08 14:30:21 -0400 |
| commit | fb088b4ff56ed76dabf4e638cb92bc1b9275fbcc (patch) | |
| tree | 53cb51db079edafd756cae6a3d5fc4fae829429c | |
| parent | 47817c4166937af24041a93e56ad9f841bf1e8f1 (diff) | |
clippy fixes
| -rw-r--r-- | src/bot.rs | 1 | ||||
| -rw-r--r-- | src/commands/meme/create.rs | 2 | ||||
| -rw-r--r-- | src/commands/meme/history.rs | 4 | ||||
| -rw-r--r-- | src/commands/meme/invoke.rs | 30 | ||||
| -rw-r--r-- | src/commands/meme/mod.rs | 11 | ||||
| -rw-r--r-- | src/commands/mod.rs | 2 | ||||
| -rw-r--r-- | src/db/mod.rs | 4 | ||||
| -rw-r--r-- | src/game.rs | 98 | ||||
| -rw-r--r-- | src/util.rs | 376 |
9 files changed, 270 insertions, 258 deletions
@@ -48,7 +48,6 @@ use serenity::{ }; use songbird::{ Call, - CoreEvent, Event, EventContext, SerenityInit, diff --git a/src/commands/meme/create.rs b/src/commands/meme/create.rs index ef6090e..2cd9465 100644 --- a/src/commands/meme/create.rs +++ b/src/commands/meme/create.rs @@ -156,7 +156,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe .arg("-i") .arg(youtube_url) .args(duration_opts) - .args(&[ + .args([ "-ac", "2", "-ar", "48000", "-f", "opus", "-acodec", "libopus", "-b:a", "96k", "-fs", "5M", "-", ]) diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs index 33a2de2..e2953d1 100644 --- a/src/commands/meme/history.rs +++ b/src/commands/meme/history.rs @@ -128,7 +128,7 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes InvocationRecord::last_n(&mut conn, n)? }; - if records.len() == 0 { + if records.is_empty() { info!("no memes in history"); return util::send(ctx, msg.channel_id, "i don't remember anything :(", msg.tts) .map_err(CommandError::from) @@ -393,7 +393,7 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul }) .join("\n"); - if result.len() == 0 { + if result.is_empty() { info!("no memes matched query"); return util::send(ctx, msg.channel_id, "no match".to_owned(), msg.tts) diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs index 13996da..2db9e83 100644 --- a/src/commands/meme/invoke.rs +++ b/src/commands/meme/invoke.rs @@ -76,7 +76,7 @@ async fn _meme( args: Args, audio_playback: AudioPlayback, ) -> CommandResult { - if args.len() == 0 || audio_playback != AudioPlayback::Optional { + if args.is_empty() || audio_playback != AudioPlayback::Optional { return rand_meme(ctx, msg, audio_playback).await; } @@ -133,18 +133,15 @@ async fn rand_meme( Ok(()) }, Err(e) => { - match e.downcast_ref::<DieselError>() { - Some(NotFound) => { - info!("random meme not found"); - return util::send(ctx, message.channel_id, "i don't know any :(", message.tts) - .map_err(CommandError::from) - .await; - }, - _ => {}, + if let Some(NotFound) = e.downcast_ref::<DieselError>() { + info!("random meme not found"); + return util::send(ctx, message.channel_id, "i don't know any :(", message.tts) + .map_err(CommandError::from) + .await; } util::send(ctx, message.channel_id, "HELP", message.tts).await?; - return Err(e.into()); + Err(e.into()) }, } } @@ -163,14 +160,11 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu send_meme(ctx, &meme, &mut conn, msg).await }, Err(e) => { - match e.downcast_ref::<DieselError>() { - Some(NotFound) => { - info!("rare meme not found"); - return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts) - .map_err(CommandError::from) - .await; - }, - _ => {}, + if let Some(NotFound) = e.downcast_ref::<DieselError>() { + info!("rare meme not found"); + return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts) + .map_err(CommandError::from) + .await; } util::send(ctx, msg.channel_id, "THE MEME MARKET IS IN FREEFALL", msg.tts) diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs index fe69b1c..c40e80a 100644 --- a/src/commands/meme/mod.rs +++ b/src/commands/meme/mod.rs @@ -74,7 +74,7 @@ async fn send_meme( msg: &Message, ) -> CommandResult { let should_tts = - t.content.as_ref().map(|t| t.len() > 0).unwrap_or(false) && random::<u32>() % 25 == 0; + t.content.as_ref().map(|t| !t.is_empty()).unwrap_or(false) && random::<u32>() % 25 == 0; debug!("sending meme (tts: {}): {:?}", should_tts, t); @@ -85,7 +85,7 @@ async fn send_meme( let ret = CreateMessage::default().tts(should_tts); match t.content { - Some(ref text) if text.len() > 0 => ret.content(text), + Some(ref text) if !text.is_empty() => ret.content(text), _ => ret, } }; @@ -98,11 +98,10 @@ async fn send_meme( msg.channel_id.send_files(ctx, vec![att], cmsg).await?; }, - None => match t.content { - Some(_) => { + None => { + if t.content.is_some() { msg.channel_id.send_message(ctx, cmsg).await?; - }, - None => {}, + } }, }; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3f69a3b..8eac09c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -36,7 +36,7 @@ pub fn register_commands(f: StandardFramework) -> StandardFramework { result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| { Box::pin(async move { - let url = match msg.content.split_whitespace().skip(1).next() { + let url = match msg.content.split_whitespace().nth(1) { Some(x) if x.starts_with("http") => x, _ => { info!("bad command formatting: '{}'", unrec); diff --git a/src/db/mod.rs b/src/db/mod.rs index 94953d5..e7ff17f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -47,7 +47,7 @@ static MIGRATE: std::sync::Once = std::sync::Once::new(); lazy_static! { static ref DB_URL: String = - env::var("DATABASE_URL").expect("no database url in environment").into(); + env::var("DATABASE_URL").expect("no database url in environment"); static ref DB_CONFIG: Config = Config::from_str(&DB_URL).expect("parsing db url as config"); static ref CONN_MGR: ConnectionManager<PgConnection> = ConnectionManager::new(DB_URL.clone()); static ref RAW_CONN_MGR: RawPgConnMgr<NoTls> = RawPgConnMgr::new(DB_CONFIG.clone(), NoTls); @@ -236,7 +236,7 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { .map(|row| (row.get::<_, i32>(0), row.get::<_, f64>(1) as i64)) .collect::<Vec<_>>(); - if elems.len() == 0 { + if elems.is_empty() { return Err(anyhow!("no rare memes found")); } diff --git a/src/game.rs b/src/game.rs index d3b0785..4011a5d 100644 --- a/src/game.rs +++ b/src/game.rs @@ -45,6 +45,7 @@ use serenity::{ }, prelude::*, }; +use tap::Pipe; use url::Url; use crate::{ @@ -88,7 +89,7 @@ lazy_static! { let default_path = PathBuf::from_str("user_id_mapping").unwrap(); let mapping_path = CONFIG.user_id_mapping.as_ref().unwrap_or(&default_path); - fs::read_to_string(mapping_path).unwrap() + fs::read_to_string(mapping_path).unwrap_or("{}".to_owned()) }; static ref USER_INFO_MAP: FnvHashMap<String, ProfileInfo> = { let v: Vec<UserInfo> = serde_json::from_str(&USER_MAP_STR).unwrap(); @@ -146,11 +147,11 @@ impl FromStr for GameStatus { fn from_str(s: &str) -> Result<Self> { use std::char; - if s.starts_with("y") { + if s.starts_with('y') { Ok(GameStatus::Installed) } else if s.starts_with("n/i") { Ok(GameStatus::NotInstalled) - } else if s.starts_with("n") { + } else if s.starts_with('n') { Ok(GameStatus::NotOwned) } else if s.chars().all(char::is_whitespace) { Ok(GameStatus::Unknown) @@ -182,7 +183,7 @@ pub enum UserLookupError { } pub fn get_user_id<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLookupError> { - let s = s.as_ref().trim_start_matches("@").to_lowercase(); + let s = s.as_ref().trim_start_matches('@').to_lowercase(); if let Some(info) = USER_INFO_MAP.get(&s) { return Ok(UserId::new(info.discord_user_id)); @@ -210,7 +211,7 @@ pub fn get_user_id<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLook let opts = nicks .into_iter() - .chain(usernames.into_iter()) + .chain(usernames) .map(|(member, _)| member.user.id) .collect::<FnvHashSet<_>>(); @@ -236,7 +237,6 @@ async fn _game( let users = { let guild = msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?; let user_args: Vec<String> = if args.rest().is_empty() { Vec::new() @@ -244,49 +244,69 @@ async fn _game( args.quoted().iter::<String>().collect::<StdResult<Vec<_>, ArgError<Infallible>>>()? }; + use serenity::futures::StreamExt; + let mut users = user_args .into_iter() + .pipe(serenity::futures::stream::iter) .filter_map(|u| { - use std::borrow::Borrow; + let guild = &guild; + async move { + use std::borrow::Borrow; - let possible = get_user_id(guild.borrow(), &u); + let possible = { + let Ok(guild) = + guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild")) + else { + error!("failed retrieving guild"); + return None; + }; - debug!("parsed userid {:?}", possible); + get_user_id(guild.borrow(), &u) + }; - match possible { - Err(UserLookupError::NotFound) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("didn't recognize {}", &u), - msg.tts, - ); - None - }, - Ok(x) => Some(x), - Err(UserLookupError::Ambiguous(x)) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("too many matches ({}) for {}", x, &u), - msg.tts, - ); - None - }, + debug!("parsed userid {:?}", possible); + + match possible { + Err(UserLookupError::NotFound) => { + let _ = util::send( + ctx, + msg.channel_id, + &format!("didn't recognize {}", &u), + msg.tts, + ) + .await; + None + }, + Ok(x) => Some(x), + Err(UserLookupError::Ambiguous(x)) => { + let _ = util::send( + ctx, + msg.channel_id, + &format!("too many matches ({}) for {}", x, &u), + msg.tts, + ) + .await; + None + }, + } } }) - .filter_map(|uid| { + .filter_map(|uid| async move { let res = DISCORD_MAP.get(&uid).map(|s| s.to_lowercase()); - if let None = res { - let _ = info!("user {} is not recognized", uid); + if res.is_none() { + info!("user {} is not recognized", uid); } res }) - .collect::<FnvHashSet<_>>(); + .collect::<FnvHashSet<_>>() + .await; + + if users.is_empty() { + let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?; - if users.len() == 0 { let pairs = guild .voice_states .iter() @@ -311,9 +331,9 @@ async fn _game( users }; - let inferred = users.len() == 0; + let inferred = users.is_empty(); - if inferred && users.len() < 2 || !inferred && users.len() < 1 { + if inferred && users.len() < 2 || !inferred && users.is_empty() { info!("too few known users to make game comparison"); util::send(ctx, msg.channel_id, "yer too lonely", msg.tts).await?; return Ok(()); @@ -375,7 +395,7 @@ async fn _game( .collect::<Vec<_>>(); let mut games_in_common = { - let game_map = user_games.values().nth(0).unwrap(); + let game_map = user_games.values().next().unwrap(); statuses.iter().fold(iter::empty().collect::<FnvHashSet<_>>(), |acc, s| { acc.union(&game_map[s]).cloned().collect() @@ -444,7 +464,7 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command let arg_user = args.single_quoted::<String>(); let user = if arg_user.is_err() { - msg.author.id.clone() + msg.author.id } else { use std::borrow::Borrow; @@ -566,7 +586,7 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command }) .join("\n"); - if found_games.len() > 0 { + if !found_games.is_empty() { util::send( ctx, msg.channel_id, diff --git a/src/util.rs b/src/util.rs index 0fd746b..e88e7b0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,188 +1,188 @@ -use chrono::Duration;
-use serenity::{
- client::Context,
- model::{
- id::{
- ChannelId,
- MessageId,
- },
- permissions::Permissions,
- },
-};
-use std::process::Stdio;
-
-use lazy_static::lazy_static;
-use log::debug;
-use regex::{
- Match,
- Regex,
-};
-use serenity::all::{
- CreateMessage,
- Message,
-};
-use url::Url;
-
-use crate::{
- commands::songbird,
- Result,
- CONFIG,
-};
-
-pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool {
- let (_sb, call) = songbird(ctx, msg).await.expect("no songbird");
-
- let call = call.lock().await;
- call.queue().current().is_some()
-}
-
-pub async fn users_listening(ctx: &Context) -> Result<bool> {
- let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?;
-
- let res = channel
- .guild()
- .and_then(|ch| ch.guild(&ctx))
- .map(|g| {
- (&g.voice_states)
- .into_iter()
- .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel()))
- })
- .unwrap_or(false);
-
- Ok(res)
-}
-
-#[inline]
-pub async fn send(
- ctx: &Context,
- channel: ChannelId,
- text: impl AsRef<str>,
- tts: bool,
-) -> Result<()> {
- send_result(ctx, channel, text, tts).await.map(|_| ())
-}
-
-pub async fn send_result(
- ctx: &Context,
- channel: ChannelId,
- text: impl AsRef<str>,
- tts: bool,
-) -> Result<MessageId> {
- let text = text.as_ref();
- debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts);
-
- let result = channel.send_message(ctx, CreateMessage::default().content(text).tts(tts)).await?;
- Ok(result.id)
-}
-
-lazy_static! {
- static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS
- | Permissions::ADD_REACTIONS
- | Permissions::SEND_MESSAGES
- | Permissions::SEND_TTS_MESSAGES
- | Permissions::MENTION_EVERYONE
- | Permissions::USE_EXTERNAL_EMOJIS
- | Permissions::CONNECT
- | Permissions::SPEAK
- | Permissions::CHANGE_NICKNAME
- | Permissions::USE_VAD
- | Permissions::ATTACH_FILES;
-}
-
-lazy_static! {
- pub static ref OAUTH_URL: Url = Url::parse(&format!(
- "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}",
- REQUIRED_PERMS.bits(),
- CONFIG.discord.auth.client_id,
- ))
- .unwrap();
-}
-
-pub async fn ytdl_url(uri: &str) -> Result<String> {
- use serde_json::Value;
- use tokio::process::Command;
-
- lazy_static! {
- static ref YTDL_COMMAND: String = {
- let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned());
- log::debug!("got ytdl: {}", result);
-
- result
- };
- }
-
- let args = [
- "-f",
- "webm[abr>0]/bestaudio/best",
- "--no-playlist",
- "--print-json",
- "--skip-download",
- uri,
- ];
-
- debug!("downloading info for uri: {uri}");
-
- let mut command = Command::new(&*YTDL_COMMAND);
- command.args(&args).stdin(Stdio::null());
-
- debug!("running command: {command:?}");
-
- let out = command.output().await?;
-
- if !out.status.success() {
- return Err(anyhow::anyhow!("running ytdl: {out:?}"));
- }
-
- let value = serde_json::from_reader(&out.stdout[..])?;
- let mut obj = match value {
- Value::Object(obj) => obj,
- other => return Err(anyhow::anyhow!("ytdl output not object: {other:?}")),
- };
-
- match obj.remove("url") {
- Some(v) => match v {
- Value::String(uri) => Ok(uri),
- other => Err(anyhow::anyhow!("url not string: {other:?}")),
- },
- None => Err(anyhow::anyhow!("no url")),
- }
-}
-pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) {
- lazy_static! {
- static ref START_REGEX: Regex =
- Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
-
- static ref DUR_REGEX: Regex =
- Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
-
- static ref END_REGEX: Regex =
- Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
- }
-
- fn parse_match(m: Option<Match>) -> u64 {
- m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0)
- }
-
- fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> {
- r.captures(s.as_ref()).map(|capt| {
- let hours = parse_match(capt.name("hours"));
- let minutes = parse_match(capt.name("minutes"));
- let seconds = parse_match(capt.name("seconds"));
-
- let result = Duration::hours(hours as i64)
- + Duration::minutes(minutes as i64)
- + Duration::seconds(seconds as i64);
-
- assert!(result >= Duration::zero());
-
- result
- })
- }
-
- let start_time = parse_captures(&START_REGEX, &s);
- let dur = parse_captures(&DUR_REGEX, &s);
- let end_time = parse_captures(&END_REGEX, s)
- .or_else(|| start_time.and_then(|start| dur.map(|d| start + d)));
-
- (start_time, end_time)
-}
+use chrono::Duration; +use serenity::{ + client::Context, + model::{ + id::{ + ChannelId, + MessageId, + }, + permissions::Permissions, + }, +}; +use std::process::Stdio; + +use lazy_static::lazy_static; +use log::debug; +use regex::{ + Match, + Regex, +}; +use serenity::all::{ + CreateMessage, + Message, +}; +use url::Url; + +use crate::{ + commands::songbird, + Result, + CONFIG, +}; + +pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool { + let (_sb, call) = songbird(ctx, msg).await.expect("no songbird"); + + let call = call.lock().await; + call.queue().current().is_some() +} + +pub async fn users_listening(ctx: &Context) -> Result<bool> { + let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?; + + let res = channel + .guild() + .and_then(|ch| ch.guild(&ctx)) + .map(|g| { + g.voice_states + .iter() + .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel())) + }) + .unwrap_or(false); + + Ok(res) +} + +#[inline] +pub async fn send( + ctx: &Context, + channel: ChannelId, + text: impl AsRef<str>, + tts: bool, +) -> Result<()> { + send_result(ctx, channel, text, tts).await.map(|_| ()) +} + +pub async fn send_result( + ctx: &Context, + channel: ChannelId, + text: impl AsRef<str>, + tts: bool, +) -> Result<MessageId> { + let text = text.as_ref(); + debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts); + + let result = channel.send_message(ctx, CreateMessage::default().content(text).tts(tts)).await?; + Ok(result.id) +} + +lazy_static! { + static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS + | Permissions::ADD_REACTIONS + | Permissions::SEND_MESSAGES + | Permissions::SEND_TTS_MESSAGES + | Permissions::MENTION_EVERYONE + | Permissions::USE_EXTERNAL_EMOJIS + | Permissions::CONNECT + | Permissions::SPEAK + | Permissions::CHANGE_NICKNAME + | Permissions::USE_VAD + | Permissions::ATTACH_FILES; +} + +lazy_static! { + pub static ref OAUTH_URL: Url = Url::parse(&format!( + "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}", + REQUIRED_PERMS.bits(), + CONFIG.discord.auth.client_id, + )) + .unwrap(); +} + +pub async fn ytdl_url(uri: &str) -> Result<String> { + use serde_json::Value; + use tokio::process::Command; + + lazy_static! { + static ref YTDL_COMMAND: String = { + let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned()); + log::debug!("got ytdl: {}", result); + + result + }; + } + + let args = [ + "-f", + "webm[abr>0]/bestaudio/best", + "--no-playlist", + "--print-json", + "--skip-download", + uri, + ]; + + debug!("downloading info for uri: {uri}"); + + let mut command = Command::new(&*YTDL_COMMAND); + command.args(args).stdin(Stdio::null()); + + debug!("running command: {command:?}"); + + let out = command.output().await?; + + if !out.status.success() { + return Err(anyhow::anyhow!("running ytdl: {out:?}")); + } + + let value = serde_json::from_reader(&out.stdout[..])?; + let mut obj = match value { + Value::Object(obj) => obj, + other => return Err(anyhow::anyhow!("ytdl output not object: {other:?}")), + }; + + match obj.remove("url") { + Some(v) => match v { + Value::String(uri) => Ok(uri), + other => Err(anyhow::anyhow!("url not string: {other:?}")), + }, + None => Err(anyhow::anyhow!("no url")), + } +} +pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) { + lazy_static! { + static ref START_REGEX: Regex = + Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); + + static ref DUR_REGEX: Regex = + Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); + + static ref END_REGEX: Regex = + Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); + } + + fn parse_match(m: Option<Match>) -> u64 { + m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0) + } + + fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> { + r.captures(s.as_ref()).map(|capt| { + let hours = parse_match(capt.name("hours")); + let minutes = parse_match(capt.name("minutes")); + let seconds = parse_match(capt.name("seconds")); + + let result = Duration::hours(hours as i64) + + Duration::minutes(minutes as i64) + + Duration::seconds(seconds as i64); + + assert!(result >= Duration::zero()); + + result + }) + } + + let start_time = parse_captures(&START_REGEX, &s); + let dur = parse_captures(&DUR_REGEX, &s); + let end_time = parse_captures(&END_REGEX, s) + .or_else(|| start_time.and_then(|start| dur.map(|d| start + d))); + + (start_time, end_time) +} |
