1use crate::inner;
2use core::{
3 fmt::{Debug, Write},
4 hash::Hash,
5};
6use ordered_float::OrderedFloat;
7
8pub 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#[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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum FormatKind {
39 #[default]
40 Decimal,
42 DmsSigned,
44 DmsLabeled,
46 DmsBare,
48}
49
50pub const DEFAULT_DECIMAL_PRECISION: usize = 8;
55pub const DEFAULT_DMS_PRECISION: usize = 6;
56pub const MINIMUM_DMS_BARE_PRECISION: usize = 4;
57
58impl 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
68impl 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 pub const fn decimal() -> Self {
89 Self::new(FormatKind::Decimal).with_default_precision()
90 }
91
92 pub const fn dms() -> Self {
94 Self::dms_signed()
95 }
96
97 pub const fn dms_signed() -> Self {
99 Self::new(FormatKind::DmsSigned).with_default_precision()
100 }
101
102 pub const fn dms_labeled() -> Self {
104 Self::new(FormatKind::DmsLabeled).with_default_precision()
105 }
106
107 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
181pub(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#[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}