use crate::clock_library::series::{GriddingLog, PhaseSeries};
use crate::precise_products::{parse_clock_rinex, parse_clock_rinex_records};
pub const KNOWN_CLOCK_TIME_SYSTEMS: [&str; 8] =
["GPS", "GLO", "GAL", "BDT", "QZS", "IRN", "UTC", "TAI"];
pub fn rinex_clock_time_system(text: &str) -> Result<(String, bool), String> {
let mut sat_system: Option<char> = None;
for line in text.lines() {
let label = line.get(60..).map(str::trim_end).unwrap_or("");
if label == "END OF HEADER" {
break;
}
if label == "RINEX VERSION / TYPE" {
sat_system = Some(line.chars().nth(40).unwrap_or(' '));
}
if label == "TIME SYSTEM ID" {
let id = line[..60].trim().to_string();
if KNOWN_CLOCK_TIME_SYSTEMS.contains(&id.as_str()) {
return Ok((id, true));
}
return Err(format!(
"RINEX clock file declares time system {id:?}, which this reader does not \
know (known: {}); refusing it rather than reading its epochs as GPS time",
KNOWN_CLOCK_TIME_SYSTEMS.join(", ")
));
}
}
let default = match sat_system.unwrap_or(' ') {
'G' | ' ' => "GPS",
'R' => "GLO",
'E' => "GAL",
'C' => "BDT",
'J' => "QZS",
'I' => "IRN",
'M' => {
return Err(
"RINEX clock file is mixed-system ('M') and declares no TIME SYSTEM ID; \
refusing to guess the time system of its epochs"
.to_string(),
)
}
other => {
return Err(format!(
"RINEX clock file has satellite system {other:?} and declares no TIME SYSTEM \
ID; refusing to guess the time system of its epochs"
))
}
};
Ok((default.to_string(), false))
}
#[derive(Clone, Debug)]
pub struct RinexClockSeries {
pub series: PhaseSeries,
pub log: GriddingLog,
pub time_system: String,
pub time_system_declared: bool,
pub record: &'static str,
}
pub fn rinex_clock_series(texts: &[&str], id: &str, tau0: f64) -> Result<RinexClockSeries, String> {
let mut system: Option<(String, bool)> = None;
for t in texts {
let ts = rinex_clock_time_system(t)?;
match &mut system {
None => system = Some(ts),
Some((s, _)) if *s != ts.0 => {
return Err(format!(
"RINEX clock files declare different time systems ({s} and {}); refusing to \
join them on one time axis",
ts.0
))
}
Some((_, declared)) => *declared = *declared && ts.1,
}
}
let (time_system, declared) = system.unwrap_or_else(|| ("GPS".to_string(), false));
let mut out: Option<(&'static str, Vec<(f64, f64)>)> = None;
for record in ["AS", "AR"] {
let mut samples: Vec<(f64, f64)> = Vec::new();
for t in texts {
let c = if record == "AS" {
parse_clock_rinex(t)?
} else {
parse_clock_rinex_records(t, record)?
};
if let Some(v) = c.sats.get(id) {
samples.extend_from_slice(v);
}
}
if !samples.is_empty() {
out = Some((record, samples));
break;
}
}
let Some((record, mut samples)) = out else {
return Err(format!("no AS or AR records for {id}"));
};
samples.sort_by(|a, b| a.0.total_cmp(&b.0));
samples.dedup_by(|a, b| a.0 == b.0);
let (series, log) = PhaseSeries::from_samples(&samples, tau0);
Ok(RinexClockSeries {
series,
log,
time_system,
time_system_declared: declared,
record,
})
}
pub fn rinex_clock_satellites(texts: &[&str]) -> Result<Vec<String>, String> {
let mut out: Vec<String> = Vec::new();
for t in texts {
out.extend(parse_clock_rinex(t)?.sats.into_keys());
}
out.sort();
out.dedup();
Ok(out)
}
pub fn parse_bipm_utclab(text: &str) -> Vec<(i64, f64)> {
let mut v: Vec<(i64, f64)> = text
.lines()
.filter_map(|l| {
let mut f = l.split_whitespace();
let m = f.next()?.parse::<i64>().ok()?;
let x = f.next()?.parse::<f64>().ok()?;
x.is_finite().then_some((m, x))
})
.collect();
v.sort_by_key(|p| p.0);
v.dedup_by_key(|p| p.0);
v
}
#[derive(Clone, Debug, PartialEq)]
pub struct CircularTRow {
pub code: String,
pub values: Vec<Option<f64>>,
pub u_a: Option<f64>,
pub u_b: Option<f64>,
pub u: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct CircularTSection1 {
pub issue: Option<u32>,
pub mjds: Vec<i64>,
pub rows: Vec<CircularTRow>,
}
impl CircularTSection1 {
pub fn series(&self, code: &str) -> Vec<(i64, f64)> {
self.rows
.iter()
.filter(|r| r.code == code)
.flat_map(|r| {
self.mjds
.iter()
.zip(&r.values)
.filter_map(|(m, v)| v.map(|v| (*m, v)))
})
.collect()
}
}
pub fn parse_circular_t_section1(text: &str) -> Result<CircularTSection1, String> {
let mut out = CircularTSection1::default();
let mut lines = text.lines();
if let Some(first) = lines.clone().next() {
out.issue = first
.trim()
.strip_prefix("CIRCULAR T")
.and_then(|r| r.split_whitespace().next())
.and_then(|n| n.parse().ok());
}
for l in lines.by_ref() {
if l.trim_start().starts_with("1 -") {
break;
}
}
let mut in_rows = false;
for l in lines {
let t = l.trim();
if t.starts_with("2 -") {
break;
}
if out.mjds.is_empty() {
if let Some(rest) = t.strip_prefix("MJD") {
out.mjds = rest
.split_whitespace()
.map_while(|f| f.parse::<i64>().ok())
.collect();
}
continue;
}
if t.starts_with("Laboratory") {
in_rows = true;
continue;
}
if !in_rows || t.is_empty() {
continue;
}
let code = t.split_whitespace().next().unwrap_or_default().to_string();
let rest = match (t.find('('), t.find(')')) {
(Some(a), Some(b)) if b > a => &t[b + 1..],
_ => &t[code.len()..],
};
let f: Vec<&str> = rest.split_whitespace().collect();
let n = out.mjds.len();
if f.len() < n + 3 {
return Err(format!("short Circular T row: {t:?}"));
}
let num = |s: &str| -> Result<Option<f64>, String> {
match s {
"-" | "NC" => Ok(None),
_ => s
.parse::<f64>()
.map(Some)
.map_err(|_| format!("bad value {s:?} in {t:?}")),
}
};
let values = f[..n]
.iter()
.map(|s| num(s))
.collect::<Result<Vec<_>, _>>()?;
out.rows.push(CircularTRow {
code,
values,
u_a: num(f[n])?,
u_b: num(f[n + 1])?,
u: num(f[n + 2])?,
});
}
if out.mjds.is_empty() {
return Err("no Section 1 MJD header found".into());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
const CLK: &str =
" 3.04 C RINEX VERSION / TYPE
END OF HEADER
AS G08 2026 03 01 00 00 0.000000 1 1.000000000000E-04
AS G08 2026 03 01 00 00 30.000000 1 1.000000100000E-04
AS G08 2026 03 01 00 01 30.000000 1 1.000000300000E-04
AS G10 2026 03 01 00 00 0.000000 1 -2.000000000000E-04
";
#[test]
fn rinex_clock_records_grid_with_gaps() {
let r = rinex_clock_series(&[CLK, CLK], "G08", 30.0).unwrap();
let (s, log) = (r.series, r.log);
assert_eq!(
(r.time_system.as_str(), r.time_system_declared),
("GPS", false)
);
assert_eq!(r.record, "AS");
assert_eq!(s.len(), 4);
assert!(s.x[2].is_nan());
assert!((s.x[3] - 1.0000003e-4).abs() < 1e-18);
assert_eq!(log, GriddingLog::default());
assert_eq!(rinex_clock_satellites(&[CLK]).unwrap(), vec!["G08", "G10"]);
assert!(rinex_clock_series(&[CLK], "G01", 30.0).is_err());
}
fn with_time_system(id: &str) -> String {
CLK.replacen(
" END OF HEADER",
&format!(
"{:<60}TIME SYSTEM ID\n{:60}END OF HEADER",
format!(" {id}"),
""
),
1,
)
}
#[test]
fn declared_time_system_is_explicit_and_unknown_is_refused() {
let gal = with_time_system("GAL");
let r = rinex_clock_series(&[&gal], "G08", 30.0).unwrap();
assert_eq!(
(r.time_system.as_str(), r.time_system_declared),
("GAL", true)
);
let ltc = with_time_system("LTC");
let e = rinex_clock_series(&[<c], "G08", 30.0).unwrap_err();
assert!(e.contains("\"LTC\"") && e.contains("refusing"), "{e}");
let e = rinex_clock_series(&[&gal, CLK], "G08", 30.0).unwrap_err();
assert!(e.contains("different time systems"), "{e}");
}
fn with_sat_system(c: char) -> String {
let mut v: Vec<char> = CLK.chars().collect();
v[40] = c;
v.into_iter().collect()
}
#[test]
fn undeclared_time_system_defaults_from_the_satellite_system() {
for (c, want) in [
(' ', "GPS"),
('G', "GPS"),
('R', "GLO"),
('E', "GAL"),
('C', "BDT"),
('J', "QZS"),
('I', "IRN"),
] {
let t = with_sat_system(c);
assert_eq!(
rinex_clock_time_system(&t).unwrap(),
(want.to_string(), false),
"{c:?}"
);
}
let t = with_time_system("UTC");
let mut v: Vec<char> = t.chars().collect();
v[40] = 'E';
let t: String = v.into_iter().collect();
assert_eq!(rinex_clock_time_system(&t).unwrap(), ("UTC".into(), true));
}
#[test]
fn undeclared_mixed_file_is_refused() {
let e = rinex_clock_time_system(&with_sat_system('M')).unwrap_err();
assert!(e.contains("mixed-system"), "{e}");
let e = rinex_clock_series(&[&with_sat_system('M')], "G08", 30.0).unwrap_err();
assert!(e.contains("mixed-system"), "{e}");
let mut v: Vec<char> = with_time_system("GPS").chars().collect();
v[40] = 'M';
let t: String = v.into_iter().collect();
assert_eq!(rinex_clock_time_system(&t).unwrap(), ("GPS".into(), true));
}
#[test]
fn declared_only_when_every_file_declared() {
let gps = with_time_system("GPS");
let r = rinex_clock_series(&[&gps, CLK], "G08", 30.0).unwrap();
assert_eq!(r.time_system, "GPS");
assert!(!r.time_system_declared);
let r = rinex_clock_series(&[&gps, &gps], "G08", 30.0).unwrap();
assert!(r.time_system_declared);
}
#[test]
fn utclab_rows() {
let t = "# header\n 60000 1.5 0.3\n60005 -\n60010 -2.25 x\nbad line\n";
assert_eq!(parse_bipm_utclab(t), vec![(60000, 1.5), (60010, -2.25)]);
}
const CIRT: &str = "CIRCULAR T 999 ISSN 1143-1393
2030 JANUARY 01, 07h UTC
1 - Difference between UTC and its local realizations UTC(k) and corresponding uncertainties.
Date 2029 0h UTC DEC 1 DEC 6 Uncertainty/ns Notes
MJD 62000 62005 uA uB u
Laboratory k [UTC-UTC(k)]/ns
AAA (Somewhere) 10.5 -2.0 0.2 3.0 3.0
BBB (Other Place) - 4.1 0.7 NC -
CCC 1.0 1.5 0.2 1.3 1.3
2 - Something else
DDD (Ignored) 9.9 9.9 0.2 1.0 1.0
";
#[test]
fn circular_t_section_1() {
let c = parse_circular_t_section1(CIRT).unwrap();
assert_eq!(c.issue, Some(999));
assert_eq!(c.mjds, vec![62000, 62005]);
assert_eq!(c.rows.len(), 3);
assert_eq!(c.rows[1].values, vec![None, Some(4.1)]);
assert_eq!(c.rows[1].u_b, None);
assert_eq!(c.rows[1].u, None);
assert_eq!(c.rows[2].code, "CCC");
assert_eq!(c.series("AAA"), vec![(62000, 10.5), (62005, -2.0)]);
assert!(parse_circular_t_section1("nothing").is_err());
}
}