runmat_runtime/builtins/strings/core/
str2double.rs1use std::borrow::Cow;
4
5use runmat_builtins::{CellArray, CharArray, StringArray, Tensor, Value};
6use runmat_macros::runtime_builtin;
7
8use crate::builtins::common::map_control_flow_with_builtin;
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ReductionNaN, ResidencyPolicy, ShapeRequirements,
12};
13use crate::builtins::common::tensor;
14use crate::builtins::strings::type_resolvers::numeric_text_scalar_or_tensor_type;
15use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
16
17#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::str2double")]
18pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
19 name: "str2double",
20 op_kind: GpuOpKind::Custom("conversion"),
21 supported_precisions: &[],
22 broadcast: BroadcastSemantics::None,
23 provider_hooks: &[],
24 constant_strategy: ConstantStrategy::InlineLiteral,
25 residency: ResidencyPolicy::GatherImmediately,
26 nan_mode: ReductionNaN::Include,
27 two_pass_threshold: None,
28 workgroup_size: None,
29 accepts_nan_mode: false,
30 notes: "Parses text on the CPU; GPU-resident inputs are gathered before conversion.",
31};
32
33#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::str2double")]
34pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
35 name: "str2double",
36 shape: ShapeRequirements::Any,
37 constant_strategy: ConstantStrategy::InlineLiteral,
38 elementwise: None,
39 reduction: None,
40 emits_nan: true,
41 notes: "Conversion builtin; not eligible for fusion and materialises host-side doubles.",
42};
43
44const ARG_TYPE_ERROR: &str =
45 "str2double: input must be a string array, character array, or cell array of character vectors";
46const CELL_ELEMENT_ERROR: &str =
47 "str2double: cell array elements must be character vectors or string scalars";
48
49fn str2double_flow(message: impl Into<String>) -> RuntimeError {
50 build_runtime_error(message)
51 .with_builtin("str2double")
52 .build()
53}
54
55fn remap_str2double_flow(err: RuntimeError) -> RuntimeError {
56 map_control_flow_with_builtin(err, "str2double")
57}
58
59#[runtime_builtin(
60 name = "str2double",
61 category = "strings/core",
62 summary = "Convert strings, character arrays, or cell arrays of text into doubles.",
63 keywords = "str2double,string to double,text conversion,gpu",
64 accel = "sink",
65 type_resolver(numeric_text_scalar_or_tensor_type),
66 builtin_path = "crate::builtins::strings::core::str2double"
67)]
68async fn str2double_builtin(value: Value) -> crate::BuiltinResult<Value> {
69 let gathered = gather_if_needed_async(&value)
70 .await
71 .map_err(remap_str2double_flow)?;
72 match gathered {
73 Value::String(text) => Ok(Value::Num(parse_numeric_scalar(&text))),
74 Value::StringArray(array) => str2double_string_array(array),
75 Value::CharArray(array) => str2double_char_array(array),
76 Value::Cell(cell) => str2double_cell_array(cell),
77 _ => Err(str2double_flow(ARG_TYPE_ERROR)),
78 }
79}
80
81fn str2double_string_array(array: StringArray) -> BuiltinResult<Value> {
82 let StringArray { data, shape, .. } = array;
83 let mut values = Vec::with_capacity(data.len());
84 for text in &data {
85 values.push(parse_numeric_scalar(text));
86 }
87 let tensor =
88 Tensor::new(values, shape).map_err(|e| str2double_flow(format!("str2double: {e}")))?;
89 Ok(tensor::tensor_into_value(tensor))
90}
91
92fn str2double_char_array(array: CharArray) -> BuiltinResult<Value> {
93 let rows = array.rows;
94 let cols = array.cols;
95 let mut values = Vec::with_capacity(rows);
96 for row in 0..rows {
97 let start = row * cols;
98 let end = start + cols;
99 let row_text: String = array.data[start..end].iter().collect();
100 values.push(parse_numeric_scalar(&row_text));
101 }
102 let tensor = Tensor::new(values, vec![rows, 1])
103 .map_err(|e| str2double_flow(format!("str2double: {e}")))?;
104 Ok(tensor::tensor_into_value(tensor))
105}
106
107fn str2double_cell_array(cell: CellArray) -> BuiltinResult<Value> {
108 let CellArray {
109 data, rows, cols, ..
110 } = cell;
111 let mut values = Vec::with_capacity(rows * cols);
112 for col in 0..cols {
113 for row in 0..rows {
114 let idx = row * cols + col;
115 let element: &Value = &data[idx];
116 let numeric = match element {
117 Value::String(text) => parse_numeric_scalar(text),
118 Value::StringArray(sa) if sa.data.len() == 1 => parse_numeric_scalar(&sa.data[0]),
119 Value::CharArray(char_vec) if char_vec.rows == 1 => {
120 let row_text: String = char_vec.data.iter().collect();
121 parse_numeric_scalar(&row_text)
122 }
123 Value::CharArray(_) => return Err(str2double_flow(CELL_ELEMENT_ERROR)),
124 _ => return Err(str2double_flow(CELL_ELEMENT_ERROR)),
125 };
126 values.push(numeric);
127 }
128 }
129 let tensor = Tensor::new(values, vec![rows, cols])
130 .map_err(|e| str2double_flow(format!("str2double: {e}")))?;
131 Ok(tensor::tensor_into_value(tensor))
132}
133
134fn parse_numeric_scalar(text: &str) -> f64 {
135 let trimmed = text.trim();
136 if trimmed.is_empty() {
137 return f64::NAN;
138 }
139
140 let lowered = trimmed.to_ascii_lowercase();
141 match lowered.as_str() {
142 "nan" => return f64::NAN,
143 "inf" | "+inf" | "infinity" | "+infinity" => return f64::INFINITY,
144 "-inf" | "-infinity" => return f64::NEG_INFINITY,
145 _ => {}
146 }
147
148 let normalized: Cow<'_, str> = if trimmed.chars().any(|c| c == 'd' || c == 'D') {
149 Cow::Owned(
150 trimmed
151 .chars()
152 .map(|c| if c == 'd' || c == 'D' { 'e' } else { c })
153 .collect(),
154 )
155 } else {
156 Cow::Borrowed(trimmed)
157 };
158
159 normalized.parse::<f64>().unwrap_or(f64::NAN)
160}
161
162#[cfg(test)]
163pub(crate) mod tests {
164 use super::*;
165 use runmat_builtins::{ResolveContext, Type};
166
167 fn str2double_builtin(value: Value) -> BuiltinResult<Value> {
168 futures::executor::block_on(super::str2double_builtin(value))
169 }
170
171 fn error_message(err: crate::RuntimeError) -> String {
172 err.message().to_string()
173 }
174
175 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
176 #[test]
177 fn str2double_string_scalar() {
178 let result = str2double_builtin(Value::String("42.5".into())).expect("str2double");
179 assert_eq!(result, Value::Num(42.5));
180 }
181
182 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
183 #[test]
184 fn str2double_string_scalar_invalid_returns_nan() {
185 let result = str2double_builtin(Value::String("abc".into())).expect("str2double");
186 match result {
187 Value::Num(v) => assert!(v.is_nan()),
188 other => panic!("expected scalar result, got {other:?}"),
189 }
190 }
191
192 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
193 #[test]
194 fn str2double_string_array_preserves_shape() {
195 let array =
196 StringArray::new(vec!["1".into(), " 2.5 ".into(), "foo".into()], vec![3, 1]).unwrap();
197 let result = str2double_builtin(Value::StringArray(array)).expect("str2double");
198 match result {
199 Value::Tensor(tensor) => {
200 assert_eq!(tensor.shape, vec![3, 1]);
201 assert_eq!(tensor.data[0], 1.0);
202 assert_eq!(tensor.data[1], 2.5);
203 assert!(tensor.data[2].is_nan());
204 }
205 Value::Num(_) => panic!("expected tensor"),
206 other => panic!("unexpected result {other:?}"),
207 }
208 }
209
210 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
211 #[test]
212 fn str2double_char_array_multiple_rows() {
213 let data: Vec<char> = vec!['4', '2', ' ', ' ', '1', '0', '0', ' '];
214 let array = CharArray::new(data, 2, 4).unwrap();
215 let result = str2double_builtin(Value::CharArray(array)).expect("str2double");
216 match result {
217 Value::Tensor(tensor) => {
218 assert_eq!(tensor.shape, vec![2, 1]);
219 assert_eq!(tensor.data[0], 42.0);
220 assert_eq!(tensor.data[1], 100.0);
221 }
222 other => panic!("expected tensor result, got {other:?}"),
223 }
224 }
225
226 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
227 #[test]
228 fn str2double_char_array_empty_rows() {
229 let array = CharArray::new(Vec::new(), 0, 0).unwrap();
230 let result = str2double_builtin(Value::CharArray(array)).expect("str2double");
231 match result {
232 Value::Tensor(tensor) => {
233 assert_eq!(tensor.shape, vec![0, 1]);
234 assert_eq!(tensor.data.len(), 0);
235 }
236 other => panic!("expected empty tensor, got {other:?}"),
237 }
238 }
239
240 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
241 #[test]
242 #[allow(
243 clippy::approx_constant,
244 reason = "Test ensures literal 3.14 text stays 3.14, not π"
245 )]
246 fn str2double_cell_array_of_text() {
247 let cell = CellArray::new(
248 vec![
249 Value::String("3.14".into()),
250 Value::CharArray(CharArray::new_row("NaN")),
251 Value::String("-Inf".into()),
252 ],
253 1,
254 3,
255 )
256 .unwrap();
257 let result = str2double_builtin(Value::Cell(cell)).expect("str2double");
258 match result {
259 Value::Tensor(tensor) => {
260 assert_eq!(tensor.shape, vec![1, 3]);
261 assert_eq!(tensor.data[0], 3.14);
262 assert!(tensor.data[1].is_nan());
263 assert_eq!(tensor.data[2], f64::NEG_INFINITY);
264 }
265 other => panic!("expected tensor result, got {other:?}"),
266 }
267 }
268
269 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
270 #[test]
271 fn str2double_cell_array_invalid_element_errors() {
272 let cell = CellArray::new(vec![Value::Num(5.0)], 1, 1).unwrap();
273 let err = error_message(str2double_builtin(Value::Cell(cell)).unwrap_err());
274 assert!(
275 err.contains("str2double"),
276 "unexpected error message: {err}"
277 );
278 }
279
280 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
281 #[test]
282 fn str2double_supports_d_exponent() {
283 let result = str2double_builtin(Value::String("1.5D3".into())).expect("str2double");
284 match result {
285 Value::Num(v) => assert_eq!(v, 1500.0),
286 other => panic!("expected scalar result, got {other:?}"),
287 }
288 }
289
290 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
291 #[test]
292 fn str2double_recognises_infinity_forms() {
293 let array = StringArray::new(
294 vec!["Inf".into(), "-Infinity".into(), "+inf".into()],
295 vec![3, 1],
296 )
297 .unwrap();
298 let result = str2double_builtin(Value::StringArray(array)).expect("str2double");
299 match result {
300 Value::Tensor(tensor) => {
301 assert_eq!(tensor.data[0], f64::INFINITY);
302 assert_eq!(tensor.data[1], f64::NEG_INFINITY);
303 assert_eq!(tensor.data[2], f64::INFINITY);
304 }
305 other => panic!("expected tensor result, got {other:?}"),
306 }
307 }
308
309 #[test]
310 fn str2double_type_is_numeric_text_scalar_or_tensor() {
311 assert_eq!(
312 numeric_text_scalar_or_tensor_type(&[Type::String], &ResolveContext::new(Vec::new())),
313 Type::Num
314 );
315 }
316}