dv_rs/
lib.rs

1#![doc = include_str!("../DOCS.md")]
2
3pub mod units;
4
5pub struct DimensionalVariable {
6    pub value: f64,
7    pub unit: [f64; units::BASE_UNITS_SIZE],
8}
9
10/// A struct representing a dimensional variable with a value and a unit.
11impl DimensionalVariable {
12    /// Creates a new DimensionalVariable with the given value and unit.
13    ///
14    /// Rules for writing the unit string:
15    /// - Use a single `/` as a delimiter between the numerator and denominator.
16    /// - Use `-` as a delimiter between individual units
17    /// - Exponents can be represented either by using `^` to indicate exponent (ex. `m^2`) or without the delimiter (ex. `m2`)
18    /// - Inverses can be represented either by negative exponents or in the denominator (ex. `m^-2` or `1/m^2`)
19    /// 
20    /// Returns an error if the unit string is invalid or contains unknown units.
21    pub fn new(value: f64, unit_str: &str) -> Result<Self, String> {
22        // Fetch the unit details from the unit map
23        let (base_unit, conversion_factor) = unit_str_to_base_unit(unit_str)
24            .map_err(|e| format!("Failed to parse unit '{}': {}", unit_str, e))?;
25
26        // Create the DimensionalVariable with the converted value
27        return Ok(DimensionalVariable {
28            value: value * conversion_factor,
29            unit: base_unit,
30        });
31    }
32
33    /// Returns the value of this DimensionalVariable.
34    pub fn value(&self) -> f64 {
35        self.value
36    }
37
38    /// Converts the value of this DimensionalVariable to the specified unit.
39    /// Returns an error if the unit string is invalid or incompatible.
40    pub fn value_in(&self, unit_str: &str) -> Result<f64, String> {
41
42        let (unit, conversion_factor) = unit_str_to_base_unit(unit_str)
43            .map_err(|e| format!("Failed to parse unit '{}': {}", unit_str, e))?;
44
45        // Check if the units are compatible
46        if self.unit != unit {
47            return Err(format!("Incompatible unit conversion to: {}", unit_str));
48        }
49        
50        return Ok(self.value / conversion_factor);
51    }
52
53    /// Returns the base unit array of this DimensionalVariable.
54    pub fn unit(&self) -> [f64; units::BASE_UNITS_SIZE] {
55        return self.unit;
56    }
57
58    /// Returns whether the variable is unitless (all base exponents are 0).
59    pub fn is_unitless(&self) -> bool {
60        return self.unit.iter().all(|&e| e == 0.0);
61    }
62
63    /// Fallible add with unit compatibility check.
64    pub fn try_add(&self, other: &DimensionalVariable) -> Result<DimensionalVariable, String> {
65        if self.unit != other.unit {
66            return Err("Incompatible units for addition".to_string());
67        }
68        return Ok(DimensionalVariable { value: self.value + other.value, unit: self.unit });
69    }
70
71    /// Fallible subtraction with unit compatibility check.
72    pub fn try_sub(&self, other: &DimensionalVariable) -> Result<DimensionalVariable, String> {
73        if self.unit != other.unit {
74            return Err("Incompatible units for subtraction".to_string());
75        }
76        return Ok(DimensionalVariable { value: self.value - other.value, unit: self.unit });
77    }
78
79    // ---- Math: powers and roots ----
80    /// Raise to integer power. Units exponents are multiplied by exp.
81    /// Returns a new DimensionalVariable.
82    pub fn powi(&self, exp: i32) -> DimensionalVariable {
83        let mut unit = self.unit;
84    for i in 0..units::BASE_UNITS_SIZE { unit[i] *= exp as f64; }
85        return DimensionalVariable { value: self.value.powi(exp), unit };
86    }
87
88    /// Raise to floating power.
89    /// Returns a new DimensionalVariable.
90    pub fn powf(&self, exp: f64) -> Result<DimensionalVariable, String> {
91        let mut unit = self.unit;
92        for i in 0..units::BASE_UNITS_SIZE { unit[i] *= exp; }
93        return Ok(DimensionalVariable { value: self.value.powf(exp), unit });
94    }
95
96    /// Square root. Allowed only when all unit exponents are value >= 0 (no complex results).
97    /// Returns a new DimensionalVariable.
98    pub fn sqrt(&self) -> Result<DimensionalVariable, String> {
99        if self.value < 0.0 {
100            return Err("sqrt of negative value".to_string());
101        }
102        return self.powf(0.5);
103    }
104
105    // ---- Math: logarithms (unitless only) ----
106    /// Natural logarithm. Requires unitless and value > 0.
107    pub fn ln(&self) -> Result<f64, String> {
108        if !self.is_unitless() { return Err("ln requires a unitless quantity".to_string()); }
109        if self.value <= 0.0 { return Err("ln domain error (value <= 0)".to_string()); }
110        Ok(self.value.ln())
111    }
112
113    /// Base-2 logarithm. Requires unitless and value > 0.
114    pub fn log2(&self) -> Result<f64, String> {
115        if !self.is_unitless() { return Err("log2 requires a unitless quantity".to_string()); }
116        if self.value <= 0.0 { return Err("log2 domain error (value <= 0)".to_string()); }
117        Ok(self.value.log2())
118    }
119
120    /// Base-10 logarithm. Requires unitless and value > 0.
121    pub fn log10(&self) -> Result<f64, String> {
122        if !self.is_unitless() { return Err("log10 requires a unitless quantity".to_string()); }
123        if self.value <= 0.0 { return Err("log10 domain error (value <= 0)".to_string()); }
124        Ok(self.value.log10())
125    }
126
127    // ---- Math: trigonometry (requires angle dimension or unitless) ----
128    /// Check if the variable has angle dimension (only rad exponent is non-zero)
129    fn is_angle(&self) -> bool {
130        // Angle dimension: [0, 0, 0, 0, 0, 0, 0, 1] (only rad exponent is 1)
131        for i in 0..units::BASE_UNITS_SIZE - 1 {
132            if self.unit[i] != 0.0 {
133                return false;
134            }
135        }
136        self.unit[units::BASE_UNITS_SIZE - 1] == 1.0
137    }
138
139    /// Sine function. Requires angle (radians) or unitless.
140    pub fn sin(&self) -> Result<f64, String> {
141        if !self.is_unitless() && !self.is_angle() { 
142            return Err("sin requires an angle or unitless quantity".to_string()); 
143        }
144        Ok(self.value.sin())
145    }
146
147    /// Cosine function. Requires angle (radians) or unitless.
148    pub fn cos(&self) -> Result<f64, String> {
149        if !self.is_unitless() && !self.is_angle() { 
150            return Err("cos requires an angle or unitless quantity".to_string()); 
151        }
152        Ok(self.value.cos())
153    }
154
155    /// Tangent function. Requires angle or unitless.
156    pub fn tan(&self) -> Result<f64, String> {
157        if !self.is_unitless() && !self.is_angle() { 
158            return Err("tan requires an angle or unitless quantity".to_string()); 
159        }
160        Ok(self.value.tan())
161    }
162
163    // ---- Scalar helpers on single values ----
164    /// Negate the value, keeping the same unit.
165    pub fn neg(&self) -> DimensionalVariable {
166        DimensionalVariable { value: -self.value, unit: self.unit }
167    }
168
169    /// Returns the absolute value, keeping the same unit.
170    pub fn abs(&self) -> DimensionalVariable {
171        DimensionalVariable { value: self.value.abs(), unit: self.unit }
172    }
173 
174}
175
176/// Convert a unit string like "m/s^2" or "kg-m/s^2" into base unit exponents and a conversion factor.
177/// Returns an error if the unit string is invalid or contains unknown units.
178fn unit_str_to_base_unit(units_str: &str) -> Result<([f64; units::BASE_UNITS_SIZE], f64), String> {
179
180    // Start by removing any parentheses or brackets
181    let cleaned_units_str = units_str.replace(['(', ')', '[', ']'], "");
182
183    // Split the cleaned string by '/' to separate numerator and denominator
184    let parts: Vec<&str> = cleaned_units_str.split('/').collect();
185    if parts.len() > 2 {
186        return Err("Unit string can only have one '/'".to_string());
187    }
188
189    let mut base_unit =  [0.0; units::BASE_UNITS_SIZE];
190    let mut conversion_factor: f64 = 1.0;
191
192    for i in 0..parts.len() {
193        // Detect whether it's the numerator or denominator
194        let denominator_multiplier = if i == 1 { -1 } else { 1 };
195
196        // Split by '-' to handle individual units, but keep '-' that is an exponent sign (after '^')
197        let units: Vec<&str> = {
198            let s = parts[i];
199            let mut out = Vec::new();
200            let mut start = 0usize;
201            let mut prev: Option<char> = None;
202            for (idx, ch) in s.char_indices() {
203                if ch == '-' && prev != Some('^') {
204                    if idx > start {
205                        out.push(&s[start..idx]);
206                    }
207                    start = idx + ch.len_utf8();
208                }
209                prev = Some(ch);
210            }
211            if start < s.len() {
212                out.push(&s[start..]);
213            }
214            out
215        };
216        for unit_str in units {
217
218            let (base, power) = read_unit_power(unit_str)?;
219
220            let unit_map = units::unit_map();
221            let unit = unit_map.get(base)
222                .ok_or_else(|| format!("Unknown unit: {}", base))?; 
223
224            for j in 0..units::BASE_UNITS_SIZE {
225                base_unit[j] += unit.base_unit[j] * (power * denominator_multiplier) as f64;
226            }
227
228            // Apply the conversion factor
229            conversion_factor *= unit.conversion_factor.powi(power * denominator_multiplier);
230        }
231    }
232
233    return Ok((base_unit, conversion_factor));
234}
235
236/// Parse a token like "m3", "m-2", or "m^3"/"m^-2" into (base, power).
237/// If no trailing exponent is found, defaults to power = 1.
238/// Returns an error if a trailing '^' has no number.
239fn read_unit_power(unit: &str) -> Result<(&str, i32), String> {
240    let u = unit.trim();
241    if u.is_empty() {
242        return Err("Empty unit token".to_string());
243    }
244
245    let bytes = u.as_bytes();
246
247    // Find the trailing digits
248    let mut end = u.len();
249    while end > 0 && bytes[end - 1].is_ascii_digit() {
250        end -= 1;
251    }
252
253    if end == u.len() {
254        // No trailing digits. If it ends with '^', that's an error; otherwise power = 1.
255        if end > 0 && bytes[end - 1] == b'^' {
256            return Err(format!("Missing exponent after '^' in \"{}\"", u));
257        }
258        return Ok((u, 1));
259    }
260
261    let mut start = end;
262    if start > 0 && (bytes[start - 1] == b'-') {
263        start -= 1;
264    }
265
266    let exp_str = &u[start..];
267    let exp: i32 = exp_str
268        .parse()
269        .map_err(|_| format!("Unable to read numeric power from \"{}\"", u))?;
270
271    // Base is everything before the exponent; strip a trailing '^' if present.
272    let mut base_end = start;
273    if base_end > 0 && bytes[base_end - 1] == b'^' {
274        base_end -= 1;
275    }
276    let base = u[..base_end].trim();
277    if base.is_empty() {
278        return Err(format!("Missing unit symbol before exponent in \"{}\"", u));
279    }
280
281    Ok((base, exp))
282}
283
284// ---- Helpers for unit arithmetic ----
285/// Add two unit exponent arrays element-wise.
286fn add_unit_exponents(a: [f64; units::BASE_UNITS_SIZE], b: [f64; units::BASE_UNITS_SIZE]) -> [f64; units::BASE_UNITS_SIZE] {
287    let mut out = a;
288    for i in 0..units::BASE_UNITS_SIZE { out[i] += b[i]; }
289    out
290}
291
292/// Subtract two unit exponent arrays element-wise.
293fn sub_unit_exponents(a: [f64; units::BASE_UNITS_SIZE], b: [f64; units::BASE_UNITS_SIZE]) -> [f64; units::BASE_UNITS_SIZE] {
294    let mut out = a;
295    for i in 0..units::BASE_UNITS_SIZE { out[i] -= b[i]; }
296    out
297}
298
299// ---- Operator trait impls ----
300use std::ops::{Add, Sub, Mul, Div, Neg, AddAssign, SubAssign, MulAssign, DivAssign};
301use std::cmp::Ordering;
302
303// Keep only reference-based binary ops to avoid duplication. Autoref handles owned values.
304impl<'a, 'b> Add<&'b DimensionalVariable> for &'a DimensionalVariable {
305    type Output = DimensionalVariable;
306    fn add(self, rhs: &'b DimensionalVariable) -> Self::Output {
307        assert!(self.unit == rhs.unit, "Incompatible units for addition: {:?} vs {:?}", self.unit, rhs.unit);
308        DimensionalVariable { value: self.value + rhs.value, unit: self.unit }
309    }
310}
311
312// Delegating wrappers for owned LHS/RHS
313impl Add<DimensionalVariable> for DimensionalVariable {
314    type Output = DimensionalVariable;
315    fn add(self, rhs: DimensionalVariable) -> Self::Output {
316        <&DimensionalVariable as Add<&DimensionalVariable>>::add(&self, &rhs)
317    }
318}
319
320impl<'b> Add<&'b DimensionalVariable> for DimensionalVariable {
321    type Output = DimensionalVariable;
322    fn add(self, rhs: &'b DimensionalVariable) -> Self::Output {
323        <&DimensionalVariable as Add<&DimensionalVariable>>::add(&self, rhs)
324    }
325}
326
327impl<'a> Add<DimensionalVariable> for &'a DimensionalVariable {
328    type Output = DimensionalVariable;
329    fn add(self, rhs: DimensionalVariable) -> Self::Output {
330        <&DimensionalVariable as Add<&DimensionalVariable>>::add(self, &rhs)
331    }
332}
333
334impl<'a, 'b> Sub<&'b DimensionalVariable> for &'a DimensionalVariable {
335    type Output = DimensionalVariable;
336    fn sub(self, rhs: &'b DimensionalVariable) -> Self::Output {
337        assert!(self.unit == rhs.unit, "Incompatible units for subtraction: {:?} vs {:?}", self.unit, rhs.unit);
338        DimensionalVariable { value: self.value - rhs.value, unit: self.unit }
339    }
340}
341
342impl Sub<DimensionalVariable> for DimensionalVariable {
343    type Output = DimensionalVariable;
344    fn sub(self, rhs: DimensionalVariable) -> Self::Output {
345        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(&self, &rhs)
346    }
347}
348
349impl<'b> Sub<&'b DimensionalVariable> for DimensionalVariable {
350    type Output = DimensionalVariable;
351    fn sub(self, rhs: &'b DimensionalVariable) -> Self::Output {
352        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(&self, rhs)
353    }
354}
355
356impl<'a> Sub<DimensionalVariable> for &'a DimensionalVariable {
357    type Output = DimensionalVariable;
358    fn sub(self, rhs: DimensionalVariable) -> Self::Output {
359        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(self, &rhs)
360    }
361}
362
363impl<'a, 'b> Mul<&'b DimensionalVariable> for &'a DimensionalVariable {
364    type Output = DimensionalVariable;
365    fn mul(self, rhs: &'b DimensionalVariable) -> Self::Output {
366        DimensionalVariable { value: self.value * rhs.value, unit: add_unit_exponents(self.unit, rhs.unit) }
367    }
368}
369
370impl Mul<DimensionalVariable> for DimensionalVariable {
371    type Output = DimensionalVariable;
372    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
373        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(&self, &rhs)
374    }
375}
376
377impl<'b> Mul<&'b DimensionalVariable> for DimensionalVariable {
378    type Output = DimensionalVariable;
379    fn mul(self, rhs: &'b DimensionalVariable) -> Self::Output {
380        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(&self, rhs)
381    }
382}
383
384impl<'a> Mul<DimensionalVariable> for &'a DimensionalVariable {
385    type Output = DimensionalVariable;
386    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
387        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(self, &rhs)
388    }
389}
390
391impl<'a, 'b> Div<&'b DimensionalVariable> for &'a DimensionalVariable {
392    type Output = DimensionalVariable;
393    fn div(self, rhs: &'b DimensionalVariable) -> Self::Output {
394        DimensionalVariable { value: self.value / rhs.value, unit: sub_unit_exponents(self.unit, rhs.unit) }
395    }
396}
397
398impl Div<DimensionalVariable> for DimensionalVariable {
399    type Output = DimensionalVariable;
400    fn div(self, rhs: DimensionalVariable) -> Self::Output {
401        <&DimensionalVariable as Div<&DimensionalVariable>>::div(&self, &rhs)
402    }
403}
404
405impl<'b> Div<&'b DimensionalVariable> for DimensionalVariable {
406    type Output = DimensionalVariable;
407    fn div(self, rhs: &'b DimensionalVariable) -> Self::Output {
408        <&DimensionalVariable as Div<&DimensionalVariable>>::div(&self, rhs)
409    }
410}
411
412impl<'a> Div<DimensionalVariable> for &'a DimensionalVariable {
413    type Output = DimensionalVariable;
414    fn div(self, rhs: DimensionalVariable) -> Self::Output {
415        <&DimensionalVariable as Div<&DimensionalVariable>>::div(self, &rhs)
416    }
417}
418
419// Assignment ops: implement only for &DimensionalVariable RHS. Owned RHS will autoref.
420impl AddAssign<&DimensionalVariable> for DimensionalVariable {
421    fn add_assign(&mut self, rhs: &DimensionalVariable) {
422        assert!(self.unit == rhs.unit, "Incompatible units for addition assignment: {:?} vs {:?}", self.unit, rhs.unit);
423        self.value += rhs.value;
424    }
425}
426
427impl SubAssign<&DimensionalVariable> for DimensionalVariable {
428    fn sub_assign(&mut self, rhs: &DimensionalVariable) {
429        assert!(self.unit == rhs.unit, "Incompatible units for subtraction assignment: {:?} vs {:?}", self.unit, rhs.unit);
430        self.value -= rhs.value;
431    }
432}
433
434impl MulAssign<&DimensionalVariable> for DimensionalVariable {
435    fn mul_assign(&mut self, rhs: &DimensionalVariable) {
436        self.value *= rhs.value;
437        self.unit = add_unit_exponents(self.unit, rhs.unit);
438    }
439}
440
441impl DivAssign<&DimensionalVariable> for DimensionalVariable {
442    fn div_assign(&mut self, rhs: &DimensionalVariable) {
443        self.value /= rhs.value;
444        self.unit = sub_unit_exponents(self.unit, rhs.unit);
445    }
446}
447
448// Scalar ops
449impl<'a> Mul<f64> for &'a DimensionalVariable {
450    type Output = DimensionalVariable;
451    fn mul(self, rhs: f64) -> Self::Output {
452        DimensionalVariable { value: self.value * rhs, unit: self.unit }
453    }
454}
455
456impl Mul<f64> for DimensionalVariable {
457    type Output = DimensionalVariable;
458    fn mul(self, rhs: f64) -> Self::Output {
459        <&DimensionalVariable as Mul<f64>>::mul(&self, rhs)
460    }
461}
462
463impl MulAssign<f64> for DimensionalVariable {
464    fn mul_assign(&mut self, rhs: f64) {
465        self.value *= rhs;
466    }
467}
468
469impl<'a> Div<f64> for &'a DimensionalVariable {
470    type Output = DimensionalVariable;
471    fn div(self, rhs: f64) -> Self::Output {
472        DimensionalVariable { value: self.value / rhs, unit: self.unit }
473    }
474}
475
476impl Div<f64> for DimensionalVariable {
477    type Output = DimensionalVariable;
478    fn div(self, rhs: f64) -> Self::Output {
479        <&DimensionalVariable as Div<f64>>::div(&self, rhs)
480    }
481}
482
483impl DivAssign<f64> for DimensionalVariable {
484    fn div_assign(&mut self, rhs: f64) {
485        self.value /= rhs;
486    }
487}
488
489// Symmetric scalar ops
490impl<'a> Mul<&'a DimensionalVariable> for f64 {
491    type Output = DimensionalVariable;
492    fn mul(self, rhs: &'a DimensionalVariable) -> Self::Output {
493        DimensionalVariable { value: self * rhs.value, unit: rhs.unit }
494    }
495}
496
497impl Mul<DimensionalVariable> for f64 {
498    type Output = DimensionalVariable;
499    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
500        <f64 as Mul<&DimensionalVariable>>::mul(self, &rhs)
501    }
502}
503
504impl<'a> Div<&'a DimensionalVariable> for f64 {
505    type Output = DimensionalVariable;
506    fn div(self, rhs: &'a DimensionalVariable) -> Self::Output {
507    DimensionalVariable { value: self / rhs.value, unit: sub_unit_exponents([0.0; units::BASE_UNITS_SIZE], rhs.unit) }
508    }
509}
510
511impl Div<DimensionalVariable> for f64 {
512    type Output = DimensionalVariable;
513    fn div(self, rhs: DimensionalVariable) -> Self::Output {
514        <f64 as Div<&DimensionalVariable>>::div(self, &rhs)
515    }
516}
517
518// Unary negation on references and delegating owned variant
519impl<'a> Neg for &'a DimensionalVariable {
520    type Output = DimensionalVariable;
521    fn neg(self) -> Self::Output {
522        DimensionalVariable { value: -self.value, unit: self.unit }
523    }
524}
525
526impl Neg for DimensionalVariable {
527    type Output = DimensionalVariable;
528    fn neg(self) -> Self::Output {
529        <&DimensionalVariable as Neg>::neg(&self)
530    }
531}
532
533// ---- Comparisons: equalities and ordering ----
534impl PartialEq for DimensionalVariable {
535    fn eq(&self, other: &Self) -> bool {
536        if self.unit != other.unit { return false; }
537        self.value == other.value
538    }
539}
540
541impl PartialOrd for DimensionalVariable {
542    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
543        if self.unit != other.unit { return None; }
544        self.value.partial_cmp(&other.value)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::read_unit_power;
551
552    #[test]
553    fn read_unit_power_basic_cases() {
554        assert_eq!(read_unit_power("m").unwrap(), ("m", 1));
555        assert_eq!(read_unit_power("m3").unwrap(), ("m", 3));
556        assert_eq!(read_unit_power("m^3").unwrap(), ("m", 3));
557        assert_eq!(read_unit_power("m^-2").unwrap(), ("m", -2));
558        assert_eq!(read_unit_power("m-2").unwrap(), ("m", -2));
559        assert_eq!(read_unit_power("  kg^2 ").unwrap(), ("kg", 2));
560        assert_eq!(read_unit_power("undef").unwrap(), ("undef", 1));    // We don't check for known units here
561    }
562
563    #[test]
564    fn read_unit_power_errors() {
565        assert!(read_unit_power("").is_err());
566        let err = read_unit_power("m^").unwrap_err();
567        assert!(err.contains("Missing exponent"));
568        let err = read_unit_power("^2").unwrap_err();
569        assert!(err.contains("Missing unit symbol"));
570    }
571}