utiles_core/parsing.rs
1//! Parsing util(e)ities
2use serde_json::Value;
3
4use crate::UtilesCoreError;
5use crate::bbox::BBox;
6use crate::errors::UtilesCoreResult;
7
8/// Parse a string into a `BBox`
9///
10/// # Errors
11///
12/// Returns error if unable to parse JSON string into a `BBox`
13pub fn parse_bbox_json(string: &str) -> UtilesCoreResult<BBox> {
14 // strip leading/trailing whitespace
15 let s = string.trim();
16 // if the first char is "{" assume it is geojson-like
17 if s.starts_with('{') {
18 // parse to serde_json::Value
19 let v: Value = serde_json::from_str(s)?;
20 // if it has a "bbox" key, use that
21 if v["bbox"].is_array() {
22 let bbox: (f64, f64, f64, f64) = serde_json::from_value(v["bbox"].clone())?;
23 return Ok(BBox::from(bbox));
24 }
25 return Err(UtilesCoreError::InvalidBbox(
26 "Invalid bbox: ".to_string() + s,
27 ));
28 }
29
30 let v: Value = serde_json::from_str(s)?;
31
32 // Assume a single pair of coordinates represents a CoordTuple
33 // and a four-element array represents a BBoxTuple
34 match v.as_array().map(Vec::len) {
35 // match len 0, 1, 3
36 Some(0 | 1 | 3) => Err(UtilesCoreError::InvalidBbox(
37 "Invalid bbox: ".to_string() + s,
38 )),
39 Some(2) => {
40 let coord: (f64, f64) = serde_json::from_value::<(f64, f64)>(v)?;
41 Ok(BBox::new(coord.0, coord.1, coord.0, coord.1))
42 }
43 Some(4) => {
44 let bbox: (f64, f64, f64, f64) = serde_json::from_value(v)?;
45 Ok(BBox::from(bbox))
46 }
47 _ => {
48 // take first four elements
49 let bbox_vec_as_arr = v.as_array();
50 match bbox_vec_as_arr {
51 None => Err(UtilesCoreError::InvalidBbox(
52 "Invalid bbox: ".to_string() + s,
53 )),
54 Some(bbox_vec) => {
55 let bbox_vec =
56 bbox_vec.iter().take(4).cloned().collect::<Vec<Value>>();
57 Ok(BBox::from(serde_json::from_value::<(f64, f64, f64, f64)>(
58 Value::Array(bbox_vec),
59 )?))
60 }
61 }
62 }
63 }
64}
65
66/// Parse a string into a `BBox`
67///
68/// # Errors
69///
70/// Returns error on bbox parsing failure
71///
72/// # Examples
73///
74/// ```
75/// use utiles_core::parsing::parse_bbox;
76/// let bbox = parse_bbox("-180,-85,180,85").unwrap();
77/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -85.0, 180.0, 85.0));
78/// ```
79///
80/// ```
81/// use utiles_core::parsing::parse_bbox;
82/// let bbox = parse_bbox("-180.0, -85.0, 180.0, 85.0").unwrap();
83/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -85.0, 180.0, 85.0));
84/// ```
85///
86/// ```
87/// use utiles_core::parsing::parse_bbox;
88/// let bbox = parse_bbox("-180.0 -85.0 180.0 85.0").unwrap();
89/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -85.0, 180.0, 85.0));
90/// ```
91///
92/// ```
93/// use utiles_core::parsing::parse_bbox;
94/// let bbox = parse_bbox("[-180.0, -85.0, 180.0, 85.0]").unwrap();
95/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -85.0, 180.0, 85.0));
96/// ```
97pub fn parse_bbox(string: &str) -> UtilesCoreResult<BBox> {
98 // strip leading/trailing whitespace
99 let s = string.trim();
100 // if the first char is "{" assume it is geojson-like
101 if s.starts_with('{') || s.starts_with('[') {
102 let bbox = parse_bbox_json(s);
103 return bbox;
104 }
105 let parts: Vec<f64> = if s.contains(',') {
106 s.split(',')
107 .map(str::trim)
108 .filter_map(|p| p.parse::<f64>().ok())
109 .collect()
110 } else if s.contains(' ') {
111 s.split(' ')
112 .map(str::trim)
113 .filter_map(|p| p.parse::<f64>().ok())
114 .collect()
115 } else {
116 vec![]
117 };
118 if parts.len() == 4 {
119 // if north < south err out
120 if parts[3] < parts[1] {
121 Err(UtilesCoreError::InvalidBbox(
122 "Invalid bbox: ".to_string() + s + " (north < south)",
123 ))
124 } else {
125 Ok(BBox::new(parts[0], parts[1], parts[2], parts[3]))
126 }
127 } else {
128 Err(UtilesCoreError::InvalidBbox(
129 "Invalid bbox: ".to_string() + s,
130 ))
131 }
132}
133
134/// Parse a string into a `BBox` with special handling of 'world' and 'planet'
135///
136/// # Errors
137///
138/// Returns error on bbox parsing failure
139///
140/// # Examples
141///
142/// ```
143/// use utiles_core::parsing::parse_bbox_ext;
144/// let bbox = parse_bbox_ext("world").unwrap();
145/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -90.0, 180.0, 90.0));
146/// ```
147///
148/// ```
149/// use utiles_core::parsing::parse_bbox_ext;
150/// let bbox = parse_bbox_ext("planet").unwrap();
151/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -90.0, 180.0, 90.0));
152/// ```
153///
154/// ```
155/// use utiles_core::parsing::parse_bbox_ext;
156/// let bbox = parse_bbox_ext("-180,-85,180,85").unwrap();
157/// assert_eq!(bbox, utiles_core::bbox::BBox::new(-180.0, -85.0, 180.0, 85.0));
158/// ```
159pub fn parse_bbox_ext(string: &str) -> UtilesCoreResult<BBox> {
160 // match 'world' or 'planet'
161 // match string/lower
162 let str_lower = string
163 .trim()
164 .trim_matches(|c| c == '\'' || c == '"')
165 .to_lowercase();
166 match str_lower.as_str() {
167 "world" | "planet" | "all" | "*" => Ok(BBox::new(-180.0, -90.0, 180.0, 90.0)),
168 "n" | "north" => Ok(BBox::new(-180.0, 0.0, 180.0, 90.0)),
169 "s" | "south" => Ok(BBox::new(-180.0, -90.0, 180.0, 0.0)),
170 "e" | "east" => Ok(BBox::new(0.0, -90.0, 180.0, 90.0)),
171 "w" | "west" => Ok(BBox::new(-180.0, -90.0, 0.0, 90.0)),
172 "ne" | "northeast" => Ok(BBox::new(0.0, 0.0, 180.0, 90.0)),
173 "nw" | "northwest" => Ok(BBox::new(-180.0, 0.0, 0.0, 90.0)),
174 "se" | "southeast" => Ok(BBox::new(0.0, -90.0, 180.0, 0.0)),
175 "sw" | "southwest" => Ok(BBox::new(-180.0, -90.0, 0.0, 0.0)),
176 _ => parse_bbox(&str_lower),
177 }
178}
179
180/// Parse a string into vector of integer strings
181///
182/// # Examples
183/// ```
184/// use utiles_core::parsing::parse_uint_strings;
185/// let ints = parse_uint_strings("1,2,3,4,5");
186/// assert_eq!(ints, vec!["1", "2", "3", "4", "5"]);
187/// ```
188///
189/// ```
190/// use utiles_core::parsing::parse_uint_strings;
191/// let ints = parse_uint_strings("x1y2z3");
192/// assert_eq!(ints, vec!["1", "2", "3"]);
193/// ```
194///
195/// ```
196/// use utiles_core::parsing::parse_uint_strings;
197/// let ints = parse_uint_strings("as;ldfkjas;ldfkj");
198/// assert_eq!(ints, Vec::<String>::new());
199/// ```
200///
201/// ```
202/// use utiles_core::parsing::parse_uint_strings;
203/// let ints = parse_uint_strings("http://example.com/tiles/3/2/1.png");
204/// assert_eq!(ints, vec!["3", "2", "1"]);
205/// ```
206#[must_use]
207pub fn parse_uint_strings(input: &str) -> Vec<&str> {
208 let mut blocks = Vec::new();
209 let mut start = None;
210 for (i, c) in input.char_indices() {
211 if c.is_ascii_digit() {
212 if start.is_none() {
213 start = Some(i);
214 }
215 } else if let Some(s) = start {
216 blocks.push(&input[s..i]);
217 start = None;
218 }
219 }
220 if let Some(s) = start {
221 blocks.push(&input[s..]);
222 }
223 blocks
224}
225
226/// Parse a string into vector of integers
227///
228/// # Panics
229///
230/// Panics if the string contains a number that cannot be parsed as u64.
231///
232/// # Examples
233/// ```
234/// use utiles_core::parsing::parse_uints;
235/// let ints = parse_uints("1,2,3,4,5");
236/// assert_eq!(ints, vec![1, 2, 3, 4, 5]);
237/// ```
238///
239/// ```
240/// use utiles_core::parsing::parse_uints;
241/// let ints = parse_uints("x1y2z3");
242/// assert_eq!(ints, vec![1, 2, 3]);
243/// ```
244///
245/// ```
246/// use utiles_core::parsing::parse_uints;
247/// let ints = parse_uints("as;ldfkjas;ldfkj");
248/// assert_eq!(ints, Vec::<u64>::new());
249/// ```
250#[must_use]
251pub fn parse_uints(input: &str) -> Vec<u64> {
252 parse_uint_strings(input)
253 .iter()
254 .flat_map(|s| s.parse::<u64>())
255 .collect()
256}
257
258/// Parse a string into a vector of signed integer strings
259///
260/// # Examples
261/// ```
262/// use utiles_core::parsing::parse_int_strings;
263/// let ints = parse_int_strings("-1,2,---3,4,-5");
264/// assert_eq!(ints, vec!["-1", "2", "-3", "4", "-5"]);
265/// ```
266///
267/// ```
268/// use utiles_core::parsing::parse_int_strings;
269/// let ints = parse_int_strings("x-1y2z-3");
270/// assert_eq!(ints, vec!["-1", "2", "-3"]);
271/// ```
272///
273/// ```
274/// use utiles_core::parsing::parse_int_strings;
275/// let ints = parse_int_strings("as;ldfkjas;ldfkj");
276/// assert_eq!(ints, Vec::<&str>::new());
277/// ```
278///
279/// ```
280/// use utiles_core::parsing::parse_int_strings;
281/// let ints = parse_int_strings("http://example.com/tiles/-3/2/1.png");
282/// assert_eq!(ints, vec!["-3", "2", "1"]);
283/// ```
284#[must_use]
285pub fn parse_int_strings(input: &str) -> Vec<&str> {
286 let mut blocks = Vec::new();
287 let mut start = None;
288 let mut is_negative = false; // flag to track if the current number is negative
289
290 for (i, c) in input.char_indices() {
291 match c {
292 '-' => {
293 // If we encounter a '-' and no number has started, note the start and set negative flag
294 if start.is_none() {
295 start = Some(i);
296 is_negative = true; // Expecting a number after this
297 } else if let Some(s) = start {
298 // If '-' follows digits or another '-', end the previous block (if valid) and start a new one
299 if !is_negative || s < i - 1 {
300 // Check if previous char was also '-' or if it's a valid number
301 blocks.push(&input[s..i]);
302 }
303 start = Some(i);
304 is_negative = true;
305 }
306 }
307 '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
308 if start.is_none() || is_negative {
309 // Start of a new number block (potentially negative)
310 start = Some(i - usize::from(is_negative)); // Include '-' in block if negative
311 }
312 is_negative = false; // Once we have digits, it's no longer just a '-'
313 }
314 _ => {
315 // For any other character, end the current number block if it exists and is valid
316 if let Some(s) = start
317 && (!is_negative || s < i - 1)
318 {
319 // Ensure it's not just a '-' without digits
320 blocks.push(&input[s..i]);
321 }
322
323 // Reset for the next number
324 start = None;
325 is_negative = false;
326 }
327 }
328 }
329 // Capture the last number block if there's one and it's valid
330 if let Some(s) = start
331 && (!is_negative || s < input.len() - 1)
332 {
333 // Ensure it's not just a '-' without digits
334 blocks.push(&input[s..]);
335 }
336 blocks
337}
338
339/// Parse a string into a vector of signed integers
340///
341/// # Examples
342/// ```
343/// use utiles_core::parsing::parse_ints;
344/// let ints = parse_ints("-1,2,---3,4,-5");
345/// assert_eq!(ints, vec![-1, 2, -3, 4, -5]);
346/// ```
347#[must_use]
348pub fn parse_ints(input: &str) -> Vec<i64> {
349 parse_int_strings(input)
350 .iter()
351 .flat_map(|s| s.parse::<i64>())
352 .collect()
353}
354
355/// Parse float string blocks from a string
356///
357/// # Examples
358/// ```
359/// use utiles_core::parsing::parse_float_blocks;
360/// let input = "-123.45..6abc--7.8.9";
361/// let blocks = parse_float_blocks(input);
362/// assert_eq!(blocks, vec!["-123.45", ".6", "-7.8", ".9"]);
363/// ```
364///
365#[must_use]
366pub fn parse_float_blocks(input: &str) -> Vec<&str> {
367 let mut blocks = Vec::new();
368 let mut start = None; // Start index of the current number block
369 let mut has_decimal = false; // Track if the current block has a decimal point
370 let mut has_digit = false; // Ensure there's at least one digit
371
372 for (i, c) in input.char_indices() {
373 match c {
374 '0'..='9' => {
375 if start.is_none() {
376 start = Some(i);
377 }
378 has_digit = true;
379 }
380 '-' => {
381 if start.is_none() && !has_digit {
382 // Start of a new number
383 start = Some(i);
384 } else if has_digit || has_decimal || start.is_some() {
385 // Malformed if in the middle of a number
386 if let Some(s) = start
387 && has_digit
388 {
389 // Ensure there's at least one digit
390 blocks.push(&input[s..i]);
391 }
392 start = Some(i); // Reset for a new potential number
393 has_decimal = false;
394 has_digit = false;
395 }
396 }
397 '.' => {
398 if !has_decimal && start.is_none() {
399 // First decimal in a new number
400 start = Some(i);
401 has_decimal = true;
402 } else if has_decimal || start.is_none() {
403 // Malformed if another decimal or no start
404 if let Some(s) = start
405 && has_digit
406 {
407 // Ensure there's at least one digit
408 blocks.push(&input[s..i]);
409 }
410 start = Some(i); // Start a new potential number
411 has_decimal = true; // Current char is '.'
412 has_digit = false;
413 } else {
414 // First decimal in an ongoing number
415 has_decimal = true;
416 }
417 }
418 _ => {
419 if let Some(s) = start
420 && has_digit
421 {
422 // Ensure there's at least one digit
423 blocks.push(&input[s..i]);
424 }
425 // Reset for the next number
426 start = None;
427 has_decimal = false;
428 has_digit = false;
429 }
430 }
431 }
432
433 // Handle the last block if it's well-formed
434 if let Some(s) = start
435 && has_digit
436 {
437 // Ensure there's at least one digit
438 blocks.push(&input[s..]);
439 }
440
441 blocks
442}
443
444#[cfg(test)]
445mod tests {
446 #![expect(clippy::unwrap_used)]
447
448 use crate::bbox::*;
449 use crate::parsing::parse_bbox;
450
451 #[test]
452 fn parse_bbox_simple() {
453 let string = r"[-180.0, -85.0, 180.0, 85.0]";
454 let bbox_result = parse_bbox(string);
455 // assert!(bbox_result.is_ok());
456 let bbox = bbox_result.unwrap();
457 assert_eq!(bbox, BBox::new(-180.0, -85.0, 180.0, 85.0));
458 }
459
460 #[test]
461 fn parse_bbox_simple_len_5() {
462 let string = r#"[-180.0, -85.0, 180.0, 85.0, "uhhhhh"]"#;
463 let bbox_result = parse_bbox(string);
464 // assert!(bbox_result.is_ok());
465 let bbox = bbox_result.unwrap();
466 assert_eq!(bbox, BBox::new(-180.0, -85.0, 180.0, 85.0));
467 }
468
469 #[test]
470 fn parse_bbox_simple_len_6() {
471 let string = r"[-180.0, -85.0, 180.0, 85.0, 0, 10]";
472 let bbox_result = parse_bbox(string);
473 // assert!(bbox_result.is_ok());
474 let bbox = bbox_result.unwrap();
475 assert_eq!(bbox, BBox::new(-180.0, -85.0, 180.0, 85.0));
476 }
477
478 #[test]
479 fn parse_bbox_str_commas() {
480 let string = r"-180.0, -85.0, 180.0, 85.0";
481 let bbox_result = parse_bbox(string);
482 // assert!(bbox_result.is_ok());
483 let bbox = bbox_result.unwrap();
484 assert_eq!(bbox, BBox::new(-180.0, -85.0, 180.0, 85.0));
485 }
486
487 #[test]
488 fn parse_bbox_str_spaces() {
489 let string = r"-180.0 -85.0 180.0 85.0";
490 let bbox_result = parse_bbox(string);
491 // assert!(bbox_result.is_ok());
492 let bbox = bbox_result.unwrap();
493 assert_eq!(bbox, BBox::new(-180.0, -85.0, 180.0, 85.0));
494 }
495
496 #[test]
497 fn parse_bbox_from_coords() {
498 let string = "[-180.0, -85.0]";
499 let bbox_result = parse_bbox(string);
500 // assert!(bbox_result.is_ok());
501 let bbox = bbox_result.unwrap();
502 assert_eq!(bbox, BBox::new(-180.0, -85.0, -180.0, -85.0));
503 }
504
505 #[test]
506 fn parse_bbox_bad() {
507 let string = r"[-180.0,]";
508 let bbox_result = parse_bbox(string);
509 assert!(bbox_result.is_err());
510 }
511
512 #[test]
513 fn parse_bbox_metadata_string() {
514 let s = "-176.696694,-14.373776,145.830505,71.341324";
515 let bbox = parse_bbox(s);
516 assert!(bbox.is_ok());
517 let bbox = bbox.unwrap();
518 assert_eq!(
519 bbox,
520 BBox::new(-176.696_694, -14.373_776, 145.830_505, 71.341_324)
521 );
522 }
523}