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 (unitless only, radians recommended) ----
128    /// Sine function. Requires unitless (radians).
129    pub fn sin(&self) -> Result<f64, String> {
130        if !self.is_unitless() { return Err("sin requires a unitless quantity (radians)".to_string()); }
131        Ok(self.value.sin())
132    }
133
134    /// Cosine function. Requires unitless (radians).
135    pub fn cos(&self) -> Result<f64, String> {
136        if !self.is_unitless() { return Err("cos requires a unitless quantity (radians)".to_string()); }
137        Ok(self.value.cos())
138    }
139
140    /// Tangent function. Requires unitless (radians).
141    pub fn tan(&self) -> Result<f64, String> {
142        if !self.is_unitless() { return Err("tan requires a unitless quantity (radians)".to_string()); }
143        Ok(self.value.tan())
144    }
145
146    // ---- Scalar helpers on single values ----
147    /// Negate the value, keeping the same unit.
148    pub fn neg(&self) -> DimensionalVariable {
149        DimensionalVariable { value: -self.value, unit: self.unit }
150    }
151
152    /// Returns the absolute value, keeping the same unit.
153    pub fn abs(&self) -> DimensionalVariable {
154        DimensionalVariable { value: self.value.abs(), unit: self.unit }
155    }
156 
157}
158
159/// Convert a unit string like "m/s^2" or "kg-m/s^2" into base unit exponents and a conversion factor.
160/// Returns an error if the unit string is invalid or contains unknown units.
161fn unit_str_to_base_unit(units_str: &str) -> Result<([f64; units::BASE_UNITS_SIZE], f64), String> {
162
163    // Start by removing any parentheses or brackets
164    let cleaned_units_str = units_str.replace(['(', ')', '[', ']'], "");
165
166    // Split the cleaned string by '/' to separate numerator and denominator
167    let parts: Vec<&str> = cleaned_units_str.split('/').collect();
168    if parts.len() > 2 {
169        return Err("Unit string can only have one '/'".to_string());
170    }
171
172    let mut base_unit =  [0.0; units::BASE_UNITS_SIZE];
173    let mut conversion_factor: f64 = 1.0;
174
175    for i in 0..parts.len() {
176        // Detect whether it's the numerator or denominator
177        let denominator_multiplier = if i == 1 { -1 } else { 1 };
178
179        // Split by '-' to handle individual units, but keep '-' that is an exponent sign (after '^')
180        let units: Vec<&str> = {
181            let s = parts[i];
182            let mut out = Vec::new();
183            let mut start = 0usize;
184            let mut prev: Option<char> = None;
185            for (idx, ch) in s.char_indices() {
186                if ch == '-' && prev != Some('^') {
187                    if idx > start {
188                        out.push(&s[start..idx]);
189                    }
190                    start = idx + ch.len_utf8();
191                }
192                prev = Some(ch);
193            }
194            if start < s.len() {
195                out.push(&s[start..]);
196            }
197            out
198        };
199        for unit_str in units {
200
201            let (base, power) = read_unit_power(unit_str)?;
202
203            let unit_map = units::unit_map();
204            let unit = unit_map.get(base)
205                .ok_or_else(|| format!("Unknown unit: {}", base))?; 
206
207            for j in 0..units::BASE_UNITS_SIZE {
208                base_unit[j] += unit.base_unit[j] * (power * denominator_multiplier) as f64;
209            }
210
211            // Apply the conversion factor
212            conversion_factor *= unit.conversion_factor.powi(power * denominator_multiplier);
213        }
214    }
215
216    return Ok((base_unit, conversion_factor));
217}
218
219/// Parse a token like "m3", "m-2", or "m^3"/"m^-2" into (base, power).
220/// If no trailing exponent is found, defaults to power = 1.
221/// Returns an error if a trailing '^' has no number.
222fn read_unit_power(unit: &str) -> Result<(&str, i32), String> {
223    let u = unit.trim();
224    if u.is_empty() {
225        return Err("Empty unit token".to_string());
226    }
227
228    let bytes = u.as_bytes();
229
230    // Find the trailing digits
231    let mut end = u.len();
232    while end > 0 && bytes[end - 1].is_ascii_digit() {
233        end -= 1;
234    }
235
236    if end == u.len() {
237        // No trailing digits. If it ends with '^', that's an error; otherwise power = 1.
238        if end > 0 && bytes[end - 1] == b'^' {
239            return Err(format!("Missing exponent after '^' in \"{}\"", u));
240        }
241        return Ok((u, 1));
242    }
243
244    let mut start = end;
245    if start > 0 && (bytes[start - 1] == b'-') {
246        start -= 1;
247    }
248
249    let exp_str = &u[start..];
250    let exp: i32 = exp_str
251        .parse()
252        .map_err(|_| format!("Unable to read numeric power from \"{}\"", u))?;
253
254    // Base is everything before the exponent; strip a trailing '^' if present.
255    let mut base_end = start;
256    if base_end > 0 && bytes[base_end - 1] == b'^' {
257        base_end -= 1;
258    }
259    let base = u[..base_end].trim();
260    if base.is_empty() {
261        return Err(format!("Missing unit symbol before exponent in \"{}\"", u));
262    }
263
264    Ok((base, exp))
265}
266
267// ---- Helpers for unit arithmetic ----
268/// Add two unit exponent arrays element-wise.
269fn add_unit_exponents(a: [f64; units::BASE_UNITS_SIZE], b: [f64; units::BASE_UNITS_SIZE]) -> [f64; units::BASE_UNITS_SIZE] {
270    let mut out = a;
271    for i in 0..units::BASE_UNITS_SIZE { out[i] += b[i]; }
272    out
273}
274
275/// Subtract two unit exponent arrays element-wise.
276fn sub_unit_exponents(a: [f64; units::BASE_UNITS_SIZE], b: [f64; units::BASE_UNITS_SIZE]) -> [f64; units::BASE_UNITS_SIZE] {
277    let mut out = a;
278    for i in 0..units::BASE_UNITS_SIZE { out[i] -= b[i]; }
279    out
280}
281
282// ---- Operator trait impls ----
283use std::ops::{Add, Sub, Mul, Div, Neg, AddAssign, SubAssign, MulAssign, DivAssign};
284use std::cmp::Ordering;
285
286// Keep only reference-based binary ops to avoid duplication. Autoref handles owned values.
287impl<'a, 'b> Add<&'b DimensionalVariable> for &'a DimensionalVariable {
288    type Output = DimensionalVariable;
289    fn add(self, rhs: &'b DimensionalVariable) -> Self::Output {
290        assert!(self.unit == rhs.unit, "Incompatible units for addition: {:?} vs {:?}", self.unit, rhs.unit);
291        DimensionalVariable { value: self.value + rhs.value, unit: self.unit }
292    }
293}
294
295// Delegating wrappers for owned LHS/RHS
296impl Add<DimensionalVariable> for DimensionalVariable {
297    type Output = DimensionalVariable;
298    fn add(self, rhs: DimensionalVariable) -> Self::Output {
299        <&DimensionalVariable as Add<&DimensionalVariable>>::add(&self, &rhs)
300    }
301}
302
303impl<'b> Add<&'b DimensionalVariable> for DimensionalVariable {
304    type Output = DimensionalVariable;
305    fn add(self, rhs: &'b DimensionalVariable) -> Self::Output {
306        <&DimensionalVariable as Add<&DimensionalVariable>>::add(&self, rhs)
307    }
308}
309
310impl<'a> Add<DimensionalVariable> for &'a DimensionalVariable {
311    type Output = DimensionalVariable;
312    fn add(self, rhs: DimensionalVariable) -> Self::Output {
313        <&DimensionalVariable as Add<&DimensionalVariable>>::add(self, &rhs)
314    }
315}
316
317impl<'a, 'b> Sub<&'b DimensionalVariable> for &'a DimensionalVariable {
318    type Output = DimensionalVariable;
319    fn sub(self, rhs: &'b DimensionalVariable) -> Self::Output {
320        assert!(self.unit == rhs.unit, "Incompatible units for subtraction: {:?} vs {:?}", self.unit, rhs.unit);
321        DimensionalVariable { value: self.value - rhs.value, unit: self.unit }
322    }
323}
324
325impl Sub<DimensionalVariable> for DimensionalVariable {
326    type Output = DimensionalVariable;
327    fn sub(self, rhs: DimensionalVariable) -> Self::Output {
328        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(&self, &rhs)
329    }
330}
331
332impl<'b> Sub<&'b DimensionalVariable> for DimensionalVariable {
333    type Output = DimensionalVariable;
334    fn sub(self, rhs: &'b DimensionalVariable) -> Self::Output {
335        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(&self, rhs)
336    }
337}
338
339impl<'a> Sub<DimensionalVariable> for &'a DimensionalVariable {
340    type Output = DimensionalVariable;
341    fn sub(self, rhs: DimensionalVariable) -> Self::Output {
342        <&DimensionalVariable as Sub<&DimensionalVariable>>::sub(self, &rhs)
343    }
344}
345
346impl<'a, 'b> Mul<&'b DimensionalVariable> for &'a DimensionalVariable {
347    type Output = DimensionalVariable;
348    fn mul(self, rhs: &'b DimensionalVariable) -> Self::Output {
349        DimensionalVariable { value: self.value * rhs.value, unit: add_unit_exponents(self.unit, rhs.unit) }
350    }
351}
352
353impl Mul<DimensionalVariable> for DimensionalVariable {
354    type Output = DimensionalVariable;
355    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
356        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(&self, &rhs)
357    }
358}
359
360impl<'b> Mul<&'b DimensionalVariable> for DimensionalVariable {
361    type Output = DimensionalVariable;
362    fn mul(self, rhs: &'b DimensionalVariable) -> Self::Output {
363        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(&self, rhs)
364    }
365}
366
367impl<'a> Mul<DimensionalVariable> for &'a DimensionalVariable {
368    type Output = DimensionalVariable;
369    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
370        <&DimensionalVariable as Mul<&DimensionalVariable>>::mul(self, &rhs)
371    }
372}
373
374impl<'a, 'b> Div<&'b DimensionalVariable> for &'a DimensionalVariable {
375    type Output = DimensionalVariable;
376    fn div(self, rhs: &'b DimensionalVariable) -> Self::Output {
377        DimensionalVariable { value: self.value / rhs.value, unit: sub_unit_exponents(self.unit, rhs.unit) }
378    }
379}
380
381impl Div<DimensionalVariable> for DimensionalVariable {
382    type Output = DimensionalVariable;
383    fn div(self, rhs: DimensionalVariable) -> Self::Output {
384        <&DimensionalVariable as Div<&DimensionalVariable>>::div(&self, &rhs)
385    }
386}
387
388impl<'b> Div<&'b DimensionalVariable> for DimensionalVariable {
389    type Output = DimensionalVariable;
390    fn div(self, rhs: &'b DimensionalVariable) -> Self::Output {
391        <&DimensionalVariable as Div<&DimensionalVariable>>::div(&self, rhs)
392    }
393}
394
395impl<'a> Div<DimensionalVariable> for &'a DimensionalVariable {
396    type Output = DimensionalVariable;
397    fn div(self, rhs: DimensionalVariable) -> Self::Output {
398        <&DimensionalVariable as Div<&DimensionalVariable>>::div(self, &rhs)
399    }
400}
401
402// Assignment ops: implement only for &DimensionalVariable RHS. Owned RHS will autoref.
403impl AddAssign<&DimensionalVariable> for DimensionalVariable {
404    fn add_assign(&mut self, rhs: &DimensionalVariable) {
405        assert!(self.unit == rhs.unit, "Incompatible units for addition assignment: {:?} vs {:?}", self.unit, rhs.unit);
406        self.value += rhs.value;
407    }
408}
409
410impl SubAssign<&DimensionalVariable> for DimensionalVariable {
411    fn sub_assign(&mut self, rhs: &DimensionalVariable) {
412        assert!(self.unit == rhs.unit, "Incompatible units for subtraction assignment: {:?} vs {:?}", self.unit, rhs.unit);
413        self.value -= rhs.value;
414    }
415}
416
417impl MulAssign<&DimensionalVariable> for DimensionalVariable {
418    fn mul_assign(&mut self, rhs: &DimensionalVariable) {
419        self.value *= rhs.value;
420        self.unit = add_unit_exponents(self.unit, rhs.unit);
421    }
422}
423
424impl DivAssign<&DimensionalVariable> for DimensionalVariable {
425    fn div_assign(&mut self, rhs: &DimensionalVariable) {
426        self.value /= rhs.value;
427        self.unit = sub_unit_exponents(self.unit, rhs.unit);
428    }
429}
430
431// Scalar ops
432impl<'a> Mul<f64> for &'a DimensionalVariable {
433    type Output = DimensionalVariable;
434    fn mul(self, rhs: f64) -> Self::Output {
435        DimensionalVariable { value: self.value * rhs, unit: self.unit }
436    }
437}
438
439impl Mul<f64> for DimensionalVariable {
440    type Output = DimensionalVariable;
441    fn mul(self, rhs: f64) -> Self::Output {
442        <&DimensionalVariable as Mul<f64>>::mul(&self, rhs)
443    }
444}
445
446impl MulAssign<f64> for DimensionalVariable {
447    fn mul_assign(&mut self, rhs: f64) {
448        self.value *= rhs;
449    }
450}
451
452impl<'a> Div<f64> for &'a DimensionalVariable {
453    type Output = DimensionalVariable;
454    fn div(self, rhs: f64) -> Self::Output {
455        DimensionalVariable { value: self.value / rhs, unit: self.unit }
456    }
457}
458
459impl Div<f64> for DimensionalVariable {
460    type Output = DimensionalVariable;
461    fn div(self, rhs: f64) -> Self::Output {
462        <&DimensionalVariable as Div<f64>>::div(&self, rhs)
463    }
464}
465
466impl DivAssign<f64> for DimensionalVariable {
467    fn div_assign(&mut self, rhs: f64) {
468        self.value /= rhs;
469    }
470}
471
472// Symmetric scalar ops
473impl<'a> Mul<&'a DimensionalVariable> for f64 {
474    type Output = DimensionalVariable;
475    fn mul(self, rhs: &'a DimensionalVariable) -> Self::Output {
476        DimensionalVariable { value: self * rhs.value, unit: rhs.unit }
477    }
478}
479
480impl Mul<DimensionalVariable> for f64 {
481    type Output = DimensionalVariable;
482    fn mul(self, rhs: DimensionalVariable) -> Self::Output {
483        <f64 as Mul<&DimensionalVariable>>::mul(self, &rhs)
484    }
485}
486
487impl<'a> Div<&'a DimensionalVariable> for f64 {
488    type Output = DimensionalVariable;
489    fn div(self, rhs: &'a DimensionalVariable) -> Self::Output {
490    DimensionalVariable { value: self / rhs.value, unit: sub_unit_exponents([0.0; units::BASE_UNITS_SIZE], rhs.unit) }
491    }
492}
493
494impl Div<DimensionalVariable> for f64 {
495    type Output = DimensionalVariable;
496    fn div(self, rhs: DimensionalVariable) -> Self::Output {
497        <f64 as Div<&DimensionalVariable>>::div(self, &rhs)
498    }
499}
500
501// Unary negation on references and delegating owned variant
502impl<'a> Neg for &'a DimensionalVariable {
503    type Output = DimensionalVariable;
504    fn neg(self) -> Self::Output {
505        DimensionalVariable { value: -self.value, unit: self.unit }
506    }
507}
508
509impl Neg for DimensionalVariable {
510    type Output = DimensionalVariable;
511    fn neg(self) -> Self::Output {
512        <&DimensionalVariable as Neg>::neg(&self)
513    }
514}
515
516// ---- Comparisons: equalities and ordering ----
517impl PartialEq for DimensionalVariable {
518    fn eq(&self, other: &Self) -> bool {
519        if self.unit != other.unit { return false; }
520        self.value == other.value
521    }
522}
523
524impl PartialOrd for DimensionalVariable {
525    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
526        if self.unit != other.unit { return None; }
527        self.value.partial_cmp(&other.value)
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::read_unit_power;
534
535    #[test]
536    fn read_unit_power_basic_cases() {
537        assert_eq!(read_unit_power("m").unwrap(), ("m", 1));
538        assert_eq!(read_unit_power("m3").unwrap(), ("m", 3));
539        assert_eq!(read_unit_power("m^3").unwrap(), ("m", 3));
540        assert_eq!(read_unit_power("m^-2").unwrap(), ("m", -2));
541        assert_eq!(read_unit_power("m-2").unwrap(), ("m", -2));
542        assert_eq!(read_unit_power("  kg^2 ").unwrap(), ("kg", 2));
543        assert_eq!(read_unit_power("undef").unwrap(), ("undef", 1));    // We don't check for known units here
544    }
545
546    #[test]
547    fn read_unit_power_errors() {
548        assert!(read_unit_power("").is_err());
549        let err = read_unit_power("m^").unwrap_err();
550        assert!(err.contains("Missing exponent"));
551        let err = read_unit_power("^2").unwrap_err();
552        assert!(err.contains("Missing unit symbol"));
553    }
554}