dv_rs/
lib.rs

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