Skip to main content

uls_core/records/
common.rs

1//! Common types used across ULS records.
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6/// Geographic coordinates in degrees/minutes/seconds format.
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct Coordinates {
9    pub lat_degrees: Option<i32>,
10    pub lat_minutes: Option<i32>,
11    pub lat_seconds: Option<f64>,
12    pub lat_direction: Option<char>,
13    pub long_degrees: Option<i32>,
14    pub long_minutes: Option<i32>,
15    pub long_seconds: Option<f64>,
16    pub long_direction: Option<char>,
17}
18
19impl Coordinates {
20    /// Creates empty coordinates.
21    pub fn empty() -> Self {
22        Self {
23            lat_degrees: None,
24            lat_minutes: None,
25            lat_seconds: None,
26            lat_direction: None,
27            long_degrees: None,
28            long_minutes: None,
29            long_seconds: None,
30            long_direction: None,
31        }
32    }
33
34    /// Returns true if all coordinate fields are empty.
35    pub fn is_empty(&self) -> bool {
36        self.lat_degrees.is_none()
37            && self.lat_minutes.is_none()
38            && self.lat_seconds.is_none()
39            && self.long_degrees.is_none()
40            && self.long_minutes.is_none()
41            && self.long_seconds.is_none()
42    }
43
44    /// Converts to decimal degrees if all required fields are present.
45    pub fn to_decimal(&self) -> Option<(f64, f64)> {
46        let lat_deg = self.lat_degrees?;
47        let lat_min = self.lat_minutes.unwrap_or(0);
48        let lat_sec = self.lat_seconds.unwrap_or(0.0);
49        let lat_dir = self.lat_direction.unwrap_or('N');
50
51        let long_deg = self.long_degrees?;
52        let long_min = self.long_minutes.unwrap_or(0);
53        let long_sec = self.long_seconds.unwrap_or(0.0);
54        let long_dir = self.long_direction.unwrap_or('W');
55
56        let mut lat = lat_deg as f64 + (lat_min as f64 / 60.0) + (lat_sec / 3600.0);
57        let mut long = long_deg as f64 + (long_min as f64 / 60.0) + (long_sec / 3600.0);
58
59        if lat_dir == 'S' {
60            lat = -lat;
61        }
62        if long_dir == 'W' {
63            long = -long;
64        }
65
66        Some((lat, long))
67    }
68}
69
70impl Default for Coordinates {
71    fn default() -> Self {
72        Self::empty()
73    }
74}
75
76/// Parse a ULS date string (MM/DD/YYYY format) into a NaiveDate.
77pub fn parse_uls_date(s: &str) -> Option<NaiveDate> {
78    let s = s.trim();
79    if s.is_empty() {
80        return None;
81    }
82
83    // Try MM/DD/YYYY format first
84    if let Ok(date) = NaiveDate::parse_from_str(s, "%m/%d/%Y") {
85        return Some(date);
86    }
87
88    // Try YYYY-MM-DD format
89    if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
90        return Some(date);
91    }
92
93    None
94}
95
96/// Parse an optional string field, returning None for empty strings.
97pub fn parse_opt_string(s: &str) -> Option<String> {
98    let s = s.trim();
99    if s.is_empty() {
100        None
101    } else {
102        Some(s.to_string())
103    }
104}
105
106/// Parse an optional i32 field.
107pub fn parse_opt_i32(s: &str) -> Option<i32> {
108    let s = s.trim();
109    if s.is_empty() {
110        None
111    } else {
112        s.parse().ok()
113    }
114}
115
116/// Parse an optional i64 field.
117pub fn parse_opt_i64(s: &str) -> Option<i64> {
118    let s = s.trim();
119    if s.is_empty() {
120        None
121    } else {
122        s.parse().ok()
123    }
124}
125
126/// Parse an optional f64 field.
127pub fn parse_opt_f64(s: &str) -> Option<f64> {
128    let s = s.trim();
129    if s.is_empty() {
130        None
131    } else {
132        s.parse().ok()
133    }
134}
135
136/// Parse an optional char field.
137pub fn parse_opt_char(s: &str) -> Option<char> {
138    let s = s.trim();
139    if s.is_empty() {
140        None
141    } else {
142        s.chars().next()
143    }
144}
145
146/// Parse a required i64 field, returning 0 if empty/invalid.
147pub fn parse_i64_or_default(s: &str) -> i64 {
148    parse_opt_i64(s).unwrap_or(0)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_coordinates_decimal() {
157        let coords = Coordinates {
158            lat_degrees: Some(40),
159            lat_minutes: Some(30),
160            lat_seconds: Some(0.0),
161            lat_direction: Some('N'),
162            long_degrees: Some(74),
163            long_minutes: Some(0),
164            long_seconds: Some(0.0),
165            long_direction: Some('W'),
166        };
167
168        let (lat, long) = coords.to_decimal().unwrap();
169        assert!((lat - 40.5).abs() < 0.001);
170        assert!((long - (-74.0)).abs() < 0.001);
171    }
172
173    #[test]
174    fn test_coordinates_empty() {
175        let coords = Coordinates::empty();
176        assert!(coords.is_empty());
177        assert!(coords.to_decimal().is_none());
178    }
179
180    #[test]
181    fn test_parse_uls_date() {
182        assert_eq!(
183            parse_uls_date("01/15/2024"),
184            Some(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
185        );
186        assert_eq!(
187            parse_uls_date("2024-01-15"),
188            Some(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
189        );
190        assert_eq!(parse_uls_date(""), None);
191        assert_eq!(parse_uls_date("invalid"), None);
192    }
193
194    #[test]
195    fn test_parse_opt_string() {
196        assert_eq!(parse_opt_string("hello"), Some("hello".to_string()));
197        assert_eq!(parse_opt_string("  hello  "), Some("hello".to_string()));
198        assert_eq!(parse_opt_string(""), None);
199        assert_eq!(parse_opt_string("   "), None);
200    }
201
202    #[test]
203    fn test_parse_opt_i32() {
204        assert_eq!(parse_opt_i32("123"), Some(123));
205        assert_eq!(parse_opt_i32("-456"), Some(-456));
206        assert_eq!(parse_opt_i32(""), None);
207        assert_eq!(parse_opt_i32("abc"), None);
208    }
209
210    #[test]
211    fn test_parse_opt_f64() {
212        assert_eq!(parse_opt_f64("123.456"), Some(123.456));
213        assert_eq!(parse_opt_f64("-0.5"), Some(-0.5));
214        assert_eq!(parse_opt_f64(""), None);
215    }
216}