pub mod mentions_hashtags {
use regex::Regex;
use std::collections::HashSet;
use std::error::Error;
#[derive(Debug, Default)]
pub struct MentionsHashtags {
pub mentions: Vec<String>,
pub hashtags: Vec<String>,
}
pub fn parse_mentions_hashtags(
description: &str,
mentions: bool,
hashtags: bool,
) -> Result<MentionsHashtags, Box<dyn Error>> {
let mut mentions_hashtags = MentionsHashtags::default();
if !mentions && !hashtags {
return Ok(mentions_hashtags);
}
if mentions {
mentions_hashtags.mentions = parse_mentions(description)?;
}
if hashtags {
mentions_hashtags.hashtags = parse_hashtags(description)?;
}
Ok(mentions_hashtags)
}
pub fn parse_mentions(description: &str) -> Result<Vec<String>, Box<dyn Error>> {
let matches = Regex::new(r"(?i)@[a-zA-Z0-9_\-.]+")?;
let unique_mentions: HashSet<String> = matches
.find_iter(description)
.map(|m| m.as_str().to_string())
.collect();
Ok(unique_mentions.into_iter().collect())
}
pub fn parse_hashtags(description: &str) -> Result<Vec<String>, Box<dyn Error>> {
let matches = Regex::new(r"(?i)#[a-zA-Z0-9_\-.]+")?;
let unique_hashtags: HashSet<String> = matches
.find_iter(description)
.map(|x| x.as_str().to_string())
.collect();
Ok(unique_hashtags.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::mentions_hashtags::*;
use std::collections::HashSet;
#[test]
fn test_youtube_mentions() {
let result = parse_mentions("@MrBeast @EmmaChamberlain @PewDiePie").unwrap();
let expected: HashSet<_> = ["@MrBeast", "@EmmaChamberlain", "@PewDiePie"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(result.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn test_tiktok_mentions_with_duplicates() {
let result = parse_mentions("@charlidamelio @charlidamelio @Khaby.Lame").unwrap();
let expected: HashSet<_> = ["@charlidamelio", "@Khaby.Lame"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(result.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn test_mentions_case_insensitive_but_preserved() {
let result = parse_mentions("@AddisonRae @addisonrae").unwrap();
assert!(result.contains(&"@AddisonRae".to_string()));
assert!(result.contains(&"@addisonrae".to_string()));
assert_eq!(result.len(), 2); }
#[test]
fn test_mentions_empty_input() {
let result = parse_mentions("").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_tiktok_hashtags_trending() {
let result = parse_hashtags("#fyp #trending #CapCut #viral").unwrap();
let expected: HashSet<_> = ["#fyp", "#trending", "#CapCut", "#viral"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(result.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn test_youtube_hashtags_mixed_case() {
let result = parse_hashtags("#Shorts #YouTubeShorts #Music #music").unwrap();
let expected: HashSet<_> = ["#Shorts", "#YouTubeShorts", "#Music", "#music"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(result.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn test_hashtags_with_punctuation() {
let result = parse_hashtags("Try this! #Challenge-2025 #fun.time #go_crazy.").unwrap();
let expected: HashSet<_> = ["#Challenge-2025", "#fun.time", "#go_crazy."]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(result.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn test_hashtags_empty_input() {
let result = parse_hashtags("").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_parse_both_mentions_and_hashtags() {
let result = parse_mentions_hashtags(
"@MrBeast just posted a new video! #fyp #MrBeastChallenge",
true,
true,
)
.unwrap();
assert!(result.mentions.contains(&"@MrBeast".to_string()));
assert!(result.hashtags.contains(&"#fyp".to_string()));
assert!(result.hashtags.contains(&"#MrBeastChallenge".to_string()));
}
#[test]
fn test_parse_none_enabled() {
let result = parse_mentions_hashtags("@Khaby.Lame #viral", false, false).unwrap();
assert!(result.mentions.is_empty());
assert!(result.hashtags.is_empty());
}
}