1use 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
11pub 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
21pub 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
31pub 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
60pub 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
71pub 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
82pub 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
93fn format_number(n: f64, ty: NumericType) -> String {
107 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 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
127pub async fn number_to_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
129 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 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 ("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", -0.0, default_units(), "0"),
174 (
175 "negative zero with a unit",
176 -0.0,
177 length(UnitLength::Millimeters),
178 "0mm",
179 ),
180 ("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 ("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 ("degrees", 90.0, angle(UnitAngle::Degrees), "90deg"),
194 ("radians", 1.5, angle(UnitAngle::Radians), "1.5rad"),
195 (
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 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 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 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 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}