1pub mod base;
32pub mod composite;
33
34use crate::dimension::{Dimension, Rational16};
35use crate::error::{UnitError, UnitResult};
36use base::BaseUnit;
37use composite::CompositeUnit;
38use std::fmt;
39use std::ops::{Div, Mul};
40
41#[derive(Clone, Debug, PartialEq)]
65pub enum Unit {
66 Base(BaseUnit),
68
69 Composite(CompositeUnit),
71
72 Dimensionless {
74 scale: f64,
76 },
77}
78
79impl Unit {
80 pub fn dimensionless() -> Self {
82 Unit::Dimensionless { scale: 1.0 }
83 }
84
85 pub fn dimensionless_scaled(scale: f64) -> Self {
87 Unit::Dimensionless { scale }
88 }
89
90 pub fn from_base(base: &BaseUnit) -> Self {
92 Unit::Base(*base)
93 }
94
95 pub fn dimension(&self) -> Dimension {
97 match self {
98 Unit::Base(b) => b.dimension,
99 Unit::Composite(c) => c.dimension(),
100 Unit::Dimensionless { .. } => Dimension::DIMENSIONLESS,
101 }
102 }
103
104 pub fn scale(&self) -> f64 {
106 match self {
107 Unit::Base(b) => b.scale,
108 Unit::Composite(c) => c.total_scale(),
109 Unit::Dimensionless { scale } => *scale,
110 }
111 }
112
113 pub fn is_dimensionless(&self) -> bool {
115 self.dimension().is_dimensionless()
116 }
117
118 pub fn offset(&self) -> f64 {
128 match self {
129 Unit::Base(b) => b.offset,
130 Unit::Composite(_) | Unit::Dimensionless { .. } => 0.0,
131 }
132 }
133
134 pub fn has_offset(&self) -> bool {
138 self.offset() != 0.0
139 }
140
141 pub fn to_si(&self, value: f64) -> f64 {
149 (value + self.offset()) * self.scale()
150 }
151
152 pub fn from_si(&self, si_value: f64) -> f64 {
157 si_value / self.scale() - self.offset()
158 }
159
160 pub fn conversion_factor(&self, to: &Unit) -> UnitResult<f64> {
169 if self.dimension() != to.dimension() {
170 return Err(UnitError::DimensionMismatch {
171 from: self.to_string(),
172 to: to.to_string(),
173 });
174 }
175 if self == to {
177 return Ok(1.0);
178 }
179 if self.has_offset() || to.has_offset() {
180 return Err(UnitError::OffsetConversion {
181 from: self.to_string(),
182 to: to.to_string(),
183 });
184 }
185 Ok(self.scale() / to.scale())
186 }
187
188 pub fn to_composite(&self) -> CompositeUnit {
190 match self {
191 Unit::Base(b) => CompositeUnit::from_base(b.symbol, b.dimension, b.scale),
192 Unit::Composite(c) => c.clone(),
193 Unit::Dimensionless { scale } => CompositeUnit::dimensionless(*scale),
194 }
195 }
196
197 pub fn pow(&self, exp: impl Into<Rational16>) -> Unit {
199 let exp = exp.into();
200 if exp.is_zero() {
201 return Unit::dimensionless();
202 }
203 if exp == Rational16::ONE {
204 return self.clone();
205 }
206 Unit::Composite(self.to_composite().pow(exp))
207 }
208
209 pub fn sqrt(&self) -> Unit {
211 self.pow(Rational16::new(1, 2))
212 }
213
214 pub fn inv(&self) -> Unit {
216 self.pow(Rational16::new(-1, 1))
217 }
218
219 pub fn symbol(&self) -> String {
221 match self {
222 Unit::Base(b) => b.symbol.to_string(),
223 Unit::Composite(c) => c.to_string(),
224 Unit::Dimensionless { scale } => {
225 if (*scale - 1.0).abs() < 1e-15 {
226 "".to_string()
227 } else {
228 format!("{}", scale)
229 }
230 }
231 }
232 }
233}
234
235impl fmt::Display for Unit {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 Unit::Base(b) => write!(f, "{}", b.symbol),
239 Unit::Composite(c) => write!(f, "{}", c),
240 Unit::Dimensionless { scale } => {
241 if (*scale - 1.0).abs() < 1e-15 {
242 write!(f, "dimensionless")
243 } else {
244 write!(f, "{}", scale)
245 }
246 }
247 }
248 }
249}
250
251impl From<BaseUnit> for Unit {
252 fn from(b: BaseUnit) -> Unit {
253 Unit::Base(b)
254 }
255}
256
257impl From<&BaseUnit> for Unit {
258 fn from(b: &BaseUnit) -> Unit {
259 Unit::Base(*b)
260 }
261}
262
263impl From<&Unit> for Unit {
264 fn from(u: &Unit) -> Unit {
265 u.clone()
266 }
267}
268
269impl Mul for Unit {
271 type Output = Unit;
272
273 fn mul(self, rhs: Unit) -> Unit {
274 Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
275 }
276}
277
278impl Mul for &Unit {
279 type Output = Unit;
280
281 fn mul(self, rhs: &Unit) -> Unit {
282 Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
283 }
284}
285
286impl Mul<&Unit> for Unit {
287 type Output = Unit;
288
289 fn mul(self, rhs: &Unit) -> Unit {
290 Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
291 }
292}
293
294impl Mul<Unit> for &Unit {
295 type Output = Unit;
296
297 fn mul(self, rhs: Unit) -> Unit {
298 Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
299 }
300}
301
302impl Div for Unit {
304 type Output = Unit;
305
306 fn div(self, rhs: Unit) -> Unit {
307 Unit::Composite(self.to_composite().div(&rhs.to_composite()))
308 }
309}
310
311impl Div for &Unit {
312 type Output = Unit;
313
314 fn div(self, rhs: &Unit) -> Unit {
315 Unit::Composite(self.to_composite().div(&rhs.to_composite()))
316 }
317}
318
319impl Div<&Unit> for Unit {
320 type Output = Unit;
321
322 fn div(self, rhs: &Unit) -> Unit {
323 Unit::Composite(self.to_composite().div(&rhs.to_composite()))
324 }
325}
326
327impl Div<Unit> for &Unit {
328 type Output = Unit;
329
330 fn div(self, rhs: Unit) -> Unit {
331 Unit::Composite(self.to_composite().div(&rhs.to_composite()))
332 }
333}
334
335impl Mul for BaseUnit {
337 type Output = Unit;
338
339 fn mul(self, rhs: BaseUnit) -> Unit {
340 Unit::from(self) * Unit::from(rhs)
341 }
342}
343
344impl Div for BaseUnit {
346 type Output = Unit;
347
348 fn div(self, rhs: BaseUnit) -> Unit {
349 Unit::from(self) / Unit::from(rhs)
350 }
351}
352
353impl Mul<Unit> for BaseUnit {
355 type Output = Unit;
356
357 fn mul(self, rhs: Unit) -> Unit {
358 Unit::from(self) * rhs
359 }
360}
361
362impl Mul<BaseUnit> for Unit {
364 type Output = Unit;
365
366 fn mul(self, rhs: BaseUnit) -> Unit {
367 self * Unit::from(rhs)
368 }
369}
370
371impl Div<Unit> for BaseUnit {
373 type Output = Unit;
374
375 fn div(self, rhs: Unit) -> Unit {
376 Unit::from(self) / rhs
377 }
378}
379
380impl Div<BaseUnit> for Unit {
382 type Output = Unit;
383
384 fn div(self, rhs: BaseUnit) -> Unit {
385 self / Unit::from(rhs)
386 }
387}
388
389impl Mul<BaseUnit> for &Unit {
391 type Output = Unit;
392
393 fn mul(self, rhs: BaseUnit) -> Unit {
394 self * &Unit::from(rhs)
395 }
396}
397
398impl Div<BaseUnit> for &Unit {
399 type Output = Unit;
400
401 fn div(self, rhs: BaseUnit) -> Unit {
402 self / &Unit::from(rhs)
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn meter() -> Unit {
411 Unit::Base(BaseUnit::new("meter", "m", &[], Dimension::LENGTH, 1.0))
412 }
413
414 fn second() -> Unit {
415 Unit::Base(BaseUnit::new("second", "s", &[], Dimension::TIME, 1.0))
416 }
417
418 fn kilometer() -> Unit {
419 Unit::Base(BaseUnit::new(
420 "kilometer",
421 "km",
422 &[],
423 Dimension::LENGTH,
424 1000.0,
425 ))
426 }
427
428 #[test]
429 fn test_unit_division() {
430 let velocity = meter() / second();
431 let dim = velocity.dimension();
432 assert_eq!(dim.length, Rational16::ONE);
433 assert_eq!(dim.time, Rational16::new(-1, 1));
434 }
435
436 #[test]
437 fn test_conversion_factor() {
438 let m = meter();
439 let km = kilometer();
440 let factor = km.conversion_factor(&m).unwrap();
441 assert!((factor - 1000.0).abs() < 1e-10);
442 }
443
444 #[test]
445 fn test_incompatible_conversion() {
446 let m = meter();
447 let s = second();
448 let result = m.conversion_factor(&s);
449 assert!(matches!(result, Err(UnitError::DimensionMismatch { .. })));
450 }
451
452 #[test]
453 fn test_unit_power() {
454 let m = meter();
455 let m2 = m.pow(2);
456 let dim = m2.dimension();
457 assert_eq!(dim.length, Rational16::new(2, 1));
458 }
459
460 #[test]
461 fn test_unit_sqrt() {
462 let m = meter();
463 let m2 = &m * &m;
464 let sqrt_m2 = m2.sqrt();
465 let dim = sqrt_m2.dimension();
466 assert_eq!(dim.length, Rational16::ONE);
467 }
468
469 #[test]
470 fn test_offset_unit_conversion_factor_rejected() {
471 let kelvin = Unit::Base(BaseUnit::new(
472 "kelvin",
473 "K",
474 &[],
475 Dimension::TEMPERATURE,
476 1.0,
477 ));
478 let celsius = Unit::Base(BaseUnit::with_offset(
479 "celsius",
480 "°C",
481 &[],
482 Dimension::TEMPERATURE,
483 1.0,
484 273.15,
485 ));
486 let result = celsius.conversion_factor(&kelvin);
487 assert!(matches!(result, Err(UnitError::OffsetConversion { .. })));
488 }
489
490 #[test]
491 fn test_offset_unit_identity_conversion_ok() {
492 let celsius = Unit::Base(BaseUnit::with_offset(
493 "celsius",
494 "°C",
495 &[],
496 Dimension::TEMPERATURE,
497 1.0,
498 273.15,
499 ));
500 let result = celsius.conversion_factor(&celsius);
501 assert!(result.is_ok());
502 assert!((result.unwrap() - 1.0).abs() < 1e-15);
503 }
504
505 #[test]
506 fn test_pow_one_preserves_unit() {
507 let celsius = Unit::Base(BaseUnit::with_offset(
508 "celsius",
509 "°C",
510 &[],
511 Dimension::TEMPERATURE,
512 1.0,
513 273.15,
514 ));
515 let powered = celsius.pow(1);
516 assert!(powered.has_offset());
517 assert_eq!(celsius, powered);
518 }
519}