llm_output_parser/
number.rs1use std::str::FromStr;
8
9use crate::error::{
10 ensure_input_within_limits, record_extracted_span, ParseError, ParseOptions, ParseTrace,
11};
12use crate::extract::preprocess_opts;
13
14pub fn parse_number<T: FromStr>(response: &str) -> Result<T, ParseError> {
32 let opts = ParseOptions::default();
33 let (value, _trace) = parse_number_with_trace(response, &opts)?;
34 Ok(value)
35}
36
37pub fn parse_number_with_trace<T: FromStr>(
39 response: &str,
40 opts: &ParseOptions,
41) -> Result<(T, ParseTrace), ParseError> {
42 ensure_input_within_limits(response, opts)?;
43
44 let cleaned = preprocess_opts(response, opts.strip_think_tags);
45 let mut trace = ParseTrace::default();
46
47 if cleaned.is_empty() {
48 return Err(ParseError::EmptyResponse);
49 }
50
51 trace.strategies_tried.push("direct_parse");
52 if let Ok(val) = cleaned.parse::<T>() {
53 trace.extracted_span = Some((0, cleaned.len()));
54 return Ok((val, trace));
55 }
56
57 let lower = cleaned.to_lowercase();
58
59 trace.strategies_tried.push("labeled_number");
60 for label in ["score:", "rating:", "result:"] {
61 if let Some(pos) = lower.find(label) {
62 let after = &cleaned[pos + label.len()..];
63 let nums = find_all_numbers(after);
64 if let Some(first) = nums.first() {
65 if let Ok(val) = first.parse::<T>() {
66 record_extracted_span(&mut trace, &cleaned, first);
67 return Ok((val, trace));
68 }
69 }
70 }
71 }
72
73 trace.strategies_tried.push("fraction_number");
74 if let Some(num) = extract_fraction::<T>(&cleaned) {
75 if let Some(fraction_text) = cleaned
76 .split_whitespace()
77 .find(|part| part.contains('/'))
78 .map(str::trim)
79 {
80 record_extracted_span(&mut trace, &cleaned, fraction_text);
81 }
82 return Ok((num, trace));
83 }
84
85 trace.strategies_tried.push("scan_all_numbers");
86 let numbers = find_all_numbers(&cleaned);
87 for candidate in numbers.iter().rev() {
89 if let Ok(val) = candidate.parse::<T>() {
90 record_extracted_span(&mut trace, &cleaned, candidate);
91 return Ok((val, trace));
92 }
93 }
94
95 Err(ParseError::NoNumber)
96}
97
98pub fn parse_number_in_range<T: FromStr + PartialOrd + std::fmt::Display>(
109 response: &str,
110 min: T,
111 max: T,
112) -> Result<T, ParseError> {
113 let opts = ParseOptions::default();
114 let (val, _trace) = parse_number_in_range_with_trace(response, min, max, &opts)?;
115 Ok(val)
116}
117
118pub fn parse_number_in_range_with_trace<T: FromStr + PartialOrd + std::fmt::Display>(
120 response: &str,
121 min: T,
122 max: T,
123 opts: &ParseOptions,
124) -> Result<(T, ParseTrace), ParseError> {
125 let (val, trace) = parse_number_with_trace::<T>(response, opts)?;
126 if val < min || val > max {
127 return Err(ParseError::NoNumber);
128 }
129 Ok((val, trace))
130}
131
132fn extract_fraction<T: FromStr>(text: &str) -> Option<T> {
134 let chars: Vec<char> = text.chars().collect();
135 let len = chars.len();
136
137 for i in 0..len {
138 if chars[i] == '/' {
139 let mut num_start = i;
141 while num_start > 0 {
142 let c = chars[num_start - 1];
143 if c.is_ascii_digit() || c == '.' || c == '-' {
144 num_start -= 1;
145 } else {
146 break;
147 }
148 }
149 if num_start < i {
150 let num_str: String = chars[num_start..i].iter().collect();
151 if let Ok(val) = num_str.parse::<T>() {
152 return Some(val);
153 }
154 }
155 }
156 }
157 None
158}
159
160fn find_all_numbers(text: &str) -> Vec<String> {
162 let mut numbers = Vec::new();
163 let chars: Vec<char> = text.chars().collect();
164 let len = chars.len();
165 let mut i = 0;
166
167 while i < len {
168 let is_negative = chars[i] == '-' && i + 1 < len && chars[i + 1].is_ascii_digit();
170 let is_digit = chars[i].is_ascii_digit();
171
172 if is_digit || is_negative {
173 let start = i;
174 if is_negative {
175 i += 1;
176 }
177 while i < len && chars[i].is_ascii_digit() {
179 i += 1;
180 }
181 if i < len && chars[i] == '.' && i + 1 < len && chars[i + 1].is_ascii_digit() {
183 i += 1; while i < len && chars[i].is_ascii_digit() {
185 i += 1;
186 }
187 }
188 let num_str: String = chars[start..i].iter().collect();
189 numbers.push(num_str);
190 continue;
191 }
192 i += 1;
193 }
194
195 numbers
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn integer() {
204 let result: i32 = parse_number("8").unwrap();
205 assert_eq!(result, 8);
206 }
207
208 #[test]
209 fn float() {
210 let result: f64 = parse_number("8.5").unwrap();
211 assert!((result - 8.5).abs() < f64::EPSILON);
212 }
213
214 #[test]
215 fn fraction() {
216 let result: i32 = parse_number("8/10").unwrap();
217 assert_eq!(result, 8);
218 }
219
220 #[test]
221 fn in_prose() {
222 let result: f64 = parse_number("I'd give it a 7.5").unwrap();
223 assert!((result - 7.5).abs() < f64::EPSILON);
224 }
225
226 #[test]
227 fn labeled() {
228 let result: i32 = parse_number("Score: 9").unwrap();
229 assert_eq!(result, 9);
230 }
231
232 #[test]
233 fn with_think() {
234 let result: f64 = parse_number("<think>considering</think>8.5").unwrap();
235 assert!((result - 8.5).abs() < f64::EPSILON);
236 }
237
238 #[test]
239 fn negative() {
240 let result: i32 = parse_number("-3").unwrap();
241 assert_eq!(result, -3);
242 }
243
244 #[test]
245 fn range_pass() {
246 let result: i32 = parse_number_in_range("8", 1, 10).unwrap();
247 assert_eq!(result, 8);
248 }
249
250 #[test]
251 fn range_fail() {
252 let result: Result<i32, _> = parse_number_in_range("15", 1, 10);
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn no_number() {
258 let result: Result<i32, _> = parse_number("great work");
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn multiple_numbers_labeled() {
264 let result: i32 = parse_number("Page 3 of 5, Score: 8").unwrap();
265 assert_eq!(result, 8);
266 }
267
268 #[test]
269 fn with_trace_records_fraction_strategy() {
270 let opts = ParseOptions::default();
271 let (result, trace): (i32, ParseTrace) = parse_number_with_trace("8/10", &opts).unwrap();
272 assert_eq!(result, 8);
273 assert!(trace.strategies_tried.contains(&"fraction_number"));
274 assert!(trace.extracted_span.is_some());
275 }
276
277 #[test]
278 fn in_range_with_trace_rejects_oversized_input() {
279 let opts = ParseOptions {
280 max_input_bytes: 4,
281 ..ParseOptions::default()
282 };
283 let err = parse_number_in_range_with_trace::<i32>("Score: 8", 1, 10, &opts).unwrap_err();
284 assert_eq!(err.kind(), "too_large");
285 }
286}