Skip to main content

kcl_lib/
fmt.rs

1use serde::Serialize;
2
3use crate::execution::types::NumericType;
4use crate::pretty::NumericSuffix;
5
6/// For the UI, display a number and its type for debugging purposes. This is
7/// used by TS.
8pub fn human_display_number(value: f64, ty: NumericType) -> String {
9    match ty {
10        NumericType::Known(unit_type) => format!("{value}: number({unit_type})"),
11        NumericType::Default { len, angle } => format!("{value} (no units, defaulting to {len} or {angle})"),
12        NumericType::Unknown => format!("{value} (number with unknown units)"),
13        NumericType::Any => format!("{value} (number with any units)"),
14    }
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, thiserror::Error)]
18#[serde(tag = "type")]
19pub enum FormatNumericSuffixError {
20    #[error("Invalid numeric suffix: {0}")]
21    Invalid(NumericSuffix),
22}
23
24/// For UI code generation, format a number with a suffix. The result must parse
25/// as a literal. If it can't be done, returns an error. `decimals` defaults to 2
26//
27/// This is used by TS.
28pub fn format_number_literal(
29    value: f64,
30    suffix: NumericSuffix,
31    decimals: Option<usize>,
32) -> Result<String, FormatNumericSuffixError> {
33    // Format to `d` decimal places maximum (matching TypeScript roundOff function)
34    let d = decimals.unwrap_or(2);
35    // Round first to ensure we don't have floating point precision issues
36    let factor = 10_f64.powi(d as i32);
37    let rounded = (value * factor).round() / factor;
38    let rounded = normalize_negative_zero(rounded);
39    // Format with up to `d` decimal places, removing trailing zeros
40    let formatted = if rounded.fract().abs() < 1e-10 {
41        // Integer value
42        format!("{:.0}", rounded)
43    } else {
44        // Format with `d` decimal places, then remove trailing zeros
45        let with_decimals = format!("{rounded:.d$}");
46        with_decimals.trim_end_matches('0').trim_end_matches('.').to_string()
47    };
48
49    match suffix {
50        // There isn't a syntactic suffix for these. For unknown, we don't want
51        // to ever generate the unknown suffix. We currently warn on it, and we
52        // may remove it in the future.
53        NumericSuffix::Length | NumericSuffix::Angle | NumericSuffix::Unknown => {
54            Err(FormatNumericSuffixError::Invalid(suffix))
55        }
56        NumericSuffix::None
57        | NumericSuffix::Count
58        | NumericSuffix::Mm
59        | NumericSuffix::Cm
60        | NumericSuffix::M
61        | NumericSuffix::Inch
62        | NumericSuffix::Ft
63        | NumericSuffix::Yd
64        | NumericSuffix::Deg
65        | NumericSuffix::Rad => Ok(format!("{formatted}{suffix}")),
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, thiserror::Error)]
70#[serde(tag = "type")]
71pub enum FormatNumericTypeError {
72    #[error("Invalid numeric type: {0:?}")]
73    Invalid(NumericType),
74}
75
76/// For UI code generation, format a number value with a suffix such that the
77/// result can parse as a literal. If it can't be done, returns an error.
78///
79/// This is used by TS.
80pub fn format_number_value(value: f64, ty: NumericType) -> Result<String, FormatNumericTypeError> {
81    let value = normalize_negative_zero(value);
82    match ty {
83        NumericType::Default { .. } => Ok(value.to_string()),
84        // There isn't a syntactic suffix for these. For unknown, we don't want
85        // to ever generate the unknown suffix. We currently warn on it, and we
86        // may remove it in the future.
87        NumericType::Unknown | NumericType::Any => Err(FormatNumericTypeError::Invalid(ty)),
88        NumericType::Known(unit_type) => unit_type
89            .to_suffix()
90            .map(|suffix| format!("{value}{suffix}"))
91            .ok_or(FormatNumericTypeError::Invalid(ty)),
92    }
93}
94
95fn normalize_negative_zero(value: f64) -> f64 {
96    if value == 0.0 { 0.0 } else { value }
97}
98
99#[cfg(test)]
100mod tests {
101    use kittycad_modeling_cmds::units::UnitAngle;
102    use kittycad_modeling_cmds::units::UnitLength;
103    use pretty_assertions::assert_eq;
104
105    use super::*;
106    use crate::execution::types::UnitType;
107
108    #[test]
109    fn test_human_display_number() {
110        assert_eq!(
111            human_display_number(1.0, NumericType::Known(UnitType::Count)),
112            "1: number(Count)"
113        );
114        assert_eq!(
115            human_display_number(1.0, NumericType::Known(UnitType::Length(UnitLength::Meters))),
116            "1: number(m)"
117        );
118        assert_eq!(
119            human_display_number(1.0, NumericType::Known(UnitType::Length(UnitLength::Millimeters))),
120            "1: number(mm)"
121        );
122        assert_eq!(
123            human_display_number(1.0, NumericType::Known(UnitType::Length(UnitLength::Inches))),
124            "1: number(in)"
125        );
126        assert_eq!(
127            human_display_number(1.0, NumericType::Known(UnitType::Length(UnitLength::Feet))),
128            "1: number(ft)"
129        );
130        assert_eq!(
131            human_display_number(1.0, NumericType::Known(UnitType::Angle(UnitAngle::Degrees))),
132            "1: number(deg)"
133        );
134        assert_eq!(
135            human_display_number(1.0, NumericType::Known(UnitType::Angle(UnitAngle::Radians))),
136            "1: number(rad)"
137        );
138        assert_eq!(
139            human_display_number(
140                1.0,
141                NumericType::Default {
142                    len: UnitLength::Millimeters,
143                    angle: UnitAngle::Degrees,
144                }
145            ),
146            "1 (no units, defaulting to mm or deg)"
147        );
148        assert_eq!(
149            human_display_number(
150                1.0,
151                NumericType::Default {
152                    len: UnitLength::Feet,
153                    angle: UnitAngle::Radians,
154                }
155            ),
156            "1 (no units, defaulting to ft or rad)"
157        );
158        assert_eq!(
159            human_display_number(1.0, NumericType::Unknown),
160            "1 (number with unknown units)"
161        );
162        assert_eq!(human_display_number(1.0, NumericType::Any), "1 (number with any units)");
163    }
164
165    #[test]
166    fn test_format_number_literal() {
167        assert_eq!(
168            format_number_literal(1.0, NumericSuffix::Length, None),
169            Err(FormatNumericSuffixError::Invalid(NumericSuffix::Length))
170        );
171        assert_eq!(
172            format_number_literal(1.0, NumericSuffix::Angle, None),
173            Err(FormatNumericSuffixError::Invalid(NumericSuffix::Angle))
174        );
175        assert_eq!(
176            format_number_literal(1.0, NumericSuffix::None, None),
177            Ok("1".to_owned())
178        );
179        assert_eq!(
180            format_number_literal(1.0, NumericSuffix::Count, None),
181            Ok("1_".to_owned())
182        );
183        assert_eq!(
184            format_number_literal(1.0, NumericSuffix::Mm, None),
185            Ok("1mm".to_owned())
186        );
187        assert_eq!(
188            format_number_literal(1.0, NumericSuffix::Cm, None),
189            Ok("1cm".to_owned())
190        );
191        assert_eq!(format_number_literal(1.0, NumericSuffix::M, None), Ok("1m".to_owned()));
192        assert_eq!(
193            format_number_literal(1.0, NumericSuffix::Inch, None),
194            Ok("1in".to_owned())
195        );
196        assert_eq!(
197            format_number_literal(1.0, NumericSuffix::Ft, None),
198            Ok("1ft".to_owned())
199        );
200        assert_eq!(
201            format_number_literal(1.0, NumericSuffix::Yd, None),
202            Ok("1yd".to_owned())
203        );
204        assert_eq!(
205            format_number_literal(1.0, NumericSuffix::Deg, None),
206            Ok("1deg".to_owned())
207        );
208        assert_eq!(
209            format_number_literal(1.0, NumericSuffix::Rad, None),
210            Ok("1rad".to_owned())
211        );
212        assert_eq!(
213            format_number_literal(1.23456, NumericSuffix::None, Some(4)),
214            Ok("1.2346".to_owned())
215        );
216        assert_eq!(
217            format_number_literal(1.2, NumericSuffix::None, Some(4)),
218            Ok("1.2".to_owned())
219        );
220        assert_eq!(
221            format_number_literal(1.0, NumericSuffix::Unknown, None),
222            Err(FormatNumericSuffixError::Invalid(NumericSuffix::Unknown))
223        );
224        assert_eq!(
225            format_number_literal(-0.0, NumericSuffix::Mm, None),
226            Ok("0mm".to_owned())
227        );
228    }
229
230    #[test]
231    fn test_format_number_value() {
232        assert_eq!(
233            format_number_value(
234                1.0,
235                NumericType::Default {
236                    len: UnitLength::Millimeters,
237                    angle: UnitAngle::Degrees,
238                }
239            ),
240            Ok("1".to_owned())
241        );
242        assert_eq!(
243            format_number_value(1.0, NumericType::Known(UnitType::GenericLength)),
244            Err(FormatNumericTypeError::Invalid(NumericType::Known(
245                UnitType::GenericLength
246            )))
247        );
248        assert_eq!(
249            format_number_value(1.0, NumericType::Known(UnitType::GenericAngle)),
250            Err(FormatNumericTypeError::Invalid(NumericType::Known(
251                UnitType::GenericAngle
252            )))
253        );
254        assert_eq!(
255            format_number_value(1.0, NumericType::Known(UnitType::Count)),
256            Ok("1_".to_owned())
257        );
258        assert_eq!(
259            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Millimeters))),
260            Ok("1mm".to_owned())
261        );
262        assert_eq!(
263            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Centimeters))),
264            Ok("1cm".to_owned())
265        );
266        assert_eq!(
267            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Meters))),
268            Ok("1m".to_owned())
269        );
270        assert_eq!(
271            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Inches))),
272            Ok("1in".to_owned())
273        );
274        assert_eq!(
275            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Feet))),
276            Ok("1ft".to_owned())
277        );
278        assert_eq!(
279            format_number_value(1.0, NumericType::Known(UnitType::Length(UnitLength::Yards))),
280            Ok("1yd".to_owned())
281        );
282        assert_eq!(
283            format_number_value(1.0, NumericType::Known(UnitType::Angle(UnitAngle::Degrees))),
284            Ok("1deg".to_owned())
285        );
286        assert_eq!(
287            format_number_value(1.0, NumericType::Known(UnitType::Angle(UnitAngle::Radians))),
288            Ok("1rad".to_owned())
289        );
290        assert_eq!(
291            format_number_value(1.0, NumericType::Unknown),
292            Err(FormatNumericTypeError::Invalid(NumericType::Unknown))
293        );
294        assert_eq!(
295            format_number_value(1.0, NumericType::Any),
296            Err(FormatNumericTypeError::Invalid(NumericType::Any))
297        );
298        assert_eq!(
299            format_number_value(
300                -0.0,
301                NumericType::Default {
302                    len: UnitLength::Millimeters,
303                    angle: UnitAngle::Degrees,
304                }
305            ),
306            Ok("0".to_owned())
307        );
308    }
309}