1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
//! # Parsing, conversion, and formatting of astronomical coordinates
//!
//! This module provides robust utilities to:
//!
//! - **Parse** textual Right Ascension (RA) and Declination (DEC) from common sexagesimal strings,
//! - **Estimate** per-measurement accuracy from the number of decimals in the input,
//! - **Convert** 3D Cartesian vectors to equatorial angles (RA, DEC),
//! - **Format** angles and vectors for human-friendly displays (sexagesimal H/M/S and D/M/S, AU vectors).
//!
//! ## Overview
//!
//! These helpers are typically used when ingesting MPC-style astrometric observations and
//! converting them into numerical values usable by orbit determination routines.
//!
//! ### Provided features
//!
//! **Parsing & accuracy**
//! -----------------
//! * [`parse_ra_to_deg`](crate::conversion::parse_ra_to_deg) — Parse a sexagesimal RA string (`"HH MM SS.SS"`) into **degrees** and **accuracy** (arcsec).
//! * [`parse_dec_to_deg`](crate::conversion::parse_dec_to_deg) — Parse a sexagesimal DEC string (`"±DD MM SS.SS"`) into **degrees** and **accuracy** (arcsec).
//! * [`arcsec_to_rad`](crate::conversion::arcsec_to_rad) — Convert **arcseconds** to **radians** (utility used by callers).
//!
//! **Angle & vector formatting**
//! -----------------
//! * [`ra_hms_prec`](crate::conversion::ra_hms_prec) — Convert RA (radians) to canonical `(HH, MM, SS.sss)` with rounding and carry, hours wrapped to `[0, 24)`.
//! * [`dec_sdms_prec`](crate::conversion::dec_sdms_prec) — Convert DEC (radians) to `(sign, DD, MM, SS.sss)` with rounding, carry, and clamping at `±90°`.
//! * [`fmt_vec3_au`](crate::conversion::fmt_vec3_au) — Format a `nalgebra::Vector3<f64>` as `[ x, y, z ] AU` with fixed decimal precision.
//!
//! **Cartesian → Equatorial**
//! -----------------
//! * [`cartesian_to_radec`](crate::conversion::cartesian_to_radec) — Convert a 3D Cartesian position vector to `(α, δ, ρ)` where `α, δ` are in **radians**,
//! and `ρ` is the input norm (same units as the input vector).
//!
//! ### Units
//!
//! - **RA** returned by [`parse_ra_to_deg`](crate::conversion::parse_ra_to_deg) is in **degrees** (`0° ≤ RA < 360°`).
//! - **DEC** returned by [`parse_dec_to_deg`](crate::conversion::parse_dec_to_deg) is in **degrees** (`−90° ≤ DEC ≤ +90°`).
//! - **Accuracy** estimates are in **arcseconds** (derived from the decimal precision of the input seconds).
//! - [`cartesian_to_radec`](crate::conversion::cartesian_to_radec) returns angles in **radians** and `ρ` in the **same unit** as the input vector.
//! - [`ra_hms_prec`](crate::conversion::ra_hms_prec) returns `(HH, MM, SS)` with `HH ∈ [0, 23]`, `MM ∈ [0, 59]`, `SS ∈ [0.0, 60.0)` after carry.
//! - [`dec_sdms_prec`](crate::conversion::dec_sdms_prec) returns `(sign, DD, MM, SS)` with `sign ∈ {'+','-'}`, `DD ∈ [0, 90]`, `MM ∈ [0, 59]`,
//! `SS ∈ [0.0, 60.0)` after carry, and clamps to `90°00′00″` at the pole.
//! - [`fmt_vec3_au`](crate::conversion::fmt_vec3_au) prints components in **fixed-point** with exactly `prec` decimals, suffixed with `" AU"`.
//!
//! ### Accuracy estimation
//!
//! Both parsers compute an **accuracy hint** directly from the number of digits after the decimal point
//! in the *seconds* field. For instance, `"12 34 56.7"` implies `0.1″`, whereas `"12 34 56"` implies `1″`.
//! This is convenient when deriving per-observation weights for orbit determination.
//!
//! ## Examples
//!
//! ```rust,no_run
//! use outfit::conversion::{parse_ra_to_deg, parse_dec_to_deg, cartesian_to_radec,
//! ra_hms_prec, dec_sdms_prec, fmt_vec3_au};
//! use nalgebra::Vector3;
//!
//! // --- Parsing with accuracy ------------------------------------------------
//! let (ra_deg, ra_acc) = parse_ra_to_deg("10 12 33.44").unwrap(); // deg, arcsec
//! let (dec_deg, dec_acc) = parse_dec_to_deg("-20 33 10.5").unwrap(); // deg, arcsec
//! println!("RA = {ra_deg:.6}° ± {ra_acc:.3}\"");
//! println!("DEC = {dec_deg:.6}° ± {dec_acc:.3}\"");
//!
//! // --- Cartesian → Equatorial (radians) ------------------------------------
//! let pos = Vector3::new(1.0, 1.0, 0.5);
//! let (alpha, delta, rho) = cartesian_to_radec(pos);
//! println!("α = {alpha} rad, δ = {delta} rad, ρ = {rho}");
//!
//! // --- Sexagesimal formatting helpers --------------------------------------
//! let (hh, mm, ss) = ra_hms_prec(alpha, 3); // RA → (HH, MM, SS.sss)
//! let (sgn, d, m, s) = dec_sdms_prec(delta, 3); // DEC → (sign, DD, MM, SS.sss)
//! println!("RA ≈ {hh:02}h{mm:02}m{ss:.3}s, DEC ≈ {sgn}{d:02}°{m:02}'{s:.3}\"");
//!
//! // --- Vector formatting (AU) ----------------------------------------------
//! let r_geo = Vector3::new(0.1234567, -1.0, 2.0);
//! println!("{}", fmt_vec3_au(&r_geo, 6)); // → "[ 0.123457, -1.000000, 2.000000 ] AU"
//! ```
//!
//! ## See also
//!
//! - Sexagesimal string rendering of seconds: `fmt_ss` (in the observations display helpers).
//! - Reference frame conversions and aberration: `ref_system` and `observations` modules.
//! - MPC/ADES ingestion modules where these utilities are typically used.
use TAU;
use Vector3;
use crate;
/// Estimate the accuracy of a numeric string based on its decimal precision.
///
/// Arguments
/// ---------------
/// * `field`: a string slice containing the numeric value (e.g., `"56.78"`), typically the last component of an angle
/// * `factor`: a scale factor to apply to the accuracy (e.g., `1.0 / 60.0` for arcminutes, `1.0 / 3600.0` for arcseconds)
///
/// Return
/// ----------
/// * `Option<f64>`: the estimated accuracy scaled by `factor`, or `None` if the input is malformed
/// Convert an angle from **arcseconds** to **radians**.
///
/// Arguments
/// -----------------
/// * `arcsec` — Angle in **arcseconds**.
///
/// Return
/// ----------
/// * Angle in **radians** (`Radian`).
/// Parse a right ascension (RA) string and convert it to degrees, with an estimate of its accuracy.
///
/// This function parses a right ascension expressed in **sexagesimal hours**
/// (`HH MM SS.SS`) and converts it into degrees. The input must have exactly
/// three whitespace-separated fields: hours, minutes, and seconds (which may
/// include fractional seconds).
///
/// # Arguments
///
/// * `ra` – A string in the format:
/// * `"HH MM SS.SS"` (e.g., `"12 30 45.67"`).
///
/// # Returns
///
/// Returns `Some((ra_deg, accuracy_arcsec))` where:
/// * `ra_deg` – Right ascension in **degrees** (0° ≤ RA < 360°),
/// * `accuracy_arcsec` – Estimated accuracy of the RA, derived from the number
/// of decimals provided in the seconds field, in **arcseconds**.
///
/// Returns `None` if:
/// * The string does not have exactly 3 whitespace-separated components,
/// * Any component fails to parse as a floating-point number.
///
/// # Formula
///
/// ```text
/// RA(deg) = (hours + minutes / 60 + seconds / 3600) × 15
/// ```
///
/// # See also
/// * [`parse_dec_to_deg`] – Parses declination strings into degrees.
/// Parse a declination (DEC) string and convert it to degrees, with an estimate of its accuracy.
///
/// This function parses a declination expressed in **sexagesimal degrees**
/// (`±DD MM SS.SS`) and converts it into degrees. The input must have exactly
/// three whitespace-separated fields: degrees (with sign), minutes, and seconds
/// (which may include fractional seconds).
///
/// # Arguments
///
/// * `dec` – A string in the format:
/// * `"±DD MM SS.SS"` (e.g., `"-23 26 45.1"` or `"+10 15 30"`).
///
/// # Returns
///
/// Returns `Some((dec_deg, accuracy_arcsec))` where:
/// * `dec_deg` – Declination in **degrees** (−90° ≤ DEC ≤ +90°),
/// * `accuracy_arcsec` – Estimated accuracy of the DEC, derived from the number
/// of decimals provided in the seconds field, in **arcseconds**.
///
/// Returns `None` if:
/// * The string does not have exactly 3 whitespace-separated components,
/// * Any component fails to parse as a floating-point number.
///
/// # Formula
///
/// ```text
/// DEC(deg) = sign × (degrees + minutes / 60 + seconds / 3600)
/// ```
///
/// # See also
/// * [`parse_ra_to_deg`] – Parses right ascension strings into degrees.
/// Format a 3D vector (AU) with a configurable fixed decimal precision.
/// The output is rendered as:
/// `[ {x:.prec}, {y:.prec}, {z:.prec} ] AU`
///
/// Arguments
/// -----------------
/// * `v`: The position vector in **astronomical units (AU)**, expressed in the
/// **equatorial mean J2000** frame if you follow the crate’s convention.
/// * `prec`: Number of fractional digits for each component (fixed‐point).
///
/// Return
/// ----------
/// * A `String` like `"[ 0.123457, -1.000000, 0.000042 ] AU"` when `prec = 6`.
///
/// Notes
/// ----------
/// * Rounding uses Rust’s default `Display` formatting for `f64` (round half
/// away from zero).
/// * No thousands separator or scientific notation is used: components are
/// printed in **fixed‐point** with exactly `prec` decimals.
/// * The function does **not** sanitize non-finite values: `NaN`, `inf`, and
/// `-inf` will be forwarded as is.
/// * Units are **not converted**: call this function only if your vector is
/// already in AU. For other units (e.g. km), provide a separate formatter.
///
/// Examples
/// ----------
/// ```rust, ignore
/// use nalgebra::Vector3;
///
/// let v = Vector3::new(0.1234567, -1.0, 2.0);
/// assert_eq!(fmt_vec3_au(&v, 3), "[ 0.123, -1.000, 2.000 ] AU");
/// assert_eq!(fmt_vec3_au(&v, 6), "[ 0.123457, -1.000000, 2.000000 ] AU");
/// ```
///
/// See also
/// ------------
/// * [`fmt_ss`](crate::time::fmt_ss) – Zero-padded seconds string for sexagesimal outputs.
/// * [`ra_hms_prec`] – RA (radians) → `(HH, MM, SS.sss)` with carry.
/// * [`dec_sdms_prec`] – DEC (radians) → `(sign, DD, MM, SS.sss)` with carry.
// --- Angle formatting --------------------------------------------------------
/// Convert a right ascension (radians) to sexagesimal **hours–minutes–seconds**
/// with rounding and carry handling.
///
/// The input angle is normalized to `[0, 2π)` (i.e., modulo one full turn),
/// then converted to **total seconds** in `[0h, 24h)`. Seconds are rounded to
/// `prec` fractional digits; potential overflows are carried into minutes, and
/// minutes into hours (hours wrap modulo 24).
///
/// Arguments
/// -----------------
/// * `rad`: Right ascension in **radians**. Any finite value is accepted;
/// negatives and values ≥ 2π are normalized to `[0, 2π)`.
/// * `prec`: Number of fractional digits for the **seconds** component.
///
/// Return
/// ----------
/// * A tuple `(HH, MM, SS)` where:
/// - `HH ∈ [0, 23]`,
/// - `MM ∈ [0, 59]`,
/// - `SS ∈ [0.0, 60.0)` after rounding and carry,
/// guaranteeing canonical sexagesimal components ready for display.
///
/// Notes
/// ----------
/// * Rounding uses `f64::round` (half away from zero), then carry is applied:
/// `59.9995s` at `prec = 3` becomes `60.000s → +1 min`.
/// * Final hours are wrapped modulo 24; e.g. a value very close to `2π` can
/// round to `24h00m00s` which is reported as `00h00m00s`.
/// * This function **does not** format strings. For zero-padded seconds like
/// `"SS.sss"`, combine with [`fmt_ss`](crate::time::fmt_ss).
///
/// See also
/// ------------
/// * [`fmt_ss`](crate::time::fmt_ss) – Enforce zero-padded second strings (`"SS.sss"`).
/// * [`dec_sdms_prec`] – Declination to `sign, DD, MM, SS.sss`.
/// Convert a declination (radians) to **signed** sexagesimal
/// **degrees–minutes–seconds** with rounding, carry, and polar clamping.
///
/// The sign is taken from the input (`'+'` for `rad ≥ 0`, `'-'` otherwise).
/// The absolute value is converted to **total arcseconds**, seconds are rounded
/// to `prec` fractional digits, and carry is applied seconds→minutes→degrees.
/// Final values are clamped to the physical pole at **±90°00′00″**.
///
/// Arguments
/// -----------------
/// * `rad`: Declination in **radians**. Typical physical range is
/// `[-π/2, +π/2]`, but any finite value is accepted; the absolute value is
/// used for the `DD/MM/SS` decomposition.
/// * `prec`: Number of fractional digits for the **seconds** component.
///
/// Return
/// ----------
/// * A tuple `(sign, DD, MM, SS)` where:
/// - `sign ∈ {'+', '-'}` reflects the input sign,
/// - `DD ∈ [0, 90]`, `MM ∈ [0, 59]`,
/// - `SS ∈ [0.0, 60.0)` after rounding and carry,
/// with a **final clamp** at the pole: if rounding would exceed `90°`,
/// the function returns `(sign, 90, 0, 0.0)`.
///
/// Notes
/// ----------
/// * Rounding uses `f64::round` (half away from zero), then carry is applied:
/// e.g. `59.9995″` at `prec = 3` becomes `60.000″ → +1′`.
/// * If carrying minutes produces `60′`, it becomes `+1°`.
/// * At the upper bound, values that round past `90°` are clamped to
/// `90°00′00.000″` to maintain a valid declination.
///
/// See also
/// ------------
/// * [`ra_hms_prec`] – Right ascension to `HH, MM, SS.sss`.
/// * [`fmt_ss`](crate::time::fmt_ss) – Zero-padded second string formatting for display.
/// Convert a 3D Cartesian position vector to right ascension and declination.
///
/// Given a position vector expressed in Cartesian coordinates (typically in an equatorial frame),
/// this function returns the corresponding right ascension (α), declination (δ), and norm (distance).
///
/// Arguments
/// ---------
/// * `cartesian_position`: 3D position vector in Cartesian coordinates [AU or any length unit].
///
/// Returns
/// --------
/// * Tuple `(α, δ, ρ)`:
/// - `α`: right ascension in radians, in the range [0, 2π).
/// - `δ`: declination in radians, in the range [−π/2, +π/2].
/// - `ρ`: Euclidean norm of the vector (distance to the origin).
///
/// Remarks
/// -------
/// * If the input vector has zero norm, the result is `(0.0, 0.0, 0.0)`.
/// * The RA computation uses `atan2` to preserve quadrant information.
/// * This function is used when converting inertial position vectors to observable angles.
///
/// # See also
/// * [`correct_aberration`](crate::observations::correct_aberration) – apply aberration correction before calling this if needed