Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tetrads #1

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading