1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// NOTE: update flag() whenever we add 2-letter country names
// TODO: improve using https://github.com/bendodson/flag-emoji-from-country-code/blob/master/FlagPlayground.playground

#[macro_use]
extern crate lazy_static;

mod countries;

use countries::{COUNTRIES, COUNTRIES_MAP};
use regex::Regex;

const FLAG_MAGIC_NUMBER: u32 = 127397;

lazy_static! {
    static ref FLAG_RE: Regex = Regex::new(r"^[\u{0001F1E6}-\u{0001F1FF}]{2}$").unwrap();
    static ref CODE_RE: Regex = Regex::new(r"^(?i)[a-z]{2}$").unwrap();
}

#[derive(Clone)]
pub(crate) struct Country {
    code: &'static str,
    names: Vec<&'static str>,
}

impl Country {
    pub fn name(&self) -> &'static str {
        self.names[0]
    }
}

pub fn code(input: &str) -> Option<&'static str> {
    flag_to_code(input).or_else(|| name_to_code(input))
}

pub fn flag(mut input: &str) -> Option<String> {
    if !CODE_RE.is_match(input) || input == "UK" {
        if let Some(code) = name_to_code(input) {
            input = code;
        }
    }
    code_to_flag(input)
}

pub fn name(mut input: &str) -> Option<&'static str> {
    if FLAG_RE.is_match(input) {
        if let Some(code) = flag_to_code(input) {
            input = code;
        }
    }
    code_to_name(input)
}

pub fn is_code(code: Option<&str>) -> bool {
    code.map_or(false, |code| {
        COUNTRIES_MAP.contains_key(code.trim().to_uppercase().as_str())
    })
}

pub fn code_to_name(code: &str) -> Option<&'static str> {
    COUNTRIES_MAP
        .get(code.trim().to_uppercase().as_str())
        .map(|country| country.name())
}

pub fn code_to_flag(code: &str) -> Option<String> {
    if is_code(Some(code)) {
        let mut flag = String::new();
        for c in code.chars() {
            if let Some(c) = std::char::from_u32(c as u32 + FLAG_MAGIC_NUMBER) {
                flag.push(c);
            } else {
                return None;
            }
        }
        Some(flag)
    } else {
        None
    }
}

pub fn is_country_flag(flag: &str) -> bool {
    FLAG_RE.is_match(flag)
}

pub fn flag_to_code(flag: &str) -> Option<&'static str> {
    let flag = flag.trim();
    if !is_country_flag(flag) {
        return None;
    }
    let mut code = String::new();
    for c in flag.chars() {
        if let Some(c) = std::char::from_u32(c as u32 - FLAG_MAGIC_NUMBER) {
            code.push(c);
        } else {
            return None;
        }
    }
    COUNTRIES_MAP.get(code.as_str()).map(|country| country.code)
}

pub fn name_to_code(name: &str) -> Option<&'static str> {
    let name = name.trim().to_lowercase();

    // exact match lookup
    for country in COUNTRIES.iter() {
        for n in &country.names {
            if n.to_lowercase() == name {
                return Some(country.code);
            }
        }
    }

    // inexact match lookup
    let matches = COUNTRIES.iter().fold(vec![], |mut matches, country| {
        for &n in &country.names {
            if fuzzy_compare(&name, n) {
                matches.push(country.code)
            }
        }
        matches
    });

    if matches.len() == 1 {
        Some(matches[0])
    } else {
        None
    }
}

fn fuzzy_compare(input: &str, name: &str) -> bool {
    let name = name.to_lowercase();

    // Cases like:
    //    "Vatican" <-> "Holy See (Vatican City State)"
    //    "Russia"  <-> "Russian Federation"
    if name.contains(input) || input.contains(&name) {
        return true;
    }

    // Cases like:
    //    "British Virgin Islands" <-> "Virgin Islands, British"
    //    "Republic of Moldova"    <-> "Moldova, Republic of"
    if name.contains(',') {
        let mut name_parts: Vec<&str> = name.split(", ").collect();
        name_parts.reverse();
        let reversed_name = name_parts.join(" ");
        if reversed_name.contains(input) || input.contains(&reversed_name) {
            return true;
        }
    }

    false
}