Skip to main content

kcl_lib/std/
string.rs

1//! Standard library string operations.
2
3use crate::errors::KclError;
4use crate::execution::ExecState;
5use crate::execution::KclValue;
6use crate::execution::types::NumericType;
7use crate::execution::types::RuntimeType;
8use crate::std::Args;
9use crate::std::args::TyF64;
10
11/// Convert all cased characters in a string to uppercase.
12pub async fn uppercase(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
13    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
14
15    Ok(KclValue::String {
16        value: text.to_uppercase(),
17        meta: args.into(),
18    })
19}
20
21/// Convert all cased characters in a string to lowercase.
22pub async fn lowercase(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
23    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
24
25    Ok(KclValue::String {
26        value: text.to_lowercase(),
27        meta: args.into(),
28    })
29}
30
31/// Compare two strings for equality.
32pub async fn is_equal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
34    let to: String = args.get_kw_arg("to", &RuntimeType::string(), exec_state)?;
35    let case_insensitive = args
36        .get_kw_arg_opt("caseInsensitive", &RuntimeType::bool(), exec_state)?
37        .unwrap_or(false);
38
39    let value = if case_insensitive {
40        unicase::eq(&text, &to)
41    } else {
42        text == to
43    };
44
45    Ok(KclValue::Bool {
46        value,
47        meta: args.into(),
48    })
49}
50
51fn trim_whitespace(text: &str, at_start: bool, at_end: bool) -> &str {
52    match (at_start, at_end) {
53        (true, true) => text.trim(),
54        (true, false) => text.trim_start(),
55        (false, true) => text.trim_end(),
56        (false, false) => text,
57    }
58}
59
60/// Remove whitespace from the start and end of a string.
61pub async fn trim(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
63    let value = trim_whitespace(&text, true, true).to_owned();
64
65    Ok(KclValue::String {
66        value,
67        meta: args.into(),
68    })
69}
70
71/// Remove whitespace from the start of a string.
72pub async fn trim_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
73    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
74    let value = trim_whitespace(&text, true, false).to_owned();
75
76    Ok(KclValue::String {
77        value,
78        meta: args.into(),
79    })
80}
81
82/// Remove whitespace from the end of a string.
83pub async fn trim_end(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
84    let text: String = args.get_unlabeled_kw_arg("text", &RuntimeType::string(), exec_state)?;
85    let value = trim_whitespace(&text, false, true).to_owned();
86
87    Ok(KclValue::String {
88        value,
89        meta: args.into(),
90    })
91}
92
93/// Render a number as text for a person to read.
94///
95/// Every `number` value has a rendering, including the non-finite ones, so this
96/// cannot fail. `f64` spells the non-finite values `inf`, `-inf`, and `NaN`;
97/// they are written out in full here instead. A suffix is appended only when
98/// the concrete unit is known; when it is not, the bare number is all that can
99/// be said truthfully about the value, so that is what callers get.
100///
101/// The output is for reading, not for parsing. Some of it is not valid KCL
102/// source, which is why `crate::fmt::format_number_value` is not used here even
103/// though it looks similar: that function generates KCL source for the user
104/// interface, so it errors on `Unknown`, `Any`, `GenericLength`, and
105/// `GenericAngle`, where this one returns the bare number.
106fn format_number(n: f64, ty: NumericType) -> String {
107    // Non-finite values are reported without a unit. There is no length that
108    // `Infinitymm` describes, so the suffix would add noise rather than meaning.
109    if n.is_nan() {
110        return "NaN".to_owned();
111    }
112    if n.is_infinite() {
113        return if n.is_sign_positive() { "Infinity" } else { "-Infinity" }.to_owned();
114    }
115
116    let value = crate::fmt::normalize_negative_zero(n);
117    let suffix = match ty {
118        // `to_suffix` yields nothing for the generic length and angle types,
119        // which is the same "units unclear" case as the arms below.
120        NumericType::Known(unit_type) => unit_type.to_suffix().unwrap_or_default(),
121        NumericType::Default { .. } | NumericType::Unknown | NumericType::Any => String::new(),
122    };
123
124    format!("{value}{suffix}")
125}
126
127/// Convert a number to human-readable text.
128pub async fn number_to_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
129    // Reading the argument as `Any` preserves whatever numeric type it already
130    // has rather than erasing it, which is what the suffix depends on.
131    let num: TyF64 = args.get_unlabeled_kw_arg("num", &RuntimeType::num_any(), exec_state)?;
132
133    Ok(KclValue::String {
134        value: format_number(num.n, num.ty),
135        meta: args.into(),
136    })
137}
138
139#[cfg(test)]
140mod tests {
141    use kcl_api::UnitAngle;
142    use kcl_api::UnitLength;
143    use pretty_assertions::assert_eq;
144
145    use super::*;
146    use crate::execution::types::UnitType;
147
148    /// The unitless default, as a value with no explicit suffix acquires.
149    fn default_units() -> NumericType {
150        NumericType::Default {
151            len: UnitLength::Millimeters,
152            angle: UnitAngle::Degrees,
153        }
154    }
155
156    fn length(len: UnitLength) -> NumericType {
157        NumericType::Known(UnitType::Length(len))
158    }
159
160    fn angle(angle: UnitAngle) -> NumericType {
161        NumericType::Known(UnitType::Angle(angle))
162    }
163
164    #[test]
165    fn format_number_covers_every_numeric_type() {
166        for (name, n, ty, expected) in [
167            // Unitless and default: no suffix.
168            ("default integer", 12.0, default_units(), "12"),
169            ("default fractional", 1.5, default_units(), "1.5"),
170            ("default negative", -7.0, default_units(), "-7"),
171            ("default zero", 0.0, default_units(), "0"),
172            // Negative zero is normalized, so it prints like positive zero.
173            ("negative zero", -0.0, default_units(), "0"),
174            (
175                "negative zero with a unit",
176                -0.0,
177                length(UnitLength::Millimeters),
178                "0mm",
179            ),
180            // Counts keep the `_` suffix.
181            ("count", 3.0, NumericType::Known(UnitType::Count), "3_"),
182            ("count fractional", 2.5, NumericType::Known(UnitType::Count), "2.5_"),
183            ("count negative", -4.0, NumericType::Known(UnitType::Count), "-4_"),
184            // Every concrete length unit keeps its canonical suffix.
185            ("millimeters", 12.0, length(UnitLength::Millimeters), "12mm"),
186            ("centimeters", 12.0, length(UnitLength::Centimeters), "12cm"),
187            ("meters", 12.0, length(UnitLength::Meters), "12m"),
188            ("inches", 1.5, length(UnitLength::Inches), "1.5in"),
189            ("feet", 2.0, length(UnitLength::Feet), "2ft"),
190            ("yards", 3.0, length(UnitLength::Yards), "3yd"),
191            ("negative length", -5.0, length(UnitLength::Millimeters), "-5mm"),
192            // Both concrete angle units keep their canonical suffix.
193            ("degrees", 90.0, angle(UnitAngle::Degrees), "90deg"),
194            ("radians", 1.5, angle(UnitAngle::Radians), "1.5rad"),
195            // Units the type system cannot pin down: the bare number is all
196            // that can be said truthfully, so no suffix is emitted.
197            (
198                "generic length",
199                12.0,
200                NumericType::Known(UnitType::GenericLength),
201                "12",
202            ),
203            ("generic angle", 90.0, NumericType::Known(UnitType::GenericAngle), "90"),
204            ("unknown", 20.0, NumericType::Unknown, "20"),
205            ("any", 12.0, NumericType::Any, "12"),
206        ] {
207            assert_eq!(format_number(n, ty), expected, "case: {name}");
208        }
209    }
210
211    #[test]
212    fn format_number_spells_out_non_finite_values() {
213        // Rust prints these as `inf`, `-inf`, and `NaN`; the words are spelled
214        // out in full instead. The unit is dropped whatever it was, so the
215        // numeric type cannot change the result.
216        for (name, n, ty, expected) in [
217            ("positive infinity", f64::INFINITY, default_units(), "Infinity"),
218            ("negative infinity", f64::NEG_INFINITY, default_units(), "-Infinity"),
219            ("nan", f64::NAN, default_units(), "NaN"),
220            (
221                "infinity with a length",
222                f64::INFINITY,
223                length(UnitLength::Millimeters),
224                "Infinity",
225            ),
226            (
227                "negative infinity with an angle",
228                f64::NEG_INFINITY,
229                angle(UnitAngle::Degrees),
230                "-Infinity",
231            ),
232            ("nan with an angle", f64::NAN, angle(UnitAngle::Degrees), "NaN"),
233            ("nan as a count", f64::NAN, NumericType::Known(UnitType::Count), "NaN"),
234            (
235                "infinity with unclear units",
236                f64::INFINITY,
237                NumericType::Unknown,
238                "Infinity",
239            ),
240        ] {
241            assert_eq!(format_number(n, ty), expected, "case: {name}");
242        }
243    }
244
245    #[test]
246    fn format_number_does_not_lose_precision() {
247        // Reading the output back is not a supported operation, but the text
248        // must still name the value exactly rather than an approximation of it,
249        // which is why no rounding or precision parameter exists. Parsing is
250        // just a convenient way to assert that no digits were dropped.
251        for (name, n) in [
252            ("one tenth", 0.1),
253            ("sum that is not exact", 0.1 + 0.2),
254            ("one third", 1.0 / 3.0),
255            ("largest finite", f64::MAX),
256            ("smallest positive normal", f64::MIN_POSITIVE),
257            ("smallest subnormal", f64::from_bits(1)),
258            ("large magnitude", 1e300),
259            ("small magnitude", 1e-300),
260            ("negative fractional", -123.456),
261        ] {
262            let text = format_number(n, default_units());
263            let reparsed: f64 = text.parse().unwrap();
264            assert_eq!(reparsed.to_bits(), n.to_bits(), "case: {name}, rendered as {text}");
265        }
266    }
267
268    #[test]
269    fn format_number_loses_the_sign_of_negative_zero() {
270        // Deliberate: `-0` and `0` are the same quantity, and the existing
271        // formatter for generated KCL already normalizes the sign away. It is
272        // the one value whose text does not name it exactly.
273        let text = format_number(-0.0, default_units());
274        assert_eq!(text, "0");
275
276        let reparsed: f64 = text.parse().unwrap();
277        assert_eq!(reparsed.to_bits(), 0.0_f64.to_bits());
278        assert_ne!(reparsed.to_bits(), (-0.0_f64).to_bits());
279    }
280
281    #[test]
282    fn format_number_never_uses_exponent_notation() {
283        // A run of 300 digits is easier to read than a mantissa and exponent
284        // for the magnitudes KCL models actually use, and Rust's `f64` display
285        // never emits an exponent, so this records the behaviour rather than
286        // asking for it.
287        for (name, n) in [
288            ("large magnitude", 1e300),
289            ("small magnitude", 1e-300),
290            ("largest finite", f64::MAX),
291            ("smallest subnormal", f64::from_bits(1)),
292        ] {
293            let text = format_number(n, default_units());
294            assert!(!text.contains('e'), "case: {name}, rendered as {text}");
295            assert!(!text.contains('E'), "case: {name}, rendered as {text}");
296        }
297    }
298}