use core::hash::{Hash, Hasher};
const POS_ZERO_BITS: u64 = 0.0_f64.to_bits();
const NEG_ZERO_BITS: u64 = (-0.0_f64).to_bits();
#[derive(Clone, Copy, Debug, Default)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
#[must_use]
pub const fn key(self) -> PointKey {
PointKey::new(self.x, self.y)
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite()
}
#[must_use]
pub fn distance_squared(self, other: Self) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
dx * dx + dy * dy
}
#[must_use]
pub fn distance(self, other: Self) -> f64 {
self.distance_squared(other).sqrt()
}
}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl Eq for Point {}
impl Hash for Point {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PointKey {
x: u64,
y: u64,
}
impl PointKey {
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self {
x: normalize_bits(x),
y: normalize_bits(y),
}
}
#[must_use]
pub const fn bits(self) -> (u64, u64) {
(self.x, self.y)
}
}
impl From<Point> for PointKey {
fn from(point: Point) -> Self {
point.key()
}
}
const fn normalize_bits(value: f64) -> u64 {
let bits = value.to_bits();
if bits == NEG_ZERO_BITS {
POS_ZERO_BITS
} else {
bits
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::{Point, PointKey};
use std::collections::HashMap;
#[test]
fn zeroes_are_one_point() {
assert_eq!(Point::new(-0.0, -0.0), Point::new(0.0, 0.0));
assert_eq!(PointKey::new(-0.0, 1.0), PointKey::new(0.0, 1.0));
assert_eq!(PointKey::new(0.0, -0.0).bits(), (0, 0));
}
#[test]
fn identity_has_no_tolerance() {
let a = Point::new(1.0, 1.0);
let b = Point::new(1.0 + f64::EPSILON, 1.0);
assert_ne!(a, b);
assert_ne!(a.key(), b.key());
}
#[test]
fn key_survives_a_hash_map_round_trip() {
let mut map = HashMap::new();
map.insert(Point::new(3.5, -0.0).key(), "here");
assert_eq!(map.get(&Point::new(3.5, 0.0).key()), Some(&"here"));
assert_eq!(map.get(&Point::new(3.5, 1e-300).key()), None);
}
#[test]
fn nan_is_not_equal_to_itself_bitwise_but_keys_alike() {
let nan = Point::new(f64::NAN, 0.0);
assert_eq!(nan.key(), nan.key());
}
#[test]
fn distance_is_euclidean() {
let a = Point::new(0.0, 0.0);
let b = Point::new(3.0, 4.0);
assert!((a.distance(b) - 5.0).abs() < 1e-15);
assert!((a.distance_squared(b) - 25.0).abs() < 1e-15);
}
}