validator_rs/
mobile.rs

1//! Mobile phone number validation with locale support
2//!
3//! This module provides comprehensive mobile phone validation for different countries
4//! and locales. It supports over 150 country/locale combinations.
5
6use regex::Regex;
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10static PHONE_PATTERNS: OnceLock<HashMap<&'static str, Regex>> = OnceLock::new();
11
12fn get_phone_patterns() -> &'static HashMap<&'static str, Regex> {
13    PHONE_PATTERNS.get_or_init(|| {
14        let mut map = HashMap::new();
15        
16        // Helper macro to insert patterns
17        macro_rules! add_pattern {
18            ($locale:expr, $pattern:expr) => {
19                map.insert($locale, Regex::new($pattern).expect("Invalid regex pattern"));
20            };
21        }
22
23        add_pattern!("am-AM", r"^(\+?374|0)(33|4[134]|55|77|88|9[13-689])\d{6}$");
24        add_pattern!("ar-AE", r"^((\+?971)|0)?5[024568]\d{7}$");
25        add_pattern!("ar-BH", r"^(\+?973)?(3|6)\d{7}$");
26        add_pattern!("ar-DZ", r"^(\+?213|0)(5|6|7)\d{8}$");
27        add_pattern!("ar-LB", r"^(\+?961)?((3|81)\d{6}|7\d{7})$");
28        add_pattern!("ar-EG", r"^((\+?20)|0)?1[0125]\d{8}$");
29        add_pattern!("ar-IQ", r"^(\+?964|0)?7[0-9]\d{8}$");
30        add_pattern!("ar-JO", r"^(\+?962|0)?7[789]\d{7}$");
31        add_pattern!("ar-KW", r"^(\+?965)([569]\d{7}|41\d{6})$");
32        add_pattern!("ar-LY", r"^((\+?218)|0)?(9[1-6]\d{7}|[1-8]\d{7,9})$");
33        add_pattern!("ar-MA", r"^(?:(?:\+|00)212|0)[5-7]\d{8}$");
34        add_pattern!("ar-OM", r"^((\+|00)968)?([79][1-9])\d{6}$");
35        add_pattern!("ar-PS", r"^(\+?970|0)5[6|9](\d{7})$");
36        add_pattern!("ar-SA", r"^(!?(\+?966)|0)?5\d{8}$");
37        add_pattern!("ar-SD", r"^((\+?249)|0)?(9[012369]|1[012])\d{7}$");
38        add_pattern!("ar-SY", r"^(!?(\+?963)|0)?9\d{8}$");
39        add_pattern!("ar-TN", r"^(\+?216)?[2459]\d{7}$");
40        add_pattern!("az-AZ", r"^(\+994|0)(10|5[015]|7[07]|99)\d{7}$");
41        add_pattern!("ar-QA", r"^(\+?974|0)?([3567]\d{7})$");
42        add_pattern!("ar-YE", r"^(((\+|00)9677|0?7)[0137]\d{7}|((\+|00)967|0)[1-7]\d{6})$");
43        add_pattern!("ar-EH", r"^(\+?212|0)[\s\-]?(5288|5289)[\s\-]?\d{5}$");
44        add_pattern!("bs-BA", r"^((((\+|00)3876)|06))((([0-3]|[5-6])\d{6})|(4\d{7}))$");
45        add_pattern!("be-BY", r"^(\+?375)?(24|25|29|33|44)\d{7}$");
46        add_pattern!("bg-BG", r"^(\+?359|0)?8[789]\d{7}$");
47        add_pattern!("bn-BD", r"^(\+?880|0)1[13456789][0-9]{8}$");
48        add_pattern!("ca-AD", r"^(\+376)?[346]\d{5}$");
49        add_pattern!("cs-CZ", r"^(\+?420)? ?[1-9][0-9]{2} ?[0-9]{3} ?[0-9]{3}$");
50        add_pattern!("da-DK", r"^(\+?45)?\s?\d{2}\s?\d{2}\s?\d{2}\s?\d{2}$");
51        add_pattern!("de-DE", r"^((\+49|0)1)(5[0-25-9]\d|6([23]|0\d?)|7([0-57-9]|6\d))\d{7,9}$");
52        add_pattern!("de-AT", r"^(\+43|0)\d{1,4}\d{3,12}$");
53        add_pattern!("de-CH", r"^(\+41|0)([1-9])\d{1,9}$");
54        add_pattern!("de-LU", r"^(\+352)?((6\d1)\d{6})$");
55        add_pattern!("dv-MV", r"^(\+?960)?(7[2-9]|9[1-9])\d{5}$");
56        add_pattern!("el-GR", r"^(\+?30|0)?6(8[5-9]|9[013-57-9])\d{7}$");
57        add_pattern!("el-CY", r"^(\+?357?)?(9(9|7|6|5|4)\d{6})$");
58        add_pattern!("en-AI", r"^(\+?1|0)264(?:2(35|92)|4(?:6[1-2]|76|97)|5(?:3[6-9]|8[1-4])|7(?:2(4|9)|72))\d{4}$");
59        add_pattern!("en-AU", r"^(\+?61|0)4\d{8}$");
60        add_pattern!("en-AG", r"^(?:\+1|1)268(?:464|7(?:1[3-9]|[28]\d|3[0246]|64|7[0-689]))\d{4}$");
61        add_pattern!("en-BM", r"^(\+?1)?441(((3|7)\d{6}$)|(5[0-3][0-9]\d{4}$)|(59\d{5}$))");
62        add_pattern!("en-BS", r"^(\+?1[-\s]?|0)?\(?242\)?[-\s]?\d{3}[-\s]?\d{4}$");
63        add_pattern!("en-GB", r"^(\+?44|0)7[1-9]\d{8}$");
64        add_pattern!("en-GG", r"^(\+?44|0)1481\d{6}$");
65        add_pattern!("en-GH", r"^(\+233|0)(20|50|24|54|27|57|26|56|23|53|28|55|59)\d{7}$");
66        add_pattern!("en-GY", r"^(\+592|0)6\d{6}$");
67        add_pattern!("en-HK", r"^(\+?852[-\s]?)?[456789]\d{3}[-\s]?\d{4}$");
68        add_pattern!("en-MO", r"^(\+?853[-\s]?)?[6]\d{3}[-\s]?\d{4}$");
69        add_pattern!("en-IE", r"^(\+?353|0)8[356789]\d{7}$");
70        add_pattern!("en-IN", r"^(\+?91|0)?[6789]\d{9}$");
71        add_pattern!("en-JM", r"^(\+?876)?\d{7}$");
72        add_pattern!("en-KE", r"^(\+?254|0)(7|1)\d{8}$");
73        add_pattern!("fr-CF", r"^(\+?236| ?)(70|75|77|72|21|22)\d{6}$");
74        add_pattern!("en-SS", r"^(\+?211|0)(9[1257])\d{7}$");
75        add_pattern!("en-KI", r"^((\+686|686)?)?( )?((6|7)(2|3|8)[0-9]{6})$");
76        add_pattern!("en-KN", r"^(?:\+1|1)869(?:46\d|48[89]|55[6-8]|66\d|76[02-7])\d{4}$");
77        add_pattern!("en-LS", r"^(\+?266)(22|28|57|58|59|27|52)\d{6}$");
78        add_pattern!("en-MT", r"^(\+?356|0)?(99|79|77|21|27|22|25)[0-9]{6}$");
79        add_pattern!("en-MU", r"^(\+?230|0)?\d{8}$");
80        add_pattern!("en-MW", r"^(\+?265|0)(((77|88|31|99|98|21)\d{7})|(((111)|1)\d{6})|(32000\d{4}))$");
81        add_pattern!("en-NA", r"^(\+?264|0)(6|8)\d{7}$");
82        add_pattern!("en-NG", r"^(\+?234|0)?[789]\d{9}$");
83        add_pattern!("en-NZ", r"^(\+?64|0)[28]\d{7,9}$");
84        add_pattern!("en-PG", r"^(\+?675|0)?(7\d|8[18])\d{6}$");
85        add_pattern!("en-PK", r"^((00|\+)?92|0)3[0-6]\d{8}$");
86        add_pattern!("en-PH", r"^(09|\+639)\d{9}$");
87        add_pattern!("en-RW", r"^(\+?250|0)?[7]\d{8}$");
88        add_pattern!("en-SG", r"^(\+65)?[3689]\d{7}$");
89        add_pattern!("en-SL", r"^(\+?232|0)\d{8}$");
90        add_pattern!("en-TZ", r"^(\+?255|0)?[67]\d{8}$");
91        add_pattern!("en-UG", r"^(\+?256|0)?[7]\d{8}$");
92        add_pattern!("en-US", r"^((\+1|1)?( |-)?)?(\([2-9][0-9]{2}\)|[2-9][0-9]{2})( |-)?([2-9][0-9]{2}( |-)?[0-9]{4})$");
93        add_pattern!("en-ZA", r"^(\+?27|0)\d{9}$");
94        add_pattern!("en-ZM", r"^(\+?26)?0[79][567]\d{7}$");
95        add_pattern!("en-ZW", r"^(\+263)[0-9]{9}$");
96        add_pattern!("en-BW", r"^(\+?267)?(7[1-8]{1})\d{6}$");
97        add_pattern!("es-AR", r"^\+?549(11|[2368]\d)\d{8}$");
98        add_pattern!("es-BO", r"^(\+?591)?(6|7)\d{7}$");
99        add_pattern!("es-CO", r"^(\+?57)?3(0(0|1|2|4|5)|1\d|2[0-4]|5(0|1))\d{7}$");
100        add_pattern!("es-CL", r"^(\+?56|0)[2-9]\d{1}\d{7}$");
101        add_pattern!("es-CR", r"^(\+506)?[2-8]\d{7}$");
102        add_pattern!("es-CU", r"^(\+53|0053)?5\d{7}$");
103        add_pattern!("es-DO", r"^(\+?1)?8[024]9\d{7}$");
104        add_pattern!("es-HN", r"^(\+?504)?[9|8|3|2]\d{7}$");
105        add_pattern!("es-EC", r"^(\+?593|0)([2-7]|9[2-9])\d{7}$");
106        add_pattern!("es-ES", r"^(\+?34)?[6|7]\d{8}$");
107        add_pattern!("es-GT", r"^(\+?502)?[2|6|7]\d{7}$");
108        add_pattern!("es-PE", r"^(\+?51)?9\d{8}$");
109        add_pattern!("es-MX", r"^(\+?52)?(1|01)?\d{10,11}$");
110        add_pattern!("es-NI", r"^(\+?505)\d{7,8}$");
111        add_pattern!("es-PA", r"^(\+?507)\d{7,8}$");
112        add_pattern!("es-PY", r"^(\+?595|0)9[9876]\d{7}$");
113        add_pattern!("es-SV", r"^(\+?503)?[67]\d{7}$");
114        add_pattern!("es-UY", r"^(\+598|0)9[1-9][\d]{6}$");
115        add_pattern!("es-VE", r"^(\+?58)?(2|4)\d{9}$");
116        add_pattern!("et-EE", r"^(\+?372)?\s?(5|8[1-4])\s?([0-9]\s?){6,7}$");
117        add_pattern!("fa-IR", r"^(\+?98[\-\s]?|0)9[0-39]\d[\-\s]?\d{3}[\-\s]?\d{4}$");
118        add_pattern!("fa-AF", r"^(\+93|0)?(2{1}[0-8]{1}|[3-5]{1}[0-4]{1})(\d{7})$");
119        add_pattern!("fi-FI", r"^(\+?358|0)\s?(4[0-6]|50)\s?(\d\s?){4,8}$");
120        add_pattern!("fj-FJ", r"^(\+?679)?\s?\d{3}\s?\d{4}$");
121        add_pattern!("fo-FO", r"^(\+?298)?\s?\d{2}\s?\d{2}\s?\d{2}$");
122        add_pattern!("fr-BF", r"^(\+226|0)[67]\d{7}$");
123        add_pattern!("fr-BJ", r"^(\+229)\d{8}$");
124        add_pattern!("fr-CD", r"^(\+?243|0)?(8|9)\d{8}$");
125        add_pattern!("fr-CM", r"^(\+?237)6[0-9]{8}$");
126        add_pattern!("fr-FR", r"^(\+?33|0)[67]\d{8}$");
127        add_pattern!("fr-GF", r"^(\+?594|0|00594)[67]\d{8}$");
128        add_pattern!("fr-GP", r"^(\+?590|0|00590)[67]\d{8}$");
129        add_pattern!("fr-MQ", r"^(\+?596|0|00596)[67]\d{8}$");
130        add_pattern!("fr-PF", r"^(\+?689)?8[789]\d{6}$");
131        add_pattern!("fr-RE", r"^(\+?262|0|00262)[67]\d{8}$");
132        add_pattern!("fr-WF", r"^(\+681)?\d{6}$");
133        add_pattern!("he-IL", r"^(\+972|0)([23489]|5[012345689]|77)[1-9]\d{6}$");
134        add_pattern!("hu-HU", r"^(\+?36|06)(20|30|31|50|70)\d{7}$");
135        add_pattern!("id-ID", r"^(\+?62|0)8(1[123456789]|2[1238]|3[1238]|5[12356789]|7[78]|9[56789]|8[123456789])([\s?|\d]{5,11})$");
136        add_pattern!("ir-IR", r"^(\+98|0)?9\d{9}$");
137        add_pattern!("it-IT", r"^(\+?39)?\s?3\d{2} ?\d{6,7}$");
138        add_pattern!("it-SM", r"^((\+378)|(0549)|(\+390549)|(\+3780549))?6\d{5,9}$");
139        add_pattern!("ja-JP", r"^(\+81[ \-]?(\(0\))?|0)[6789]0[ \-]?\d{4}[ \-]?\d{4}$");
140        add_pattern!("ka-GE", r"^(\+?995)?(79\d{7}|5\d{8})$");
141        add_pattern!("kk-KZ", r"^(\+?7|8)?7\d{9}$");
142        add_pattern!("kl-GL", r"^(\+?299)?\s?\d{2}\s?\d{2}\s?\d{2}$");
143        add_pattern!("ko-KR", r"^((\+?82)[ \-]?)?0?1([0|1|6|7|8|9]{1})[ \-]?\d{3,4}[ \-]?\d{4}$");
144        add_pattern!("ky-KG", r"^(\+996\s?)?(22[0-9]|50[0-9]|55[0-9]|70[0-9]|75[0-9]|77[0-9]|880|990|995|996|997|998)\s?\d{3}\s?\d{3}$");
145        add_pattern!("lt-LT", r"^(\+370|8)\d{8}$");
146        add_pattern!("lv-LV", r"^(\+?371)2\d{7}$");
147        add_pattern!("mg-MG", r"^((\+?261|0)(2|3)\d)?\d{7}$");
148        add_pattern!("mk-MK", r"^(\+?389|0)?((?:2[2-9]\d{6}|(?:3[1-4]|4[2-8])\d{6}|500\d{5}|5[2-9]\d{6}|7[0-9][2-9]\d{5}|8[1-9]\d{6}|800\d{5}|8009\d{4}))$");
149        add_pattern!("mn-MN", r"^(\+|00|011)?976(77|81|88|91|94|95|96|99)\d{6}$");
150        add_pattern!("my-MM", r"^(\+?959|09|9)(2[5-7]|3[1-2]|4[0-5]|6[6-9]|7[5-9]|9[6-9])[0-9]{7}$");
151        add_pattern!("ms-MY", r"^(\+?60|0)1(([0145](-|\s)?\d{7,8})|([236-9](-|\s)?\d{7}))$");
152        add_pattern!("mz-MZ", r"^(\+?258)?8[234567]\d{7}$");
153        add_pattern!("nb-NO", r"^(\+?47)?[49]\d{7}$");
154        add_pattern!("ne-NP", r"^(\+?977)?9[78]\d{8}$");
155        add_pattern!("nl-BE", r"^(\+?32|0)4\d{8}$");
156        add_pattern!("nl-NL", r"^(((\+|00)?31\(0\))|((\+|00)?31)|0)6{1}\d{8}$");
157        add_pattern!("nl-AW", r"^(\+)?297(56|59|64|73|74|99)\d{5}$");
158        add_pattern!("nn-NO", r"^(\+?47)?[49]\d{7}$");
159        add_pattern!("pl-PL", r"^(\+?48)? ?([5-8]\d|45) ?\d{3} ?\d{2} ?\d{2}$");
160        add_pattern!("pt-BR", r"^((\+?55\ ?[1-9]{2}\ ?)|(\+?55\ ?\([1-9]{2}\)\ ?)|(0[1-9]{2}\ ?)|(\([1-9]{2}\)\ ?)|([1-9]{2}\ ?))((\d{4}\-?\d{4})|(9[1-9]{1}\d{3}\-?\d{4}))$");
161        add_pattern!("pt-PT", r"^(\+?351)?9[1236]\d{7}$");
162        add_pattern!("pt-AO", r"^(\+?244)?9\d{8}$");
163        add_pattern!("ro-MD", r"^(\+?373|0)((6(0|1|2|6|7|8|9))|(7(6|7|8|9)))\d{6}$");
164        add_pattern!("ro-RO", r"^(\+?40|0)\s?7\d{2}(\/|\s|\.|-)?\d{3}(\s|\.|-)?\d{3}$");
165        add_pattern!("ru-RU", r"^(\+?7|8)?9\d{9}$");
166        add_pattern!("si-LK", r"^(?:0|94|\+94)?(7(0|1|2|4|5|6|7|8)( |-)?)\d{7}$");
167        add_pattern!("sl-SI", r"^(\+386\s?|0)(\d{1}\s?\d{3}\s?\d{2}\s?\d{2}|\d{2}\s?\d{3}\s?\d{3})$");
168        add_pattern!("sk-SK", r"^(\+?421)? ?[1-9][0-9]{2} ?[0-9]{3} ?[0-9]{3}$");
169        add_pattern!("so-SO", r"^(\+?252|0)((6[0-9])\d{7}|(7[1-9])\d{7})$");
170        add_pattern!("sq-AL", r"^(\+355|0)6[2-9]\d{7}$");
171        add_pattern!("sr-RS", r"^(\+3816|06)[- \d]{5,9}$");
172        add_pattern!("sv-SE", r"^(\+?46|0)[\s\-]?7[\s\-]?[02369]([\s\-]?\d){7}$");
173        add_pattern!("tg-TJ", r"^(\+?992)?[5][5]\d{7}$");
174        add_pattern!("th-TH", r"^(\+66|66|0)\d{9}$");
175        add_pattern!("tr-TR", r"^(\+?90|0)?5\d{9}$");
176        add_pattern!("tk-TM", r"^(\+993|993|8)\d{8}$");
177        add_pattern!("uk-UA", r"^(\+?38)?0(50|6[36-8]|7[357]|9[1-9])\d{7}$");
178        add_pattern!("uz-UZ", r"^(\+?998)?(6[125-79]|7[1-69]|88|9\d)\d{7}$");
179        add_pattern!("vi-VN", r"^((\+?84)|0)((3([2-9]))|(5([25689]))|(7([0|6-9]))|(8([1-9]))|(9([0-9])))([0-9]{7})$");
180        add_pattern!("zh-CN", r"^((\+|00)86)?(1[3-9]|9[28])\d{9}$");
181        add_pattern!("zh-TW", r"^(\+?886\-?|0)?9\d{8}$");
182        add_pattern!("dz-BT", r"^(\+?975|0)?(17|16|77|02)\d{6}$");
183
184        // Add aliases
185        let en_us = map.get("en-US").expect("en-US pattern must exist").clone();
186        map.insert("en-CA", en_us.clone());
187        map.insert("fr-CA", en_us);
188        
189        let nl_be = map.get("nl-BE").expect("nl-BE pattern must exist").clone();
190        map.insert("fr-BE", nl_be);
191        
192        let en_hk = map.get("en-HK").expect("en-HK pattern must exist").clone();
193        map.insert("zh-HK", en_hk);
194        
195        let en_mo = map.get("en-MO").expect("en-MO pattern must exist").clone();
196        map.insert("zh-MO", en_mo);
197        
198        let en_ie = map.get("en-IE").expect("en-IE pattern must exist").clone();
199        map.insert("ga-IE", en_ie);
200        
201        let de_ch = map.get("de-CH").expect("de-CH pattern must exist").clone();
202        map.insert("fr-CH", de_ch.clone());
203        map.insert("it-CH", de_ch);
204
205        map
206    })
207}
208
209/// Options for mobile phone validation
210#[derive(Debug, Clone, Default)]
211pub struct MobileOptions {
212    /// If true, the phone number must start with '+'
213    pub strict_mode: bool,
214}
215
216/// Locale type for validation
217#[derive(Debug, Clone)]
218pub enum Locale {
219    /// A specific locale (e.g., "en-US", "fr-FR")
220    Specific(String),
221    /// Multiple locales - validates if the number matches any of them
222    Multiple(Vec<String>),
223    /// Any locale - validates against all available patterns
224    Any,
225}
226
227impl From<&str> for Locale {
228    fn from(s: &str) -> Self {
229        if s.is_empty() || s == "any" {
230            Locale::Any
231        } else {
232            Locale::Specific(s.to_string())
233        }
234    }
235}
236
237impl From<String> for Locale {
238    fn from(s: String) -> Self {
239        if s.is_empty() || s == "any" {
240            Locale::Any
241        } else {
242            Locale::Specific(s)
243        }
244    }
245}
246
247impl From<Vec<String>> for Locale {
248    fn from(v: Vec<String>) -> Self {
249        if v.is_empty() {
250            Locale::Any
251        } else {
252            Locale::Multiple(v)
253        }
254    }
255}
256
257impl From<Vec<&str>> for Locale {
258    fn from(v: Vec<&str>) -> Self {
259        if v.is_empty() {
260            Locale::Any
261        } else {
262            Locale::Multiple(v.iter().map(|s| s.to_string()).collect())
263        }
264    }
265}
266
267/// Validates a mobile phone number with locale and options
268///
269/// # Examples
270///
271/// ```
272/// use validator_rs::mobile::{is_mobile_phone, Locale, MobileOptions};
273///
274/// // Validate US phone number
275/// assert!(is_mobile_phone("4155552671", Locale::from("en-US"), None).unwrap());
276///
277/// // Validate with strict mode (must start with +)
278/// let options = MobileOptions { strict_mode: true };
279/// assert!(is_mobile_phone("+14155552671", Locale::from("en-US"), Some(options)).unwrap());
280///
281/// // Validate against any locale
282/// assert!(is_mobile_phone("+447911123456", Locale::Any, None).unwrap());
283/// ```
284pub fn is_mobile_phone(
285    phone: &str,
286    locale: Locale,
287    options: Option<MobileOptions>,
288) -> Result<bool, String> {
289    if phone.is_empty() {
290        return Ok(false);
291    }
292
293    let opts = options.unwrap_or_default();
294
295    // Check strict mode
296    if opts.strict_mode && !phone.starts_with('+') {
297        return Ok(false);
298    }
299
300    let patterns = get_phone_patterns();
301
302    match locale {
303        Locale::Specific(ref loc) => {
304            if let Some(pattern) = patterns.get(loc.as_str()) {
305                Ok(pattern.is_match(phone))
306            } else {
307                Err(format!("Invalid locale '{}'", loc))
308            }
309        }
310        Locale::Multiple(ref locales) => {
311            for loc in locales {
312                if let Some(pattern) = patterns.get(loc.as_str()) {
313                    if pattern.is_match(phone) {
314                        return Ok(true);
315                    }
316                }
317            }
318            Ok(false)
319        }
320        Locale::Any => {
321            for pattern in patterns.values() {
322                if pattern.is_match(phone) {
323                    return Ok(true);
324                }
325            }
326            Ok(false)
327        }
328    }
329}
330
331/// Validates a mobile phone number (convenience function using 'any' locale)
332///
333/// # Examples
334///
335/// ```
336/// use validator_rs::mobile::is_valid_phone;
337///
338/// assert!(is_valid_phone("+14155552671"));
339/// assert!(is_valid_phone("+447911123456"));
340/// assert!(!is_valid_phone("abc123"));
341/// ```
342pub fn is_valid_phone(phone: &str) -> bool {
343    is_mobile_phone(phone, Locale::Any, None).unwrap_or(false)
344}
345
346/// Returns a list of all supported locales
347pub fn get_supported_locales() -> Vec<&'static str> {
348    let mut locales: Vec<&str> = get_phone_patterns().keys().copied().collect();
349    locales.sort_unstable();
350    locales
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_us_phones() {
359        let locale = Locale::from("en-US");
360        // US pattern requires specific formats
361        assert!(is_mobile_phone("+14155552671", locale.clone(), None).unwrap());
362        assert!(is_mobile_phone("4155552671", locale.clone(), None).unwrap());
363        assert!(is_mobile_phone("415 555 2671", locale.clone(), None).unwrap());
364        assert!(is_mobile_phone("(415) 555-2671", locale.clone(), None).unwrap());
365        assert!(!is_mobile_phone("123", locale, None).unwrap());
366    }
367
368    #[test]
369    fn test_uk_phones() {
370        let locale = Locale::from("en-GB");
371        assert!(is_mobile_phone("+447911123456", locale.clone(), None).unwrap());
372        assert!(is_mobile_phone("07911123456", locale.clone(), None).unwrap());
373        assert!(!is_mobile_phone("0123456789", locale, None).unwrap());
374    }
375
376    #[test]
377    fn test_strict_mode() {
378        let locale = Locale::from("en-US");
379        let options = Some(MobileOptions { strict_mode: true });
380        
381        assert!(is_mobile_phone("+14155552671", locale.clone(), options.clone()).unwrap());
382        assert!(!is_mobile_phone("4155552671", locale, options).unwrap());
383    }
384
385    #[test]
386    fn test_multiple_locales() {
387        let locale = Locale::from(vec!["en-US", "en-GB"]);
388        
389        assert!(is_mobile_phone("+14155552671", locale.clone(), None).unwrap());
390        assert!(is_mobile_phone("+447911123456", locale.clone(), None).unwrap());
391        assert!(!is_mobile_phone("+331234567", locale, None).unwrap()); // French number
392    }
393
394    #[test]
395    fn test_any_locale() {
396        assert!(is_mobile_phone("+14155552671", Locale::Any, None).unwrap()); // US
397        assert!(is_mobile_phone("+447911123456", Locale::Any, None).unwrap()); // UK
398        assert!(is_mobile_phone("+33612345678", Locale::Any, None).unwrap()); // France
399        assert!(is_mobile_phone("+81 90 1234 5678", Locale::Any, None).unwrap()); // Japan
400        assert!(!is_mobile_phone("abc123", Locale::Any, None).unwrap());
401    }
402
403    #[test]
404    fn test_invalid_locale() {
405        let result = is_mobile_phone("+14155552671", Locale::from("xx-XX"), None);
406        assert!(result.is_err());
407        assert!(result.unwrap_err().contains("Invalid locale"));
408    }
409
410    #[test]
411    fn test_convenience_function() {
412        assert!(is_valid_phone("+14155552671"));
413        assert!(is_valid_phone("+447911123456"));
414        assert!(!is_valid_phone("abc123"));
415        assert!(!is_valid_phone(""));
416    }
417
418    #[test]
419    fn test_various_countries() {
420        // France
421        assert!(is_mobile_phone("+33612345678", Locale::from("fr-FR"), None).unwrap());
422        
423        // Germany - mobile numbers start with 015x, 016x, 017x
424        assert!(is_mobile_phone("+4915123456789", Locale::from("de-DE"), None).unwrap());
425        
426        // India
427        assert!(is_mobile_phone("+919876543210", Locale::from("en-IN"), None).unwrap());
428        
429        // Australia
430        assert!(is_mobile_phone("+61412345678", Locale::from("en-AU"), None).unwrap());
431        
432        // Brazil - test simpler format
433        assert!(is_mobile_phone("+5511912345678", Locale::from("pt-BR"), None).unwrap());
434    }
435
436    #[test]
437    fn test_get_supported_locales() {
438        let locales = get_supported_locales();
439        assert!(!locales.is_empty());
440        assert!(locales.contains(&"en-US"));
441        assert!(locales.contains(&"en-GB"));
442        assert!(locales.contains(&"fr-FR"));
443    }
444
445    #[test]
446    fn test_aliases() {
447        // Canada should use US pattern
448        assert!(is_mobile_phone("+14155552671", Locale::from("en-CA"), None).unwrap());
449        assert!(is_mobile_phone("+14155552671", Locale::from("fr-CA"), None).unwrap());
450    }
451
452    #[test]
453    fn test_am_am() {
454        let locale = Locale::from("am-AM");
455        assert!(is_mobile_phone("+37433123456", locale.clone(), None).unwrap());
456        assert!(is_mobile_phone("+37441123456", locale.clone(), None).unwrap());
457        assert!(is_mobile_phone("055123456", locale.clone(), None).unwrap());
458        assert!(!is_mobile_phone("+37403498855", locale.clone(), None).unwrap());
459        assert!(!is_mobile_phone("+37416498123", locale, None).unwrap());
460    }
461
462    #[test]
463    fn test_ar_ae() {
464        let locale = Locale::from("ar-AE");
465        assert!(is_mobile_phone("+971502674453", locale.clone(), None).unwrap());
466        assert!(is_mobile_phone("0585215778", locale.clone(), None).unwrap());
467        assert!(is_mobile_phone("585215778", locale.clone(), None).unwrap());
468        assert!(!is_mobile_phone("+971511498855", locale, None).unwrap());
469    }
470
471    #[test]
472    fn test_ar_sa() {
473        let locale = Locale::from("ar-SA");
474        assert!(is_mobile_phone("0556578654", locale.clone(), None).unwrap());
475        assert!(is_mobile_phone("+966556578654", locale.clone(), None).unwrap());
476        assert!(is_mobile_phone("596578654", locale.clone(), None).unwrap());
477        assert!(!is_mobile_phone("+9665626626262", locale, None).unwrap());
478    }
479
480    #[test]
481    fn test_zh_cn() {
482        let locale = Locale::from("zh-CN");
483        assert!(is_mobile_phone("13523333233", locale.clone(), None).unwrap());
484        assert!(is_mobile_phone("+8616238234822", locale.clone(), None).unwrap());
485        assert!(is_mobile_phone("008616238234822", locale.clone(), None).unwrap());
486        assert!(!is_mobile_phone("12345", locale.clone(), None).unwrap());
487        assert!(!is_mobile_phone("010-38238383", locale, None).unwrap());
488    }
489
490    #[test]
491    fn test_zh_tw() {
492        let locale = Locale::from("zh-TW");
493        assert!(is_mobile_phone("0987123456", locale.clone(), None).unwrap());
494        assert!(is_mobile_phone("+886987123456", locale.clone(), None).unwrap());
495        assert!(is_mobile_phone("886-987123456", locale.clone(), None).unwrap());
496        assert!(!is_mobile_phone("0-987123456", locale, None).unwrap());
497    }
498
499    #[test]
500    fn test_ja_jp() {
501        let locale = Locale::from("ja-JP");
502        assert!(is_mobile_phone("09012345678", locale.clone(), None).unwrap());
503        assert!(is_mobile_phone("080 1234 5678", locale.clone(), None).unwrap());
504        assert!(is_mobile_phone("+8190-1234-5678", locale.clone(), None).unwrap());
505        assert!(!is_mobile_phone("0312345678", locale, None).unwrap());
506    }
507
508    #[test]
509    fn test_ko_kr() {
510        let locale = Locale::from("ko-KR");
511        assert!(is_mobile_phone("+82-010-1234-5678", locale.clone(), None).unwrap());
512        assert!(is_mobile_phone("010-123-5678", locale.clone(), None).unwrap());
513        assert!(is_mobile_phone("01012345678", locale.clone(), None).unwrap());
514        assert!(!is_mobile_phone("+82 10 1234 567", locale, None).unwrap());
515    }
516
517    #[test]
518    fn test_es_es() {
519        let locale = Locale::from("es-ES");
520        assert!(is_mobile_phone("+34654789321", locale.clone(), None).unwrap());
521        assert!(is_mobile_phone("654789321", locale.clone(), None).unwrap());
522        assert!(is_mobile_phone("+34714789321", locale.clone(), None).unwrap());
523        assert!(!is_mobile_phone("65478932", locale, None).unwrap());
524    }
525
526    #[test]
527    fn test_pt_br() {
528        let locale = Locale::from("pt-BR");
529        assert!(is_mobile_phone("+55 12 996551215", locale.clone(), None).unwrap());
530        assert!(is_mobile_phone("5511914314567", locale.clone(), None).unwrap());
531        assert!(is_mobile_phone("(11) 94123-4567", locale.clone(), None).unwrap());
532        assert!(!is_mobile_phone("+55 11 90431-4567", locale, None).unwrap());
533    }
534
535    #[test]
536    fn test_it_it() {
537        let locale = Locale::from("it-IT");
538        assert!(is_mobile_phone("370 3175423", locale.clone(), None).unwrap());
539        assert!(is_mobile_phone("+39 310 7688449", locale.clone(), None).unwrap());
540        assert!(!is_mobile_phone("011 7387545", locale, None).unwrap());
541    }
542
543    #[test]
544    fn test_pl_pl() {
545        let locale = Locale::from("pl-PL");
546        assert!(is_mobile_phone("+48512689767", locale.clone(), None).unwrap());
547        assert!(is_mobile_phone("657562855", locale.clone(), None).unwrap());
548        assert!(is_mobile_phone("+48 56 376 87 47", locale.clone(), None).unwrap());
549        assert!(!is_mobile_phone("3454535", locale, None).unwrap());
550    }
551
552    #[test]
553    fn test_tr_tr() {
554        let locale = Locale::from("tr-TR");
555        assert!(is_mobile_phone("+905321234567", locale.clone(), None).unwrap());
556        assert!(is_mobile_phone("05321234567", locale.clone(), None).unwrap());
557        assert!(!is_mobile_phone("12345", locale, None).unwrap());
558    }
559
560    #[test]
561    fn test_en_in() {
562        let locale = Locale::from("en-IN");
563        assert!(is_mobile_phone("+919876543210", locale.clone(), None).unwrap());
564        assert!(is_mobile_phone("919876543210", locale.clone(), None).unwrap());
565        assert!(is_mobile_phone("09876543210", locale.clone(), None).unwrap());
566        assert!(!is_mobile_phone("12345", locale, None).unwrap());
567    }
568
569    #[test]
570    fn test_vi_vn() {
571        let locale = Locale::from("vi-VN");
572        assert!(is_mobile_phone("0336012403", locale.clone(), None).unwrap());
573        assert!(is_mobile_phone("+84586012403", locale.clone(), None).unwrap());
574        assert!(is_mobile_phone("84981577798", locale.clone(), None).unwrap());
575        assert!(!is_mobile_phone("01678912345", locale, None).unwrap());
576    }
577
578    #[test]
579    fn test_en_sg() {
580        let locale = Locale::from("en-SG");
581        assert!(is_mobile_phone("87654321", locale.clone(), None).unwrap());
582        assert!(is_mobile_phone("+6587654321", locale.clone(), None).unwrap());
583        assert!(!is_mobile_phone("12345678", locale, None).unwrap());
584    }
585
586    #[test]
587    fn test_ar_eg() {
588        let locale = Locale::from("ar-EG");
589        assert!(is_mobile_phone("+201004513789", locale.clone(), None).unwrap());
590        assert!(is_mobile_phone("01090124576", locale.clone(), None).unwrap());
591        assert!(is_mobile_phone("1090124576", locale.clone(), None).unwrap());
592        assert!(!is_mobile_phone("+201404513789", locale, None).unwrap());
593    }
594
595    #[test]
596    fn test_id_id() {
597        let locale = Locale::from("id-ID");
598        assert!(is_mobile_phone("0811 778 998", locale.clone(), None).unwrap());
599        assert!(is_mobile_phone("+62811778998", locale.clone(), None).unwrap());
600        assert!(is_mobile_phone("628993123618190", locale.clone(), None).unwrap());
601        assert!(!is_mobile_phone("0217123456", locale, None).unwrap());
602    }
603
604    #[test]
605    fn test_th_th() {
606        let locale = Locale::from("th-TH");
607        assert!(is_mobile_phone("0912345678", locale.clone(), None).unwrap());
608        assert!(is_mobile_phone("+66912345678", locale.clone(), None).unwrap());
609        assert!(is_mobile_phone("66912345678", locale.clone(), None).unwrap());
610        assert!(!is_mobile_phone("67812345623", locale, None).unwrap());
611    }
612
613    #[test]
614    fn test_ru_ru() {
615        let locale = Locale::from("ru-RU");
616        assert!(is_mobile_phone("+79676338855", locale.clone(), None).unwrap());
617        assert!(is_mobile_phone("79676338855", locale.clone(), None).unwrap());
618        assert!(is_mobile_phone("89676338855", locale.clone(), None).unwrap());
619        assert!(!is_mobile_phone("+9676338855", locale, None).unwrap());
620    }
621
622    #[test]
623    fn test_nb_no() {
624        let locale = Locale::from("nb-NO");
625        assert!(is_mobile_phone("+4796338855", locale.clone(), None).unwrap());
626        assert!(is_mobile_phone("4796338855", locale.clone(), None).unwrap());
627        assert!(is_mobile_phone("46338855", locale.clone(), None).unwrap());
628        assert!(!is_mobile_phone("+4676338855", locale, None).unwrap());
629    }
630
631    #[test]
632    fn test_da_dk() {
633        let locale = Locale::from("da-DK");
634        assert!(is_mobile_phone("12345678", locale.clone(), None).unwrap());
635        assert!(is_mobile_phone("+45 12 34 56 78", locale.clone(), None).unwrap());
636        assert!(!is_mobile_phone("12 34 56", locale, None).unwrap());
637    }
638
639    #[test]
640    fn test_sv_se() {
641        let locale = Locale::from("sv-SE");
642        assert!(is_mobile_phone("+46701234567", locale.clone(), None).unwrap());
643        assert!(is_mobile_phone("0721234567", locale.clone(), None).unwrap());
644        assert!(!is_mobile_phone("+46301234567", locale, None).unwrap());
645    }
646
647    #[test]
648    fn test_fi_fi() {
649        let locale = Locale::from("fi-FI");
650        assert!(is_mobile_phone("+358505557171", locale.clone(), None).unwrap());
651        assert!(is_mobile_phone("0455571", locale.clone(), None).unwrap());
652        assert!(!is_mobile_phone("045557", locale, None).unwrap());
653    }
654
655    #[test]
656    fn test_strict_mode_comprehensive() {
657        let options = Some(MobileOptions { strict_mode: true });
658        
659        // Should pass - all start with +
660        assert!(is_mobile_phone("+254728530234", Locale::Any, options.clone()).unwrap());
661        assert!(is_mobile_phone("+299 12 34 56", Locale::Any, options.clone()).unwrap());
662        assert!(is_mobile_phone("+94766660206", Locale::Any, options.clone()).unwrap());
663        
664        // Should fail - don't start with +
665        assert!(!is_mobile_phone("254728530234", Locale::Any, options.clone()).unwrap());
666        assert!(!is_mobile_phone("0728530234", Locale::Any, options.clone()).unwrap());
667        assert!(!is_mobile_phone("766667206", Locale::Any, options).unwrap());
668    }
669
670    #[test]
671    fn test_any_locale_comprehensive() {
672        // Test a variety of valid numbers from different countries
673        let valid_numbers = vec![
674            "+14155552671",      // US
675            "+447911123456",     // UK
676            "+33612345678",      // France
677            "+4915123456789",    // Germany
678            "+919876543210",     // India
679            "+61412345678",      // Australia
680            "+5511912345678",    // Brazil
681            "+81 90 1234 5678",  // Japan
682            "+8613523333233",    // China
683            "+82 10 1234 5678",  // South Korea
684            "+34654789321",      // Spain
685            "+39 310 7688449",   // Italy
686            "+48512689767",      // Poland
687            "+79676338855",      // Russia
688            "+66912345678",      // Thailand
689            "+84586012403",      // Vietnam
690            "+6587654321",       // Singapore
691        ];
692
693        for number in valid_numbers {
694            assert!(is_mobile_phone(number, Locale::Any, None).unwrap(), 
695                    "Failed for: {}", number);
696        }
697
698        // Test invalid numbers
699        let invalid_numbers = vec!["", "asdf", "1", "12345"];
700        for number in invalid_numbers {
701            assert!(!is_mobile_phone(number, Locale::Any, None).unwrap(),
702                    "Should have failed for: {}", number);
703        }
704    }
705
706    #[test]
707    fn test_empty_locale_defaults_to_any() {
708        // Empty locale should default to 'any'
709        assert!(is_mobile_phone("+14155552671", Locale::from(""), None).unwrap());
710        assert!(is_mobile_phone("+447911123456", Locale::from(""), None).unwrap());
711    }
712
713    #[test]
714    fn test_ar_jo() {
715        let locale = Locale::from("ar-JO");
716        assert!(is_mobile_phone("0796477263", locale.clone(), None).unwrap());
717        assert!(is_mobile_phone("+962796477263", locale.clone(), None).unwrap());
718        assert!(!is_mobile_phone("00962786725261", locale, None).unwrap());
719    }
720
721    #[test]
722    fn test_es_mx() {
723        let locale = Locale::from("es-MX");
724        assert!(is_mobile_phone("+52019654789321", locale.clone(), None).unwrap());
725        assert!(is_mobile_phone("5219654789321", locale.clone(), None).unwrap());
726        assert!(!is_mobile_phone("65478932", locale, None).unwrap());
727    }
728
729    #[test]
730    fn test_es_ar() {
731        let locale = Locale::from("es-AR");
732        assert!(is_mobile_phone("5491143214321", locale.clone(), None).unwrap());
733        assert!(is_mobile_phone("+5491143214321", locale.clone(), None).unwrap());
734        assert!(!is_mobile_phone("1143214321", locale, None).unwrap());
735    }
736
737    #[test]
738    fn test_nl_nl() {
739        let locale = Locale::from("nl-NL");
740        assert!(is_mobile_phone("0670123456", locale.clone(), None).unwrap());
741        assert!(is_mobile_phone("+31612345678", locale.clone(), None).unwrap());
742        assert!(!is_mobile_phone("012345678", locale, None).unwrap());
743    }
744
745    #[test]
746    fn test_el_gr() {
747        let locale = Locale::from("el-GR");
748        assert!(is_mobile_phone("+306944848966", locale.clone(), None).unwrap());
749        assert!(is_mobile_phone("6944848966", locale.clone(), None).unwrap());
750        assert!(!is_mobile_phone("6924567890", locale, None).unwrap());
751    }
752}
753