use std::fmt;
use std::marker::PhantomData;
use std::ops::{Add, Div, Mul, Neg, Sub};
pub trait Unit: Copy + Clone + fmt::Debug + PartialEq + Eq {
const NAME: &'static str;
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Dimension<U: Unit> {
raw: i64,
_unit: PhantomData<U>,
}
impl<U: Unit> Dimension<U> {
pub const ZERO: Self = Self {
raw: 0,
_unit: PhantomData,
};
pub const fn new(raw: i64) -> Self {
Self {
raw,
_unit: PhantomData,
}
}
pub const fn raw(self) -> i64 {
self.raw
}
}
impl<U: Unit> fmt::Debug for Dimension<U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.raw, U::NAME)
}
}
impl<U: Unit> Default for Dimension<U> {
fn default() -> Self {
Self::ZERO
}
}
impl<U: Unit> PartialOrd for Dimension<U> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<U: Unit> Ord for Dimension<U> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.raw.cmp(&other.raw)
}
}
impl<U: Unit> Add for Dimension<U> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.raw + rhs.raw)
}
}
impl<U: Unit> Sub for Dimension<U> {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.raw - rhs.raw)
}
}
impl<U: Unit> Mul<i64> for Dimension<U> {
type Output = Self;
fn mul(self, rhs: i64) -> Self {
Self::new(self.raw * rhs)
}
}
impl<U: Unit> Div<i64> for Dimension<U> {
type Output = Self;
fn div(self, rhs: i64) -> Self {
Self::new(self.raw / rhs)
}
}
impl<U: Unit> Neg for Dimension<U> {
type Output = Self;
fn neg(self) -> Self {
Self::new(-self.raw)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Twips;
impl Unit for Twips {
const NAME: &'static str = "twip";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct HalfPoints;
impl Unit for HalfPoints {
const NAME: &'static str = "hp";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Emu;
impl Unit for Emu {
const NAME: &'static str = "emu";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EighthPoints;
impl Unit for EighthPoints {
const NAME: &'static str = "ep";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FractionPoints;
impl Unit for FractionPoints {
const NAME: &'static str = "fp4096";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Points;
impl Unit for Points {
const NAME: &'static str = "pt";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ThousandthPercent;
impl Unit for ThousandthPercent {
const NAME: &'static str = "‰%";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SixtieThousandthDeg;
impl Unit for SixtieThousandthDeg {
const NAME: &'static str = "∠60k";
}
impl Dimension<Twips> {
pub fn to_half_points(self) -> Dimension<HalfPoints> {
Dimension::new(self.raw / 10)
}
pub fn to_emu(self) -> Dimension<Emu> {
Dimension::new(self.raw * 635)
}
pub fn to_points_f32(self) -> f32 {
self.raw as f32 / 20.0
}
}
impl Dimension<HalfPoints> {
pub fn to_twips(self) -> Dimension<Twips> {
Dimension::new(self.raw * 10)
}
pub fn to_points_f32(self) -> f32 {
self.raw as f32 / 2.0
}
}
impl Dimension<Emu> {
pub fn to_twips(self) -> Dimension<Twips> {
Dimension::new(self.raw / 635)
}
pub fn to_points_f32(self) -> f32 {
self.raw as f32 / 12700.0
}
}
impl Dimension<EighthPoints> {
pub fn to_half_points(self) -> Dimension<HalfPoints> {
Dimension::new(self.raw / 4)
}
pub fn to_points_f32(self) -> f32 {
self.raw as f32 / 8.0
}
}
impl Dimension<ThousandthPercent> {
pub fn to_fraction(self) -> f32 {
self.raw as f32 / 100_000.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-6, "{a} != {b}");
}
#[test]
fn twips_to_half_points() {
assert_eq!(Dimension::<Twips>::new(20).to_half_points().raw(), 2);
assert_eq!(Dimension::<Twips>::new(1440).to_half_points().raw(), 144);
}
#[test]
fn twips_emu_roundtrip() {
assert_eq!(Dimension::<Twips>::new(1440).to_emu().raw(), 914_400);
assert_eq!(Dimension::<Emu>::new(914_400).to_twips().raw(), 1440);
}
#[test]
fn to_points_conversions() {
approx(Dimension::<Twips>::new(1440).to_points_f32(), 72.0);
approx(Dimension::<Twips>::new(240).to_points_f32(), 12.0);
approx(Dimension::<HalfPoints>::new(24).to_points_f32(), 12.0);
approx(Dimension::<Emu>::new(12_700).to_points_f32(), 1.0);
approx(Dimension::<EighthPoints>::new(8).to_points_f32(), 1.0);
}
#[test]
fn half_points_to_twips() {
assert_eq!(Dimension::<HalfPoints>::new(24).to_twips().raw(), 240);
}
#[test]
fn eighth_points_to_half_points() {
assert_eq!(Dimension::<EighthPoints>::new(8).to_half_points().raw(), 2);
}
#[test]
fn thousandth_percent_to_fraction() {
approx(
Dimension::<ThousandthPercent>::new(50_000).to_fraction(),
0.5,
);
approx(
Dimension::<ThousandthPercent>::new(100_000).to_fraction(),
1.0,
);
approx(Dimension::<ThousandthPercent>::new(0).to_fraction(), 0.0);
}
#[test]
fn arithmetic_ops() {
let a = Dimension::<Twips>::new(100);
let b = Dimension::<Twips>::new(30);
assert_eq!((a + b).raw(), 130);
assert_eq!((a - b).raw(), 70);
assert_eq!((a * 3).raw(), 300);
assert_eq!((a / 4).raw(), 25); assert_eq!((-a).raw(), -100);
}
#[test]
fn zero_and_default() {
assert_eq!(Dimension::<Twips>::ZERO.raw(), 0);
assert_eq!(Dimension::<Emu>::default().raw(), 0);
}
#[test]
fn ordering_and_equality() {
assert!(Dimension::<Twips>::new(10) < Dimension::<Twips>::new(20));
assert_eq!(Dimension::<Twips>::new(5), Dimension::<Twips>::new(5));
}
}