From 46e6277002949a67e2d543ba16bcf83fd508f912 Mon Sep 17 00:00:00 2001 From: Alex Tacescu Date: Thu, 11 Dec 2025 00:00:18 -0800 Subject: [PATCH] GH-65: Set up string representations for each language supported --- VERSION.toml | 2 +- core/Cargo.toml | 2 +- core/src/lib.rs | 64 ++++++++++++++++++++++ core/tests/units_tests.rs | 81 ++++++++++++++++++++++++++++ cpp/capi/src/lib.rs | 20 +++++++ cpp/include/dv.hpp | 15 ++++++ cpp/include/dv_c.h | 2 + python/Cargo.toml | 2 +- python/pyproject.toml | 2 +- python/src/lib.rs | 108 +++++++++++++++++++------------------- 10 files changed, 240 insertions(+), 58 deletions(-) diff --git a/VERSION.toml b/VERSION.toml index 6167061..922ae62 100644 --- a/VERSION.toml +++ b/VERSION.toml @@ -6,4 +6,4 @@ major = 0 minor = 3 -patch = 1 +patch = 2 diff --git a/core/Cargo.toml b/core/Cargo.toml index 3609b83..cc7a52b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dv" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = [ "Alex Tacescu ",] description = "Core Rust library for DimensionalVariable, a multi-language library for handling physical quantities with units." diff --git a/core/src/lib.rs b/core/src/lib.rs index a318250..1aa70af 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -220,6 +220,70 @@ impl DimensionalVariable { } +impl std::fmt::Display for DimensionalVariable { + /// Formats the DimensionalVariable as a string in the form "value unit". + /// + /// The unit is displayed in fraction style (e.g., "m/s^2") with: + /// - Positive exponents in the numerator + /// - Negative exponents in the denominator (as positive values) + /// - Exponents of 1 are omitted + /// - Units with exponent 0 are omitted + /// - Unitless quantities show "(unitless)" + /// + /// Examples: + /// - 9.81 m/s^2 for acceleration + /// - 100 kg*m^2/s^2 for energy + /// - 3.14 rad for angle + /// - 5 (unitless) for dimensionless quantities + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut numerator_parts: Vec = Vec::new(); + let mut denominator_parts: Vec = Vec::new(); + + for (i, &exp) in self.unit.iter().enumerate() { + if exp == 0.0 { + continue; + } + + let unit_symbol = units::BASE_UNITS[i]; + + if exp > 0.0 { + if exp == 1.0 { + numerator_parts.push(unit_symbol.to_string()); + } else if exp == exp.trunc() { + // Integer exponent + numerator_parts.push(format!("{}^{}", unit_symbol, exp as i32)); + } else { + // Fractional exponent + numerator_parts.push(format!("{}^{}", unit_symbol, exp)); + } + } else { + let abs_exp = exp.abs(); + if abs_exp == 1.0 { + denominator_parts.push(unit_symbol.to_string()); + } else if abs_exp == abs_exp.trunc() { + // Integer exponent + denominator_parts.push(format!("{}^{}", unit_symbol, abs_exp as i32)); + } else { + // Fractional exponent + denominator_parts.push(format!("{}^{}", unit_symbol, abs_exp)); + } + } + } + + let unit_str = if numerator_parts.is_empty() && denominator_parts.is_empty() { + "(unitless)".to_string() + } else if denominator_parts.is_empty() { + numerator_parts.join("*") + } else if numerator_parts.is_empty() { + format!("1/{}", denominator_parts.join("*")) + } else { + format!("{}/{}", numerator_parts.join("*"), denominator_parts.join("*")) + }; + + write!(f, "{} {}", self.value, unit_str) + } +} + /// Arcsin function for f64 input, returns DimensionalVariable in radians. pub fn asin(x: f64) -> Result { return DimensionalVariable::new(x, "").unwrap().asin(); diff --git a/core/tests/units_tests.rs b/core/tests/units_tests.rs index a3a3459..8863034 100644 --- a/core/tests/units_tests.rs +++ b/core/tests/units_tests.rs @@ -116,3 +116,84 @@ fn electrical_units() { let power = dv::new(20.0, "W").expect(FAIL_MSG); assert!(power == &voltage * ¤t, "Power calculation failed"); } + +// ===== Display / to_string() tests ===== + +#[test] +fn to_string_simple_unit() { + let length = dv::new(5.0, "m").expect(FAIL_MSG); + assert_eq!(length.to_string(), "5 m"); + + let mass = dv::new(10.0, "kg").expect(FAIL_MSG); + assert_eq!(mass.to_string(), "10 kg"); + + let time = dv::new(3.5, "s").expect(FAIL_MSG); + assert_eq!(time.to_string(), "3.5 s"); +} + +#[test] +fn to_string_unitless() { + let unitless = dv::new(42.0, "").expect(FAIL_MSG); + assert_eq!(unitless.to_string(), "42 (unitless)"); +} + +#[test] +fn to_string_with_exponents() { + let area = dv::new(100.0, "m^2").expect(FAIL_MSG); + assert_eq!(area.to_string(), "100 m^2"); + + let volume = dv::new(8.0, "m^3").expect(FAIL_MSG); + assert_eq!(volume.to_string(), "8 m^3"); +} + +#[test] +fn to_string_with_denominator() { + let velocity = dv::new(10.0, "m/s").expect(FAIL_MSG); + assert_eq!(velocity.to_string(), "10 m/s"); + + let acceleration = dv::new(9.81, "m/s^2").expect(FAIL_MSG); + assert_eq!(acceleration.to_string(), "9.81 m/s^2"); +} + +#[test] +fn to_string_complex_units() { + // Force: kg*m/s^2 + let force = dv::new(1.0, "N").expect(FAIL_MSG); + assert_eq!(force.to_string(), "1 m*kg/s^2"); + + // Energy: kg*m^2/s^2 + let energy = dv::new(100.0, "J").expect(FAIL_MSG); + assert_eq!(energy.to_string(), "100 m^2*kg/s^2"); + + // Power: kg*m^2/s^3 + let power = dv::new(50.0, "W").expect(FAIL_MSG); + assert_eq!(power.to_string(), "50 m^2*kg/s^3"); +} + +#[test] +fn to_string_only_denominator() { + // Create a frequency (1/s) by dividing unitless by time + let one = dv::new(60.0, "").expect(FAIL_MSG); + let time = dv::new(1.0, "s").expect(FAIL_MSG); + let frequency = &one / &time; + assert_eq!(frequency.to_string(), "60 1/s"); + + // Also test via negative exponent + let freq2 = dv::new(100.0, "s^-1").expect(FAIL_MSG); + assert_eq!(freq2.to_string(), "100 1/s"); +} + +#[test] +fn to_string_angle() { + use std::f64::consts::PI; + let angle = dv::new(PI, "rad").expect(FAIL_MSG); + assert!(angle.to_string().starts_with("3.14159")); + assert!(angle.to_string().ends_with(" rad")); +} + +#[test] +fn to_string_format_macro() { + let dv_val = dv::new(2.5, "m/s").expect(FAIL_MSG); + let formatted = format!("The velocity is {}", dv_val); + assert_eq!(formatted, "The velocity is 2.5 m/s"); +} diff --git a/cpp/capi/src/lib.rs b/cpp/capi/src/lib.rs index 42bfc04..d7ee198 100644 --- a/cpp/capi/src/lib.rs +++ b/cpp/capi/src/lib.rs @@ -224,6 +224,26 @@ pub extern "C" fn dv_var_atan(a: *const dv_var) -> *mut dv_var { // Free-standing trigonometric functions that take raw f64 and return angle in radians +/// Convert a dv_var to a string representation. +/// Returns a newly allocated C string that must be freed with dv_free_cstring. +/// Returns null on error. +#[no_mangle] +pub extern "C" fn dv_var_to_string(ptr_: *const dv_var) -> *mut c_char { + if ptr_.is_null() { + set_last_error("null dv_var".to_string()); + return ptr::null_mut(); + } + let v = unsafe { &(*ptr_) }; + let s = v.inner.to_string(); + match CString::new(s) { + Ok(c) => c.into_raw(), + Err(_) => { + set_last_error("failed to create C string".to_string()); + ptr::null_mut() + } + } +} + #[no_mangle] pub extern "C" fn dv_asin(x: c_double) -> *mut dv_var { match dv_rs::asin(x) { diff --git a/cpp/include/dv.hpp b/cpp/include/dv.hpp index 5b637b2..d84986c 100644 --- a/cpp/include/dv.hpp +++ b/cpp/include/dv.hpp @@ -2,6 +2,7 @@ #include "dv_c.h" #include #include +#include namespace dv { @@ -51,6 +52,20 @@ class DV { DV powf(double e) const { return from_new(dv_var_powf(cptr(), e)); } DV sqrt() const { return from_new(dv_var_sqrt(cptr())); } + /// Convert to string representation (e.g., "9.81 m/s^2") + std::string to_string() const { + char* s = dv_var_to_string(ptr_); + if (!s) throw std::runtime_error(last_error()); + std::string result(s); + dv_free_cstring(s); + return result; + } + + /// Stream output operator + friend std::ostream& operator<<(std::ostream& os, const DV& v) { + return os << v.to_string(); + } + // Inverse trigonometric functions (return angle in radians) DV asin() const { return from_new(dv_var_asin(cptr())); } DV acos() const { return from_new(dv_var_acos(cptr())); } diff --git a/cpp/include/dv_c.h b/cpp/include/dv_c.h index 37a93ba..376c169 100644 --- a/cpp/include/dv_c.h +++ b/cpp/include/dv_c.h @@ -14,6 +14,7 @@ const char* dv_last_error_message(void); // Util size_t dv_base_units_size(void); +void dv_free_cstring(char* s); // Free a string returned by dv_var_to_string // Lifecycle dv_var* dv_var_new(double value, const char* unit_str); @@ -23,6 +24,7 @@ void dv_var_free(dv_var* v); double dv_var_value(const dv_var* v); int dv_var_is_unitless(const dv_var* v); int dv_var_value_in(const dv_var* v, const char* unit_str, double* out_value); +char* dv_var_to_string(const dv_var* v); // Returns allocated string, must free with dv_free_cstring // Arithmetic dv_var* dv_var_add(const dv_var* a, const dv_var* b); diff --git a/python/Cargo.toml b/python/Cargo.toml index ceda7d6..bc7c4d6 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dv_py" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = [ "Alex Tacescu ",] homepage = "https://dv.alextac.com" diff --git a/python/pyproject.toml b/python/pyproject.toml index b7fcaf3..f5d38bb 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "dv_py" -version = "0.3.1" +version = "0.3.2" description = "Python bindings for dv (DimensionalVariable) - keeping track of units and dimensions for physical quantities" readme = "pip-readme.md" requires-python = ">=3.8" diff --git a/python/src/lib.rs b/python/src/lib.rs index 366f66e..935c0fb 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -21,7 +21,7 @@ pyo3::create_exception!(dv_pyo3, DVError, pyo3::exceptions::PyException); /// 0.02 #[pyclass(name = "DimensionalVariable")] struct PyDV { - inner: DimensionalVariable, + dv: DimensionalVariable, } #[pymethods] @@ -40,7 +40,7 @@ impl PyDV { #[new] fn new(value: f64, unit: &str) -> PyResult { match DimensionalVariable::new(value, unit) { - Ok(inner) => Ok(PyDV { inner }), + Ok(dv) => Ok(PyDV { dv }), Err(e) => Err(DVError::new_err(e)), } } @@ -50,7 +50,7 @@ impl PyDV { /// Returns: /// float: The value in SI base units fn value(&self) -> f64 { - self.inner.value() + self.dv.value() } /// Convert the value to the specified unit. @@ -64,7 +64,7 @@ impl PyDV { /// Raises: /// DVError: If units are incompatible or unit string is invalid fn value_in(&self, unit: &str) -> PyResult { - match self.inner.value_in(unit) { + match self.dv.value_in(unit) { Ok(v) => Ok(v), Err(e) => Err(DVError::new_err(e)), } @@ -75,7 +75,7 @@ impl PyDV { /// Returns: /// bool: True if dimensionless, False otherwise fn is_unitless(&self) -> bool { - self.inner.is_unitless() + self.dv.is_unitless() } /// Get the base unit exponents as a tuple. @@ -83,22 +83,22 @@ impl PyDV { /// Returns: /// tuple: (m, kg, s, K, A, mol, cd, rad) exponents fn base_units(&self) -> (f64, f64, f64, f64, f64, f64, f64, f64) { - let units = self.inner.unit(); + let units = self.dv.unit(); (units[0], units[1], units[2], units[3], units[4], units[5], units[6], units[7]) } /// Addition operator. fn __add__(&self, other: &PyDV) -> PyResult { - match self.inner.try_add(&other.inner) { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.try_add(&other.dv) { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } /// Subtraction operator. fn __sub__(&self, other: &PyDV) -> PyResult { - match self.inner.try_sub(&other.inner) { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.try_sub(&other.dv) { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -108,9 +108,9 @@ impl PyDV { // Try to extract as another PyDV if let Ok(other_dv) = other.downcast::() { let other_borrow = other_dv.borrow(); - Ok(PyDV { inner: &self.inner * &other_borrow.inner }) + Ok(PyDV { dv: &self.dv * &other_borrow.dv }) } else if let Ok(scalar) = other.extract::() { - Ok(PyDV { inner: &self.inner * scalar }) + Ok(PyDV { dv: &self.dv * scalar }) } else { Err(PyTypeError::new_err("Cannot multiply DV with this type")) } @@ -119,7 +119,7 @@ impl PyDV { /// Right multiplication operator (scalar * DV). fn __rmul__(&self, other: &Bound<'_, PyAny>) -> PyResult { if let Ok(scalar) = other.extract::() { - Ok(PyDV { inner: scalar * &self.inner }) + Ok(PyDV { dv: scalar * &self.dv }) } else { Err(PyTypeError::new_err("Cannot multiply this type with DV")) } @@ -130,9 +130,9 @@ impl PyDV { // Try to extract as another PyDV if let Ok(other_dv) = other.downcast::() { let other_borrow = other_dv.borrow(); - Ok(PyDV { inner: &self.inner / &other_borrow.inner }) + Ok(PyDV { dv: &self.dv / &other_borrow.dv }) } else if let Ok(scalar) = other.extract::() { - Ok(PyDV { inner: &self.inner / scalar }) + Ok(PyDV { dv: &self.dv / scalar }) } else { Err(PyTypeError::new_err("Cannot divide DV by this type")) } @@ -141,7 +141,7 @@ impl PyDV { /// Right division operator (scalar / DV). fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> PyResult { if let Ok(scalar) = other.extract::() { - Ok(PyDV { inner: scalar / &self.inner }) + Ok(PyDV { dv: scalar / &self.dv }) } else { Err(PyTypeError::new_err("Cannot divide this type by DV")) } @@ -156,10 +156,10 @@ impl PyDV { /// DV: The result of raising to the power fn __pow__(&self, exponent: &Bound<'_, PyAny>, _modulo: Option<&Bound<'_, PyAny>>) -> PyResult { if let Ok(exp_int) = exponent.extract::() { - Ok(PyDV { inner: self.inner.powi(exp_int) }) + Ok(PyDV { dv: self.dv.powi(exp_int) }) } else if let Ok(exp_float) = exponent.extract::() { - match self.inner.powf(exp_float) { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.powf(exp_float) { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } else { @@ -169,27 +169,27 @@ impl PyDV { /// Negation operator. fn __neg__(&self) -> PyDV { - PyDV { inner: -&self.inner } + PyDV { dv: -&self.dv } } /// Absolute value. fn __abs__(&self) -> PyDV { - PyDV { inner: self.inner.abs() } + PyDV { dv: self.dv.abs() } } /// Equality comparison. fn __eq__(&self, other: &PyDV) -> bool { - self.inner == other.inner + self.dv == other.dv } /// Inequality comparison. fn __ne__(&self, other: &PyDV) -> bool { - self.inner != other.inner + self.dv != other.dv } /// Less than comparison. fn __lt__(&self, other: &PyDV) -> PyResult { - match self.inner.partial_cmp(&other.inner) { + match self.dv.partial_cmp(&other.dv) { Some(std::cmp::Ordering::Less) => Ok(true), Some(_) => Ok(false), None => Err(DVError::new_err("Cannot compare values with incompatible units")), @@ -198,7 +198,7 @@ impl PyDV { /// Less than or equal comparison. fn __le__(&self, other: &PyDV) -> PyResult { - match self.inner.partial_cmp(&other.inner) { + match self.dv.partial_cmp(&other.dv) { Some(std::cmp::Ordering::Less) | Some(std::cmp::Ordering::Equal) => Ok(true), Some(_) => Ok(false), None => Err(DVError::new_err("Cannot compare values with incompatible units")), @@ -207,7 +207,7 @@ impl PyDV { /// Greater than comparison. fn __gt__(&self, other: &PyDV) -> PyResult { - match self.inner.partial_cmp(&other.inner) { + match self.dv.partial_cmp(&other.dv) { Some(std::cmp::Ordering::Greater) => Ok(true), Some(_) => Ok(false), None => Err(DVError::new_err("Cannot compare values with incompatible units")), @@ -216,21 +216,21 @@ impl PyDV { /// Greater than or equal comparison. fn __ge__(&self, other: &PyDV) -> PyResult { - match self.inner.partial_cmp(&other.inner) { + match self.dv.partial_cmp(&other.dv) { Some(std::cmp::Ordering::Greater) | Some(std::cmp::Ordering::Equal) => Ok(true), Some(_) => Ok(false), None => Err(DVError::new_err("Cannot compare values with incompatible units")), } } - /// String representation. + /// String representation (for developers/debugging). fn __repr__(&self) -> String { - format!("DV(value={}, base_units={:?})", self.inner.value(), self.inner.unit()) + format!("DimensionalVariable({}, '{}')", self.dv.value(), self.dv.to_string().split_whitespace().skip(1).collect::>().join("")) } - /// String conversion. + /// String conversion (human-readable). fn __str__(&self) -> String { - format!("{} (SI base units)", self.inner.value()) + self.dv.to_string() } // Mathematical functions @@ -243,8 +243,8 @@ impl PyDV { /// Raises: /// DVError: If the operation fails fn sqrt(&self) -> PyResult { - match self.inner.sqrt() { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.sqrt() { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -257,8 +257,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless fn ln(&self) -> PyResult { - match self.inner.ln() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.ln() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -271,8 +271,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless fn log2(&self) -> PyResult { - match self.inner.log2() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.log2() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -285,8 +285,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless fn log10(&self) -> PyResult { - match self.inner.log10() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.log10() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -299,8 +299,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not an angle fn sin(&self) -> PyResult { - match self.inner.sin() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.sin() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -313,8 +313,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not an angle fn cos(&self) -> PyResult { - match self.inner.cos() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.cos() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -327,8 +327,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not an angle fn tan(&self) -> PyResult { - match self.inner.tan() { - Ok(result) => Ok(PyDV { inner: DimensionalVariable { value: result, unit: [0.0; 8] } }), + match self.dv.tan() { + Ok(result) => Ok(PyDV { dv: DimensionalVariable { value: result, unit: [0.0; 8] } }), Err(e) => Err(DVError::new_err(e)), } } @@ -341,8 +341,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless or outside [-1, 1] fn asin(&self) -> PyResult { - match self.inner.asin() { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.asin() { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -355,8 +355,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless or outside [-1, 1] fn acos(&self) -> PyResult { - match self.inner.acos() { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.acos() { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -369,8 +369,8 @@ impl PyDV { /// Raises: /// DVError: If the value is not unitless fn atan(&self) -> PyResult { - match self.inner.atan() { - Ok(result) => Ok(PyDV { inner: result }), + match self.dv.atan() { + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -380,7 +380,7 @@ impl PyDV { #[pyfunction] fn asin(x: f64) -> PyResult { match dv_rs::asin(x) { - Ok(result) => Ok(PyDV { inner: result }), + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -389,7 +389,7 @@ fn asin(x: f64) -> PyResult { #[pyfunction] fn acos(x: f64) -> PyResult { match dv_rs::acos(x) { - Ok(result) => Ok(PyDV { inner: result }), + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } } @@ -398,7 +398,7 @@ fn acos(x: f64) -> PyResult { #[pyfunction] fn atan(x: f64) -> PyResult { match dv_rs::atan(x) { - Ok(result) => Ok(PyDV { inner: result }), + Ok(result) => Ok(PyDV { dv: result }), Err(e) => Err(DVError::new_err(e)), } }