Skip to main content

truecalc_core/eval/functions/parser/convert/
mod.rs

1use crate::eval::functions::check_arity;
2use crate::types::{ErrorKind, Value};
3
4#[derive(PartialEq)]
5enum UnitCategory {
6    Length,
7    Mass,
8    Time,
9    Temperature,
10    Pressure,
11    Energy,
12    Power,
13    Force,
14    Speed,
15    Area,
16    Volume,
17    Magnetism,
18    Information,
19}
20
21fn get_unit(name: &str) -> Option<(UnitCategory, f64)> {
22    match name {
23        // Length (base: m)
24        "m"          => Some((UnitCategory::Length, 1.0)),
25        "km"         => Some((UnitCategory::Length, 1000.0)),
26        "mi"         => Some((UnitCategory::Length, 1609.344)),
27        "nmi" | "Nmi" => Some((UnitCategory::Length, 1852.0)),
28        "ft"         => Some((UnitCategory::Length, 0.3048)),
29        "in"         => Some((UnitCategory::Length, 0.0254)),
30        "yd"         => Some((UnitCategory::Length, 0.9144)),
31        "cm"         => Some((UnitCategory::Length, 0.01)),
32        "mm"         => Some((UnitCategory::Length, 0.001)),
33        "Å" | "Ang"  => Some((UnitCategory::Length, 1e-10)),
34        "ly"         => Some((UnitCategory::Length, 9.4607304725808e15)),
35        "pica"       => Some((UnitCategory::Length, 0.00423333333)),
36        "pt"         => Some((UnitCategory::Length, 0.000352778)),
37        "ell"        => Some((UnitCategory::Length, 1.143)),
38        "parsec"     => Some((UnitCategory::Length, 3.08568e16)),
39        "survey_mi"  => Some((UnitCategory::Length, 1609.3472)),
40
41        // Mass (base: kg)
42        "kg"     => Some((UnitCategory::Mass, 1.0)),
43        "g"      => Some((UnitCategory::Mass, 0.001)),
44        "lbm"    => Some((UnitCategory::Mass, 0.45359237)),
45        "oz"     => Some((UnitCategory::Mass, 0.028349523125)),
46        "mg"     => Some((UnitCategory::Mass, 1e-6)),
47        "grain"  => Some((UnitCategory::Mass, 6.479891e-5)),
48        "cwt"    => Some((UnitCategory::Mass, 45.359237)),
49        "uk_cwt" => Some((UnitCategory::Mass, 50.80234544)),
50        "ton"    => Some((UnitCategory::Mass, 907.18474)),
51        "uk_ton" => Some((UnitCategory::Mass, 1016.0469088)),
52        "stone"  => Some((UnitCategory::Mass, 6.35029318)),
53        "slug"   => Some((UnitCategory::Mass, 14.59390294)),
54
55        // Time (base: s)
56        "s" | "sec" => Some((UnitCategory::Time, 1.0)),
57        "mn" | "min" => Some((UnitCategory::Time, 60.0)),
58        "hr"        => Some((UnitCategory::Time, 3600.0)),
59        "day"       => Some((UnitCategory::Time, 86400.0)),
60        "yr"        => Some((UnitCategory::Time, 31557600.0)),
61
62        // Temperature (special handling, factor unused)
63        "C" | "cel"  => Some((UnitCategory::Temperature, 0.0)),
64        "F" | "fah"  => Some((UnitCategory::Temperature, 0.0)),
65        "K" | "kel"  => Some((UnitCategory::Temperature, 0.0)),
66        "Rank"       => Some((UnitCategory::Temperature, 0.0)),
67
68        // Pressure (base: Pa)
69        "Pa"   => Some((UnitCategory::Pressure, 1.0)),
70        "atm"  => Some((UnitCategory::Pressure, 101325.0)),
71        "mmHg" => Some((UnitCategory::Pressure, 133.322387415)),
72        "psi"  => Some((UnitCategory::Pressure, 6894.757293168)),
73        "Torr" => Some((UnitCategory::Pressure, 133.322387415)),
74
75        // Energy (base: J)
76        "J"     => Some((UnitCategory::Energy, 1.0)),
77        "kJ"    => Some((UnitCategory::Energy, 1000.0)),
78        "cal"   => Some((UnitCategory::Energy, 4.184)),
79        "kcal"  => Some((UnitCategory::Energy, 4184.0)),
80        "eV"    => Some((UnitCategory::Energy, 1.602176634e-19)),
81        "BTU"   => Some((UnitCategory::Energy, 1055.05585262)),
82        "Wh"    => Some((UnitCategory::Energy, 3600.0)),
83        "kWh"   => Some((UnitCategory::Energy, 3600000.0)),
84        "HPh"   => Some((UnitCategory::Energy, 2684519.5368856)),
85        "ft-lb" => Some((UnitCategory::Energy, 1.3558179483314)),
86
87        // Power (base: W)
88        "W"  => Some((UnitCategory::Power, 1.0)),
89        "kW" => Some((UnitCategory::Power, 1000.0)),
90        "HP" => Some((UnitCategory::Power, 745.69987158227)),
91        "PS" => Some((UnitCategory::Power, 735.49875)),
92
93        // Force (base: N)
94        "N"    => Some((UnitCategory::Force, 1.0)),
95        "lbf"  => Some((UnitCategory::Force, 4.4482216152605)),
96        "dyn"  => Some((UnitCategory::Force, 1e-5)),
97        "pond" => Some((UnitCategory::Force, 0.00980665)),
98
99        // Speed (base: m/s)
100        "m/s"   => Some((UnitCategory::Speed, 1.0)),
101        "m/h"   => Some((UnitCategory::Speed, 0.00027778)),
102        "kn"    => Some((UnitCategory::Speed, 0.51444)),
103        "mph"   => Some((UnitCategory::Speed, 0.44704)),
104        "admkn" => Some((UnitCategory::Speed, 0.514773)),
105
106        // Area (base: m²)
107        // GS accepts both "m2" and "m^2" notation for squared units.
108        "m2"  | "m^2"   => Some((UnitCategory::Area, 1.0)),
109        "km2" | "km^2"  => Some((UnitCategory::Area, 1e6)),
110        "ft2" | "ft^2"  => Some((UnitCategory::Area, 0.09290304)),
111        "in2" | "in^2"  => Some((UnitCategory::Area, 0.00064516)),
112        "yd2" | "yd^2"  => Some((UnitCategory::Area, 0.83612736)),
113        "mi2" | "mi^2"  => Some((UnitCategory::Area, 2589988.110336)),
114        "ha"   => Some((UnitCategory::Area, 10000.0)),
115        "ar"   => Some((UnitCategory::Area, 100.0)),
116        "acre" => Some((UnitCategory::Area, 4046.8564224)),
117
118        // Volume (base: L)
119        // GS accepts both "m3" and "m^3" notation for cubed units.
120        "l" | "L" | "lt" => Some((UnitCategory::Volume, 1.0)),
121        "ml" | "mL"      => Some((UnitCategory::Volume, 0.001)),
122        "m3"  | "m^3"    => Some((UnitCategory::Volume, 1000.0)),
123        "km3" | "km^3"   => Some((UnitCategory::Volume, 1e12)),
124        "ft3" | "ft^3"   => Some((UnitCategory::Volume, 28.316846592)),
125        "in3" | "in^3"   => Some((UnitCategory::Volume, 0.016387064)),
126        "yd3" | "yd^3"   => Some((UnitCategory::Volume, 764.554857984)),
127        "mi3" | "mi^3"   => Some((UnitCategory::Volume, 4_168_181_825.440_579_4)),
128        "gal"            => Some((UnitCategory::Volume, 3.785411784)),
129        "qt"             => Some((UnitCategory::Volume, 0.946352946)),
130        "cup"            => Some((UnitCategory::Volume, 0.2365882365)),
131        "fl_oz"          => Some((UnitCategory::Volume, 0.0295735295625)),
132        "tsp"            => Some((UnitCategory::Volume, 0.00492892159375)),
133        "tbs"            => Some((UnitCategory::Volume, 0.01478676478125)),
134        "uk_pt"          => Some((UnitCategory::Volume, 0.56826125)),
135        "uk_qt"          => Some((UnitCategory::Volume, 1.1365225)),
136        "uk_gal"         => Some((UnitCategory::Volume, 4.54609)),
137        "ang3" | "ang^3" => Some((UnitCategory::Volume, 1e-30)),
138
139        // Magnetism (base: T)
140        "T"  => Some((UnitCategory::Magnetism, 1.0)),
141        "ga" => Some((UnitCategory::Magnetism, 0.0001)),
142
143        // Information (base: bit)
144        "bit"   => Some((UnitCategory::Information, 1.0)),
145        "byte"  => Some((UnitCategory::Information, 8.0)),
146        "kbit"  => Some((UnitCategory::Information, 1000.0)),
147        "kbyte" => Some((UnitCategory::Information, 8000.0)),
148        "Mbit"  => Some((UnitCategory::Information, 1e6)),
149        "Mbyte" => Some((UnitCategory::Information, 8e6)),
150        "Gbit"  => Some((UnitCategory::Information, 1e9)),
151        "Gbyte" => Some((UnitCategory::Information, 8e9)),
152        "Tbit"  => Some((UnitCategory::Information, 1e12)),
153        "Tbyte" => Some((UnitCategory::Information, 8e12)),
154
155        _ => None,
156    }
157}
158
159fn is_temperature(name: &str) -> bool {
160    matches!(name, "C" | "cel" | "F" | "fah" | "K" | "kel" | "Rank")
161}
162
163fn convert_temperature(value: f64, from: &str, to: &str) -> f64 {
164    let celsius = match from {
165        "C" | "cel" => value,
166        "F" | "fah" => (value - 32.0) * 5.0 / 9.0,
167        "K" | "kel" => value - 273.15,
168        "Rank"      => (value - 491.67) * 5.0 / 9.0,
169        _ => unreachable!(),
170    };
171    match to {
172        "C" | "cel" => celsius,
173        "F" | "fah" => celsius * 9.0 / 5.0 + 32.0,
174        "K" | "kel" => celsius + 273.15,
175        "Rank"      => (celsius + 273.15) * 9.0 / 5.0,
176        _ => unreachable!(),
177    }
178}
179
180fn coerce_to_number(v: &Value) -> Option<f64> {
181    match v {
182        Value::Number(n) => Some(*n),
183        Value::Bool(b)   => Some(if *b { 1.0 } else { 0.0 }),
184        Value::Date(n)   => Some(*n),
185        _ => None,
186    }
187}
188
189pub fn convert_fn(args: &[Value]) -> Value {
190    if let Some(err) = check_arity(args, 3, 3) {
191        return err;
192    }
193
194    let value = match coerce_to_number(&args[0]) {
195        Some(n) => n,
196        None => return Value::Error(ErrorKind::Value),
197    };
198
199    let from = match &args[1] {
200        Value::Text(s)  => s.clone(),
201        Value::Error(_) => return args[1].clone(),
202        _ => return Value::Error(ErrorKind::Value),
203    };
204
205    let to = match &args[2] {
206        Value::Text(s)  => s.clone(),
207        Value::Error(_) => return args[2].clone(),
208        _ => return Value::Error(ErrorKind::Value),
209    };
210
211    if is_temperature(&from) || is_temperature(&to) {
212        if !is_temperature(&from) || !is_temperature(&to) {
213            return Value::Error(ErrorKind::NA);
214        }
215        let result = convert_temperature(value, &from, &to);
216        if result.is_finite() {
217            Value::Number(result)
218        } else {
219            Value::Error(ErrorKind::Num)
220        }
221    } else {
222        let (from_cat, from_factor) = match get_unit(&from) {
223            Some(u) => u,
224            None => return Value::Error(ErrorKind::NA),
225        };
226        let (to_cat, to_factor) = match get_unit(&to) {
227            Some(u) => u,
228            None => return Value::Error(ErrorKind::NA),
229        };
230        if from_cat != to_cat {
231            return Value::Error(ErrorKind::NA);
232        }
233        let result = value * from_factor / to_factor;
234        if result.is_finite() {
235            Value::Number(result)
236        } else {
237            Value::Error(ErrorKind::Num)
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests;