mathypad_core/units/
value.rs

1//! Unit value representation and operations
2
3use super::types::{Unit, UnitType};
4use crate::{FLOAT_EPSILON, MAX_INTEGER_FOR_FORMATTING};
5
6/// Represents a numeric value with an optional unit
7#[derive(Debug, Clone)]
8pub struct UnitValue {
9    pub value: f64,
10    pub unit: Option<Unit>,
11}
12
13impl UnitValue {
14    /// Create a new UnitValue
15    pub fn new(value: f64, unit: Option<Unit>) -> Self {
16        UnitValue { value, unit }
17    }
18
19    /// Convert this value to a different unit of the same type
20    pub fn to_unit(&self, target_unit: &Unit) -> Option<UnitValue> {
21        match &self.unit {
22            Some(current_unit) => {
23                // Special handling for rate unit conversions first
24                if let (
25                    Unit::RateUnit(curr_num, curr_denom),
26                    Unit::RateUnit(targ_num, targ_denom),
27                ) = (current_unit, target_unit)
28                {
29                    // Check if numerators are compatible (same currency/unit type)
30                    if curr_num.unit_type() == targ_num.unit_type() && curr_num == targ_num {
31                        // Convert the denominator to match the target
32                        let curr_denom_base = curr_denom.to_base_value(1.0);
33                        let targ_denom_base = targ_denom.to_base_value(1.0);
34
35                        // Rate conversion: if we have $/month and want $/year,
36                        // we need to multiply by how many current periods fit in target period
37                        // For $/month to $/year: $5/month * 12 months/year = $60/year
38                        let conversion_factor = targ_denom_base / curr_denom_base;
39                        let converted_value = self.value * conversion_factor;
40
41                        return Some(UnitValue::new(converted_value, Some(target_unit.clone())));
42                    } else if curr_num.unit_type() == UnitType::Currency
43                        && targ_num.unit_type() == UnitType::Currency
44                    {
45                        // Different currencies - not convertible
46                        return None;
47                    }
48                }
49
50                // Check if units are the same type or compatible data rates
51                if current_unit.unit_type() == target_unit.unit_type()
52                    || self.can_convert_between_data_rates(current_unit, target_unit)
53                {
54                    let base_value = current_unit.to_base_value(self.value);
55                    let converted_value = target_unit.clone().from_base_value(base_value);
56                    Some(UnitValue::new(converted_value, Some(target_unit.clone())))
57                }
58                // Check for special conversions between bits and bytes
59                else if self.can_convert_between_bits_bytes(current_unit, target_unit) {
60                    self.convert_bits_bytes(current_unit, target_unit)
61                } else {
62                    None // Can't convert between different unit types
63                }
64            }
65            None => {
66                // Handle conversion from dimensionless value to percentage
67                use super::types::UnitType;
68                if target_unit.unit_type() == UnitType::Percentage {
69                    let converted_value = target_unit.clone().from_base_value(self.value);
70                    Some(UnitValue::new(converted_value, Some(target_unit.clone())))
71                } else {
72                    None // No unit to convert from
73                }
74            }
75        }
76    }
77
78    /// Check if conversion between data rates with different time units is possible
79    fn can_convert_between_data_rates(&self, current: &Unit, target: &Unit) -> bool {
80        use super::types::UnitType;
81        matches!(
82            (current.unit_type(), target.unit_type()),
83            (UnitType::DataRate { .. }, UnitType::DataRate { .. })
84        )
85    }
86
87    /// Check if conversion between bits and bytes is possible
88    fn can_convert_between_bits_bytes(&self, current: &Unit, target: &Unit) -> bool {
89        use super::types::UnitType;
90        matches!(
91            (current.unit_type(), target.unit_type()),
92            (UnitType::Bit, UnitType::Data)
93                | (UnitType::Data, UnitType::Bit)
94                | (UnitType::BitRate, UnitType::DataRate { .. })
95                | (UnitType::DataRate { .. }, UnitType::BitRate)
96        )
97    }
98
99    /// Convert between bits and bytes (8 bits = 1 byte)
100    fn convert_bits_bytes(&self, current: &Unit, target: &Unit) -> Option<UnitValue> {
101        use super::types::UnitType;
102
103        match (current.unit_type(), target.unit_type()) {
104            // Bit to Byte conversion
105            (UnitType::Bit, UnitType::Data) => {
106                let bits = current.to_base_value(self.value); // Convert to base bits
107                let bytes = bits / 8.0; // 8 bits = 1 byte
108                let converted_value = target.clone().from_base_value(bytes);
109                Some(UnitValue::new(converted_value, Some(target.clone())))
110            }
111            // Byte to Bit conversion
112            (UnitType::Data, UnitType::Bit) => {
113                let bytes = current.to_base_value(self.value); // Convert to base bytes
114                let bits = bytes * 8.0; // 1 byte = 8 bits
115                let converted_value = target.clone().from_base_value(bits);
116                Some(UnitValue::new(converted_value, Some(target.clone())))
117            }
118            // Bit rate to Byte rate conversion
119            (UnitType::BitRate, UnitType::DataRate { .. }) => {
120                let bits_per_sec = current.to_base_value(self.value); // Convert to base bits/sec
121                let bytes_per_sec = bits_per_sec / 8.0; // 8 bits/sec = 1 byte/sec
122                let converted_value = target.clone().from_base_value(bytes_per_sec);
123                Some(UnitValue::new(converted_value, Some(target.clone())))
124            }
125            // Byte rate to Bit rate conversion
126            (UnitType::DataRate { .. }, UnitType::BitRate) => {
127                let bytes_per_sec = current.to_base_value(self.value); // Convert to base bytes/sec
128                let bits_per_sec = bytes_per_sec * 8.0; // 1 byte/sec = 8 bits/sec
129                let converted_value = target.clone().from_base_value(bits_per_sec);
130                Some(UnitValue::new(converted_value, Some(target.clone())))
131            }
132            _ => None,
133        }
134    }
135
136    /// Format the value for display
137    pub fn format(&self) -> String {
138        let formatted_value =
139            if self.value.fract() == 0.0 && self.value.abs() < MAX_INTEGER_FOR_FORMATTING {
140                format_number_with_commas(self.value as i64)
141            } else {
142                format_decimal_with_commas(self.value)
143            };
144
145        match &self.unit {
146            Some(unit) => format!("{} {}", formatted_value, unit.display_name()),
147            None => formatted_value,
148        }
149    }
150}
151
152/// Format a number with comma separators
153fn format_number_with_commas(num: i64) -> String {
154    let num_str = num.to_string();
155    let mut result = String::new();
156    let chars: Vec<char> = num_str.chars().collect();
157
158    let is_negative = chars.first() == Some(&'-');
159    let start_idx = if is_negative { 1 } else { 0 };
160
161    if is_negative {
162        result.push('-');
163    }
164
165    for (i, ch) in chars[start_idx..].iter().enumerate() {
166        if i > 0 && (chars.len() - start_idx - i) % 3 == 0 {
167            result.push(',');
168        }
169        result.push(*ch);
170    }
171
172    result
173}
174
175/// Format a decimal number with comma separators (for whole part)
176fn format_decimal_with_commas(num: f64) -> String {
177    if num.abs() < FLOAT_EPSILON {
178        return "0".to_string();
179    }
180
181    let is_negative = num < 0.0;
182    let abs_num = num.abs();
183
184    let formatted = format!("{:.3}", abs_num);
185
186    // Split into whole and decimal parts
187    let parts: Vec<&str> = formatted.split('.').collect();
188    if parts.len() != 2 {
189        return if is_negative {
190            format!("-{}", formatted)
191        } else {
192            formatted
193        };
194    }
195
196    let whole_part = parts[0];
197    let decimal_part = parts[1];
198
199    // Add commas to whole part
200    let whole_with_commas = if whole_part == "0" {
201        "0".to_string()
202    } else {
203        let whole_chars: Vec<char> = whole_part.chars().collect();
204        let mut result = String::new();
205
206        for (i, ch) in whole_chars.iter().enumerate() {
207            if i > 0 && (whole_chars.len() - i) % 3 == 0 {
208                result.push(',');
209            }
210            result.push(*ch);
211        }
212        result
213    };
214
215    // Remove trailing zeros from decimal part
216    let decimal_trimmed = decimal_part.trim_end_matches('0');
217
218    let formatted_result = if decimal_trimmed.is_empty() {
219        whole_with_commas
220    } else {
221        format!("{}.{}", whole_with_commas, decimal_trimmed)
222    };
223
224    if is_negative {
225        format!("-{}", formatted_result)
226    } else {
227        formatted_result
228    }
229}