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
#![no_std]

extern crate libm;

mod wordlist;

use libm::F64Ext;
use wordlist::WORDLIST;

const TILES_SIZES: &[f64] = &[5.625, 0.125, 0.00277777777, 0.00006172839, 0.00000308642];
const RASTER_SIZE: &[(usize, usize)] = &[(32, 64), (45, 45), (45, 45), (45, 45), (20, 20)];

fn get_word_idx<'a>(word: &'a str) -> Result<usize, Error<'a>> {
    WORDLIST.iter()
        .enumerate()
        .find(|(_, w)| **w == word)
        .map(|(idx, _)| idx)
        .ok_or(Error::InvalidWord(word))
}

/// Decode words supplied as iterator over string slices.
///
/// # Example:
/// ```
/// use where39::from_words;
///
/// let words = &["slush", "battle", "damage", "dentist"][..];
///
/// let (lat, lng) = from_words(words.iter()).unwrap();
/// assert_eq!(lat, 51.02561728383f64);
/// assert_eq!(lng, 13.72333333297f64);
/// ```
pub fn from_words<'a, I, S>(words: I) -> Result<(f64, f64), Error<'a>>
where I: Iterator<Item=&'a S> + ExactSizeIterator,
      S: AsRef<str> + 'a + ?Sized
{
    if words.len() > 5 {
        return Err(Error::TooManyWords(words.len()))
    }

    let (lat, lng) = words.enumerate()
        .map(|(idx, word)| {
            let word_idx_limit = RASTER_SIZE[idx].0 * RASTER_SIZE[idx].1;
            let word_idx = get_word_idx(word.as_ref())?;

            if word_idx >= word_idx_limit {
                return Err(Error::InvalidWord(word.as_ref()));
            }

            let lat_idx = word_idx / RASTER_SIZE[idx].1;
            let long_idx = word_idx % RASTER_SIZE[idx].1;

            Ok((
                (lat_idx as f64) * TILES_SIZES[idx],
                (long_idx as f64) * TILES_SIZES[idx],
            ))
        })
        .try_fold((-90f64, -180f64), |acc, coords| {
            match coords {
                Ok((lat, long)) => Ok((acc.0 + lat, acc.1 + long)),
                Err(e) => Err(e)
            }
        })?;

    Ok((lat, lng))
}

/// Encode coordinates represented as a latitude (in range -90..=+90) and a longitude (in range
/// -180..=+180) to five words. You may shorten the array to four elements according to the original
/// implementation.
///
/// # Example:
/// ```
/// use where39::to_words;
///
/// let code = to_words(51.02561728383f64, 13.72333333297f64).unwrap();
/// let expected = &["slush", "battle", "damage", "dentist"][..];
/// assert_eq!(expected, &code[..4]);
/// ```
pub fn to_words(lat: f64, lng: f64) -> Result<[&'static str; 5], Error<'static>> {
    if (-90f64..=90f64).contains(&lat) == false || (-180f64..=180f64).contains(&lng) == false {
        return Err(Error::CoordinatesOutOfRange)
    }

    let mut lat = lat + 90f64;
    let mut lng = ((lng - 180f64) % 360f64) + 360f64;

    let mut code = [""; 5];

    for (idx, code_word) in code.iter_mut().enumerate() {
        let lat_idx_f64 = (lat / TILES_SIZES[idx]).floor();
        let lng_idx_f64 = (lng / TILES_SIZES[idx]).floor();

        lat -= lat_idx_f64 * TILES_SIZES[idx];
        lng -= lng_idx_f64 * TILES_SIZES[idx];

        let word_idx = (lat_idx_f64 as usize) * RASTER_SIZE[idx].1 + (lng_idx_f64 as usize);
        *code_word = WORDLIST[word_idx];
    }

    Ok(code)
}

#[derive(Debug)]
pub enum Error<'a> {
    InvalidWord(&'a str),
    TooManyWords(usize),
    CoordinatesOutOfRange
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_words_to_coords() {
        let words = &["slush", "battle", "damage", "dentist"][..];

        let (lat, lng) = super::from_words(words.iter()).unwrap();
        assert_eq!(lat, 51.02561728383f64);
        assert_eq!(lng, 13.72333333297f64);
    }

    #[test]
    fn test_coords_to_words() {
        let code = super::to_words(51.02561728383f64, 13.72333333297f64).unwrap();
        let expected = &["slush", "battle", "damage", "dentist"][..];

        assert_eq!(expected, &code[..4]);
    }
}