├── src ├── date │ ├── validate.rs │ ├── mod.rs │ └── converter.rs ├── utils │ └── mod.rs ├── lib.rs ├── national_code │ └── mod.rs ├── phone_number │ ├── landline.rs │ ├── mod.rs │ └── mobile.rs ├── persian_content │ └── mod.rs ├── banking │ ├── mod.rs │ ├── sheba.rs │ ├── bank_codes_table.rs │ ├── card_number_extractor.rs │ └── sheba_table.rs ├── number_suffix │ └── mod.rs ├── translate │ └── mod.rs ├── province │ ├── mod.rs │ └── city.rs └── digit │ └── mod.rs ├── .gitignore ├── Makefile ├── CHANGELOG.md ├── .github └── workflows │ ├── linting.yml │ └── test.yml ├── Cargo.toml ├── LICENSE.md └── README.md /src/date/validate.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | doc 4 | .vscode 5 | -------------------------------------------------------------------------------- /src/date/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod converter; 2 | pub mod validate; 3 | 4 | #[cfg(test)] 5 | pub mod tests {} 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @cargo test 3 | 4 | check: 5 | @cargo +nightly fmt 6 | @cargo clippy -- -D clippy::all 7 | @cargo +nightly udeps 8 | @cargo outdated -wR 9 | @cargo update --dry-run 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased (2021-X-X) 2 | ### Features 3 | * Digit Converter 4 | * Persian Content Checks 5 | * National Code Validator 6 | * Numeric Ordinal Suffixes 7 | * Phone Number Validator 8 | * Province & City 9 | 10 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | macro_rules! impl_trait_for_string_types { 2 | ($name_trait:ident) => { 3 | impl $name_trait for String {} 4 | impl $name_trait for str {} 5 | // maybe other string type 6 | }; 7 | } 8 | 9 | pub(crate) use impl_trait_for_string_types; 10 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Fmt 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | format: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | - run: cargo fmt --check 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run tests 20 | run: cargo test --verbose --all-features 21 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | 3 | pub mod banking; 4 | pub mod date; 5 | pub mod digit; 6 | pub mod national_code; 7 | pub mod number_suffix; 8 | pub mod persian_content; 9 | pub mod phone_number; 10 | pub mod province; 11 | #[cfg(feature = "translate")] 12 | pub mod translate; 13 | pub(crate) mod utils; 14 | 15 | pub type Result = 16 | std::result::Result>; 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "persian-tools" 3 | version = "0.1.0" 4 | authors = ["rustland-fa developers"] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/rustland-fa/persian-tools-rs" 9 | description = """ 10 | A set of tools and helpers related to Persian language or in general, Iran! 11 | """ 12 | keywords = ["persian", "tool", "format", "tools", "iran"] 13 | categories = ["algorithms", "date-and-time", "encoding", "internationalization", "localization"] 14 | include = ["src/", "*.md"] 15 | 16 | [features] 17 | default = [] 18 | translate = ["dep:reqwest"] 19 | 20 | [dependencies] 21 | strum = { version = "0.25", features = ["derive"] } 22 | num-traits = "0.2" 23 | reqwest = { version = "0.11", features = ["blocking"], optional = true } 24 | phf = { version = "0.11", features = ["macros"] } 25 | 26 | -------------------------------------------------------------------------------- /src/national_code/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::impl_trait_for_string_types; 2 | 3 | pub trait NationalCode: AsRef { 4 | /// Takes a string and check if it's a valid Iranian national code or not. 5 | fn is_valid_national_code(&self) -> bool { 6 | let text = self.as_ref(); 7 | if text.len() != 10 { 8 | return false; 9 | } 10 | 11 | let digits: Vec = text.chars().map_while(|c| c.to_digit(10)).collect(); 12 | if digits.len() != 10 { 13 | return false; 14 | } 15 | 16 | let last = digits[9]; 17 | let sum = (0..9).map(|x| digits[x] * (10 - x) as u32).sum::() % 11; 18 | 19 | (sum < 2 && last == sum) || (last + sum == 11) 20 | } 21 | } 22 | 23 | impl_trait_for_string_types!(NationalCode); 24 | 25 | #[cfg(test)] 26 | mod test { 27 | use super::*; 28 | 29 | #[test] 30 | fn is_valid_national_code_test() { 31 | assert!("3020588391".is_valid_national_code()); 32 | assert!(!"3020588392".is_valid_national_code()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Rustland-fa Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # persian-tools 2 | 3 | [![Test Status](https://github.com/rustland-fa/persian-tools-rs/workflows/test/badge.svg?event=push)](https://github.com/rustland-fa/persian-tools-rs/actions) 4 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 5 | [![Crate](https://img.shields.io/crates/v/persian-tools)](https://crates.io/crates/persian-tools) 6 | [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/rustland-fa/persian-tools-rs/blob/master/LICENSE.md) 7 | 8 | A set of helpers to sanitize, convert or transform information related to Persian language and/or Iran. 9 | 10 | ## Features 11 | | Feature | Status | 12 | | ------------------------------------------ | -------------- | 13 | | National Code Validator | ✅ Done | 14 | | Numeric Conversions | ✅ Done | 15 | | Persian Content Checks | ✅ Done | 16 | | Numeric Ordinal Suffixes | ✅ Done | 17 | | Phone Number | ✅ Done | 18 | | Bank-related Helper Functions | ✅ Done | 19 | | Date Conversion | ✅ Done | 20 | | Regions (Province & City) | 🚧 In Progress | 21 | 22 | 23 | --- 24 | Inspired by the javascript version of [persian-tools](https://github.com/persian-tools/persian-tools) 25 | -------------------------------------------------------------------------------- /src/phone_number/landline.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | province::{IranProvince, PROVINCES}, 5 | utils::impl_trait_for_string_types, 6 | }; 7 | 8 | /// A trait helper to work with landline numbers. 9 | pub trait LandlineNumber: AsRef { 10 | /// Check if the landline number is valid. 11 | fn is_valid_landline_number(&self) -> bool { 12 | let text = self.as_ref(); 13 | let skip = super::get_num_skip(text); 14 | 15 | if text.len() - skip != 10 { 16 | return false; 17 | } 18 | 19 | let mut chars = text.chars().skip(skip); 20 | 21 | chars.by_ref().take(2).all(|c| ('1'..='9').contains(&c)) 22 | && chars.all(|c| c.is_ascii_digit()) 23 | } 24 | 25 | /// Get three-digit prefix of a landline number. 26 | fn get_prefix_landline_number(&self) -> crate::Result { 27 | let text = self.as_ref(); 28 | let skip = super::get_num_skip(text); 29 | 30 | if text.len() - skip != 10 { 31 | return Err("Invalid landline number".into()); 32 | } 33 | 34 | text.chars() 35 | .skip(skip) 36 | .take(2) 37 | .try_fold(String::from("0"), |mut acc, c| { 38 | if ('1'..='9').contains(&c) { 39 | acc.push(c); 40 | Ok(acc) 41 | } else { 42 | Err("Invalid landline number".into()) 43 | } 44 | }) 45 | } 46 | 47 | /// Get province of the landline number. 48 | fn get_province_from_landline_number(&self) -> crate::Result> { 49 | self.get_prefix_landline_number().map(|p| { 50 | PROVINCES.into_iter().find_map(|(k, v)| { 51 | if v.prefix_phone == p { 52 | Some(IranProvince::from_str(k).unwrap()) 53 | } else { 54 | None 55 | } 56 | }) 57 | }) 58 | } 59 | } 60 | 61 | impl_trait_for_string_types!(LandlineNumber); 62 | -------------------------------------------------------------------------------- /src/persian_content/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use crate::utils::impl_trait_for_string_types; 4 | 5 | static HAS_PERSIAN_CHAR: RangeInclusive = '\u{0600}'..='\u{06FF}'; 6 | 7 | /// Set of helpers for manipulating Persian text. 8 | pub trait PersianContent: AsRef { 9 | /// Checks if a text has at least a Persian char in it. 10 | fn has_persian_char(&self) -> bool { 11 | self.as_ref().chars().any(|c| HAS_PERSIAN_CHAR.contains(&c)) 12 | } 13 | 14 | /// Checks if a text is in Persian. 15 | fn is_persian_str(&self) -> bool { 16 | self.as_ref() 17 | .chars() 18 | .filter(|c| c.is_alphabetic()) // First remove the non-alphabetic chars 19 | .all(|c| c.is_ascii_punctuation() || HAS_PERSIAN_CHAR.contains(&c)) 20 | } 21 | 22 | /// Calculates how much of the text is in Persian Alphabet. 23 | /// It doesn't count the numbers and other non-alphabetical chars like " « , ، 24 | fn persian_percentage(&self) -> u8 { 25 | let (persian_chars_len, len) = 26 | self.as_ref() 27 | .chars() 28 | .fold((0u32, 0u32), |(mut pc, mut len), c| { 29 | if c.is_alphabetic() { 30 | if HAS_PERSIAN_CHAR.contains(&c) { 31 | pc += 1; 32 | } 33 | len += 1; 34 | } 35 | (pc, len) 36 | }); 37 | 38 | (persian_chars_len * 100).checked_div(len).unwrap_or(100) as u8 39 | } 40 | } 41 | 42 | impl_trait_for_string_types!(PersianContent); 43 | 44 | #[cfg(test)] 45 | mod test { 46 | use super::*; 47 | 48 | #[test] 49 | fn has_persian_char_test() { 50 | assert!("ok this is text with ص".has_persian_char()); 51 | assert!(!"ok this is text with".has_persian_char()); 52 | assert!(!"阴阳".has_persian_char()); 53 | } 54 | 55 | #[test] 56 | fn is_persian_str_test() { 57 | assert!("سلام".is_persian_str()); 58 | assert!("گفت: «سلام»".is_persian_str()); 59 | assert!(!"ok this is text with".is_persian_str()); 60 | assert!(!"阴阳".is_persian_str()); 61 | assert!(!"Hello".is_persian_str()); 62 | assert!("سلام 😛".is_persian_str()); 63 | } 64 | 65 | #[test] 66 | fn persian_percentage_test() { 67 | assert_eq!("".persian_percentage(), 100); 68 | assert_eq!("سلام".persian_percentage(), 100); 69 | assert_eq!("گفت: «سلام»".persian_percentage(), 100); 70 | assert_eq!("ok this is text with".persian_percentage(), 0); 71 | assert_eq!("ok this is text with ص".persian_percentage(), 5); 72 | assert_eq!("阴阳".persian_percentage(), 0); 73 | assert_eq!("me من".persian_percentage(), 50); 74 | assert_eq!("阴阳 «من»".persian_percentage(), 50); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/phone_number/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod landline; 2 | pub mod mobile; 3 | 4 | static NUMBER_PREFIX: [&str; 4] = ["+98", "0", "98", "0098"]; 5 | 6 | fn get_num_skip(text: &str) -> usize { 7 | for prefix in NUMBER_PREFIX { 8 | if text.starts_with(prefix) { 9 | return prefix.len(); 10 | } 11 | } 12 | 0 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::{landline::*, mobile::*}; 18 | use crate::province::IranProvince; 19 | 20 | #[test] 21 | fn is_valid_mobile_number_test() { 22 | assert!("09398254166".is_valid_mobile_number()); 23 | assert!("+989398254166".is_valid_mobile_number()); 24 | assert!(!"+98939825416621121121122133313".is_valid_mobile_number()); 25 | } 26 | 27 | #[test] 28 | fn is_valid_landline_number_test() { 29 | assert!("03434144188".is_valid_landline_number()); 30 | assert!(!"0343414418".is_valid_landline_number()); 31 | assert!(!"034341441810000000000000000023323232".is_valid_landline_number()); 32 | } 33 | 34 | #[test] 35 | fn get_prefix_landline_number_test() { 36 | assert_eq!("03498254166".get_prefix_landline_number().unwrap(), "034"); 37 | assert_eq!("+983498254166".get_prefix_landline_number().unwrap(), "034"); 38 | assert!("+98".get_operator_name_from_mobile_number().is_err()); 39 | } 40 | 41 | #[test] 42 | fn get_province_from_landline_number_test() { 43 | assert_eq!( 44 | "03498254166" 45 | .get_province_from_landline_number() 46 | .unwrap() 47 | .unwrap(), 48 | IranProvince::Kerman 49 | ); 50 | assert_eq!( 51 | "+982198254166" 52 | .get_province_from_landline_number() 53 | .unwrap() 54 | .unwrap(), 55 | IranProvince::Tehran 56 | ); 57 | assert!("+98999999999".get_province_from_landline_number().is_err()); 58 | } 59 | 60 | #[test] 61 | fn get_operator_name_from_mobile_number_test() { 62 | assert_eq!( 63 | "09324341133" 64 | .get_operator_name_from_mobile_number() 65 | .unwrap(), 66 | IranMobileOperator::Taliya 67 | ); 68 | assert_eq!( 69 | "+989324341133" 70 | .get_operator_name_from_mobile_number() 71 | .unwrap(), 72 | IranMobileOperator::Taliya 73 | ); 74 | assert_eq!( 75 | "+989134341133" 76 | .get_operator_name_from_mobile_number() 77 | .unwrap(), 78 | IranMobileOperator::MCI 79 | ); 80 | assert_eq!( 81 | "+989999048230" 82 | .get_operator_name_from_mobile_number() 83 | .unwrap(), 84 | IranMobileOperator::SamanTel 85 | ); 86 | assert!("+98999999999" 87 | .get_operator_name_from_mobile_number() 88 | .is_err()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/banking/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::banking::bank_codes_table::BANK_CODE_TABLE; 2 | use crate::utils::impl_trait_for_string_types; 3 | 4 | pub mod bank_codes_table; 5 | pub mod card_number_extractor; 6 | pub mod sheba; 7 | mod sheba_table; 8 | 9 | /// Set of helpers for the banking system of Iran. 10 | pub trait Banking: AsRef { 11 | /// Checks if the bank card number is valid or not. 12 | fn is_valid_bank_card_number(&self) -> bool { 13 | let text = self.as_ref(); 14 | if text.len() != 16 { 15 | return false; 16 | } 17 | 18 | let digits: Vec = text.chars().map_while(|c| c.to_digit(10)).collect(); 19 | if digits.len() != 16 || digits.iter().sum::() == 0 { 20 | return false; 21 | } 22 | 23 | let sum = digits 24 | .into_iter() 25 | .enumerate() 26 | .map(|(idx, x)| { 27 | let mut sub_digit = x * if idx % 2 == 0 { 2 } else { 1 }; 28 | if sub_digit > 9 { 29 | sub_digit -= 9 30 | } 31 | sub_digit 32 | }) 33 | .sum::(); 34 | 35 | sum % 10 == 0 36 | } 37 | 38 | /// Get the bank name from card number. 39 | fn get_bank_name_from_card_number(&self) -> Option<&'static str> { 40 | let number = self.as_ref(); 41 | number.is_valid_bank_card_number().then(|| { 42 | BANK_CODE_TABLE 43 | .get(&number[0..6]) 44 | .map(|bc| bc.name) 45 | .unwrap() 46 | }) 47 | } 48 | } 49 | 50 | impl_trait_for_string_types!(Banking); 51 | 52 | #[cfg(test)] 53 | mod test { 54 | use super::*; 55 | 56 | #[test] 57 | fn is_valid_card_number_test() { 58 | assert!(!"9999999999999999".is_valid_bank_card_number()); 59 | assert!(!"1234567890111213".is_valid_bank_card_number()); 60 | assert!(!"abcdefghi0111213".is_valid_bank_card_number()); 61 | assert!(!"6395991167965611".is_valid_bank_card_number()); 62 | assert!("6395991167965615".is_valid_bank_card_number()); 63 | assert!(Banking::is_valid_bank_card_number("6395991167965615")); 64 | 65 | let string = "6395991167965615".to_string(); 66 | assert!(string.is_valid_bank_card_number()); 67 | assert!(Banking::is_valid_bank_card_number(&string)); 68 | } 69 | 70 | #[test] 71 | fn get_bank_name_from_card_number_test() { 72 | assert_eq!( 73 | Some("بانک قوامین"), 74 | "6395991167965615".get_bank_name_from_card_number() 75 | ); 76 | assert_eq!( 77 | Some("بانک کشاورزی"), 78 | "6037701689095443".get_bank_name_from_card_number() 79 | ); 80 | assert_eq!( 81 | Some("بانک سامان"), 82 | "6219861034529007".get_bank_name_from_card_number() 83 | ); 84 | assert_eq!(None, "".get_bank_name_from_card_number()); 85 | 86 | let string = "6395991167965615".to_string(); 87 | assert_eq!(Some("بانک قوامین"), string.get_bank_name_from_card_number()); 88 | assert_eq!( 89 | Some("بانک قوامین"), 90 | Banking::get_bank_name_from_card_number(&string) 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/phone_number/mobile.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use strum::EnumString; 3 | 4 | use crate::utils::*; 5 | 6 | /// List of Iranian mobile operators. 7 | // in future phf crate if support enums as key we must replace str with enum 8 | pub static IRAN_MOBILE_OPERATORS: phf::Map<&str, &[&str]> = phf::phf_map! { 9 | "MCI" => { 10 | &[ 11 | "0910", "0911", "0912", "0913", "0914", "0915", "0916", "0917", "0918", "0919", 12 | "0990", "0991", "0992", "0993", "0994", 13 | ] 14 | }, 15 | "Irancell" => { 16 | &[ 17 | "0930", "0933", "0935", "0936", "0937", "0938", "0939", "0901", "0902", "0903", 18 | "0904", "0905", "0941", 19 | ] 20 | }, 21 | "RightTel" => { 22 | &["0920", "0921", "0922"] 23 | }, 24 | "Taliya" => { 25 | &["0932"] 26 | }, 27 | "MTCE" => { 28 | &["0931"] 29 | }, 30 | "TeleKish" => { 31 | &["0934"] 32 | }, 33 | "ApTel" => { 34 | &["099910", "099911", "099913"] 35 | }, 36 | "Azartel" => { 37 | &["099914"] 38 | }, 39 | "SamanTel" => { 40 | &["099990", "099999", "099998", "099997", "099996"] 41 | }, 42 | "LotusTel" => { 43 | &["09990"] 44 | }, 45 | "ShatelMobile" => { 46 | &["099810", "099811", "099812", "099814", "099815"] 47 | }, 48 | "ArianTel" => { 49 | &["09998"] 50 | }, 51 | "Anarestan" => { 52 | &["0994"] 53 | }, 54 | 55 | }; 56 | 57 | #[derive(Debug, PartialEq, Eq, Hash, EnumString)] 58 | pub enum IranMobileOperator { 59 | MCI, 60 | MTCE, 61 | TeleKish, 62 | ApTel, 63 | Azartel, 64 | SamanTel, 65 | LotusTel, 66 | ArianTel, 67 | Anarestan, 68 | Irancell, 69 | RightTel, 70 | Taliya, 71 | ShatelMobile, 72 | } 73 | 74 | /// A trait helper to work with mobile numbers. 75 | pub trait MobileNumber: AsRef { 76 | /// Check if the mobile number is valid. 77 | fn is_valid_mobile_number(&self) -> bool { 78 | let text = self.as_ref(); 79 | let skip = super::get_num_skip(text); 80 | 81 | if text.len() - skip != 10 { 82 | return false; 83 | } 84 | 85 | let mut chars = text.chars().skip(skip); 86 | 87 | chars.next().is_some_and(|c| c == '9') && chars.all(|c| c.is_ascii_digit()) 88 | } 89 | 90 | /// Get the operator name of the mobile number. 91 | fn get_operator_name_from_mobile_number(&self) -> crate::Result { 92 | let text = self.as_ref(); 93 | let skip = super::get_num_skip(text); 94 | 95 | if text.len() - skip != 10 { 96 | return Err("Invalid mobile number".into()); 97 | } 98 | 99 | let number = format!("0{}", &text[skip..]); 100 | 101 | IRAN_MOBILE_OPERATORS 102 | .into_iter() 103 | .find_map(|(k, v)| { 104 | v.iter() 105 | .any(|x| x == &&number[..x.len()]) 106 | .then(|| IranMobileOperator::from_str(k).unwrap()) 107 | }) 108 | .ok_or("Can't find the operator".into()) 109 | } 110 | } 111 | 112 | impl_trait_for_string_types!(MobileNumber); 113 | -------------------------------------------------------------------------------- /src/number_suffix/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::impl_trait_for_string_types; 2 | 3 | static ORDINAL_SUFFIX: [&str; 7] = ["ام", "اُم", "ا", "اُ", "امین", "اُمین", "ین"]; 4 | 5 | /// Set of helpers to add ordinal suffixes to Persian numbers. 6 | pub trait NumberSuffix: AsRef { 7 | /// Add ordinal suffix to numbers. 8 | /// For example, will convert Persian text of "Panj" to "Panjom" 9 | fn add_ordinal_suffix_short(&self) -> String { 10 | let mut number = self.as_ref().trim().to_string(); 11 | 12 | if !number.is_empty() { 13 | if number.ends_with('ی') { 14 | number.push_str("‌اُم"); // it includes a ZWNJ char! 15 | } else if number.ends_with("سه") { 16 | // remove the last char 17 | number.pop(); 18 | number.push_str("وم"); 19 | } else { 20 | number.push('م'); 21 | } 22 | } 23 | 24 | number 25 | } 26 | 27 | /// Add ordinal suffix to numbers. 28 | /// For example, will convert Persian text of "Panj" to "Panjomin" 29 | fn add_ordinal_suffix_long(&self) -> String { 30 | let mut number = self.as_ref().to_string(); 31 | 32 | if !number.is_empty() { 33 | number = number.add_ordinal_suffix_short(); 34 | number.push_str("ین"); 35 | } 36 | 37 | number 38 | } 39 | 40 | fn remove_ordinal_suffix(&self) -> String { 41 | let mut number = self.as_ref().to_string(); 42 | if !number.is_empty() { 43 | for suffix in ORDINAL_SUFFIX { 44 | while number.ends_with(suffix) { 45 | number.replace_range(number.len() - suffix.len().., ""); 46 | } 47 | } 48 | 49 | if number.ends_with("سوم") { 50 | number = number.replace("سوم", "سه"); 51 | } else if number.ends_with('م') { 52 | number.pop(); 53 | } else if number.eq("اول") { 54 | number = "یک".to_string(); 55 | } 56 | // U+200C is Zero-width non-joiner 57 | while number.ends_with(['\u{200c}', ' ']) { 58 | number.pop(); 59 | } 60 | } 61 | 62 | number 63 | } 64 | } 65 | 66 | impl_trait_for_string_types!(NumberSuffix); 67 | 68 | #[cfg(test)] 69 | mod test { 70 | use super::*; 71 | 72 | #[test] 73 | fn add_ordinal_suffix_short_test() { 74 | assert_eq!("چهل و سه".add_ordinal_suffix_short(), "چهل و سوم"); 75 | assert_eq!("چهل و پنج".add_ordinal_suffix_short(), "چهل و پنجم"); 76 | assert_eq!("سی".add_ordinal_suffix_short(), "سی‌اُم"); 77 | assert_eq!("یک".add_ordinal_suffix_short(), "یکم"); 78 | assert_eq!("".add_ordinal_suffix_short(), ""); 79 | } 80 | 81 | #[test] 82 | fn add_ordinal_suffix_long_test() { 83 | assert_eq!("چهل و سه".add_ordinal_suffix_long(), "چهل و سومین"); 84 | assert_eq!("چهل و پنج".add_ordinal_suffix_long(), "چهل و پنجمین"); 85 | assert_eq!("سی".add_ordinal_suffix_long(), "سی‌اُمین"); 86 | assert_eq!("یک".add_ordinal_suffix_long(), "یکمین"); 87 | assert_eq!("".add_ordinal_suffix_long(), ""); 88 | } 89 | 90 | #[test] 91 | fn remove_ordinal_suffix_test() { 92 | assert_eq!("چهل و سوم".remove_ordinal_suffix(), "چهل و سه"); 93 | assert_eq!("چهل و سومین".remove_ordinal_suffix(), "چهل و سه"); 94 | assert_eq!("چهل و پنجم".remove_ordinal_suffix(), "چهل و پنج"); 95 | assert_eq!("چهل و پنجمین".remove_ordinal_suffix(), "چهل و پنج"); 96 | assert_eq!("سی‌اُم".remove_ordinal_suffix(), "سی"); 97 | assert_eq!("سی‌اُمین".remove_ordinal_suffix(), "سی"); 98 | assert_eq!("یکم".remove_ordinal_suffix(), "یک"); 99 | assert_eq!("یکمین".remove_ordinal_suffix(), "یک"); 100 | assert_eq!("اول".remove_ordinal_suffix(), "یک"); 101 | assert_eq!("اولین".remove_ordinal_suffix(), "یک"); 102 | assert_eq!("".remove_ordinal_suffix(), ""); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/translate/mod.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::USER_AGENT; 2 | use std::str::FromStr; 3 | use strum::{Display, EnumString}; 4 | 5 | use crate::utils::impl_trait_for_string_types; 6 | 7 | /// Languages that can used for input and output of the [`translate`] function. 8 | #[derive(Debug, Clone, PartialEq, Copy, Hash, Display, EnumString)] 9 | #[strum(ascii_case_insensitive)] 10 | pub enum Language { 11 | #[strum(serialize = "english", serialize = "en", serialize = "انگلیسی")] 12 | English, 13 | #[strum(serialize = "farsi", serialize = "fa", serialize = "فارسی")] 14 | Farsi, 15 | #[strum(serialize = "arabic", serialize = "ar", serialize = "عربی")] 16 | Arabic, 17 | #[strum(serialize = "chinese", serialize = "zh", serialize = "چینی")] 18 | Chinese, 19 | #[strum(serialize = "french", serialize = "fr", serialize = "فرانسوی")] 20 | French, 21 | #[strum(serialize = "german", serialize = "de", serialize = "آلمانی")] 22 | German, 23 | #[strum(serialize = "italian", serialize = "it", serialize = "ایتالیایی")] 24 | Italian, 25 | #[strum(serialize = "japanese", serialize = "ja", serialize = "ژاپنی")] 26 | Japanese, 27 | #[strum(serialize = "portuguese", serialize = "pt", serialize = "پرتغالی")] 28 | Portuguese, 29 | #[strum(serialize = "russian", serialize = "ru", serialize = "روسی")] 30 | Russian, 31 | #[strum(serialize = "spanish", serialize = "es", serialize = "اسپانیایی")] 32 | Spanish, 33 | } 34 | 35 | impl Language { 36 | /// Return the language with the language code name. (ex. "ar", "de") 37 | pub fn as_code(&self) -> &'static str { 38 | match self { 39 | Language::English => "en", 40 | Language::Farsi => "fa", 41 | Language::Arabic => "ar", 42 | Language::Chinese => "zh", 43 | Language::French => "fr", 44 | Language::German => "de", 45 | Language::Italian => "it", 46 | Language::Japanese => "ja", 47 | Language::Portuguese => "pt", 48 | Language::Russian => "ru", 49 | Language::Spanish => "es", 50 | } 51 | } 52 | 53 | /// Create a Language from &str like "en", "French" or "ژاپنی". Case Doesn't matter. 54 | pub fn from(s: &str) -> crate::Result { 55 | Self::from_str(s).map_err(|e| e.into()) 56 | } 57 | } 58 | 59 | pub trait Translate: AsRef { 60 | const BASE_URL: &'static str = 61 | "https://translate.google.com/translate_a/single?&client=gtx&sl=auto"; 62 | const USER_AGENT_VALUE:&'static str = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"; 63 | 64 | fn translate(&self, target: Language) -> crate::Result { 65 | let url = format!( 66 | "{url}&tl={target}&hl={target}&dt=t&text={text}", 67 | url = Self::BASE_URL, 68 | target = target.as_code(), 69 | text = &self.as_ref(), 70 | ); 71 | reqwest::blocking::Client::new() 72 | .get(url) 73 | .header(USER_AGENT, Self::USER_AGENT_VALUE) 74 | .send()? 75 | .text() 76 | .map_err(|e| e.to_string()) 77 | .and_then(|s| { 78 | match s 79 | .find('"') 80 | .map(|i| i + 1) 81 | .and_then(|i| s[i..].find('"').map(|i2| (i, i2 + i))) 82 | { 83 | Some((start, end)) if start != 0 && end != start => { 84 | Ok(s[start..end].to_owned()) 85 | } 86 | _ => Err("Does Not Exist".to_string()), 87 | } 88 | }) 89 | .map_err(|e| e.into()) 90 | } 91 | } 92 | 93 | impl_trait_for_string_types!(Translate); 94 | 95 | #[cfg(test)] 96 | mod translate_test { 97 | use super::{Language, Translate}; 98 | 99 | #[test] 100 | fn translate() { 101 | assert_eq!( 102 | "سلام دنیا", 103 | r#"Hello, World"#.translate(Language::Farsi).unwrap() 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/banking/sheba.rs: -------------------------------------------------------------------------------- 1 | use crate::{banking::sheba_table::ShebaInfo, utils::impl_trait_for_string_types}; 2 | 3 | use crate::banking::sheba_table::SHEBA_CODE_TABLE; 4 | 5 | pub trait ShebaNumber: AsRef { 6 | fn is_valid_sheba_code(&self) -> bool { 7 | self.iso_7064_mod_97_10().is_ok_and(|i| i == 1) 8 | } 9 | 10 | fn get_sheba_info(&self) -> Option { 11 | if !self.is_valid_sheba_code() { 12 | return None; 13 | } 14 | 15 | let digits = self.as_ref(); 16 | SHEBA_CODE_TABLE 17 | .get(&digits[4..7]) 18 | .map(|sc| sc.process(digits)) 19 | } 20 | 21 | fn iso_7064_mod_97_10(&self) -> crate::Result { 22 | let sheba_code = self.as_ref(); 23 | // check if sheba is in valid format (^IR[0-9]{24}$) 24 | if !(sheba_code.len() == 26 25 | && sheba_code.starts_with("IR") 26 | && sheba_code.chars().skip(2).all(|c| c.is_ascii_digit())) 27 | { 28 | return Err("invalid sheba code".into()); 29 | } 30 | 31 | let d1 = sheba_code.as_bytes()[0] - 65 + 10; 32 | let d2 = sheba_code.as_bytes()[1] - 65 + 10; 33 | let mut remainder = format!("{}{}{}{}", &sheba_code[4..], d1, d2, &sheba_code[2..4]); 34 | let mut block; 35 | loop { 36 | let len = remainder.len(); 37 | if len <= 2 { 38 | break; 39 | } 40 | let pos = if len > 9 { 9 } else { len }; 41 | block = &remainder[..pos]; 42 | remainder = format!("{}{}", block.parse::()? % 97, &remainder[pos..]); 43 | } 44 | Ok(remainder.parse::()? % 97) 45 | } 46 | } 47 | 48 | impl_trait_for_string_types!(ShebaNumber); 49 | 50 | #[cfg(test)] 51 | mod sheba_code { 52 | use crate::banking::sheba_table::{ 53 | process_parsian, process_pasargad, process_shahr, ShebaAccountNumber, 54 | }; 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn sheba_code_validate() { 60 | assert!("IR210180000000009190404878".is_valid_sheba_code()); 61 | assert!(!"123332132131432498654433".is_valid_sheba_code()); 62 | assert!(!"IR1233321321314324986544323222".is_valid_sheba_code()); 63 | assert!(!"IR1233321222".is_valid_sheba_code()); 64 | } 65 | 66 | #[test] 67 | fn sheba_code_info() { 68 | assert!("IR210180000000009190404878".get_sheba_info().is_some()); 69 | assert!("IR012345678901234567890123".get_sheba_info().is_none()); 70 | assert!("IR012345678A01234567890123".get_sheba_info().is_none()); 71 | 72 | let sheba_info_pasargad = ShebaInfo { 73 | code: "057", 74 | nickname: "pasargad", 75 | name: "Pasargad Bank", 76 | persian_name: "بانک پاسارگاد", 77 | account_number: Some(ShebaAccountNumber { 78 | normal: "220800134473701".to_owned(), 79 | formatted: "220-800-13447370-1".to_owned(), 80 | }), 81 | process: Some(process_pasargad), 82 | }; 83 | let sheba_info_shahr = ShebaInfo { 84 | code: "061", 85 | nickname: "shahr", 86 | name: "City Bank", 87 | persian_name: "بانک شهر", 88 | account_number: Some(ShebaAccountNumber { 89 | normal: "700796858044".to_owned(), 90 | formatted: "700796858044".to_owned(), 91 | }), 92 | process: Some(process_shahr), 93 | }; 94 | let sheba_info_parsian = ShebaInfo { 95 | code: "054", 96 | nickname: "parsian", 97 | name: "Parsian Bank", 98 | persian_name: "بانک پارسیان", 99 | account_number: Some(ShebaAccountNumber { 100 | normal: "020817909002".to_owned(), 101 | formatted: "002-00817909-002".to_owned(), 102 | }), 103 | process: Some(process_parsian), 104 | }; 105 | 106 | assert_eq!( 107 | "IR550570022080013447370101".get_sheba_info(), 108 | Some(sheba_info_pasargad) 109 | ); 110 | assert_eq!( 111 | "IR790610000000700796858044".get_sheba_info(), 112 | Some(sheba_info_shahr) 113 | ); 114 | assert_eq!( 115 | "IR820540102680020817909002".get_sheba_info(), 116 | Some(sheba_info_parsian) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/banking/bank_codes_table.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct BankCode { 3 | pub code: &'static str, 4 | pub name: &'static str, 5 | } 6 | 7 | pub static BANK_CODE_TABLE: phf::Map<&str, BankCode> = phf::phf_map! { 8 | "636214" => BankCode { 9 | name: "بانک آینده", 10 | code: "636214", 11 | }, 12 | "627412" => BankCode { 13 | name: "بانک اقتصاد نوین", 14 | code: "627412", 15 | }, 16 | "627381" => BankCode { 17 | name: "بانک انصار", 18 | code: "627381", 19 | }, 20 | "505785" => BankCode { 21 | name: "بانک ایران زمین", 22 | code: "505785", 23 | }, 24 | "622106" => BankCode { 25 | name: "بانک پارسیان", 26 | code: "622106", 27 | }, 28 | "627884" => BankCode { 29 | name: "بانک پارسیان", 30 | code: "627884", 31 | }, 32 | "502229" => BankCode { 33 | name: "بانک پاسارگاد", 34 | code: "502229", 35 | }, 36 | "639347" => BankCode { 37 | name: "بانک پاسارگاد", 38 | code: "639347", 39 | }, 40 | "627760" => BankCode { 41 | name: "پست بانک ایران", 42 | code: "627760", 43 | }, 44 | "585983" => BankCode { 45 | name: "بانک تجارت", 46 | code: "585983", 47 | }, 48 | "627353" => BankCode { 49 | name: "بانک تجارت", 50 | code: "627353", 51 | }, 52 | "502908" => BankCode { 53 | name: "بانک توسعه تعاون", 54 | code: "502908", 55 | }, 56 | "207177" => BankCode { 57 | name: "بانک توسعه صادرات", 58 | code: "207177", 59 | }, 60 | "627648" => BankCode { 61 | name: "بانک توسعه صادرات", 62 | code: "627648", 63 | }, 64 | "636949" => BankCode { 65 | name: "بانک حکمت ایرانیان", 66 | code: "636949", 67 | }, 68 | "585949" => BankCode { 69 | name: "بانک خاورمیانه", 70 | code: "585949", 71 | }, 72 | "502938" => BankCode { 73 | name: "بانک دی", 74 | code: "502938", 75 | }, 76 | "504172" => BankCode { 77 | name: "بانک رسالت", 78 | code: "504172", 79 | }, 80 | "589463" => BankCode { 81 | name: "بانک رفاه کارگران", 82 | code: "589463", 83 | }, 84 | "621986" => BankCode { 85 | name: "بانک سامان", 86 | code: "621986", 87 | }, 88 | "589210" => BankCode { 89 | name: "بانک سپه", 90 | code: "589210", 91 | }, 92 | "639607" => BankCode { 93 | name: "بانک سرمایه", 94 | code: "639607", 95 | }, 96 | "639346" => BankCode { 97 | name: "بانک سینا", 98 | code: "639346", 99 | }, 100 | "502806" => BankCode { 101 | name: "بانک شهر", 102 | code: "502806", 103 | }, 104 | "504706" => BankCode { 105 | name: "بانک شهر", 106 | code: "504706", 107 | }, 108 | "603769" => BankCode { 109 | name: "بانک صادرات ایران", 110 | code: "603769", 111 | }, 112 | "903769" => BankCode { 113 | name: "بانک صادرات ایران", 114 | code: "903769", 115 | }, 116 | "627961" => BankCode { 117 | name: "بانک صنعت و معدن", 118 | code: "627961", 119 | }, 120 | "639370" => BankCode { 121 | name: "بانک قرض الحسنه مهر", 122 | code: "639370", 123 | }, 124 | "639599" => BankCode { 125 | name: "بانک قوامین", 126 | code: "639599", 127 | }, 128 | "627488" => BankCode { 129 | name: "بانک کارآفرین", 130 | code: "627488", 131 | }, 132 | "603770" => BankCode { 133 | name: "بانک کشاورزی", 134 | code: "603770", 135 | }, 136 | "639217" => BankCode { 137 | name: "بانک کشاورزی", 138 | code: "639217", 139 | }, 140 | "505416" => BankCode { 141 | name: "بانک گردشگری", 142 | code: "505416", 143 | }, 144 | "505426" => BankCode { 145 | name: "بانک گردشگری", 146 | code: "505426", 147 | }, 148 | "636797" => BankCode { 149 | name: "بانک مرکزی ایران", 150 | code: "636797", 151 | }, 152 | "628023" => BankCode { 153 | name: "بانک مسکن", 154 | code: "628023", 155 | }, 156 | "610433" => BankCode { 157 | name: "بانک ملت", 158 | code: "610433", 159 | }, 160 | "991975" => BankCode { 161 | name: "بانک ملت", 162 | code: "991975", 163 | }, 164 | "170019" => BankCode { 165 | name: "بانک ملی ایران", 166 | code: "170019", 167 | }, 168 | "603799" => BankCode { 169 | name: "بانک ملی ایران", 170 | code: "603799", 171 | }, 172 | "606373" => BankCode { 173 | name: "بانک مهر ایران", 174 | code: "606373", 175 | }, 176 | "505801" => BankCode { 177 | name: "موسسه کوثر", 178 | code: "505801", 179 | }, 180 | "606256" => BankCode { 181 | name: "موسسه اعتباری ملل", 182 | code: "606256", 183 | }, 184 | "628157" => BankCode { 185 | name: "موسسه اعتباری توسعه", 186 | code: "628157", 187 | }, 188 | }; 189 | -------------------------------------------------------------------------------- /src/date/converter.rs: -------------------------------------------------------------------------------- 1 | static DAY_SUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 2 | static DAY_SUM_KABISE: [u32; 12] = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; 3 | 4 | #[derive(PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)] 5 | pub struct JalaliDate { 6 | pub day: u32, 7 | pub year: u32, 8 | pub month: u32, 9 | } 10 | 11 | pub fn convert_gregorian_to_jalali(year: u32, month: u32, day: u32) -> crate::Result { 12 | if !(1..=12).contains(&month) { 13 | return Err("Month should be between 1 and 12".into()); 14 | } 15 | 16 | let days_sum = if year_is_leap(year) { 17 | DAY_SUM_KABISE[month as usize - 1] + day 18 | } else { 19 | DAY_SUM[month as usize - 1] + day 20 | }; 21 | 22 | if days_sum < 79 { 23 | let days_sum = days_sum + dey_jan_diff(year); 24 | let jalai_year = year - 622; 25 | 26 | if days_sum % 30 == 0 { 27 | Ok(JalaliDate { 28 | year: jalai_year, 29 | day: 30, 30 | month: (days_sum / 30) + 9, 31 | }) 32 | } else { 33 | Ok(JalaliDate { 34 | year: jalai_year, 35 | day: days_sum % 30, 36 | month: (days_sum / 30) + 10, 37 | }) 38 | } 39 | } else { 40 | let days_sum = days_sum - 79; 41 | let jalali_year = year - 621; 42 | 43 | if days_sum <= 186 { 44 | if days_sum % 31 == 0 { 45 | Ok(JalaliDate { 46 | day: 31, 47 | year: jalali_year, 48 | month: days_sum / 31, 49 | }) 50 | } else { 51 | Ok(JalaliDate { 52 | day: days_sum % 31, 53 | year: jalali_year, 54 | month: (days_sum / 31) + 1, 55 | }) 56 | } 57 | } else { 58 | let days_sum = days_sum - 186; 59 | if days_sum % 30 == 0 { 60 | Ok(JalaliDate { 61 | day: 30, 62 | year: jalali_year, 63 | month: (days_sum / 30) + 6, 64 | }) 65 | } else { 66 | Ok(JalaliDate { 67 | day: days_sum % 30, 68 | year: jalali_year, 69 | month: (days_sum / 30) + 7, 70 | }) 71 | } 72 | } 73 | } 74 | } 75 | 76 | // Gets the day difference between Persian month, Dey and Gregorian month January 77 | fn dey_jan_diff(year: u32) -> u32 { 78 | if year_is_leap(year) { 79 | return 11; 80 | } 81 | 82 | 10 83 | } 84 | 85 | fn year_is_leap(gregorian_year: u32) -> bool { 86 | ((gregorian_year % 100) != 0 && (gregorian_year % 4) == 0) 87 | || ((gregorian_year % 100) == 0 && (gregorian_year % 400) == 0) 88 | } 89 | 90 | static GREGORIAN_MONTHS: [u32; 12] = [30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 28, 31]; 91 | static GREGORIAN_MONTH_LEAP: [u32; 12] = [30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29, 31]; 92 | 93 | #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] 94 | pub struct GregorianDate { 95 | pub year: u32, 96 | pub month: u32, 97 | pub day: u32, 98 | } 99 | 100 | /// month range is 1..12 101 | /// day starts from 1 102 | pub fn convert_jalali_to_gregorian( 103 | year: u32, 104 | month: u32, 105 | day: u32, 106 | ) -> crate::Result { 107 | let mut gregorian_year = year + 621; 108 | let gregorian_day_of_month; 109 | let gregorian_month; 110 | let march_day_diff = if year_is_leap(gregorian_year) { 12 } else { 11 }; 111 | let day_count = if (1..=6).contains(&month) { 112 | (month - 1) * 31 + day 113 | } else { 114 | (6 * 31) + (month - 7) * 30 + day 115 | }; 116 | 117 | if day_count < march_day_diff { 118 | gregorian_day_of_month = day_count + (31 - march_day_diff); 119 | gregorian_month = 3; 120 | } else { 121 | let mut remain_days = day_count - march_day_diff; 122 | let mut i = 0; 123 | 124 | if year_is_leap(gregorian_year + 1) { 125 | while remain_days > GREGORIAN_MONTHS[i] { 126 | remain_days -= GREGORIAN_MONTH_LEAP[i]; 127 | i += 1; 128 | } 129 | } else { 130 | while remain_days > GREGORIAN_MONTHS[i] { 131 | remain_days -= GREGORIAN_MONTHS[i]; 132 | i += 1; 133 | } 134 | } 135 | 136 | gregorian_day_of_month = remain_days; 137 | 138 | if i > 8 { 139 | gregorian_month = i - 8; 140 | gregorian_year += 1; 141 | } else { 142 | gregorian_month = i + 4; 143 | } 144 | } 145 | 146 | Ok(GregorianDate { 147 | year: gregorian_year, 148 | month: gregorian_month as u32, 149 | day: gregorian_day_of_month, 150 | }) 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use super::{ 156 | convert_gregorian_to_jalali, convert_jalali_to_gregorian, GregorianDate, JalaliDate, 157 | }; 158 | 159 | #[test] 160 | fn convert_date() { 161 | let result = convert_gregorian_to_jalali(2022, 2, 6).unwrap(); 162 | assert_eq!( 163 | result, 164 | JalaliDate { 165 | day: 17, 166 | month: 11, 167 | year: 1400 168 | } 169 | ); 170 | } 171 | 172 | #[test] 173 | fn convert_date_2() { 174 | let result = convert_gregorian_to_jalali(2015, 11, 23).unwrap(); 175 | assert_eq!( 176 | result, 177 | JalaliDate { 178 | day: 2, 179 | month: 9, 180 | year: 1394 181 | } 182 | ); 183 | } 184 | 185 | #[test] 186 | fn jalali_to_gregorian_date() { 187 | let result = convert_jalali_to_gregorian(1402, 8, 24).unwrap(); 188 | assert_eq!( 189 | result, 190 | GregorianDate { 191 | day: 15, 192 | month: 11, 193 | year: 2023 194 | } 195 | ); 196 | } 197 | 198 | #[test] 199 | fn jalali_to_gregorian_date_2() { 200 | let result = convert_jalali_to_gregorian(1402, 3, 3).unwrap(); 201 | assert_eq!( 202 | result, 203 | GregorianDate { 204 | day: 24, 205 | month: 5, 206 | year: 2023 207 | } 208 | ); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/province/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod city; 2 | 3 | use city::*; 4 | use strum::{Display, EnumString}; 5 | 6 | // in future if phf support enum as key enum must be replace with string 7 | pub static PROVINCES: phf::Map<&str, Province> = phf::phf_map! { 8 | "Alborz" => Province{ 9 | prefix_phone : "026", 10 | farsi_name : "", 11 | latin_name : "", 12 | cities : &ALBORZ_CITIES, 13 | }, 14 | "Ardabil" => Province{ 15 | prefix_phone : "045", 16 | farsi_name : "", 17 | latin_name : "", 18 | cities : &ARDABIL_CITIES, 19 | }, 20 | "AzerbaijanEast" => Province{ 21 | prefix_phone : "041", 22 | farsi_name : "", 23 | latin_name : "", 24 | cities : &AZERBAIJAN_EAST_CITIES, 25 | }, 26 | "AzerbaijanWest" => Province{ 27 | prefix_phone : "044", 28 | farsi_name : "", 29 | latin_name : "", 30 | cities : &AZERBAIJAN_WEST_CITIES, 31 | }, 32 | "Bushehr" => Province{ 33 | prefix_phone : "077", 34 | farsi_name : "", 35 | latin_name : "", 36 | cities : &BUSHEHR_CITIES, 37 | }, 38 | "ChaharMahaalAndBakhtiari" => Province{ 39 | prefix_phone : "038", 40 | farsi_name : "", 41 | latin_name : "", 42 | cities : &CHAHARMAHAAL_AND_BAKHTIARI_CITIES, 43 | }, 44 | "Fars" => Province{ 45 | prefix_phone : "071", 46 | farsi_name : "", 47 | latin_name : "", 48 | cities : &FARS_CITIES, 49 | }, 50 | "Gilan" => Province{ 51 | prefix_phone : "013", 52 | farsi_name : "", 53 | latin_name : "", 54 | cities : &GILAN_CITIES, 55 | }, 56 | "Golestan" => Province{ 57 | prefix_phone : "017", 58 | farsi_name : "", 59 | latin_name : "", 60 | cities : &GOLESTAN_CITIES, 61 | }, 62 | "Hamadan" => Province{ 63 | prefix_phone : "081", 64 | farsi_name : "", 65 | latin_name : "", 66 | cities : &HAMADAN_CITIES, 67 | }, 68 | "Hormozgan" => Province{ 69 | prefix_phone : "076", 70 | farsi_name : "", 71 | latin_name : "", 72 | cities : &HORMOZGAN_CITIES, 73 | }, 74 | "Ilam" => Province{ 75 | prefix_phone : "084", 76 | farsi_name : "", 77 | latin_name : "", 78 | cities : &ILAM_CITIES, 79 | }, 80 | "Isfahan" => Province{ 81 | prefix_phone : "031", 82 | farsi_name : "", 83 | latin_name : "", 84 | cities : &ISFAHAN_CITIES, 85 | }, 86 | "Kerman" => Province{ 87 | prefix_phone : "034", 88 | farsi_name : "", 89 | latin_name : "", 90 | cities : &KERMAN_CITIES, 91 | }, 92 | "Kermanshah" => Province{ 93 | prefix_phone : "083", 94 | farsi_name : "", 95 | latin_name : "", 96 | cities : &KERMANSHAH_CITIES, 97 | }, 98 | "KhorasanNorth" => Province{ 99 | prefix_phone : "058", 100 | farsi_name : "", 101 | latin_name : "", 102 | cities : &KHORASAN_NORTH_CITIES, 103 | }, 104 | "KhorasanRazavi" => Province{ 105 | prefix_phone : "051", 106 | farsi_name : "", 107 | latin_name : "", 108 | cities : &KHORASAN_RAZAVI_CITIES, 109 | }, 110 | "KhorasanSouth" => Province{ 111 | prefix_phone : "056", 112 | farsi_name : "", 113 | latin_name : "", 114 | cities : &KHORASAN_SOUTH_CITIES, 115 | }, 116 | "Khuzestan" => Province{ 117 | prefix_phone : "061", 118 | farsi_name : "", 119 | latin_name : "", 120 | cities : &KHUZESTAN_CITIES, 121 | }, 122 | "KohgiluyehAndBoyerAhmad" => Province{ 123 | prefix_phone : "074", 124 | farsi_name : "", 125 | latin_name : "", 126 | cities : &KOHGILUYEH_ANDBOYER_AHMAD_CITIES, 127 | }, 128 | "Kurdistan" => Province{ 129 | prefix_phone : "078", 130 | farsi_name : "", 131 | latin_name : "", 132 | cities : &KURDISTAN_CITIES, 133 | }, 134 | "Lorestan" => Province{ 135 | prefix_phone : "066", 136 | farsi_name : "", 137 | latin_name : "", 138 | cities : &LORESTAN_CITIES, 139 | }, 140 | "Markazi" => Province{ 141 | prefix_phone : "086", 142 | farsi_name : "", 143 | latin_name : "", 144 | cities : &MARKAZI_CITIES, 145 | }, 146 | "Mazandaran" => Province{ 147 | prefix_phone : "011", 148 | farsi_name : "", 149 | latin_name : "", 150 | cities : &MAZANDARAN_CITIES, 151 | }, 152 | "Qazvin" => Province{ 153 | prefix_phone : "028", 154 | farsi_name : "", 155 | latin_name : "", 156 | cities : &QAZVIN_CITIES, 157 | }, 158 | "Qom" => Province{ 159 | prefix_phone : "025", 160 | farsi_name : "", 161 | latin_name : "", 162 | cities : &QOM_CITIES, 163 | }, 164 | "Semnan" => Province{ 165 | prefix_phone : "023", 166 | farsi_name : "", 167 | latin_name : "", 168 | cities : &SEMNAN_CITIES, 169 | }, 170 | "SistanAndBaluchestan" => Province{ 171 | prefix_phone : "054", 172 | farsi_name : "", 173 | latin_name : "", 174 | cities : &SISTAN_AND_BALUCHESTAN_CITIES, 175 | }, 176 | "Tehran" => Province{ 177 | prefix_phone : "021", 178 | farsi_name : "", 179 | latin_name : "", 180 | cities : &TEHRAN_CITIES, 181 | }, 182 | "Yazd" => Province{ 183 | prefix_phone : "035", 184 | farsi_name : "", 185 | latin_name : "", 186 | cities : &YAZD_CITIES, 187 | }, 188 | "Zanjan" => Province{ 189 | prefix_phone : "024", 190 | farsi_name : "", 191 | latin_name : "", 192 | cities : &ZANJAN_CITIES, 193 | }, 194 | }; 195 | 196 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 197 | pub enum IranProvince { 198 | Alborz, 199 | Ardabil, 200 | AzerbaijanEast, 201 | AzerbaijanWest, 202 | Bushehr, 203 | ChaharMahaalAndBakhtiari, 204 | Fars, 205 | Gilan, 206 | Golestan, 207 | Hamadan, 208 | Hormozgan, 209 | Ilam, 210 | Isfahan, 211 | Kerman, 212 | Kermanshah, 213 | KhorasanNorth, 214 | KhorasanRazavi, 215 | KhorasanSouth, 216 | Khuzestan, 217 | KohgiluyehAndBoyerAhmad, 218 | Kurdistan, 219 | Lorestan, 220 | Markazi, 221 | Mazandaran, 222 | Qazvin, 223 | Qom, 224 | Semnan, 225 | SistanAndBaluchestan, 226 | Tehran, 227 | Yazd, 228 | Zanjan, 229 | } 230 | 231 | pub struct Province { 232 | pub prefix_phone: &'static str, 233 | pub farsi_name: &'static str, 234 | pub latin_name: &'static str, 235 | pub cities: &'static phf::Map<&'static str, city::City>, 236 | } 237 | -------------------------------------------------------------------------------- /src/banking/card_number_extractor.rs: -------------------------------------------------------------------------------- 1 | use super::Banking; 2 | use crate::{digit::Digit, utils::impl_trait_for_string_types}; 3 | 4 | /// Card number information 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct CardNumber { 7 | /// Base card-number 8 | pub base: String, 9 | /// Card-number without any extra character 10 | pub pure: String, 11 | /// Start Index of card-number (based on chars) 12 | pub index: usize, 13 | /// Card-numbers is valid or not 14 | pub is_valid: Option, 15 | /// bank name of card-number 16 | // TODO(saeid): maybe make this `Option>`? 17 | pub bank_name: Option<&'static str>, 18 | } 19 | 20 | /// Extract card number options 21 | #[derive(Clone, Copy, Debug)] 22 | pub struct ExtractCardNumberOptions { 23 | /// Check if every card-numbers is valid or not 24 | pub check_validation: bool, 25 | /// Detect Bank's name by extracted card-number 26 | pub detect_bank_name: bool, 27 | /// Return list of only valid card-numbers 28 | pub filter_valid_card_numbers: bool, 29 | } 30 | 31 | impl ExtractCardNumberOptions { 32 | /// Enable all `check_validation`, `detect_bank_name` and `filter_valid_card_numbers` options. 33 | pub fn all() -> Self { 34 | Self { 35 | check_validation: true, 36 | detect_bank_name: true, 37 | filter_valid_card_numbers: true, 38 | } 39 | } 40 | 41 | /// Disable all options. 42 | pub fn none() -> Self { 43 | Self { 44 | check_validation: false, 45 | detect_bank_name: false, 46 | filter_valid_card_numbers: false, 47 | } 48 | } 49 | } 50 | 51 | impl Default for ExtractCardNumberOptions { 52 | fn default() -> Self { 53 | Self { 54 | check_validation: true, 55 | detect_bank_name: false, 56 | filter_valid_card_numbers: true, 57 | } 58 | } 59 | } 60 | 61 | pub trait ExtractCardNumber: AsRef { 62 | /// Extract all the card numbers. 63 | fn extract_card_numbers(&self, options: ExtractCardNumberOptions) -> Vec { 64 | let digits = self.as_ref(); 65 | let mut result = Vec::new(); 66 | 67 | let mut len = 0; 68 | let mut base = String::with_capacity(20); 69 | let mut pure = String::with_capacity(16); 70 | for c in digits.chars() { 71 | match CharType::new(&c) { 72 | CharType::Digit => { 73 | base.push(c); 74 | pure.push(c); 75 | len += 1; 76 | 77 | // a valid iranian card-number have 16 digits 78 | if len == 16 { 79 | if pure.have_non_en_digit() { 80 | // if there is any non english digit replace them with english digits 81 | pure = pure.digits_to_en(); 82 | } 83 | 84 | result.push(CardNumber { 85 | base: base.clone(), 86 | is_valid: options 87 | .check_validation 88 | .then(|| pure.is_valid_bank_card_number()), 89 | bank_name: if options.detect_bank_name { 90 | pure.get_bank_name_from_card_number() 91 | } else { 92 | None 93 | }, 94 | pure: pure.clone(), 95 | index: result.len() + 1, 96 | }); 97 | // clear buffers and len after we pushed the information to result 98 | base.clear(); 99 | pure.clear(); 100 | len = 0; 101 | } 102 | } 103 | CharType::Seperator => base.push(c), 104 | CharType::Other => { 105 | // clear buffers and len in case of unsupported character 106 | base.clear(); 107 | pure.clear(); 108 | len = 0; 109 | } 110 | } 111 | } 112 | 113 | if options.filter_valid_card_numbers { 114 | result.retain(|c| c.is_valid.unwrap_or(true)); 115 | } 116 | 117 | result 118 | } 119 | } 120 | 121 | enum CharType { 122 | Digit, 123 | Seperator, 124 | Other, 125 | } 126 | 127 | impl CharType { 128 | fn new(c: &char) -> Self { 129 | match c { 130 | '0'..='9' | '\u{0660}'..='\u{0669}' | '\u{06F0}'..='\u{06F9}' => Self::Digit, 131 | '-' | '_' | '*' | '.' => Self::Seperator, 132 | _ => Self::Other, 133 | } 134 | } 135 | } 136 | 137 | impl_trait_for_string_types!(ExtractCardNumber); 138 | 139 | #[cfg(test)] 140 | mod test { 141 | use super::*; 142 | 143 | macro_rules! create_card { 144 | ($base:literal, $pure:literal, $index:literal) => { 145 | CardNumber { 146 | base: $base.to_owned(), 147 | pure: $pure.to_owned(), 148 | index: $index, 149 | is_valid: None, 150 | bank_name: None, 151 | } 152 | }; 153 | ($base:literal, $pure:literal, $index:literal, $is_valid:literal) => { 154 | CardNumber { 155 | base: $base.to_owned(), 156 | pure: $pure.to_owned(), 157 | index: $index, 158 | is_valid: Some($is_valid), 159 | bank_name: None, 160 | } 161 | }; 162 | ($base:literal, $pure:literal, $index:literal, $is_valid:literal, $bank_name:literal) => { 163 | CardNumber { 164 | base: $base.to_owned(), 165 | pure: $pure.to_owned(), 166 | index: $index, 167 | is_valid: Some($is_valid), 168 | bank_name: Some($bank_name), 169 | } 170 | }; 171 | } 172 | 173 | #[test] 174 | fn extract_card_number_test() { 175 | // Should find and extract 4 Card Numbers 176 | let string = r#"شماره کارتم رو برات نوشتم: 177 | 6219-8610-3452-9007 178 | اینم یه شماره کارت دیگه ای که دارم 179 | 5022291070873466 180 | ۵۰۲۲۲۹۱۰۸۱۸۷۳۴۶۶ 181 | ۵۰۲۲-۲۹۱۰-۷۰۸۷-۳۴۶۶"#; 182 | 183 | // Should find and extract 4 Card Numbers 184 | let cards = vec![ 185 | create_card!("6219-8610-3452-9007", "6219861034529007", 1), 186 | create_card!("5022291070873466", "5022291070873466", 2), 187 | create_card!("۵۰۲۲۲۹۱۰۸۱۸۷۳۴۶۶", "5022291081873466", 3), 188 | create_card!("۵۰۲۲-۲۹۱۰-۷۰۸۷-۳۴۶۶", "5022291070873466", 4), 189 | ]; 190 | 191 | assert_eq!( 192 | cards, 193 | string.extract_card_numbers(ExtractCardNumberOptions { 194 | check_validation: false, 195 | ..Default::default() 196 | }) 197 | ); 198 | 199 | // Should find and format the Card-Number into Text that includes Persian & English digits 200 | { 201 | let string = "شماره کارتم رو برات نوشتم: ۵۰۲۲-2910-7۰۸۷-۳۴۶۶"; 202 | 203 | let card = create_card!("۵۰۲۲-2910-7۰۸۷-۳۴۶۶", "5022291070873466", 1); 204 | 205 | assert_eq!( 206 | vec![card], 207 | string.extract_card_numbers(ExtractCardNumberOptions { 208 | check_validation: false, 209 | ..Default::default() 210 | }) 211 | ); 212 | } 213 | 214 | // Should validate extract card-numbers 215 | let cards = vec![ 216 | create_card!("6219-8610-3452-9007", "6219861034529007", 1, true), 217 | create_card!("5022291070873466", "5022291070873466", 2, true), 218 | create_card!("۵۰۲۲۲۹۱۰۸۱۸۷۳۴۶۶", "5022291081873466", 3, false), 219 | create_card!("۵۰۲۲-۲۹۱۰-۷۰۸۷-۳۴۶۶", "5022291070873466", 4, true), 220 | ]; 221 | 222 | assert_eq!( 223 | cards, 224 | string.extract_card_numbers(ExtractCardNumberOptions { 225 | check_validation: true, 226 | filter_valid_card_numbers: false, 227 | ..Default::default() 228 | }) 229 | ); 230 | 231 | // Should return only valid card-numbers 232 | let cards = vec![ 233 | create_card!("6219-8610-3452-9007", "6219861034529007", 1, true), 234 | create_card!("5022291070873466", "5022291070873466", 2, true), 235 | create_card!("۵۰۲۲-۲۹۱۰-۷۰۸۷-۳۴۶۶", "5022291070873466", 4, true), 236 | ]; 237 | 238 | assert_eq!( 239 | cards, 240 | string.extract_card_numbers(ExtractCardNumberOptions { 241 | check_validation: true, 242 | filter_valid_card_numbers: true, 243 | ..Default::default() 244 | }) 245 | ); 246 | 247 | // Should detect Banks number for valid card-numbers 248 | let cards = vec![ 249 | create_card!( 250 | "6219-8610-3452-9007", 251 | "6219861034529007", 252 | 1, 253 | true, 254 | "بانک سامان" 255 | ), 256 | create_card!( 257 | "5022291070873466", 258 | "5022291070873466", 259 | 2, 260 | true, 261 | "بانک پاسارگاد" 262 | ), 263 | create_card!( 264 | "۵۰۲۲-۲۹۱۰-۷۰۸۷-۳۴۶۶", 265 | "5022291070873466", 266 | 4, 267 | true, 268 | "بانک پاسارگاد" 269 | ), 270 | ]; 271 | 272 | assert_eq!( 273 | cards, 274 | string.extract_card_numbers(ExtractCardNumberOptions { 275 | check_validation: true, 276 | filter_valid_card_numbers: true, 277 | detect_bank_name: true 278 | }) 279 | ); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/banking/sheba_table.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Eq)] 2 | pub struct ShebaAccountNumber { 3 | pub normal: String, 4 | pub formatted: String, 5 | } 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | pub struct ShebaInfo { 9 | pub code: &'static str, 10 | pub nickname: &'static str, 11 | pub name: &'static str, 12 | pub persian_name: &'static str, 13 | pub account_number: Option, 14 | pub process: Option, 15 | } 16 | 17 | impl ShebaInfo { 18 | /// Process the sheba if needed and return a newly created [`ShebaInfo`]. 19 | pub fn process(&self, sheba_code: &str) -> ShebaInfo { 20 | let mut sheba_clone = self.clone(); 21 | if let Some(process) = self.process { 22 | process(&mut sheba_clone, sheba_code); 23 | } 24 | sheba_clone 25 | } 26 | } 27 | 28 | pub static SHEBA_CODE_TABLE: phf::Map<&str, ShebaInfo> = phf::phf_map! { 29 | "010" => ShebaInfo{ 30 | code: "010", 31 | nickname: "central-bank", 32 | name: "Central Bank of Iran", 33 | persian_name: "بانک مرکزی جمهوری اسلامی ایران", 34 | account_number: None, 35 | process: None, 36 | }, 37 | "011" => ShebaInfo{ 38 | code: "011", 39 | nickname: "sanat-o-madan", 40 | name: "Sanat O Madan Bank", 41 | persian_name: "بانک صنعت و معدن", 42 | account_number: None, 43 | process: None, 44 | }, 45 | "012" => ShebaInfo{ 46 | code: "012", 47 | nickname: "mellat", 48 | name: "Mellat Bank", 49 | persian_name: "بانک ملت", 50 | account_number: None, 51 | process: None, 52 | }, 53 | "013" => ShebaInfo{ 54 | code: "013", 55 | nickname: "refah", 56 | name: "Refah Bank", 57 | persian_name: "بانک رفاه کارگران", 58 | account_number: None, 59 | process: None, 60 | }, 61 | "014" => ShebaInfo{ 62 | code: "014", 63 | nickname: "maskan", 64 | name: "Maskan Bank", 65 | persian_name: "بانک مسکن", 66 | account_number: None, 67 | process: None, 68 | }, 69 | "015" => ShebaInfo{ 70 | code: "015", 71 | nickname: "sepah", 72 | name: "Sepah Bank", 73 | persian_name: "بانک سپه", 74 | account_number: None, 75 | process: None, 76 | }, 77 | "016" => ShebaInfo{ 78 | code: "016", 79 | nickname: "keshavarzi", 80 | name: "Keshavarzi", 81 | persian_name: "بانک کشاورزی", 82 | account_number: None, 83 | process: None, 84 | }, 85 | "017" => ShebaInfo{ 86 | code: "017", 87 | nickname: "melli", 88 | name: "Melli", 89 | persian_name: "بانک ملی ایران", 90 | account_number: None, 91 | process: None, 92 | }, 93 | "018" => ShebaInfo{ 94 | code: "018", 95 | nickname: "tejarat", 96 | name: "Tejarat Bank", 97 | persian_name: "بانک تجارت", 98 | account_number: None, 99 | process: None, 100 | }, 101 | "019" => ShebaInfo{ 102 | code: "019", 103 | nickname: "saderat", 104 | name: "Saderat Bank", 105 | persian_name: "بانک صادرات ایران", 106 | account_number: None, 107 | process: None, 108 | }, 109 | "020" => ShebaInfo{ 110 | code: "020", 111 | nickname: "tosee-saderat", 112 | name: "Tose Saderat Bank", 113 | persian_name: "بانک توسعه صادرات", 114 | account_number: None, 115 | process: None, 116 | }, 117 | "021" => ShebaInfo{ 118 | code: "021", 119 | nickname: "post", 120 | name: "Post Bank", 121 | persian_name: "پست بانک ایران", 122 | account_number: None, 123 | process: None, 124 | }, 125 | "022" => ShebaInfo{ 126 | code: "022", 127 | nickname: "toose-taavon", 128 | name: "Tosee Taavon Bank", 129 | persian_name: "بانک توسعه تعاون", 130 | account_number: None, 131 | process: None, 132 | }, 133 | "051" => ShebaInfo{ 134 | code: "051", 135 | nickname: "tosee", 136 | name: "Tosee Bank", 137 | persian_name: "موسسه اعتباری توسعه", 138 | account_number: None, 139 | process: None, 140 | }, 141 | "052" => ShebaInfo{ 142 | code: "052", 143 | nickname: "ghavamin", 144 | name: "Ghavamin Bank", 145 | persian_name: "بانک قوامین", 146 | account_number: None, 147 | process: None, 148 | }, 149 | "053" => ShebaInfo{ 150 | code: "053", 151 | nickname: "karafarin", 152 | name: "Karafarin Bank", 153 | persian_name: "بانک کارآفرین", 154 | account_number: None, 155 | process: None, 156 | }, 157 | "054" => ShebaInfo{ 158 | code: "054", 159 | nickname: "parsian", 160 | name: "Parsian Bank", 161 | persian_name: "بانک پارسیان", 162 | account_number: None, 163 | process: Some(process_parsian), 164 | }, 165 | "055" => ShebaInfo{ 166 | code: "055", 167 | nickname: "eghtesad-novin", 168 | name: "Eghtesad Novin Bank", 169 | persian_name: "بانک اقتصاد نوین", 170 | account_number: None, 171 | process: None, 172 | }, 173 | "056" => ShebaInfo{ 174 | code: "056", 175 | nickname: "saman", 176 | name: "Saman Bank", 177 | persian_name: "بانک سامان", 178 | account_number: None, 179 | process: None, 180 | }, 181 | "057" => ShebaInfo{ 182 | code: "057", 183 | nickname: "pasargad", 184 | name: "Pasargad Bank", 185 | persian_name: "بانک پاسارگاد", 186 | account_number: None, 187 | process: Some(process_pasargad), 188 | }, 189 | "058" => ShebaInfo{ 190 | code: "058", 191 | nickname: "sarmayeh", 192 | name: "Sarmayeh Bank", 193 | persian_name: "بانک سرمایه", 194 | account_number: None, 195 | process: None, 196 | }, 197 | "059" => ShebaInfo{ 198 | code: "059", 199 | nickname: "sina", 200 | name: "Sina Bank", 201 | persian_name: "بانک سینا", 202 | account_number: None, 203 | process: None, 204 | }, 205 | "060" => ShebaInfo{ 206 | code: "060", 207 | nickname: "mehr-iran", 208 | name: "Mehr Iran Bank", 209 | persian_name: "بانک مهر ایران", 210 | account_number: None, 211 | process: None, 212 | }, 213 | "061" => ShebaInfo{ 214 | code: "061", 215 | nickname: "shahr", 216 | name: "City Bank", 217 | persian_name: "بانک شهر", 218 | account_number: None, 219 | process: Some(process_shahr), 220 | }, 221 | "062" => ShebaInfo{ 222 | code: "062", 223 | nickname: "ayandeh", 224 | name: "Ayandeh Bank", 225 | persian_name: "بانک آینده", 226 | account_number: None, 227 | process: None, 228 | }, 229 | "063" => ShebaInfo{ 230 | code: "063", 231 | nickname: "ansar", 232 | name: "Ansar Bank", 233 | persian_name: "بانک انصار", 234 | account_number: None, 235 | process: None, 236 | }, 237 | "064" => ShebaInfo{ 238 | code: "064", 239 | nickname: "gardeshgari", 240 | name: "Gardeshgari Bank", 241 | persian_name: "بانک گردشگری", 242 | account_number: None, 243 | process: None, 244 | }, 245 | "065" => ShebaInfo{ 246 | code: "065", 247 | nickname: "hekmat-iranian", 248 | name: "Hekmat Iranian Bank", 249 | persian_name: "بانک حکمت ایرانیان", 250 | account_number: None, 251 | process: None, 252 | }, 253 | "066" => ShebaInfo{ 254 | code: "066", 255 | nickname: "dey", 256 | name: "Dey Bank", 257 | persian_name: "بانک دی", 258 | account_number: None, 259 | process: None, 260 | }, 261 | "069" => ShebaInfo{ 262 | code: "069", 263 | nickname: "iran-zamin", 264 | name: "Iran Zamin Bank", 265 | persian_name: "بانک ایران زمین", 266 | account_number: None, 267 | process: None, 268 | }, 269 | "070" => ShebaInfo{ 270 | code: "070", 271 | nickname: "resalat", 272 | name: "Resalat Bank", 273 | persian_name: "بانک قرض الحسنه رسالت", 274 | account_number: None, 275 | process: None, 276 | }, 277 | "073" => ShebaInfo{ 278 | code: "073", 279 | nickname: "kosar", 280 | name: "Kosar Credit Institute", 281 | persian_name: "موسسه اعتباری کوثر", 282 | account_number: None, 283 | process: None, 284 | }, 285 | "075" => ShebaInfo{ 286 | code: "075", 287 | nickname: "melal", 288 | name: "Melal Credit Institute", 289 | persian_name: "موسسه اعتباری ملل", 290 | account_number: None, 291 | process: None, 292 | }, 293 | "078" => ShebaInfo{ 294 | code: "078", 295 | nickname: "middle-east-bank", 296 | name: "Middle East Bank", 297 | persian_name: "بانک خاورمیانه", 298 | account_number: None, 299 | process: None, 300 | }, 301 | "080" => ShebaInfo{ 302 | code: "080", 303 | nickname: "noor-bank", 304 | name: "Noor Credit Institution", 305 | persian_name: "موسسه اعتباری نور", 306 | account_number: None, 307 | process: None, 308 | }, 309 | "079" => ShebaInfo{ 310 | code: "079", 311 | nickname: "mehr-eqtesad", 312 | name: "Mehr Eqtesad Bank", 313 | persian_name: "بانک مهر اقتصاد", 314 | account_number: None, 315 | process: None, 316 | }, 317 | "090" => ShebaInfo{ 318 | code: "090", 319 | nickname: "mehr-iran", 320 | name: "Mehr Iran Bank", 321 | persian_name: "بانک مهر ایران", 322 | account_number: None, 323 | process: None, 324 | }, 325 | "095" => ShebaInfo{ 326 | code: "095", 327 | nickname: "iran-venezuela", 328 | name: "Iran and Venezuela Bank", 329 | persian_name: "بانک ایران و ونزوئلا", 330 | account_number: None, 331 | process: None, 332 | }, 333 | }; 334 | 335 | pub(super) fn process_parsian(sheba: &mut ShebaInfo, sheba_code: &str) { 336 | let substr = &sheba_code[14..]; 337 | sheba.account_number = Some(ShebaAccountNumber { 338 | normal: substr.to_owned(), 339 | formatted: format!("0{}-0{}-{}", &substr[0..2], &substr[2..9], &substr[9..12]), 340 | }); 341 | } 342 | 343 | pub(super) fn process_pasargad(sheba: &mut ShebaInfo, sheba_code: &str) { 344 | let mut idx = 7; 345 | for ch in sheba_code[7..].chars() { 346 | if ch != '0' { 347 | break; 348 | } 349 | idx += 1; 350 | } 351 | let substr = &sheba_code[idx..sheba_code.len() - 2]; 352 | sheba.account_number = Some(ShebaAccountNumber { 353 | normal: substr.to_owned(), 354 | formatted: format!( 355 | "{}-{}-{}-{}", 356 | &substr[0..3], 357 | &substr[3..6], 358 | &substr[6..14], 359 | &substr[14..15] 360 | ), 361 | }); 362 | } 363 | 364 | pub(super) fn process_shahr(sheba: &mut ShebaInfo, sheba_code: &str) { 365 | let mut idx = 7; 366 | for ch in sheba_code[7..].chars() { 367 | if ch != '0' { 368 | break; 369 | } 370 | idx += 1; 371 | } 372 | let substr = &sheba_code[idx..]; 373 | sheba.account_number = Some(ShebaAccountNumber { 374 | normal: substr.to_owned(), 375 | formatted: substr.to_owned(), 376 | }); 377 | } 378 | -------------------------------------------------------------------------------- /src/digit/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::*; 2 | use std::convert::TryFrom; 3 | 4 | /// Supported language variants. 5 | #[derive(Clone, Copy)] 6 | pub enum Lang { 7 | En, 8 | Fa, 9 | Ar, 10 | } 11 | 12 | /// Set of helpers to manipulate Persian (or Arabic!) digits. 13 | pub trait Digit: AsRef { 14 | const DIGITS: [[char; 10]; 3] = [ 15 | ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], 16 | ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'], 17 | ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'], 18 | ]; 19 | 20 | /// Takes a string that may contain some digits, and 21 | /// replaces the source language digits with the destination 22 | /// language digits. 23 | fn digits_convert(&self, from: Lang, to: Lang) -> String { 24 | let src = self.as_ref(); 25 | src.chars() 26 | .map( 27 | |v| match Self::DIGITS[from as usize].iter().position(|&r| r == v) { 28 | Some(v) => Self::DIGITS[to as usize][v], 29 | None => v, 30 | }, 31 | ) 32 | .collect() 33 | } 34 | 35 | /// Takes a string that may contain English digits, and returns 36 | /// a string that represents the same digits but in Persian. 37 | fn digits_en_to_fa(&self) -> String { 38 | self.digits_convert(Lang::En, Lang::Fa) 39 | } 40 | 41 | /// Takes a string that may contain Persian digits, and returns 42 | /// a string that represents the same digits but in English. 43 | fn digits_fa_to_en(&self) -> String { 44 | self.digits_convert(Lang::Fa, Lang::En) 45 | } 46 | 47 | /// Takes a string that may contain Arabic digits, and returns 48 | /// a string that represents the same digits but in Persian. 49 | fn digits_ar_to_fa(&self) -> String { 50 | self.digits_convert(Lang::Ar, Lang::Fa) 51 | } 52 | 53 | /// Takes a string that may contain Arabic digits, and returns 54 | /// a string that represents the same digits but in English. 55 | fn digits_ar_to_en(&self) -> String { 56 | self.digits_convert(Lang::Ar, Lang::En) 57 | } 58 | 59 | /// Takes a string that may contain Persian or Arabic digits, and returns 60 | /// a string that represents the same digits but in English. 61 | fn digits_to_en(&self) -> String { 62 | self.digits_convert(Lang::Ar, Lang::En) 63 | .digits_convert(Lang::Fa, Lang::En) 64 | } 65 | 66 | /// Check if the string have any non english (arabic, persian) digits. 67 | fn have_non_en_digit(&self) -> bool { 68 | self.as_ref() 69 | .chars() 70 | .any(|c| Self::DIGITS.iter().skip(1).any(|d| d.contains(&c))) 71 | } 72 | } 73 | 74 | impl_trait_for_string_types!(Digit); 75 | 76 | /// The multipliers of the persian number system, up to a billion. 77 | pub static MULTIPLIERS: phf::Map<&str, u32> = phf::phf_map! { 78 | "هزار" => 1_000, 79 | "میلیون" => 1_000_000, 80 | "میلیارد" => 1_000_000_000, 81 | }; 82 | 83 | /// Fixed numbers that we interpret at face-value, as-is, because they cannot be broken down into 84 | /// smaller parts. 85 | /// 86 | /// Includes [1-20], [30, 40, ..., 100], and [100, 200, ..., 900] 87 | // TODO: probably move to another file, too much bloat here. 88 | // TOOD: Is it 'nohsad' or 'noh sad'? 'haftsad or 'haft sad'? 89 | pub static FACE_VALUE: phf::Map<&str, u16> = phf::phf_map! { 90 | "صفر" => 0, 91 | "یک" => 1, 92 | "دو" => 2, 93 | "سه" => 3, 94 | "چهار" => 4, 95 | "پنج" => 5, 96 | "شش" => 6, 97 | "هفت" => 7, 98 | "هشت" => 8, 99 | "نه" => 9, 100 | "ده" => 10, 101 | "یازده" => 11, 102 | "دوازده" => 12, 103 | "سیزده" => 13, 104 | "چهارده" => 14, 105 | "پانزده" => 15, 106 | "شانزده" => 16, 107 | "هفده" => 17, 108 | "هجده" => 18, 109 | "نوزده" => 19, 110 | "بیست" => 20, 111 | "سی" => 30, 112 | "چهل" => 40, 113 | "پنجاه" => 50, 114 | "شصت" => 60, 115 | "هفتاد" => 70, 116 | "هشتاد" => 80, 117 | "نود" => 90, 118 | "صد" => 100, 119 | "دویست" => 200, 120 | "سیصد" => 300, 121 | "جهارصد" => 400, 122 | "پانصد" => 500, 123 | "ششصد" => 600, 124 | "هفتصد" => 700, 125 | "هشتصد" => 800, 126 | "نهصد" => 900, 127 | }; 128 | 129 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 130 | enum TokenType { 131 | Multiplier(u32), 132 | FaceValue(u16), 133 | } 134 | 135 | impl TryFrom<&str> for TokenType { 136 | type Error = &'static str; 137 | 138 | fn try_from(token: &str) -> Result { 139 | if let Some(v) = FACE_VALUE.get(token) { 140 | Ok(TokenType::FaceValue(*v)) 141 | } else if let Some(m) = MULTIPLIERS.get(token) { 142 | Ok(TokenType::Multiplier(*m)) 143 | } else { 144 | Err("Unsupported token") 145 | } 146 | } 147 | } 148 | 149 | /// Extension trait for conversion of number strings in written format into numbers. 150 | /// 151 | /// See [`words_to_number`] for more details. 152 | pub trait WordsToNumber: AsRef { 153 | /// Convert `&self` into a number with given type `N`. 154 | /// 155 | /// Needless to say, the resulting number must fit into `N`. 156 | /// 157 | /// Supports only values from [0, 999_999_999_999]. 158 | /// 159 | /// The given string may contain only tokens being equal to either of [`FACE_VALUE`] or 160 | /// [`MULTIPLIERS`], or the special case of "و". Any other token will lead to a parse error. 161 | // IMPLEMENTATION NOTE: we follow the semantics of a basic stack-machine: Any face-value type 162 | // will be accumulated, until a multiplier is seen, at which point, it is multiplied into the 163 | // accumulated face-values, and we reset. 164 | fn words_to_number< 165 | N: num_traits::Zero 166 | + num_traits::One 167 | + num_traits::CheckedMul 168 | + num_traits::CheckedAdd 169 | + std::convert::TryFrom 170 | + std::convert::TryFrom 171 | + std::fmt::Debug 172 | + Copy, 173 | >( 174 | &self, 175 | ) -> crate::Result { 176 | // TODO: ^^ maybe make a module-level Result alias. 177 | const CANT_CONVERT: &str = "Given number does not fit in the provided`N`"; 178 | 179 | let parsed = self 180 | .as_ref() 181 | .split(' ') 182 | .filter(|t| *t != "و") 183 | .map(TokenType::try_from) 184 | .collect::, _>>()?; 185 | 186 | let mut final_value: N = num_traits::Zero::zero(); 187 | let mut intermediary_value: N = num_traits::Zero::zero(); 188 | 189 | let check_add = |lhs: N, rhs: N| lhs.checked_add(&rhs).ok_or(CANT_CONVERT); 190 | 191 | let checked_mul = |lhs: N, rhs: N| lhs.checked_mul(&rhs).ok_or(CANT_CONVERT); 192 | let mut last: Option = None; 193 | 194 | for t in parsed { 195 | match t { 196 | TokenType::FaceValue(v) => { 197 | let v_n: N = v.try_into().map_err(|_| CANT_CONVERT)?; 198 | 199 | intermediary_value = check_add(intermediary_value, v_n)?; 200 | last = Some(TokenType::FaceValue(v)); 201 | } 202 | TokenType::Multiplier(m) => { 203 | if last.map_or(false, |last| matches!(last, TokenType::Multiplier(_))) { 204 | return Err("Incorrect format: two multipliers in a row".into()); 205 | } 206 | 207 | // a bit of helper: if this is the first iteration, you can omit a 'یک' and we 208 | // replace it here. 209 | if last.is_none() { 210 | intermediary_value = num_traits::One::one(); 211 | } 212 | 213 | let m_n: N = m.try_into().map_err(|_| CANT_CONVERT)?; 214 | intermediary_value = checked_mul(intermediary_value, m_n)?; 215 | final_value = check_add(final_value, intermediary_value)?; 216 | 217 | intermediary_value = num_traits::Zero::zero(); 218 | last = Some(TokenType::Multiplier(m)); 219 | } 220 | } 221 | } 222 | 223 | if let Some(TokenType::FaceValue(_)) = last { 224 | final_value = final_value 225 | .checked_add(&intermediary_value) 226 | .ok_or(CANT_CONVERT)?; 227 | } 228 | 229 | Ok(final_value) 230 | } 231 | } 232 | 233 | impl_trait_for_string_types!(WordsToNumber); 234 | 235 | #[cfg(test)] 236 | mod word_to_number { 237 | use super::*; 238 | 239 | #[test] 240 | fn words_to_number_basic() { 241 | // a random example to begin with. 242 | assert_eq!( 243 | "هفتصد و بیست و یک هزار و دویست و بیست و یک" 244 | .words_to_number::() 245 | .unwrap(), 246 | 721221, 247 | ); 248 | } 249 | 250 | #[test] 251 | fn face_value_only() { 252 | // face value only. 253 | assert_eq!("یک".words_to_number::().unwrap(), 1); 254 | assert_eq!("یازده".words_to_number::().unwrap(), 11); 255 | } 256 | 257 | #[test] 258 | fn one_multiplier() { 259 | // 1 multiplier 260 | assert_eq!("بیست و یک".words_to_number::().unwrap(), 21); 261 | assert_eq!("نود و نه".words_to_number::().unwrap(), 99); 262 | assert_eq!("دویست و هشت".words_to_number::().unwrap(), 208); 263 | 264 | assert_eq!("هزار و یازده".words_to_number::().unwrap(), 1011); 265 | assert_eq!("یک هزار و یازده".words_to_number::().unwrap(), 1011); 266 | 267 | assert_eq!( 268 | "میلیون و یازده".words_to_number::().unwrap(), 269 | 1_000_011 270 | ); 271 | 272 | assert_eq!( 273 | "صد و دوازده هزار و بیست و شش" 274 | .words_to_number::() 275 | .unwrap(), 276 | 112_026 277 | ); 278 | } 279 | 280 | #[test] 281 | fn two_multiplier() { 282 | // 2 multiplier 283 | assert_eq!( 284 | "صد و دوازده میلیون و بیست و شش هزار و پانصد و بیست و نه" 285 | .words_to_number::() 286 | .unwrap(), 287 | 112_026_529 288 | ); 289 | } 290 | 291 | #[test] 292 | fn duplicate_and_token_is_fixed() { 293 | // duplicate "and" is taken care of: TODO: maybe we don't want this. 294 | assert_eq!( 295 | "صد و و دوازده هزار و بیست و شش" 296 | .words_to_number::() 297 | .unwrap(), 298 | 112_026 299 | ); 300 | } 301 | 302 | #[test] 303 | fn invalid_token_fails() { 304 | // any gibberish fails 305 | assert!("صد و و کیان هزار و بیست و شش" 306 | .words_to_number::() 307 | .is_err()); 308 | } 309 | 310 | #[test] 311 | fn boundaries() { 312 | // boundaries 313 | // assert_eq!("صفر".words_to_number::().unwrap(), 0); 314 | assert_eq!( 315 | "نهصد و نود و نه میلیارد و نهصد و نود و نه میلیون و نهصد و نود و نه هزار و نهصد و نود و نه" 316 | .words_to_number::() 317 | .unwrap(), 318 | 999_999_999_999 319 | ) 320 | } 321 | 322 | #[test] 323 | fn can_work_in_generic() { 324 | // generic N size must be big enough 325 | assert!( 326 | "نهصد و نود و نه میلیارد و نهصد و نود و نه میلیون و نهصد و نود و نه هزار و نهصد و نود و نه" 327 | .words_to_number::() 328 | .is_err() 329 | ); 330 | 331 | // for u8 332 | assert_eq!("دویست و پنجاه و پنج".words_to_number::().unwrap(), 255); 333 | assert!("دویست و پنجاه و شش".words_to_number::().is_err()); 334 | 335 | // for u16 336 | assert_eq!( 337 | "شصت و پنج هزار و پانصد و سی و پنج" 338 | .words_to_number::() 339 | .unwrap(), 340 | 65535 341 | ); 342 | assert!("شصت و پنج هزار و پانصد و سی و شش" 343 | .words_to_number::() 344 | .is_err()); 345 | 346 | // for u32 347 | assert_eq!( 348 | "چهار میلیارد و دویست و نود و چهار میلیون و نهصد شصت و هفت هزار و دویست و نود و پنج" 349 | .words_to_number::() 350 | .unwrap(), 351 | 4_294_967_295 352 | ); 353 | assert!( 354 | "چهار میلیارد و دویست و نود و چهار میلیون و نهصد شصت و هفت هزار و دویست و نود و شش" 355 | .words_to_number::() 356 | .is_err(), 357 | ); 358 | } 359 | } 360 | #[cfg(test)] 361 | mod digits { 362 | use super::*; 363 | 364 | #[test] 365 | fn digits_en_to_fa() { 366 | assert_eq!("0123456789abc".digits_en_to_fa(), "۰۱۲۳۴۵۶۷۸۹abc"); 367 | } 368 | 369 | #[test] 370 | fn digits_fa_to_en() { 371 | assert_eq!("۰۱۲۳۴۵۶۷۸۹abc".digits_fa_to_en(), "0123456789abc"); 372 | } 373 | 374 | #[test] 375 | fn digits_ar_to_fa() { 376 | assert_eq!("٠١٢٣٤٥٦٧٨٩abc".digits_ar_to_fa(), "۰۱۲۳۴۵۶۷۸۹abc"); 377 | } 378 | 379 | #[test] 380 | fn digits_ar_to_en() { 381 | assert_eq!("٠١٢٣٤٥٦٧٨٩abc".digits_ar_to_en(), "0123456789abc"); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/province/city.rs: -------------------------------------------------------------------------------- 1 | use strum::{Display, EnumString}; 2 | 3 | #[derive(Debug)] 4 | pub struct City { 5 | pub farsi_name: &'static str, 6 | pub latin_name: &'static str, 7 | } 8 | 9 | pub static ALBORZ_CITIES: phf::Map<&str, City> = phf::phf_map! { 10 | "" => City{ 11 | farsi_name : "", 12 | latin_name : "", 13 | }, 14 | // TODO ... 15 | }; 16 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 17 | pub enum Alborz { 18 | // TODO add all cities 19 | } 20 | 21 | pub static ARDABIL_CITIES: phf::Map<&str, City> = phf::phf_map! { 22 | "" => City{ 23 | farsi_name : "", 24 | latin_name : "", 25 | }, 26 | // TODO ... 27 | }; 28 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 29 | pub enum Ardabil { 30 | // TODO add all cities 31 | } 32 | 33 | pub static AZERBAIJAN_EAST_CITIES: phf::Map<&str, City> = phf::phf_map! { 34 | "" => City{ 35 | farsi_name : "", 36 | latin_name : "", 37 | }, 38 | // TODO ... 39 | }; 40 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 41 | pub enum AzerbaijanEast { 42 | // TODO add all cities 43 | } 44 | 45 | pub static AZERBAIJAN_WEST_CITIES: phf::Map<&str, City> = phf::phf_map! { 46 | "" => City{ 47 | farsi_name : "", 48 | latin_name : "", 49 | }, 50 | // TODO ... 51 | }; 52 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 53 | pub enum AzerbaijanWest { 54 | // TODO add all cities 55 | } 56 | 57 | pub static BUSHEHR_CITIES: phf::Map<&str, City> = phf::phf_map! { 58 | "" => City{ 59 | farsi_name : "", 60 | latin_name : "", 61 | }, 62 | // TODO ... 63 | }; 64 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 65 | pub enum Bushehr { 66 | // TODO add all cities 67 | } 68 | 69 | pub static CHAHARMAHAAL_AND_BAKHTIARI_CITIES: phf::Map<&str, City> = phf::phf_map! { 70 | "" => City{ 71 | farsi_name : "", 72 | latin_name : "", 73 | }, 74 | // TODO ... 75 | }; 76 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 77 | pub enum ChaharMahaalAndBakhtiari { 78 | // TODO add all cities 79 | } 80 | 81 | pub static FARS_CITIES: phf::Map<&str, City> = phf::phf_map! { 82 | "" => City{ 83 | farsi_name : "", 84 | latin_name : "", 85 | }, 86 | // TODO ... 87 | }; 88 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 89 | pub enum Fars { 90 | // TODO add all cities 91 | } 92 | 93 | pub static GILAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 94 | "Rasht" => City{ 95 | farsi_name : "رشت", 96 | latin_name : "Rasht", 97 | }, 98 | "Langerood" => City{ 99 | farsi_name : "لنگرود", 100 | latin_name : "Langerood", 101 | }, 102 | "Lahijan" => City{ 103 | farsi_name : "لاهیجان", 104 | latin_name : "Lahijan", 105 | }, 106 | "Astara" => City{ 107 | farsi_name : "آستارا", 108 | latin_name : "Astara", 109 | }, 110 | "Shaft" => City{ 111 | farsi_name : "شفت", 112 | latin_name : "Shaft", 113 | }, 114 | "Masal" => City{ 115 | farsi_name : "ماسال", 116 | latin_name : "Masal", 117 | }, 118 | "Siahkal" => City{ 119 | farsi_name : "سیاهکل", 120 | latin_name : "Siahkal", 121 | }, 122 | "BandarAnzali" => City{ 123 | farsi_name : "بندرانزلی", 124 | latin_name : "BandarAnzali", 125 | }, 126 | "Talesh" => City{ 127 | farsi_name : "تالش", 128 | latin_name : "Talesh", 129 | }, 130 | "Rudsar" => City{ 131 | farsi_name : "رودسر", 132 | latin_name : "Rudsar", 133 | }, 134 | "Rudbar" => City{ 135 | farsi_name : "رودبار", 136 | latin_name : "Rudbar", 137 | }, 138 | "Fouman" => City{ 139 | farsi_name : "فومن", 140 | latin_name : "Fouman", 141 | }, 142 | "Amlash" => City{ 143 | farsi_name : "املش", 144 | latin_name : "Amlash", 145 | }, 146 | "Rezvanshahr" => City{ 147 | farsi_name : "رضوانشهر", 148 | latin_name : "Rezvanshahr", 149 | }, 150 | "SomeSara" => City{ 151 | farsi_name : "صومعه‌ سرا", 152 | latin_name : "SomeSara", 153 | }, 154 | "AstanehAshrafieh" => City{ 155 | farsi_name : "آستانه اشرفیه", 156 | latin_name : "AstanehAshrafieh", 157 | }, 158 | "Khomam" => City{ 159 | farsi_name : "خمام", 160 | latin_name : "Khomam", 161 | }, 162 | }; 163 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 164 | pub enum Gilan { 165 | Rasht, 166 | Langerood, 167 | Lahijan, 168 | Astara, 169 | Shaft, 170 | Masal, 171 | Siahkal, 172 | BandarAnzali, 173 | Talesh, 174 | Rudsar, 175 | Rudbar, 176 | Fouman, 177 | Amlash, 178 | Rezvanshahr, 179 | SomeSara, 180 | AstanehAshrafieh, 181 | Khomam, 182 | } 183 | 184 | pub static GOLESTAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 185 | "" => City{ 186 | farsi_name : "", 187 | latin_name : "", 188 | }, 189 | // TODO ... 190 | }; 191 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 192 | pub enum Golestan { 193 | // TODO add all cities 194 | } 195 | 196 | pub static HAMADAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 197 | "" => City{ 198 | farsi_name : "", 199 | latin_name : "", 200 | }, 201 | // TODO ... 202 | }; 203 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 204 | pub enum Hamadan { 205 | // TODO add all cities 206 | } 207 | 208 | pub static HORMOZGAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 209 | "" => City{ 210 | farsi_name : "", 211 | latin_name : "", 212 | }, 213 | // TODO ... 214 | }; 215 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 216 | pub enum Hormozgan { 217 | // TODO add all cities 218 | } 219 | 220 | pub static ILAM_CITIES: phf::Map<&str, City> = phf::phf_map! { 221 | "" => City{ 222 | farsi_name : "", 223 | latin_name : "", 224 | }, 225 | // TODO ... 226 | }; 227 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 228 | pub enum Ilam { 229 | // TODO add all cities 230 | } 231 | 232 | pub static ISFAHAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 233 | "" => City{ 234 | farsi_name : "", 235 | latin_name : "", 236 | }, 237 | // TODO ... 238 | }; 239 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 240 | pub enum Isfahan { 241 | // TODO add all cities 242 | } 243 | 244 | pub static KERMAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 245 | "" => City{ 246 | farsi_name : "", 247 | latin_name : "", 248 | }, 249 | // TODO ... 250 | }; 251 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 252 | pub enum Kerman { 253 | // TODO add all cities 254 | } 255 | 256 | pub static KERMANSHAH_CITIES: phf::Map<&str, City> = phf::phf_map! { 257 | "" => City{ 258 | farsi_name : "", 259 | latin_name : "", 260 | }, 261 | // TODO ... 262 | }; 263 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 264 | pub enum Kermanshah { 265 | // TODO add all cities 266 | } 267 | 268 | pub static KHORASAN_NORTH_CITIES: phf::Map<&str, City> = phf::phf_map! { 269 | "" => City{ 270 | farsi_name : "", 271 | latin_name : "", 272 | }, 273 | // TODO ... 274 | }; 275 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 276 | pub enum KhorasanNorth { 277 | // TODO add all cities 278 | } 279 | 280 | pub static KHORASAN_RAZAVI_CITIES: phf::Map<&str, City> = phf::phf_map! { 281 | "" => City{ 282 | farsi_name : "", 283 | latin_name : "", 284 | }, 285 | // TODO ... 286 | }; 287 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 288 | pub enum KhorasanRazavi { 289 | // TODO add all cities 290 | } 291 | 292 | pub static KHORASAN_SOUTH_CITIES: phf::Map<&str, City> = phf::phf_map! { 293 | "" => City{ 294 | farsi_name : "", 295 | latin_name : "", 296 | }, 297 | // TODO ... 298 | }; 299 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 300 | pub enum KhorasanSouth { 301 | // TODO add all cities 302 | } 303 | 304 | pub static KHUZESTAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 305 | "" => City{ 306 | farsi_name : "", 307 | latin_name : "", 308 | }, 309 | // TODO ... 310 | }; 311 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 312 | pub enum Khuzestan { 313 | // TODO add all cities 314 | } 315 | 316 | pub static KOHGILUYEH_ANDBOYER_AHMAD_CITIES: phf::Map<&str, City> = phf::phf_map! { 317 | "" => City{ 318 | farsi_name : "", 319 | latin_name : "", 320 | }, 321 | // TODO ... 322 | }; 323 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 324 | pub enum KohgiluyehAndBoyerAhmad { 325 | // TODO add all cities 326 | } 327 | 328 | pub static KURDISTAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 329 | "" => City{ 330 | farsi_name : "", 331 | latin_name : "", 332 | }, 333 | // TODO ... 334 | }; 335 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 336 | pub enum Kurdistan { 337 | // TODO add all cities 338 | } 339 | 340 | pub static LORESTAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 341 | "" => City{ 342 | farsi_name : "", 343 | latin_name : "", 344 | }, 345 | // TODO ... 346 | }; 347 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 348 | pub enum Lorestan { 349 | // TODO add all cities 350 | } 351 | 352 | pub static MARKAZI_CITIES: phf::Map<&str, City> = phf::phf_map! { 353 | "" => City{ 354 | farsi_name : "", 355 | latin_name : "", 356 | }, 357 | // TODO ... 358 | }; 359 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 360 | pub enum Markazi { 361 | // TODO add all cities 362 | } 363 | 364 | pub static MAZANDARAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 365 | "" => City{ 366 | farsi_name : "", 367 | latin_name : "", 368 | }, 369 | // TODO ... 370 | }; 371 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 372 | pub enum Mazandaran { 373 | // TODO add all cities 374 | } 375 | 376 | pub static QAZVIN_CITIES: phf::Map<&str, City> = phf::phf_map! { 377 | "" => City{ 378 | farsi_name : "", 379 | latin_name : "", 380 | }, 381 | // TODO ... 382 | }; 383 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 384 | pub enum Qazvin { 385 | // TODO add all cities 386 | } 387 | 388 | pub static QOM_CITIES: phf::Map<&str, City> = phf::phf_map! { 389 | "" => City{ 390 | farsi_name : "", 391 | latin_name : "", 392 | }, 393 | // TODO ... 394 | }; 395 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 396 | pub enum Qom { 397 | // TODO add all cities 398 | } 399 | 400 | pub static SEMNAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 401 | "" => City{ 402 | farsi_name : "", 403 | latin_name : "", 404 | }, 405 | // TODO ... 406 | }; 407 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 408 | pub enum Semnan { 409 | // TODO add all cities 410 | } 411 | 412 | pub static SISTAN_AND_BALUCHESTAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 413 | "Khash" => City{ 414 | farsi_name : "خاش", 415 | latin_name : "Khash", 416 | }, 417 | "Zahedan" => City{ 418 | farsi_name : "زاهدان", 419 | latin_name : "Zahedan", 420 | }, 421 | "Zabol" => City{ 422 | farsi_name : "زابل", 423 | latin_name : "Zabol", 424 | }, 425 | "Iranshahr" => City{ 426 | farsi_name : "ایرانشهر", 427 | latin_name : "Iranshahr", 428 | }, 429 | "Chabahar" => City{ 430 | farsi_name : "چابهار", 431 | latin_name : "Chabahar", 432 | }, 433 | "Saravan" => City{ 434 | farsi_name : "سراوان", 435 | latin_name : "Saravan", 436 | }, 437 | "Nikshahr" => City{ 438 | farsi_name : "نیکشهر", 439 | latin_name : "Nikshahr", 440 | }, 441 | "Rask" => City{ 442 | farsi_name : "راسک", 443 | latin_name : "Rask", 444 | }, 445 | "Konarak" => City{ 446 | farsi_name : "کنارک", 447 | latin_name : "Konarak", 448 | }, 449 | "Zahak" => City{ 450 | farsi_name : "زهک", 451 | latin_name : "Zahak", 452 | }, 453 | "Delgan" => City{ 454 | farsi_name : "دلگان", 455 | latin_name : "Delgan", 456 | }, 457 | "SibVaSoran" => City{ 458 | farsi_name : "سیب و سوران", 459 | latin_name : "SibVaSoran", 460 | }, 461 | "Hirmand" => City{ 462 | farsi_name : "هیرمند", 463 | latin_name : "Hirmand", 464 | }, 465 | "Mehrestan" => City{ 466 | farsi_name : "مهرستان", 467 | latin_name : "Mehrestan", 468 | }, 469 | "Mirjaveh" => City{ 470 | farsi_name : "میرجاوه", 471 | latin_name : "Mirjaveh", 472 | }, 473 | "Ghasreghand" => City{ 474 | farsi_name : "قصرقند", 475 | latin_name : "Ghasreghand", 476 | }, 477 | "Nimroz" => City{ 478 | farsi_name : "نیمروز", 479 | latin_name : "Nimroz", 480 | }, 481 | "Haamon" => City{ 482 | farsi_name : "هامون", 483 | latin_name : "Haamon", 484 | }, 485 | "Fenoj" => City{ 486 | farsi_name : "فنوج", 487 | latin_name : "Fenoj", 488 | }, 489 | "Bempor" => City{ 490 | farsi_name : "بمپور", 491 | latin_name : "Bempor", 492 | }, 493 | "Taftan" => City{ 494 | farsi_name : "تفتان", 495 | latin_name : "Taftan", 496 | }, 497 | "Dashtyari" => City{ 498 | farsi_name : "دشتیاری", 499 | latin_name : "Dashtyari", 500 | }, 501 | "Sarbaz" => City{ 502 | farsi_name : "سرباز", 503 | latin_name : "Sarbaz", 504 | }, 505 | "Golshan" => City{ 506 | farsi_name : "گلشن", 507 | latin_name : "Golshan", 508 | }, 509 | }; 510 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 511 | pub enum SistanAndBaluchestan { 512 | Khash, 513 | Zahedan, 514 | Zabol, 515 | Iranshahr, 516 | Chabahar, 517 | Saravan, 518 | Nikshahr, 519 | Rask, 520 | Konarak, 521 | Zahak, 522 | Delgan, 523 | SibVaSoran, 524 | Hirmand, 525 | Mehrestan, 526 | Mirjaveh, 527 | Ghasreghand, 528 | Nimroz, 529 | Haamon, 530 | Fenoj, 531 | Bempor, 532 | Taftan, 533 | Dashtyari, 534 | Sarbaz, 535 | Golshan, 536 | } 537 | 538 | pub static TEHRAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 539 | "EslamShahr"=> City{ 540 | farsi_name : "اسلامشهر", 541 | latin_name : "EslamShahr", 542 | }, 543 | "Baharestan"=> City{ 544 | farsi_name : "بهارستان", 545 | latin_name : "Baharestan", 546 | }, 547 | "Pakdasht"=> City{ 548 | farsi_name : "پاکدشت", 549 | latin_name : "Pakdasht", 550 | }, 551 | "Pardis"=> City{ 552 | farsi_name : "پردیس", 553 | latin_name : "Pardis", 554 | }, 555 | "Pishva"=> City{ 556 | farsi_name : "پیشوا", 557 | latin_name : "Pishva", 558 | }, 559 | "Tehran"=> City{ 560 | farsi_name : "تهران", 561 | latin_name : "Tehran", 562 | }, 563 | "Damavand"=> City{ 564 | farsi_name : "دماوند", 565 | latin_name : "Damavand", 566 | }, 567 | "Robatkarim"=> City{ 568 | farsi_name : "رباط‌کریم", 569 | latin_name : "Robatkarim", 570 | }, 571 | "Rey"=> City{ 572 | farsi_name : "ری", 573 | latin_name : "Rey", 574 | }, 575 | "Shemiranat"=> City{ 576 | farsi_name : "شمیرانات", 577 | latin_name : "Shemiranat", 578 | }, 579 | "Shahryar"=> City{ 580 | farsi_name : "شهریار", 581 | latin_name : "Shahryar", 582 | }, 583 | "Ghods"=> City{ 584 | farsi_name : "قدس", 585 | latin_name : "Ghods", 586 | }, 587 | "Gharchak"=> City{ 588 | farsi_name : "قرچک", 589 | latin_name : "Gharchak", 590 | }, 591 | "Firozkoh"=> City{ 592 | farsi_name : "فیروزکوه", 593 | latin_name : "Firozkoh", 594 | }, 595 | "Malard"=> City{ 596 | farsi_name : "ملارد", 597 | latin_name : "Malard", 598 | }, 599 | "Varamin"=> City{ 600 | farsi_name : "ورامین", 601 | latin_name : "Varamin", 602 | }, 603 | }; 604 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 605 | pub enum Tehran { 606 | EslamShahr, 607 | Baharestan, 608 | Pakdasht, 609 | Pardis, 610 | Pishva, 611 | Tehran, 612 | Damavand, 613 | Robatkarim, 614 | Rey, 615 | Shemiranat, 616 | Shahryar, 617 | Ghods, 618 | Gharchak, 619 | Firozkoh, 620 | Malard, 621 | Varamin, 622 | } 623 | 624 | pub static YAZD_CITIES: phf::Map<&str, City> = phf::phf_map! { 625 | "Yasd" => City{ 626 | farsi_name : "یزد", 627 | latin_name : "Yasd", 628 | }, 629 | "Maybod" => City{ 630 | farsi_name : "میبد", 631 | latin_name : "Maybod", 632 | }, 633 | "Ardakan" => City{ 634 | farsi_name : "اردکان", 635 | latin_name : "Ardakan", 636 | }, 637 | "Mehriz" => City{ 638 | farsi_name : "مهریز", 639 | latin_name : "Mehriz", 640 | }, 641 | "Abarkoh" => City{ 642 | farsi_name : "ابرکوه", 643 | latin_name : "Abarkoh", 644 | }, 645 | "Bafgh" => City{ 646 | farsi_name : "بافق", 647 | latin_name : "Bafgh", 648 | }, 649 | "Taft" => City{ 650 | farsi_name : "تفت", 651 | latin_name : "Taft", 652 | }, 653 | "Khatam" => City{ 654 | farsi_name : "خاتم", 655 | latin_name : "Khatam", 656 | }, 657 | "Ashkezar" => City{ 658 | farsi_name : "اشکذر", 659 | latin_name : "Ashkezar", 660 | }, 661 | "Bahabad" => City{ 662 | farsi_name : "بهاباد", 663 | latin_name : "Bahabad", 664 | }, 665 | }; 666 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 667 | pub enum Yazd { 668 | Yasd, 669 | Maybod, 670 | Ardakan, 671 | Mehriz, 672 | Abarkoh, 673 | Bafgh, 674 | Taft, 675 | Khatam, 676 | Ashkezar, 677 | Bahabad, 678 | } 679 | 680 | pub static ZANJAN_CITIES: phf::Map<&str, City> = phf::phf_map! { 681 | "Zanjan" => City{ 682 | farsi_name : "زنجان", 683 | latin_name : "Zanjan", 684 | }, 685 | "Abhar" => City{ 686 | farsi_name : "ابهر", 687 | latin_name : "Abhar", 688 | }, 689 | "Khodabandeh" => City{ 690 | farsi_name : "خدابنده", 691 | latin_name : "Khodabandeh", 692 | }, 693 | "KhoramDare" => City{ 694 | farsi_name : "خرمدره", 695 | latin_name : "Khoramdare", 696 | }, 697 | "Taram" => City{ 698 | farsi_name : "طارم", 699 | latin_name : "Taram", 700 | }, 701 | "MahNeshan" => City{ 702 | farsi_name : "ماهنشان", 703 | latin_name : "Mahneshan", 704 | }, 705 | "EijRod" => City{ 706 | farsi_name : "ایجرود", 707 | latin_name : "Eijrod", 708 | }, 709 | "Soltanie" => City{ 710 | farsi_name : "سلطانیه", 711 | latin_name : "Soltanie", 712 | }, 713 | }; 714 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Display)] 715 | pub enum Zanjan { 716 | Zanjan, 717 | Abhar, 718 | Khodabandeh, 719 | Khoramdare, 720 | Taram, 721 | Mahneshan, 722 | Eijrod, 723 | Soltanie, 724 | } 725 | --------------------------------------------------------------------------------