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
use regex::Regex;

use crate::utils::{Coordinate, Error, DIGITS};

pub fn decode(code: &str) -> Result<Coordinate, Error> {
    if !is_valid_code(code) {
        return Err(Error::InvalidCode(String::from(code)));
    }

    let (lat, lon) = code
        .chars()
        .filter(|c| c != &'+')
        .enumerate()
        .fold((Vec::new(), Vec::new()), split_reducer);

    let res = resolution(code);
    let lat = decode_axis(lat) + res / 2.0 - 90.0;
    let lon = decode_axis(lon) + res / 2.0 - 180.0;

    Ok(Coordinate {
        latitude: lat,
        longitude: lon,
    })
}

fn decode_axis(axis: Vec<char>) -> f64 {
    let (result, _) = axis
        .iter()
        .map(|c| digit_to_value(c))
        .fold((0.0, 20.0), axis_reducer);

    result
}

fn digit_to_value(x: &char) -> usize {
    DIGITS.chars().position(|c| &c == x).unwrap()
}

fn axis_reducer(memo: (f64, f64), value: usize) -> (f64, f64) {
    let (result, pos_value) = memo;
    let result = result + pos_value * value as f64;
    let pos_value = pos_value / 20.0;
    (result, pos_value)
}

fn split_reducer(axes: (Vec<char>, Vec<char>), digit: (usize, char)) -> (Vec<char>, Vec<char>) {
    let (idx, c) = digit;
    let (mut lat, mut lon) = axes;

    if idx % 2 == 0 {
        lat.push(c);
    } else {
        lon.push(c)
    }

    return (lat, lon);
}

fn is_valid_code(code: &str) -> bool {
    let pair = format!("[{}]{{2}}", DIGITS);
    let pair_or_zero = format!("([{}]|0){{2}}", DIGITS);
    let exp = format!(
        "^{pair}({pairOrZero}){{0,3}}[+]({pair})?$",
        pair = pair,
        pairOrZero = pair_or_zero
    );
    let re = Regex::new(&exp).unwrap();
    re.is_match(&code)
}

fn resolution(code: &str) -> f64 {
    let length = code.chars().filter(|c| c != &'+' && c != &'0').count();
    return 20.0 / (20_u32.pow((length as u32 / 2) - 1) as f64);
}

#[cfg(test)]
mod tests {
    use crate::{
        decode::decode,
        utils::{Coordinate, Error},
    };

    #[test]
    fn it_returns_none_for_invalid_codes() {
        assert_eq!(decode("foo"), Err(Error::InvalidCode("foo".to_string())));
    }

    #[test]
    fn it_returns_the_coordinate_for_valid_codes() {
        if let Ok(Coordinate {
            latitude,
            longitude,
        }) = decode("9FFW84J9+XG")
        {
            assert_eq!(format!("{:.6}", latitude), "59.332438");
            assert_eq!(format!("{:.6}", longitude), "18.118813");
        }
    }
}