From 13230cd503b66b343639fc37d096e27c89635924 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Mirzaei Date: Fri, 1 Dec 2023 23:48:11 +0330 Subject: [PATCH] add phone number module --- Cargo.toml | 3 +- src/lib.rs | 3 + src/phone_number/mod.rs | 144 +++++++++++ src/phone_number/operators.rs | 447 ++++++++++++++++++++++++++++++++++ 4 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 src/phone_number/mod.rs create mode 100644 src/phone_number/operators.rs diff --git a/Cargo.toml b/Cargo.toml index 31ec143..92e1158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ include = ["src/**/*.rs", "Cargo.toml", "LICENSE", "README.md"] [dependencies] urlencoding = {version = "2.1.3", optional = true} - +regex = "1.10.2" [features] default = [] @@ -32,6 +32,7 @@ remove-ordinal-suffix = [] to-persian-chars = [] url-fix = ["dep:urlencoding"] verity-card-number = [] +phone-number = [] [package.metadata.docs.rs] all-features = true diff --git a/src/lib.rs b/src/lib.rs index 3ce878f..d6b0e7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,3 +27,6 @@ pub mod url_fix; #[cfg(feature = "verity-card-number")] pub mod verity_card_number; + +#[cfg(feature = "phone-number")] +pub mod phone_number; \ No newline at end of file diff --git a/src/phone_number/mod.rs b/src/phone_number/mod.rs new file mode 100644 index 0000000..fd0dc85 --- /dev/null +++ b/src/phone_number/mod.rs @@ -0,0 +1,144 @@ +pub mod operators; + +use regex::Regex; + +const MOBILE_REGEX: &str = r#"^(\+98|98|0098|0)?9(\d{2})\d{7}$"#; +pub const PREFIXES : [&str;4] = ["+98", "98", "0098", "0"]; + +/// This is a simple function that checks if a phone number valid or not +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::is_phone_valid; +/// +/// assert_eq!(is_phone_valid("00989122221811"),true); +/// assert_eq!(is_phone_valid("09185371111"),true); +/// assert_eq!(is_phone_valid("20989122221811"),false); +/// ``` +pub fn is_phone_valid(phone_number :&str) -> bool { + let regex = Regex::new(MOBILE_REGEX).unwrap(); + regex.is_match(phone_number) +} + +/// returns phone prefix for example +98 98 based on given phone number +/// This function returns an `Option<&str>`, where `Some(prefix)` contains the extracted prefix, +/// and `None` is returned if no valid prefix is found. +/// +/// # Warning +/// This function is desgined to only works for Iran phone number prefixes ("+98", "98", "0098", "0") +/// +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::get_phone_prefix; +/// +/// assert_eq!(get_phone_prefix("00989122221811"),Some("0098")); +/// assert_eq!(get_phone_prefix("09122221811"),Some("0")); +/// assert_eq!(get_phone_prefix("29122221811"),None); +/// ``` +pub fn get_phone_prefix(phone_number :&str) -> Option<&str> { + PREFIXES.into_iter().find(|&prefix| phone_number.starts_with(prefix)) +} + +/// replaces current phone number prefix with your desired prefix +/// +/// # Warning +/// if phone number is not valid it would return None +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::phone_number_normalizer; +/// +/// assert_eq!(phone_number_normalizer("00989022002580" , "+98") , Some("+989022002580".to_string())); +/// assert_eq!(phone_number_normalizer("9191282819921" , "0") , None); +/// ``` +pub fn phone_number_normalizer(phone_number :&str , new_prefix :&str) -> Option{ + + if !is_phone_valid(phone_number){ + return None; + } + + if let Some(prefix) = get_phone_prefix(phone_number) { + let ( _ , splited) = phone_number.split_at(prefix.len()); + return Some(format!("{new_prefix}{splited}")); + } + + Some(format!("{new_prefix}{phone_number}")) +} + +/// returns operator prefix of phone number (919,912,...) +/// +/// # Warning +/// if phone number is not valid it would return None +/// +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::get_operator_prefix; +/// +/// assert_eq!(get_operator_prefix("00989013708555") , Some("901")); +/// assert_eq!(get_operator_prefix("00988013708555") , None); +/// ``` +pub fn get_operator_prefix(phone_number :&str) -> Option<&str> { + if !is_phone_valid(phone_number) { + return None; + } + + for prefix in PREFIXES { + if phone_number.starts_with(prefix) { + return Some(&phone_number[prefix.len()..prefix.len()+3]); + } + } + + None +} + +#[cfg(test)] +mod test_phone_number { + use super::*; + + #[test] + fn check_phone_number_valid() { + assert_eq!(is_phone_valid("9122221811"),true); + assert_eq!(is_phone_valid("09122221811"),true); + assert_eq!(is_phone_valid("+989122221811"),true); + assert_eq!(is_phone_valid("12903908"),false); + assert_eq!(is_phone_valid("901239812390812908"),false); + } + + #[test] + fn test_phone_number_normilizer(){ + // normalize to 0 + assert_eq!(phone_number_normalizer("+989373708555", "0") , Some("09373708555".to_string())); + assert_eq!(phone_number_normalizer("989373708555" , "0") , Some("09373708555".to_string())); + assert_eq!(phone_number_normalizer("00989022002580" , "0") , Some("09022002580".to_string())); + assert_eq!(phone_number_normalizer("09122002580" , "0") , Some("09122002580".to_string())); + assert_eq!(phone_number_normalizer("9322002580" , "0") , Some("09322002580".to_string())); + + // normalize to +98 + assert_eq!(phone_number_normalizer("09373708555" , "+98") , Some("+989373708555".to_string())); + assert_eq!(phone_number_normalizer("09022002580" , "+98") , Some("+989022002580".to_string())); + assert_eq!(phone_number_normalizer("09122002580" , "+98") , Some("+989122002580".to_string())); + assert_eq!(phone_number_normalizer("9322002580" , "+98") , Some("+989322002580".to_string())); + assert_eq!(phone_number_normalizer("00989022002580" , "+98") , Some("+989022002580".to_string())); + + } + + #[test] + fn test_phone_number_normilizer_invalid_phone(){ + assert_eq!(phone_number_normalizer("09132222" , "+98") , None); + assert_eq!(phone_number_normalizer("9191282819921" , "0") , None); + } + + #[test] + fn test_operator_prefix(){ + assert_eq!(get_operator_prefix("+989373708555") , Some("937")); + assert_eq!(get_operator_prefix("00989013708555") , Some("901")); + assert_eq!(get_operator_prefix("00988013708555") , None); + } + +} \ No newline at end of file diff --git a/src/phone_number/operators.rs b/src/phone_number/operators.rs new file mode 100644 index 0000000..b8f8dc5 --- /dev/null +++ b/src/phone_number/operators.rs @@ -0,0 +1,447 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub enum Operators { + ShatelMobile, + MCI, + Irancell, + Taliya, + RightTel, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SimType { + Permanent, + Credit, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OperatorDetails<'a> { + pub province: Vec<&'a str>, + pub base: &'a str, + pub model: Option<&'a str>, + pub operator: Operators, + pub sim_types: Vec, +} + +/// returns operator details for mci +pub fn mci() -> HashMap<&'static str, OperatorDetails<'static>> { + HashMap::from([ + ( + "910", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "914", + OperatorDetails { + base: "آذربایجان غربی", + province: vec!["آذربایجان شرقی", "اردبیل", "اصفهان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "911", + OperatorDetails { + base: "مازندران", + province: vec!["گلستان", "گیلان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "912", + OperatorDetails { + base: "تهران", + province: vec![ + "البرز", + "زنجان", + "سمنان", + "قزوین", + "قم", + "برخی از شهرستان های استان مرکزی", + ], + sim_types: vec![SimType::Permanent], + operator: Operators::MCI, + model: None, + }, + ), + ( + "913", + OperatorDetails { + base: "اصفهان", + province: vec!["یزد", "چهارمحال و بختیاری", "کرمان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "915", + OperatorDetails { + base: "خراسان رضوی", + province: vec!["خراسان شمالی", "خراسان جنوبی", "سیستان و بلوچستان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "916", + OperatorDetails { + base: "خوزستان", + province: vec!["لرستان", "فارس", "اصفهان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "917", + OperatorDetails { + base: "فارس", + province: vec!["بوشهر", "کهگیلویه و بویر احمد", "هرمزگان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "918", + OperatorDetails { + base: "کرمانشاه", + province: vec!["کردستان", "ایلام", "همدان"], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "919", + OperatorDetails { + base: "تهران", + province: vec!["البرز", "سمنان", "قم", "قزوین", "زنجان"], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "990", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "991", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "992", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "993", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "994", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "995", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ( + "996", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + }, + ), + ]) +} + +/// returns operator details for talia +pub fn talia() -> HashMap<&'static str, OperatorDetails<'static>> { + HashMap::from([( + "932", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::Taliya, + model: None, + }, + )]) +} + +/// returns operator details for RightTel +pub fn right_tel() -> HashMap<&'static str, OperatorDetails<'static>> { + HashMap::from([ + ( + "920", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent], + operator: Operators::RightTel, + model: None, + }, + ), + ( + "921", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::RightTel, + model: None, + }, + ), + ( + "922", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::RightTel, + model: None, + }, + ), + ( + "923", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::RightTel, + model: None, + }, + ), + ]) +} + +/// returns operator details for irancell +pub fn irancell() -> HashMap<&'static str, OperatorDetails<'static>> { + let basic_model = OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit, SimType::Permanent], + operator: Operators::Irancell, + model: None, + }; + + HashMap::from([ + ("930", basic_model.clone()), + ("933", basic_model.clone()), + ("935", basic_model.clone()), + ("936", basic_model.clone()), + ("937", basic_model.clone()), + ("938", basic_model.clone()), + ("939", basic_model.clone()), + ("901", basic_model.clone()), + ("902", basic_model.clone()), + ("903", basic_model.clone()), + ("905", basic_model.clone()), + ("900", basic_model.clone()), + ( + "904", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::Irancell, + model: Some("سیم‌کارت کودک"), + }, + ), + ( + "941", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::Irancell, + model: Some("TD-LTE"), + }, + ), + ]) +} + +/// returns operator details for Shatel Mobile +pub fn shatel_mobile() -> HashMap<&'static str, OperatorDetails<'static>> { + HashMap::from([( + "998", + OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::ShatelMobile, + model: None, + }, + )]) +} + +/// returns all operators details +pub fn all_operators() -> HashMap<&'static str, OperatorDetails<'static>> { + let mut all_operators = HashMap::new(); + + all_operators.extend(mci()); + all_operators.extend(talia()); + all_operators.extend(right_tel()); + all_operators.extend(irancell()); + all_operators.extend(shatel_mobile()); + + all_operators +} + +/// a list of all available Iran operators prefixes +pub fn prefixs() -> Vec<&'static str> { + all_operators().into_keys().collect() +} + +/// returns operator details of givin prefix for example (912, 919, 913) +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::operators::{*}; +/// +/// assert_eq!( +/// get_prefix_details("910"), +/// Some(OperatorDetails { +/// base: "کشوری", +/// province: vec![], +/// sim_types: vec![SimType::Permanent, SimType::Credit], +/// operator: Operators::MCI, +/// model: None, +/// },) +/// ); +/// assert_eq!(get_prefix_details("9100"), None); +/// ``` +pub fn get_prefix_details(prefix: &str) -> Option { + let mut all_prefixes = all_operators(); + + all_prefixes.remove(prefix) +} + +/// returns operator details of givin phone number +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::phone_number::operators::{*}; +/// +/// assert_eq!(get_phone_details("09195431812") , Some(OperatorDetails { +/// base: "تهران", +/// province: vec!["البرز", "سمنان", "قم", "قزوین", "زنجان"], +/// sim_types: vec![SimType::Credit], +/// operator: Operators::MCI, +/// model: None, +/// },)); +/// +/// assert_eq!(get_phone_details("009195431812") , None); +/// ``` +pub fn get_phone_details(phone_number: &str) -> Option { + + if !super::is_phone_valid(phone_number) { + return None; + } + + let prefix = super::get_operator_prefix(phone_number).unwrap(); + get_prefix_details(prefix) +} + + +#[cfg(test)] +mod test_mobile_operators { + use super::*; + + #[test] + fn test_get_phone_prefix_operator() { + assert_eq!( + get_prefix_details("904"), + Some(OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Credit], + operator: Operators::Irancell, + model: Some("سیم‌کارت کودک"), + },) + ); + + assert_eq!( + get_prefix_details("910"), + Some(OperatorDetails { + base: "کشوری", + province: vec![], + sim_types: vec![SimType::Permanent, SimType::Credit], + operator: Operators::MCI, + model: None, + },) + ); + + assert_eq!(get_prefix_details("9100"), None); + } + + #[test] + fn test_get_phone_details(){ + + assert_eq!(get_phone_details("09195431812") , Some(OperatorDetails { + base: "تهران", + province: vec!["البرز", "سمنان", "قم", "قزوین", "زنجان"], + sim_types: vec![SimType::Credit], + operator: Operators::MCI, + model: None, + },)); + + assert_eq!(get_phone_details("009195431812") , None); + + } + +}