Skip to main content

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}