Skip to main content

lat_long/
fmt.rs

1use crate::inner;
2use core::{
3    fmt::{Debug, Write},
4    hash::Hash,
5};
6use ordered_float::OrderedFloat;
7
8// ---------------------------------------------------------------------------
9// Public Types
10// ---------------------------------------------------------------------------
11
12pub trait Formatter {
13    fn format<W: Write>(&self, f: &mut W, options: &FormatOptions) -> std::fmt::Result;
14
15    fn to_formatted_string(&self, fmt: &FormatOptions) -> String {
16        let mut buffer = String::new();
17        self.format(&mut buffer, fmt).unwrap();
18        buffer
19    }
20}
21
22/// The default format is [`FormatKind::Decimal`] (plain decimal degrees).
23/// When you use the alternate flag (`{:#}`) the default DMS variant is used.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
25pub struct FormatOptions {
26    precision: Option<usize>,
27    kind: FormatKind,
28    labels: Option<(char, char)>,
29}
30
31/// | Variant      | Example              |
32/// |--------------|----------------------|
33/// | `Decimal`    | `48.8582`            |
34/// | `DmsSigned`  | `48° 51′ 29.6″`      |
35/// | `DmsLabeled` | `48° 51′ 29.6″ N`    |
36/// | `DmsBare`    | `+048:51:29.600000`  |
37#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum FormatKind {
39    #[default]
40    /// Decimal degrees, e.g. `48.8582`. This format has a *default* precision of 8 decimal places.
41    Decimal,
42    /// Degrees with Unicode symbols, e.g. `-48° 51′ 29.600000″`. This format has a *default* precision of 6 decimal places.
43    DmsSigned,
44    /// Degrees with a cardinal-direction label, e.g. `48° 51′ 29.600000″ N`. This format has a *default* precision of 6 decimal places.
45    DmsLabeled,
46    /// Degrees with no symbols, e.g. `048:51:29.600000`. This format has a *minimum* precision of 4 decimal places, and a *default* precision of 6.
47    DmsBare,
48}
49
50// ---------------------------------------------------------------------------
51// Public Constants
52// ---------------------------------------------------------------------------
53
54pub const DEFAULT_DECIMAL_PRECISION: usize = 8;
55pub const DEFAULT_DMS_PRECISION: usize = 6;
56pub const MINIMUM_DMS_BARE_PRECISION: usize = 4;
57
58// ---------------------------------------------------------------------------
59// Implementations >> Formatter
60// ---------------------------------------------------------------------------
61
62impl Formatter for OrderedFloat<f64> {
63    fn format<W: Write>(&self, f: &mut W, options: &FormatOptions) -> std::fmt::Result {
64        formatter_impl(*self, f, options)
65    }
66}
67
68// ---------------------------------------------------------------------------
69// Implementations >> FormatOptions
70// ---------------------------------------------------------------------------
71
72impl From<FormatKind> for FormatOptions {
73    fn from(kind: FormatKind) -> Self {
74        Self::new(kind)
75    }
76}
77
78impl FormatOptions {
79    const fn new(kind: FormatKind) -> Self {
80        Self {
81            precision: None,
82            kind,
83            labels: None,
84        }
85    }
86
87    /// Return a [`FormatOptions`] for decimal degrees with the default precision.
88    pub const fn decimal() -> Self {
89        Self::new(FormatKind::Decimal).with_default_precision()
90    }
91
92    /// Return a [`FormatOptions`] for degrees, minutes, seconds with the default precision.
93    pub const fn dms() -> Self {
94        Self::dms_signed()
95    }
96
97    /// Return a [`FormatOptions`] for signed degrees, minutes, seconds with the default precision.
98    pub const fn dms_signed() -> Self {
99        Self::new(FormatKind::DmsSigned).with_default_precision()
100    }
101
102    /// Return a [`FormatOptions`] for labeled degrees, minutes, seconds with the default precision.
103    pub const fn dms_labeled() -> Self {
104        Self::new(FormatKind::DmsLabeled).with_default_precision()
105    }
106
107    /// Return a [`FormatOptions`] for bare degrees, minutes, seconds with the default precision.
108    pub const fn dms_bare() -> Self {
109        Self::new(FormatKind::DmsBare).with_default_precision()
110    }
111
112    pub const fn with_precision(mut self, precision: usize) -> Self {
113        self.precision = Some(precision);
114        self
115    }
116
117    pub const fn with_default_precision(mut self) -> Self {
118        match self.kind {
119            FormatKind::Decimal => self.precision = Some(DEFAULT_DECIMAL_PRECISION),
120            _ => self.precision = Some(DEFAULT_DMS_PRECISION),
121        }
122        self
123    }
124
125    pub const fn with_labels(mut self, labels: (char, char)) -> Self {
126        self.labels = Some(labels);
127        self
128    }
129
130    pub const fn with_latitude_labels(mut self) -> Self {
131        self.labels = Some(('N', 'S'));
132        self
133    }
134
135    pub const fn with_longitude_labels(mut self) -> Self {
136        self.labels = Some(('E', 'W'));
137        self
138    }
139
140    pub const fn kind(&self) -> FormatKind {
141        self.kind
142    }
143
144    pub const fn is_decimal(&self) -> bool {
145        matches!(self.kind(), FormatKind::Decimal)
146    }
147
148    pub const fn is_dms(&self) -> bool {
149        self.is_dms_signed() || self.is_dms_labeled() || self.is_dms_bare()
150    }
151
152    pub const fn is_dms_signed(&self) -> bool {
153        matches!(self.kind(), FormatKind::DmsSigned)
154    }
155
156    pub const fn is_dms_labeled(&self) -> bool {
157        matches!(self.kind(), FormatKind::DmsLabeled)
158    }
159
160    pub const fn is_dms_bare(&self) -> bool {
161        matches!(self.kind(), FormatKind::DmsBare)
162    }
163
164    pub const fn precision(&self) -> Option<usize> {
165        self.precision
166    }
167
168    pub const fn labels(&self) -> Option<(char, char)> {
169        self.labels
170    }
171
172    pub fn positive_label(&self) -> Option<char> {
173        self.labels.as_ref().map(|l| l.0)
174    }
175
176    pub fn negative_label(&self) -> Option<char> {
177        self.labels.as_ref().map(|l| l.1)
178    }
179}
180
181// ---------------------------------------------------------------------------
182// Internal Functions
183// ---------------------------------------------------------------------------
184
185pub(crate) fn formatter_impl<W: Write>(
186    angle: OrderedFloat<f64>,
187    f: &mut W,
188    options: &FormatOptions,
189) -> std::fmt::Result {
190    match options.kind() {
191        FormatKind::Decimal => {
192            if let Some(precision) = options.precision() {
193                write!(f, "{:.precision$}", angle.into_inner())
194            } else {
195                write!(f, "{}", angle.into_inner())
196            }
197        }
198        FormatKind::DmsSigned => {
199            let (degrees, minutes, seconds) = inner::to_degrees_minutes_seconds(angle);
200            if let Some(precision) = options.precision() {
201                write!(f, "{degrees}° {minutes}′ {seconds:.precision$}″")
202            } else {
203                write!(f, "{degrees}° {minutes}′ {seconds}″")
204            }
205        }
206        FormatKind::DmsLabeled => {
207            let (degrees, minutes, seconds) = inner::to_degrees_minutes_seconds(angle);
208            let (positive, negative) = options.labels().expect("No labels provided");
209            if let Some(precision) = options.precision() {
210                write!(
211                    f,
212                    "{}° {}′ {:.precision$}″ {}",
213                    degrees.abs(),
214                    minutes,
215                    seconds,
216                    if angle > inner::ZERO {
217                        positive.to_string()
218                    } else if angle < inner::ZERO {
219                        negative.to_string()
220                    } else {
221                        "".to_string()
222                    }
223                )
224            } else {
225                write!(
226                    f,
227                    "{}° {}′ {}″ {}",
228                    degrees.abs(),
229                    minutes,
230                    seconds,
231                    if angle > inner::ZERO {
232                        positive.to_string()
233                    } else if angle < inner::ZERO {
234                        negative.to_string()
235                    } else {
236                        "".to_string()
237                    }
238                )
239            }
240        }
241        FormatKind::DmsBare => {
242            let (degrees, minutes, seconds) = inner::to_degrees_minutes_seconds(angle);
243            let precision = if let Some(precision) = options.precision()
244                && precision >= 4
245            {
246                precision
247            } else {
248                MINIMUM_DMS_BARE_PRECISION
249            };
250            let width = precision + 3;
251            write!(f, "{degrees:+04}:{minutes:02}:{seconds:0width$.precision$}",)
252        }
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Unit Tests
258// ---------------------------------------------------------------------------
259
260#[cfg(test)]
261mod tests {
262    use crate::fmt::{FormatOptions, Formatter};
263    use ordered_float::OrderedFloat;
264
265    #[test]
266    fn test_float_to_string_positive() {
267        assert_eq!(
268            OrderedFloat(45.508333)
269                .to_formatted_string(&FormatOptions::decimal().with_precision(6)),
270            "45.508333"
271        );
272    }
273
274    #[test]
275    fn test_float_to_string_negative() {
276        assert_eq!(
277            OrderedFloat(-45.508333)
278                .to_formatted_string(&FormatOptions::decimal().with_precision(6)),
279            "-45.508333"
280        );
281    }
282
283    #[test]
284    fn test_float_to_string_signed_positive() {
285        assert_eq!(
286            OrderedFloat(45.508333).to_formatted_string(&FormatOptions::dms_signed()),
287            "45° 30′ 29.998800″"
288        );
289    }
290
291    #[test]
292    fn test_float_to_string_signed_negative() {
293        assert_eq!(
294            OrderedFloat(-45.508333).to_formatted_string(&FormatOptions::dms_signed()),
295            "-45° 30′ 29.998800″"
296        );
297    }
298
299    #[test]
300    fn test_float_to_degree_string_labeled_positive() {
301        assert_eq!(
302            OrderedFloat(45.508333)
303                .to_formatted_string(&FormatOptions::dms_labeled().with_latitude_labels()),
304            "45° 30′ 29.998800″ N"
305        );
306    }
307
308    #[test]
309    fn test_float_to_string_labeled_negative() {
310        assert_eq!(
311            OrderedFloat(-45.508333)
312                .to_formatted_string(&FormatOptions::dms_labeled().with_latitude_labels()),
313            "45° 30′ 29.998800″ S"
314        );
315    }
316
317    #[test]
318    fn test_float_to_string_bare_positive() {
319        assert_eq!(
320            OrderedFloat(45.508333).to_formatted_string(&FormatOptions::dms_bare()),
321            "+045:30:29.998800"
322        );
323    }
324
325    #[test]
326    fn test_float_to_string_bare_negative() {
327        assert_eq!(
328            OrderedFloat(-45.508333).to_formatted_string(&FormatOptions::dms_bare()),
329            "-045:30:29.998800"
330        );
331    }
332}