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