Skip to content

Commit

Permalink
Merge pull request #1 from BIS-Brecon/tetrads
Browse files Browse the repository at this point in the history
Tetrads
  • Loading branch information
BezBIS authored Apr 22, 2024
2 parents 123d0da + 757f248 commit 28e8701
Show file tree
Hide file tree
Showing 10 changed files with 574 additions and 26 deletions.
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ authors = ["Berwyn Powell"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/BIS-Brecon/gridish"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
tetrads = []

[dependencies]
geo-types = "0.7.13"
serde = { version = "1", optional = true }

[dev-dependencies]
criterion = "0.5.1"
serde_json = "1"

[[bench]]
name = "parsing"
harness = false

[[bench]]
name = "printing"
harness = false
66 changes: 66 additions & 0 deletions benches/printing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use criterion::criterion_group;
use criterion::criterion_main;
use criterion::BenchmarkId;
use criterion::Criterion;
use criterion::Throughput;
use gridish::Precision;
use gridish::{OSGB, OSI};

const PRECISIONS: [Precision; 6] = [
Precision::_100Km,
Precision::_10Km,
Precision::_1Km,
Precision::_100M,
Precision::_10M,
Precision::_1M,
];

const EASTINGS: u32 = 123_456;
const NORTHINGS: u32 = 234_567;

pub fn to_string_osgb(c: &mut Criterion) {
let mut group = c.benchmark_group("to_string_osgb");

for precision in PRECISIONS.iter() {
group.throughput(Throughput::Elements(1));

group.bench_with_input(
BenchmarkId::from_parameter(format!("{} digits", precision.digits())),
precision,
|b, &precision| {
b.iter(|| {
OSGB::new(EASTINGS, NORTHINGS, precision)
.unwrap()
.to_string()
});
},
);
}

group.finish();
}

pub fn to_string_osi(c: &mut Criterion) {
let mut group = c.benchmark_group("to_string_osi");

for precision in PRECISIONS.iter() {
group.throughput(Throughput::Elements(1));

group.bench_with_input(
BenchmarkId::from_parameter(format!("{} digits", precision.digits())),
precision,
|b, &precision| {
b.iter(|| {
OSI::new(EASTINGS, NORTHINGS, precision)
.unwrap()
.to_string()
});
},
);
}

group.finish();
}

criterion_group!(benches, to_string_osgb, to_string_osi);
criterion_main!(benches);
1 change: 1 addition & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub const _500KM: u32 = 500_000;
pub const _100KM: u32 = 100_000;
pub const _10KM: u32 = 10_000;
pub const _2KM: u32 = 2_000;
pub const _1KM: u32 = 1_000;
pub const _100M: u32 = 100;
pub const _10M: u32 = 10;
Expand Down
115 changes: 105 additions & 10 deletions src/coordinates/point.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::constants::_100KM;
use crate::constants::*;
use crate::coordinates::metres::Metres;
use crate::grid::{coords_to_square, square_to_coords};
use crate::{utils, Error, Precision};
use std::fmt::Display;
use std::str::FromStr;

#[cfg(feature = "tetrads")]
use crate::grid::{coords_to_tetrad, tetrad_to_coords};

/// The core of the British and Irish national grids.
/// A coordinate point that can represent any location
/// on a 500km grid at up to 1m precision.
Expand Down Expand Up @@ -48,19 +51,38 @@ impl FromStr for Point {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.chars().next() {
Some(c) => {
// Determine grid square
// Determine grid square and add to easting and northings
let (column, row) = square_to_coords(&c)?;
let eastings = column as u32 * _100KM;
let northings = row as u32 * _100KM;

// Special case for Tetrads
#[cfg(feature = "tetrads")]
if s.len() == 4 {
if let Some(c) = s.chars().last() {
if c.is_ascii_alphabetic() {
// Get Tetrad square
let (column, row) = tetrad_to_coords(&c)?;
let eastings = eastings + (column as u32 * _2KM);
let northings = northings + (row as u32 * _2KM);

// Calculate digits
let (east, north, _precision) = utils::digits(&s[1..s.len() - 1])?;

return Ok(Self {
eastings: (eastings + east).try_into()?,
northings: (northings + north).try_into()?,
precision: Precision::_2Km,
});
}
}
}

// Parse digits and precision
let (eastings, northings, precision) = utils::digits(&s[1..s.len()])?;

// Finally, add grid square to eastings and northings.
let eastings = (column as u32 * _100KM) + eastings;
let northings = (row as u32 * _100KM) + northings;

let (east, north, precision) = utils::digits(&s[1..s.len()])?;
Ok(Self {
eastings: eastings.try_into()?,
northings: northings.try_into()?,
eastings: (eastings + east).try_into()?,
northings: (northings + north).try_into()?,
precision,
})
}
Expand All @@ -80,6 +102,25 @@ impl Display for Point {
// Unwrapping here as metres are type checked to fit into bounds.
let letter = coords_to_square(column, row).unwrap();

// Special case for Tetrads
#[cfg(feature = "tetrads")]
if self.precision == Precision::_2Km {
// Determine tetrad.
let tetrad_column = ((eastings % _10KM) / _2KM) as usize;
let tetrad_row = ((northings % _10KM) / _2KM) as usize;
// Unwrapping here as metres are type checked to fit into bounds.
let tetrad = coords_to_tetrad(tetrad_column, tetrad_row).unwrap();

return write!(
f,
"{}{}{}{}",
letter,
self.eastings.padded(Precision::_10Km),
self.northings.padded(Precision::_10Km),
tetrad
);
}

write!(
f,
"{}{}{}",
Expand Down Expand Up @@ -154,3 +195,57 @@ mod test {
}
}
}

#[cfg(feature = "tetrads")]
#[cfg(test)]
mod test_tetrad {
use crate::coordinates::point::Point;
use crate::precision::Precision;

struct TestPoint {
eastings: u32,
northings: u32,
precision: Precision,
}

const VALID_TETRADS: [(&'static str, TestPoint); 2] = [
(
"L03P",
TestPoint {
eastings: 4_000,
northings: 238_000,
precision: Precision::_2Km,
},
),
(
"N24R",
TestPoint {
eastings: 226_000,
northings: 242_000,
precision: Precision::_2Km,
},
),
];

#[test]
fn parses_valid_tetrads() {
for point in VALID_TETRADS {
let grid_point: Point = point.0.parse().unwrap();

assert_eq!(grid_point.eastings.inner(), point.1.eastings);
assert_eq!(grid_point.northings.inner(), point.1.northings);
assert_eq!(grid_point.precision, point.1.precision);
}
}

#[test]
fn prints_valid_strings() {
for point in VALID_TETRADS {
let eastings = point.1.eastings.try_into().unwrap();
let northings = point.1.northings.try_into().unwrap();
let grid_point = Point::new(eastings, northings, point.1.precision);

assert_eq!(grid_point.to_string(), point.0);
}
}
}
105 changes: 101 additions & 4 deletions src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ const GRID: [char; 25] = [
'K', 'A', 'B', 'C', 'D', 'E',
];

pub fn square_to_coords(square: &char) -> Result<(usize, usize), Error> {
Ok(grid_to_coords(square, &GRID)?)
}

pub fn coords_to_square(column: usize, row: usize) -> Result<char, Error> {
Ok(coords_to_grid(column, row, &GRID)?)
}

/// Return the coordinates of the given grid square.
/// This is zero-based and scale agnostic, so H => (1, 3);
pub fn square_to_coords(square: &char) -> Result<(usize, usize), Error> {
let index = GRID
fn grid_to_coords(square: &char, grid: &[char]) -> Result<(usize, usize), Error> {
let index = grid
.iter()
.position(|x| x == square)
.ok_or_else(|| Error::ParseError(format!("{square} is not a valid grid square.")))?;
Expand All @@ -26,16 +34,33 @@ pub fn square_to_coords(square: &char) -> Result<(usize, usize), Error> {

/// Returns the grid square of the given coordinates.
/// This is zero-based and scale agnostic, so (1, 1) => R;
pub fn coords_to_square(column: usize, row: usize) -> Result<char, Error> {
fn coords_to_grid(column: usize, row: usize, grid: &[char]) -> Result<char, Error> {
if column >= GRID_WIDTH || row >= GRID_WIDTH {
Err(Error::OutOfBounds)
} else {
let index = column + (GRID_WIDTH * row);

Ok(*GRID.get(index).ok_or_else(|| Error::OutOfBounds)?)
Ok(*grid.get(index).ok_or_else(|| Error::OutOfBounds)?)
}
}

/// The grid used for tetrad coordinates.
#[cfg(feature = "tetrads")]
const TETRAD_GRID: [char; 25] = [
'A', 'F', 'K', 'Q', 'V', 'B', 'G', 'L', 'R', 'W', 'C', 'H', 'M', 'S', 'X', 'D', 'I', 'N', 'T',
'Y', 'E', 'J', 'P', 'U', 'Z',
];

#[cfg(feature = "tetrads")]
pub fn tetrad_to_coords(square: &char) -> Result<(usize, usize), Error> {
Ok(grid_to_coords(square, &TETRAD_GRID)?)
}

#[cfg(feature = "tetrads")]
pub fn coords_to_tetrad(column: usize, row: usize) -> Result<char, Error> {
Ok(coords_to_grid(column, row, &TETRAD_GRID)?)
}

#[cfg(test)]
mod test {
use crate::{
Expand Down Expand Up @@ -113,3 +138,75 @@ mod test {
}
}
}

#[cfg(feature = "tetrads")]
#[cfg(test)]
mod test_tetrad {
use crate::grid::{coords_to_tetrad, tetrad_to_coords};
use crate::Error;

const VALID_TETRADS: [(char, (usize, usize)); 25] = [
('A', (0, 0)),
('B', (0, 1)),
('C', (0, 2)),
('D', (0, 3)),
('E', (0, 4)),
('F', (1, 0)),
('G', (1, 1)),
('H', (1, 2)),
('I', (1, 3)),
('J', (1, 4)),
('K', (2, 0)),
('L', (2, 1)),
('M', (2, 2)),
('N', (2, 3)),
('P', (2, 4)),
('Q', (3, 0)),
('R', (3, 1)),
('S', (3, 2)),
('T', (3, 3)),
('U', (3, 4)),
('V', (4, 0)),
('W', (4, 1)),
('X', (4, 2)),
('Y', (4, 3)),
('Z', (4, 4)),
];

#[test]
fn valid_letters_return_coords_tetrad() {
for square in VALID_TETRADS {
assert_eq!(tetrad_to_coords(&square.0), Ok(square.1));
}
}

#[test]
fn invalid_letters_are_rejected_tetrad() {
let squares = ['a', 'O', '0', '@'];

for square in squares {
assert_eq!(
tetrad_to_coords(&square),
Err(Error::ParseError(format!(
"{square} is not a valid grid square."
)))
);
}
}

#[test]
fn valid_coords_return_letter_tetrad() {
for square in VALID_TETRADS {
assert_eq!(coords_to_tetrad(square.1 .0, square.1 .1), Ok(square.0));
}
}

#[test]
fn invalid_coords_are_rejected_tetrad() {
let coords = [(0, 5), (5, 0)];

for coord in coords {
assert_eq!(coords_to_tetrad(coord.0, coord.1), Err(Error::OutOfBounds));
}
}
}
Loading

0 comments on commit 28e8701

Please sign in to comment.