Skip to main content

mh_locator/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3#![warn(missing_docs)]
4#![forbid(unsafe_code)]
5
6use core::fmt;
7use core::ops;
8use core::result;
9use core::str::FromStr;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
12/// Errors for this crate.
13pub enum Error {
14    /// Parsing a locator failed.
15    ParseError,
16    /// The latitude and longitude coordinates are out of range.
17    CoordinatesOutOfRangeError,
18}
19impl fmt::Display for Error {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Error::ParseError => write!(f, "parse error"),
23            Error::CoordinatesOutOfRangeError => write!(f, "coordinates out of range error"),
24        }
25    }
26}
27impl core::error::Error for Error {}
28
29type Result<T> = result::Result<T, Error>;
30
31const RAW_DIGIT_LIMIT: [u8; 8] = [18, 18, 10, 10, 24, 24, 10, 10];
32
33const FROM_CHAR_FNS: [fn(char) -> Result<u8>; 8] = [
34    from_char_18,
35    from_char_18,
36    from_char_10,
37    from_char_10,
38    from_char_24,
39    from_char_24,
40    from_char_10,
41    from_char_10,
42];
43
44const TO_CHAR_FNS: [fn(u32) -> char; 8] = [
45    to_char_18, to_char_18, //
46    to_char_10, to_char_10, //
47    to_char_24, to_char_24, //
48    to_char_10, to_char_10, //
49];
50
51#[doc(hidden)]
52pub trait GridParam: private::Sealed {
53    /// The integer type used by the internal representation.
54    type UInt;
55
56    /// The length of the string representation.
57    const LEN: usize;
58
59    /// The longitude step per grid element.
60    const LON_STEP: f64;
61    /// The latitude step per grid element.
62    const LAT_STEP: f64;
63
64    /// The maximal internal integer value of `lon` and `lat`.
65    const INT_MAX: Self::UInt;
66
67    /// Returns the positional value of a raw digit in a locator.
68    fn position_value(i: usize) -> Self::UInt;
69
70    /// Returns the internal integer representation of a coordinate.
71    fn internal_int(a: f64, step: f64) -> Self::UInt;
72}
73mod private {
74    pub trait Sealed {}
75
76    impl Sealed for super::Grid4Param {}
77    impl Sealed for super::Grid6Param {}
78    impl Sealed for super::Grid8Param {}
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82#[doc(hidden)]
83pub struct Grid4Param;
84impl GridParam for Grid4Param {
85    type UInt = u8;
86
87    const LEN: usize = 4;
88
89    const LON_STEP: f64 = 2.0;
90    const LAT_STEP: f64 = 1.0;
91
92    const INT_MAX: Self::UInt = 180 - 1;
93
94    fn position_value(i: usize) -> Self::UInt {
95        [10, 1][i]
96    }
97
98    fn internal_int(a: f64, step: f64) -> Self::UInt {
99        // The division could possibly round up values minimally less than
100        // the exclusive limit to the limit itself, which would then cause
101        // the result to exceed the maximal integer value.
102        // Prevent that explicitly.
103        ((a / step) as u8).min(Self::INT_MAX)
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108#[doc(hidden)]
109pub struct Grid6Param;
110impl GridParam for Grid6Param {
111    type UInt = u16;
112
113    const LEN: usize = 6;
114
115    const LON_STEP: f64 = 2.0 / 24.0;
116    const LAT_STEP: f64 = 1.0 / 24.0;
117
118    const INT_MAX: Self::UInt = 180 * 24 - 1;
119
120    fn position_value(i: usize) -> Self::UInt {
121        [10 * 24, 24, 1][i]
122    }
123
124    fn internal_int(a: f64, step: f64) -> Self::UInt {
125        ((a / step) as u16).min(Self::INT_MAX)
126    }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
130#[doc(hidden)]
131pub struct Grid8Param;
132impl GridParam for Grid8Param {
133    type UInt = u16;
134
135    const LEN: usize = 8;
136
137    const LON_STEP: f64 = 2.0 / 24.0 / 10.0;
138    const LAT_STEP: f64 = 1.0 / 24.0 / 10.0;
139
140    const INT_MAX: Self::UInt = 180 * 24 * 10 - 1;
141
142    fn position_value(i: usize) -> Self::UInt {
143        [10 * 24 * 10, 24 * 10, 10, 1][i]
144    }
145
146    fn internal_int(a: f64, step: f64) -> Self::UInt {
147        ((a / step) as u16).min(Self::INT_MAX)
148    }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152/// An `N`-character Maidenhead locator.
153pub struct Grid<P>
154where
155    P: GridParam,
156    f64: From<P::UInt>,
157{
158    /// The longitude of the western edge of the grid element
159    /// in steps of two degrees counting from the antimeridian.
160    lon: P::UInt,
161    /// The latitude of the southern edge of the grid element
162    /// in steps of one degree counting from the south pole.
163    lat: P::UInt,
164}
165
166impl<P> Grid<P>
167where
168    P: GridParam,
169    P::UInt: Copy,
170    f64: From<P::UInt>,
171{
172    const LEN: usize = P::LEN;
173
174    fn position_value(i: usize) -> P::UInt {
175        P::position_value(i)
176    }
177
178    /// Constructs a locator from raw digit values.
179    ///
180    /// This avoids unnecessary encoding to and decoding from characters
181    /// when decoding locators in FT8.
182    ///
183    /// # Examples
184    /// ```
185    /// # use std::str::FromStr;
186    /// # use mh_locator::Grid4;
187    /// let g = Grid4::from_raw_digits(&[0, 1, 2, 3]);
188    /// assert_eq!(Grid4::from_str("AB23"), g);
189    /// ```
190    pub fn from_raw_digits(ds: &[u8]) -> Result<Grid<P>>
191    where
192        P::UInt: From<u8>,
193        P::UInt: ops::Mul<P::UInt, Output = P::UInt>,
194        P::UInt: ops::AddAssign<P::UInt>,
195    {
196        if ds.len() != Self::LEN || !ds.iter().enumerate().all(|(i, &d)| d < RAW_DIGIT_LIMIT[i]) {
197            return Err(Error::ParseError);
198        }
199
200        let mut lon = P::UInt::from(0);
201        let mut lat = P::UInt::from(0);
202        for i in 0..(Self::LEN / 2) {
203            lon += Self::position_value(i) * P::UInt::from(ds[2 * i + 0]);
204            lat += Self::position_value(i) * P::UInt::from(ds[2 * i + 1]);
205        }
206
207        Ok(Grid { lon, lat })
208    }
209
210    /// Returns the longitude of the western edge of the grid element.
211    pub fn west_lon(&self) -> f64 {
212        (f64::from(self.lon)) * P::LON_STEP - 180.0
213    }
214    /// Returns the longitude of the center of the grid element.
215    pub fn center_lon(&self) -> f64 {
216        (f64::from(self.lon) + 0.5) * P::LON_STEP - 180.0
217    }
218    /// Returns the longitude of the eastern edge of the grid element.
219    pub fn east_lon(&self) -> f64 {
220        (f64::from(self.lon) + 1.0) * P::LON_STEP - 180.0
221    }
222
223    /// Returns the latitude of the southern edge of the grid element.
224    pub fn south_lat(&self) -> f64 {
225        (f64::from(self.lat)) * P::LAT_STEP - 90.0
226    }
227    /// Returns the latitude of the center of the grid element.
228    pub fn center_lat(&self) -> f64 {
229        (f64::from(self.lat) + 0.5) * P::LAT_STEP - 90.0
230    }
231    /// Returns the latitude of the northern edge of the grid element.
232    pub fn north_lat(&self) -> f64 {
233        (f64::from(self.lat) + 1.0) * P::LAT_STEP - 90.0
234    }
235
236    /// Returns the coordinates of the center of the grid element as a pair.
237    ///
238    /// # Examples
239    /// ```
240    /// # use std::str::FromStr;
241    /// # use mh_locator::Grid4;
242    /// let g = Grid4::from_str("JO31").unwrap();
243    /// assert_eq!((51.5, 7.0), g.center_lat_lon());
244    /// ```
245    pub fn center_lat_lon(&self) -> (f64, f64) {
246        (self.center_lat(), self.center_lon())
247    }
248
249    /// Returns the grid element for a given latitude and longitude.
250    ///
251    /// # Examples
252    /// ```
253    /// # use std::str::FromStr;
254    /// # use mh_locator::Grid4;
255    /// let g = Grid4::from_lat_lon(51.4385, 7.0249);
256    /// assert_eq!(Grid4::from_str("JO31"), g);
257    /// ```
258    pub fn from_lat_lon(lat: f64, lon: f64) -> Result<Self> {
259        if -180.0 <= lon && lon < 180.0 && -90.0 <= lat && lat < 90.0 {
260            let lon = P::internal_int(lon + 180.0, P::LON_STEP);
261            let lat = P::internal_int(lat + 90.0, P::LAT_STEP);
262            Ok(Grid { lon, lat })
263        } else {
264            Err(Error::CoordinatesOutOfRangeError)
265        }
266    }
267}
268
269/// A four-character Maidenhead locator.
270pub type Grid4 = Grid<Grid4Param>;
271impl Grid4 {
272    /// Returns the raw digits corresponding to the characters of the locator.
273    ///
274    /// This avoids unnecessary encoding to and decoding from characters
275    /// when encoding locators in FT8.
276    ///
277    /// # Examples
278    /// ```
279    /// # use std::str::FromStr;
280    /// # use mh_locator::Grid4;
281    /// let g = Grid4::from_str("AB23").unwrap();
282    /// assert_eq!([0, 1, 2, 3], g.raw_digits());
283    /// ```
284    // We can't yet implement this generically for `Grid` in stable Rust,
285    // due to the `constant expression depends on a generic parameter` issue.
286    // The array type depends on `Self::LEN`.
287    pub fn raw_digits(&self) -> [u8; Self::LEN] {
288        let mut lon = self.lon;
289        let mut lat = self.lat;
290        let mut ds = [0; Self::LEN];
291        for i in 0..(Self::LEN / 2) {
292            ds[2 * i + 0] = (lon / Self::position_value(i)) as u8;
293            ds[2 * i + 1] = (lat / Self::position_value(i)) as u8;
294
295            lon %= Self::position_value(i);
296            lat %= Self::position_value(i);
297        }
298        ds
299    }
300}
301
302impl FromStr for Grid4 {
303    type Err = Error;
304
305    /// Parses a string `s` to return a value of type `Grid4`.
306    ///
307    /// Letters are parsed case-insensitively.
308    ///
309    /// # Examples
310    /// ```
311    /// # use std::str::FromStr;
312    /// # use mh_locator::Grid4;
313    /// assert!(Grid4::from_str("AB23").is_ok());
314    /// assert!(Grid4::from_str("ab23").is_ok());
315    /// ```
316    fn from_str(s: &str) -> Result<Self> {
317        if s.len() != Self::LEN || !s.is_ascii() {
318            return Err(Error::ParseError);
319        }
320
321        let mut ds = [0; Self::LEN];
322        for (i, c) in s.chars().enumerate() {
323            ds[i] = FROM_CHAR_FNS[i](c)?;
324        }
325        Grid4::from_raw_digits(&ds)
326    }
327}
328
329impl fmt::Display for Grid4 {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        let ds = self.raw_digits();
332
333        for (i, d) in ds.into_iter().enumerate() {
334            write!(f, "{}", TO_CHAR_FNS[i](d.into()))?;
335        }
336        Ok(())
337    }
338}
339
340/// A six-character Maidenhead locator.
341#[allow(private_interfaces)]
342pub type Grid6 = Grid<Grid6Param>;
343impl Grid6 {
344    /// Returns the raw digits corresponding to the characters of the locator.
345    ///
346    /// This avoids unnecessary encoding to and decoding from characters
347    /// when encoding locators in FT8.
348    pub fn raw_digits(&self) -> [u8; Self::LEN] {
349        let mut lon = self.lon;
350        let mut lat = self.lat;
351        let mut ds = [0; Self::LEN];
352        for i in 0..(Self::LEN / 2) {
353            ds[2 * i + 0] = (lon / Self::position_value(i)) as u8;
354            ds[2 * i + 1] = (lat / Self::position_value(i)) as u8;
355
356            lon %= Self::position_value(i);
357            lat %= Self::position_value(i);
358        }
359        ds
360    }
361}
362
363impl FromStr for Grid6 {
364    type Err = Error;
365
366    /// Parses a string `s` to return a value of type `Grid6`.
367    ///
368    /// Letters are parsed case-insensitively.
369    ///
370    /// # Examples
371    /// ```
372    /// # use std::str::FromStr;
373    /// # use mh_locator::Grid6;
374    /// assert!(Grid6::from_str("AB23ef").is_ok());
375    /// assert!(Grid6::from_str("AB23EF").is_ok());
376    /// assert!(Grid6::from_str("ab23ef").is_ok());
377    /// ```
378    fn from_str(s: &str) -> Result<Self> {
379        if s.len() != Self::LEN || !s.is_ascii() {
380            return Err(Error::ParseError);
381        }
382
383        let mut ds = [0; Self::LEN];
384        for (i, c) in s.chars().enumerate() {
385            ds[i] = FROM_CHAR_FNS[i](c)?;
386        }
387        Grid6::from_raw_digits(&ds)
388    }
389}
390impl fmt::Display for Grid6 {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        let ds = self.raw_digits();
393
394        for (i, d) in ds.into_iter().enumerate() {
395            write!(f, "{}", TO_CHAR_FNS[i](d.into()))?;
396        }
397        Ok(())
398    }
399}
400
401/// An eight-character Maidenhead locator.
402#[allow(private_interfaces)]
403pub type Grid8 = Grid<Grid8Param>;
404impl Grid8 {
405    /// Returns the raw digits corresponding to the characters of the locator.
406    ///
407    /// This avoids unnecessary encoding to and decoding from characters
408    /// when encoding locators in FT8.
409    pub fn raw_digits(&self) -> [u8; Self::LEN] {
410        let mut lon = self.lon;
411        let mut lat = self.lat;
412        let mut ds = [0; Self::LEN];
413        for i in 0..(Self::LEN / 2) {
414            ds[2 * i + 0] = (lon / Self::position_value(i)) as u8;
415            ds[2 * i + 1] = (lat / Self::position_value(i)) as u8;
416
417            lon %= Self::position_value(i);
418            lat %= Self::position_value(i);
419        }
420        ds
421    }
422}
423
424impl FromStr for Grid8 {
425    type Err = Error;
426
427    /// Parses a string `s` to return a value of type `Grid8`.
428    ///
429    /// Letters are parsed case-insensitively.
430    ///
431    /// # Examples
432    /// ```
433    /// # use std::str::FromStr;
434    /// # use mh_locator::Grid8;
435    /// assert!(Grid8::from_str("AB23ef67").is_ok());
436    /// assert!(Grid8::from_str("AB23EF67").is_ok());
437    /// assert!(Grid8::from_str("ab23ef67").is_ok());
438    /// ```
439    fn from_str(s: &str) -> Result<Self> {
440        if s.len() != Self::LEN || !s.is_ascii() {
441            return Err(Error::ParseError);
442        }
443
444        let mut ds = [0; Self::LEN];
445        for (i, c) in s.chars().enumerate() {
446            ds[i] = FROM_CHAR_FNS[i](c)?;
447        }
448        Grid8::from_raw_digits(&ds)
449    }
450}
451impl fmt::Display for Grid8 {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        let ds = self.raw_digits();
454
455        for (i, d) in ds.into_iter().enumerate() {
456            write!(f, "{}", TO_CHAR_FNS[i](d.into()))?;
457        }
458        Ok(())
459    }
460}
461
462fn from_char_18(c: char) -> Result<u8> {
463    if ('A'..='R').contains(&c) {
464        Ok((u32::from(c) - u32::from('A')) as u8)
465    } else if ('a'..='r').contains(&c) {
466        Ok((u32::from(c) - u32::from('a')) as u8)
467    } else {
468        Err(Error::ParseError)
469    }
470}
471
472fn from_char_10(c: char) -> Result<u8> {
473    if c.is_ascii_digit() {
474        Ok((u32::from(c) - u32::from('0')) as u8)
475    } else {
476        Err(Error::ParseError)
477    }
478}
479
480fn from_char_24(c: char) -> Result<u8> {
481    if ('A'..='X').contains(&c) {
482        Ok((u32::from(c) - u32::from('A')) as u8)
483    } else if ('a'..='x').contains(&c) {
484        Ok((u32::from(c) - u32::from('a')) as u8)
485    } else {
486        Err(Error::ParseError)
487    }
488}
489
490fn to_char_18(d: u32) -> char {
491    assert!(d < 18);
492    char::from_u32(u32::from('A') + d).unwrap()
493}
494
495fn to_char_10(d: u32) -> char {
496    assert!(d < 10);
497    char::from_u32(u32::from('0') + d).unwrap()
498}
499
500fn to_char_24(d: u32) -> char {
501    assert!(d < 24);
502    char::from_u32(u32::from('A') + d).unwrap()
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_grid4_from_str() {
511        let grid_min = Grid4 { lon: 0, lat: 0 };
512        let grid_max = Grid4 { lon: 179, lat: 179 };
513
514        assert_eq!(Ok(grid_min), Grid4::from_str("AA00"));
515        assert_eq!(Ok(grid_min), Grid4::from_str("aa00"));
516
517        assert_eq!(Ok(grid_max), Grid4::from_str("RR99"));
518        assert_eq!(Ok(grid_max), Grid4::from_str("rr99"));
519
520        assert_eq!(Err(Error::ParseError), Grid4::from_str("RR99xx"));
521        assert_eq!(Err(Error::ParseError), Grid4::from_str("RR99xx99"));
522
523        assert_eq!(Err(Error::ParseError), Grid4::from_str(""));
524        assert_eq!(Err(Error::ParseError), Grid4::from_str("AA0"));
525        assert_eq!(Err(Error::ParseError), Grid4::from_str("AA00 "));
526
527        assert_eq!(Err(Error::ParseError), Grid4::from_str("-A00"));
528        assert_eq!(Err(Error::ParseError), Grid4::from_str("SA00"));
529        assert_eq!(Err(Error::ParseError), Grid4::from_str("AS00"));
530        assert_eq!(Err(Error::ParseError), Grid4::from_str("A-00"));
531        assert_eq!(Err(Error::ParseError), Grid4::from_str("AA-0"));
532        assert_eq!(Err(Error::ParseError), Grid4::from_str("AAA0"));
533        assert_eq!(Err(Error::ParseError), Grid4::from_str("AA0-"));
534        assert_eq!(Err(Error::ParseError), Grid4::from_str("AA0A"));
535    }
536
537    #[test]
538    fn test_grid6_from_str() {
539        let grid_min = Grid6 { lon: 0, lat: 0 };
540        let grid_max = Grid6 {
541            lon: 4319,
542            lat: 4319,
543        };
544        assert_eq!(Ok(grid_min), Grid6::from_str("AA00aa"));
545        assert_eq!(Ok(grid_min), Grid6::from_str("AA00AA"));
546        assert_eq!(Ok(grid_min), Grid6::from_str("aa00aa"));
547
548        assert_eq!(Ok(grid_max), Grid6::from_str("RR99xx"));
549        assert_eq!(Ok(grid_max), Grid6::from_str("RR99XX"));
550        assert_eq!(Ok(grid_max), Grid6::from_str("rr99xx"));
551
552        assert_eq!(Err(Error::ParseError), Grid6::from_str("RR99"));
553        assert_eq!(Err(Error::ParseError), Grid6::from_str("RR99xx99"));
554
555        assert_eq!(Err(Error::ParseError), Grid6::from_str(""));
556        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00a"));
557        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00aa "));
558
559        assert_eq!(Err(Error::ParseError), Grid6::from_str("-A00aa"));
560        assert_eq!(Err(Error::ParseError), Grid6::from_str("SA00aa"));
561        assert_eq!(Err(Error::ParseError), Grid6::from_str("A-00aa"));
562        assert_eq!(Err(Error::ParseError), Grid6::from_str("AS00aa"));
563        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA-0aa"));
564        assert_eq!(Err(Error::ParseError), Grid6::from_str("AAA0aa"));
565        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA0-aa"));
566        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA0Aaa"));
567        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00-a"));
568        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00ya"));
569        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00a-"));
570        assert_eq!(Err(Error::ParseError), Grid6::from_str("AA00ay"));
571    }
572
573    #[test]
574    fn test_grid8_from_str() {
575        let grid_min = Grid8 { lon: 0, lat: 0 };
576        let grid_max = Grid8 {
577            lon: 43199,
578            lat: 43199,
579        };
580        assert_eq!(Ok(grid_min), Grid8::from_str("AA00aa00"));
581        assert_eq!(Ok(grid_min), Grid8::from_str("AA00AA00"));
582        assert_eq!(Ok(grid_min), Grid8::from_str("aa00aa00"));
583
584        assert_eq!(Ok(grid_max), Grid8::from_str("RR99xx99"));
585        assert_eq!(Ok(grid_max), Grid8::from_str("RR99XX99"));
586        assert_eq!(Ok(grid_max), Grid8::from_str("rr99xx99"));
587
588        assert_eq!(Err(Error::ParseError), Grid8::from_str("RR99"));
589        assert_eq!(Err(Error::ParseError), Grid8::from_str("RR99xx"));
590
591        assert_eq!(Err(Error::ParseError), Grid8::from_str(""));
592        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aa0"));
593        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aa00 "));
594
595        assert_eq!(Err(Error::ParseError), Grid8::from_str("-A00aa00"));
596        assert_eq!(Err(Error::ParseError), Grid8::from_str("SA00aa00"));
597        assert_eq!(Err(Error::ParseError), Grid8::from_str("A-00aa00"));
598        assert_eq!(Err(Error::ParseError), Grid8::from_str("AS00aa00"));
599        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA-0aa00"));
600        assert_eq!(Err(Error::ParseError), Grid8::from_str("AAA0aa00"));
601        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA0-aa00"));
602        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA0Aaa00"));
603        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00-a00"));
604        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00ya00"));
605        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00a-00"));
606        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00ay00"));
607        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aa-0"));
608        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aaA0"));
609        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aa0-"));
610        assert_eq!(Err(Error::ParseError), Grid8::from_str("AA00aa0A"));
611    }
612
613    fn abs_diff(x: f64, y: f64) -> f64 {
614        (x - y).abs()
615    }
616    const EPS: f64 = 1e-12;
617
618    #[test]
619    fn test_grid4_west() {
620        assert_eq!(-180.0, Grid4::from_str("AA00").unwrap().west_lon());
621        assert_eq!(178.0, Grid4::from_str("RR99").unwrap().west_lon());
622        assert_eq!(6.0, Grid4::from_str("JO31").unwrap().west_lon());
623    }
624
625    #[test]
626    fn test_grid6_west() {
627        let lon = Grid6::from_str("AA00aa").unwrap().west_lon();
628        assert!(abs_diff(lon, -180.0) < EPS);
629
630        let lon = Grid6::from_str("RR99xx").unwrap().west_lon();
631        assert!(abs_diff(lon, 180.0 - 5.0 / 60.0) < EPS);
632
633        let lon = Grid6::from_str("JO31mk").unwrap().west_lon();
634        assert!(abs_diff(lon, 6.0 + 12.0 * 5.0 / 60.0) < EPS);
635    }
636
637    #[test]
638    fn test_grid8_west() {
639        let lon = Grid8::from_str("AA00aa00").unwrap().west_lon();
640        assert!(abs_diff(lon, -180.0) < EPS);
641
642        let lon = Grid8::from_str("RR99xx99").unwrap().west_lon();
643        assert!(abs_diff(lon, 180.0 - 30.0 / 3600.0) < EPS);
644
645        let lon = Grid8::from_str("JO31mk25").unwrap().west_lon();
646        assert!(abs_diff(lon, 6.0 + 12.0 * 5.0 / 60.0 + 2.0 * 30.0 / 3600.0) < EPS);
647    }
648
649    #[test]
650    fn test_grid4_east() {
651        assert_eq!(-178.0, Grid4::from_str("AA00").unwrap().east_lon());
652        assert_eq!(180.0, Grid4::from_str("RR99").unwrap().east_lon());
653        assert_eq!(8.0, Grid4::from_str("JO31").unwrap().east_lon());
654    }
655
656    #[test]
657    fn test_grid6_east() {
658        let lon = Grid6::from_str("AA00aa").unwrap().east_lon();
659        assert!(abs_diff(lon, -180.0 + 5.0 / 60.0) < EPS);
660
661        let lon = Grid6::from_str("RR99xx").unwrap().east_lon();
662        assert!(abs_diff(lon, 180.0) < EPS);
663
664        let lon = Grid6::from_str("JO31mk").unwrap().east_lon();
665        assert!(abs_diff(lon, 6.0 + 13.0 * 5.0 / 60.0) < EPS);
666    }
667
668    #[test]
669    fn test_grid8_east() {
670        let lon = Grid8::from_str("AA00aa00").unwrap().east_lon();
671        assert!(abs_diff(lon, -180.0 + 30.0 / 3600.0) < EPS);
672
673        let lon = Grid8::from_str("RR99xx99").unwrap().east_lon();
674        assert!(abs_diff(lon, 180.0) < EPS);
675
676        let lon = Grid8::from_str("JO31mk25").unwrap().east_lon();
677        assert!(abs_diff(lon, 6.0 + 12.0 * 5.0 / 60.0 + 3.0 * 30.0 / 3600.0) < EPS);
678    }
679
680    #[test]
681    fn test_grid4_south() {
682        assert_eq!(-90.0, Grid4::from_str("AA00").unwrap().south_lat());
683        assert_eq!(89.0, Grid4::from_str("RR99").unwrap().south_lat());
684        assert_eq!(51.0, Grid4::from_str("JO31").unwrap().south_lat());
685    }
686
687    #[test]
688    fn test_grid6_south() {
689        let lat = Grid6::from_str("AA00aa").unwrap().south_lat();
690        assert!(abs_diff(lat, -90.0) < EPS);
691
692        let lat = Grid6::from_str("RR99xx").unwrap().south_lat();
693        assert!(abs_diff(lat, 90.0 - 2.5 / 60.0) < EPS);
694
695        let lat = Grid6::from_str("JO31mk").unwrap().south_lat();
696        assert!(abs_diff(lat, 51.0 + 10.0 * 2.5 / 60.0) < EPS);
697    }
698
699    #[test]
700    fn test_grid8_south() {
701        let lat = Grid8::from_str("AA00aa00").unwrap().south_lat();
702        assert!(abs_diff(lat, -90.0) < EPS);
703
704        let lat = Grid8::from_str("RR99xx99").unwrap().south_lat();
705        assert!(abs_diff(lat, 90.0 - 15.0 / 3600.0) < EPS);
706
707        let lat = Grid8::from_str("JO31mk25").unwrap().south_lat();
708        assert!(abs_diff(lat, 51.0 + 10.0 * 2.5 / 60.0 + 5.0 * 15.0 / 3600.0) < EPS);
709    }
710
711    #[test]
712    fn test_grid4_north() {
713        assert_eq!(-89.0, Grid4::from_str("AA00").unwrap().north_lat());
714        assert_eq!(90.0, Grid4::from_str("RR99").unwrap().north_lat());
715        assert_eq!(52.0, Grid4::from_str("JO31").unwrap().north_lat());
716    }
717
718    #[test]
719    fn test_grid6_north() {
720        let lat = Grid6::from_str("AA00aa").unwrap().north_lat();
721        assert!(abs_diff(lat, -90.0 + 2.5 / 60.0) < EPS);
722
723        let lat = Grid6::from_str("RR99xx").unwrap().north_lat();
724        assert!(abs_diff(lat, 90.0) < EPS);
725
726        let lat = Grid6::from_str("JO31mk").unwrap().north_lat();
727        assert!(abs_diff(lat, 51.0 + 11.0 * 2.5 / 60.0) < EPS);
728    }
729
730    #[test]
731    fn test_grid8_north() {
732        let lat = Grid8::from_str("AA00aa00").unwrap().north_lat();
733        assert!(abs_diff(lat, -90.0 + 15.0 / 3600.0) < EPS);
734
735        let lat = Grid8::from_str("RR99xx99").unwrap().north_lat();
736        assert!(abs_diff(lat, 90.0) < EPS);
737
738        let lat = Grid8::from_str("JO31mk25").unwrap().north_lat();
739        assert!(abs_diff(lat, 51.0 + 10.0 * 2.5 / 60.0 + 6.0 * 15.0 / 3600.0) < EPS);
740    }
741
742    #[test]
743    fn test_grid4_center() {
744        let g0 = Grid4::from_str("AA00").unwrap();
745        assert_eq!((-89.5, -179.0), g0.center_lat_lon());
746
747        let g1 = Grid4::from_str("RR99").unwrap();
748        assert_eq!((89.5, 179.0), g1.center_lat_lon());
749
750        let g2 = Grid4::from_str("JO31").unwrap();
751        assert_eq!((51.5, 7.0), g2.center_lat_lon());
752    }
753
754    #[test]
755    fn test_grid6_center() {
756        let (lat, lon) = Grid6::from_str("AA00aa").unwrap().center_lat_lon();
757        assert!(abs_diff(lat, -90.0 + 1.25 / 60.0) < EPS);
758        assert!(abs_diff(lon, -180.0 + 2.5 / 60.0) < EPS);
759
760        let (lat, lon) = Grid6::from_str("RR99xx").unwrap().center_lat_lon();
761        assert!(abs_diff(lat, 90.0 - 1.25 / 60.0) < EPS);
762        assert!(abs_diff(lon, 180.0 - 2.5 / 60.0) < EPS);
763
764        let (lat, lon) = Grid6::from_str("JO31mk").unwrap().center_lat_lon();
765        assert!(abs_diff(lat, 51.0 + 10.5 * 2.5 / 60.0) < EPS);
766        assert!(abs_diff(lon, 6.0 + 12.5 * 5.0 / 60.0) < EPS);
767    }
768
769    #[test]
770    fn test_grid8_center() {
771        let (lat, lon) = Grid8::from_str("AA00aa00").unwrap().center_lat_lon();
772        assert!(abs_diff(lat, -90.0 + 7.5 / 3600.0) < EPS);
773        assert!(abs_diff(lon, -180.0 + 15.0 / 3600.0) < EPS);
774
775        let (lat, lon) = Grid8::from_str("RR99xx99").unwrap().center_lat_lon();
776        assert!(abs_diff(lat, 90.0 - 7.5 / 3600.0) < EPS);
777        assert!(abs_diff(lon, 180.0 - 15.0 / 3600.0) < EPS);
778
779        let (lat, lon) = Grid8::from_str("JO31mk25").unwrap().center_lat_lon();
780        assert!(abs_diff(lat, 51.0 + 10.0 * 2.5 / 60.0 + 5.5 * 15.0 / 3600.0) < EPS);
781        assert!(abs_diff(lon, 6.0 + 12.0 * 5.0 / 60.0 + 2.5 * 30.0 / 3600.0) < EPS);
782    }
783
784    #[test]
785    fn test_grid4_from_lat_lon() {
786        assert_eq!(
787            Ok(Grid4::from_str("AA00").unwrap()),
788            Grid4::from_lat_lon(-90.0, -180.0)
789        );
790        assert_eq!(
791            Ok(Grid4::from_str("RR99").unwrap()),
792            Grid4::from_lat_lon(89.9999, 179.9999)
793        );
794        assert_eq!(
795            Ok(Grid4::from_str("JO31").unwrap()),
796            Grid4::from_lat_lon(51.4385, 7.0249)
797        );
798
799        assert_eq!(
800            Err(Error::CoordinatesOutOfRangeError),
801            Grid4::from_lat_lon(-90.1, 0.0)
802        );
803        assert_eq!(
804            Err(Error::CoordinatesOutOfRangeError),
805            Grid4::from_lat_lon(90.1, 0.0)
806        );
807        assert_eq!(
808            Err(Error::CoordinatesOutOfRangeError),
809            Grid4::from_lat_lon(0.0, -180.1)
810        );
811        assert_eq!(
812            Err(Error::CoordinatesOutOfRangeError),
813            Grid4::from_lat_lon(0.0, 180.1)
814        );
815    }
816
817    #[test]
818    fn test_grid6_from_lat_lon() {
819        assert_eq!(
820            Ok(Grid6::from_str("AA00aa").unwrap()),
821            Grid6::from_lat_lon(-90.0, -180.0)
822        );
823        assert_eq!(
824            Ok(Grid6::from_str("RR99xx").unwrap()),
825            Grid6::from_lat_lon(89.9999, 179.9999)
826        );
827        assert_eq!(
828            Ok(Grid6::from_str("JO31mk").unwrap()),
829            Grid6::from_lat_lon(51.4385, 7.0249)
830        );
831
832        assert_eq!(
833            Err(Error::CoordinatesOutOfRangeError),
834            Grid6::from_lat_lon(-90.1, 0.0)
835        );
836        assert_eq!(
837            Err(Error::CoordinatesOutOfRangeError),
838            Grid6::from_lat_lon(90.1, 0.0)
839        );
840        assert_eq!(
841            Err(Error::CoordinatesOutOfRangeError),
842            Grid6::from_lat_lon(0.0, -180.1)
843        );
844        assert_eq!(
845            Err(Error::CoordinatesOutOfRangeError),
846            Grid6::from_lat_lon(0.0, 180.1)
847        );
848    }
849
850    #[test]
851    fn test_grid8_from_lat_lon() {
852        assert_eq!(
853            Ok(Grid8::from_str("AA00aa00").unwrap()),
854            Grid8::from_lat_lon(-90.0, -180.0)
855        );
856        assert_eq!(
857            Ok(Grid8::from_str("RR99xx99").unwrap()),
858            Grid8::from_lat_lon(89.9999, 179.9999)
859        );
860        assert_eq!(
861            Ok(Grid8::from_str("JO31mk25").unwrap()),
862            Grid8::from_lat_lon(51.4385, 7.0249)
863        );
864
865        assert_eq!(
866            Err(Error::CoordinatesOutOfRangeError),
867            Grid8::from_lat_lon(-90.1, 0.0)
868        );
869        assert_eq!(
870            Err(Error::CoordinatesOutOfRangeError),
871            Grid8::from_lat_lon(90.1, 0.0)
872        );
873        assert_eq!(
874            Err(Error::CoordinatesOutOfRangeError),
875            Grid8::from_lat_lon(0.0, -180.1)
876        );
877        assert_eq!(
878            Err(Error::CoordinatesOutOfRangeError),
879            Grid8::from_lat_lon(0.0, 180.1)
880        );
881    }
882
883    #[test]
884    fn test_grid4_from_raw_digits() {
885        assert_eq!(
886            Ok(Grid4::from_str("AA00").unwrap()),
887            Grid4::from_raw_digits(&[0, 0, 0, 0])
888        );
889        assert_eq!(
890            Ok(Grid4::from_str("RR99").unwrap()),
891            Grid4::from_raw_digits(&[17, 17, 9, 9])
892        );
893        assert_eq!(
894            Ok(Grid4::from_str("JO31").unwrap()),
895            Grid4::from_raw_digits(&[9, 14, 3, 1])
896        );
897    }
898
899    #[test]
900    fn test_grid6_from_raw_digits() {
901        assert_eq!(
902            Ok(Grid6::from_str("AA00aa").unwrap()),
903            Grid6::from_raw_digits(&[0, 0, 0, 0, 0, 0])
904        );
905        assert_eq!(
906            Ok(Grid6::from_str("RR99xx").unwrap()),
907            Grid6::from_raw_digits(&[17, 17, 9, 9, 23, 23])
908        );
909        assert_eq!(
910            Ok(Grid6::from_str("JO31mk").unwrap()),
911            Grid6::from_raw_digits(&[9, 14, 3, 1, 12, 10])
912        );
913    }
914
915    #[test]
916    fn test_grid8_from_raw_digits() {
917        assert_eq!(
918            Ok(Grid8::from_str("AA00aa00").unwrap()),
919            Grid8::from_raw_digits(&[0, 0, 0, 0, 0, 0, 0, 0])
920        );
921        assert_eq!(
922            Ok(Grid8::from_str("RR99xx99").unwrap()),
923            Grid8::from_raw_digits(&[17, 17, 9, 9, 23, 23, 9, 9])
924        );
925        assert_eq!(
926            Ok(Grid8::from_str("JO31mk25").unwrap()),
927            Grid8::from_raw_digits(&[9, 14, 3, 1, 12, 10, 2, 5])
928        );
929
930        assert_eq!(
931            Err(Error::ParseError),
932            Grid8::from_raw_digits(&[18, 0, 0, 0, 0, 0, 0, 0])
933        );
934        assert_eq!(
935            Err(Error::ParseError),
936            Grid8::from_raw_digits(&[0, 18, 0, 0, 0, 0, 0, 0])
937        );
938        assert_eq!(
939            Err(Error::ParseError),
940            Grid8::from_raw_digits(&[0, 0, 10, 0, 0, 0, 0, 0])
941        );
942        assert_eq!(
943            Err(Error::ParseError),
944            Grid8::from_raw_digits(&[0, 0, 0, 10, 0, 0, 0, 0])
945        );
946        assert_eq!(
947            Err(Error::ParseError),
948            Grid8::from_raw_digits(&[0, 0, 0, 0, 24, 0, 0, 0])
949        );
950        assert_eq!(
951            Err(Error::ParseError),
952            Grid8::from_raw_digits(&[0, 0, 0, 0, 0, 24, 0, 0])
953        );
954        assert_eq!(
955            Err(Error::ParseError),
956            Grid8::from_raw_digits(&[0, 0, 0, 0, 0, 0, 10, 0])
957        );
958        assert_eq!(
959            Err(Error::ParseError),
960            Grid8::from_raw_digits(&[0, 0, 0, 0, 0, 0, 0, 10])
961        );
962    }
963
964    #[test]
965    fn test_grid4_raw_digits() {
966        assert_eq!(
967            [0, 0, 0, 0], //
968            Grid4::from_str("AA00").unwrap().raw_digits()
969        );
970        assert_eq!(
971            [17, 17, 9, 9],
972            Grid4::from_str("RR99").unwrap().raw_digits()
973        );
974        assert_eq!(
975            [9, 14, 3, 1], //
976            Grid4::from_str("JO31").unwrap().raw_digits()
977        );
978    }
979
980    #[test]
981    fn test_grid6_raw_digits() {
982        assert_eq!(
983            [0, 0, 0, 0, 0, 0],
984            Grid6::from_str("AA00aa").unwrap().raw_digits()
985        );
986        assert_eq!(
987            [17, 17, 9, 9, 23, 23],
988            Grid6::from_str("RR99xx").unwrap().raw_digits()
989        );
990        assert_eq!(
991            [9, 14, 3, 1, 12, 10],
992            Grid6::from_str("JO31mk").unwrap().raw_digits()
993        );
994    }
995
996    #[test]
997    fn test_grid8_raw_digits() {
998        assert_eq!(
999            [0, 0, 0, 0, 0, 0, 0, 0],
1000            Grid8::from_str("AA00aa00").unwrap().raw_digits()
1001        );
1002        assert_eq!(
1003            [17, 17, 9, 9, 23, 23, 9, 9],
1004            Grid8::from_str("RR99xx99").unwrap().raw_digits()
1005        );
1006        assert_eq!(
1007            [9, 14, 3, 1, 12, 10, 2, 5],
1008            Grid8::from_str("JO31mk25").unwrap().raw_digits()
1009        );
1010    }
1011
1012    #[test]
1013    fn test_send() {
1014        fn assert_send<T: Send>() {}
1015        assert_send::<Grid4>();
1016        assert_send::<Grid6>();
1017        assert_send::<Grid8>();
1018    }
1019
1020    #[test]
1021    fn test_sync() {
1022        fn assert_sync<T: Sync>() {}
1023        assert_sync::<Grid4>();
1024        assert_sync::<Grid6>();
1025        assert_sync::<Grid8>();
1026    }
1027}