Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

major = 0
minor = 3
patch = 1
patch = 2
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dv"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = [ "Alex Tacescu <alextac98@gmail.com>",]
description = "Core Rust library for DimensionalVariable, a multi-language library for handling physical quantities with units."
Expand Down
64 changes: 64 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = Vec::new();
let mut denominator_parts: Vec<String> = 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<DimensionalVariable, String> {
return DimensionalVariable::new(x, "").unwrap().asin();
Expand Down
81 changes: 81 additions & 0 deletions core/tests/units_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,84 @@ fn electrical_units() {
let power = dv::new(20.0, "W").expect(FAIL_MSG);
assert!(power == &voltage * &current, "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");
}
20 changes: 20 additions & 0 deletions cpp/capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions cpp/include/dv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "dv_c.h"
#include <stdexcept>
#include <string>
#include <ostream>

namespace dv {

Expand Down Expand Up @@ -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())); }
Expand Down
2 changes: 2 additions & 0 deletions cpp/include/dv_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dv_py"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = [ "Alex Tacescu <alextac98@gmail.com>",]
homepage = "https://dv.alextac.com"
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading