use crate::{ATTOS_PER_DAY, Dt, DtErr, DtErrKind, Real, SEC_PER_DAY_F, an_err};
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::Ordering;
#[derive(Debug, Clone, Copy, Default)]
pub enum Separator {
#[default]
Whitespace,
Comma,
Tab,
Pipe,
Semicolon,
}
#[derive(Debug, Clone, Copy)]
pub struct Ut1Columns {
pub mjd: usize,
pub ut1_utc: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Ut1Format {
#[default]
Finals2000A,
C04,
Custom(Ut1Columns),
}
#[derive(Debug, Clone, Copy)]
pub struct Ut1Row {
pub mjd: Real,
pub ut1_minus_utc: Real,
}
#[derive(Debug, Clone)]
pub struct Ut1Data {
rows: Vec<Ut1Row>,
}
impl Ut1Data {
pub const MAX_LINE_LEN: usize = 8192;
fn try_parse_row(trimmed: &str, format: Ut1Format, separator: Separator) -> Option<Ut1Row> {
let parts: Vec<&str> = match separator {
Separator::Whitespace => trimmed.split_whitespace().collect(),
Separator::Comma => trimmed.split(',').map(|s| s.trim()).collect(),
Separator::Tab => trimmed.split('\t').map(|s| s.trim()).collect(),
Separator::Pipe => trimmed.split('|').map(|s| s.trim()).collect(),
Separator::Semicolon => trimmed.split(';').map(|s| s.trim()).collect(),
};
if parts.len() < 2 {
return None;
}
let (mjd, ut1_utc) = match format {
Ut1Format::Finals2000A => {
let mjd_idx = parts.iter().position(|p| {
p.contains('.') && p.parse::<Real>().map_or(false, |v| v > 30000.0)
})?;
let mut flag_count = 0;
let mut ut1_value: Option<Real> = None;
for i in (mjd_idx + 1)..parts.len() {
let token = parts[i];
let is_flag = token == "I"
|| token == "P"
|| token.starts_with("I-")
|| token.starts_with("P-");
if is_flag {
flag_count += 1;
if flag_count == 2 {
let value_str = if token.starts_with("I-") || token.starts_with("P-") {
&token[1..]
} else if i + 1 < parts.len() {
parts[i + 1]
} else {
break;
};
if let Ok(val) = value_str.parse::<Real>() {
ut1_value = Some(val);
}
break;
}
}
}
let ut1 = ut1_value?;
let mjd_val = parts[mjd_idx].parse::<Real>().ok()?;
(Some(mjd_val), Some(ut1))
}
Ut1Format::C04 => {
let (mjd_str, ut1_str) = (parts.get(4)?, parts.get(7)?);
let mjd = mjd_str.parse::<Real>().ok()?;
let ut1 = ut1_str.parse::<Real>().ok()?;
(Some(mjd), Some(ut1))
}
Ut1Format::Custom(cols) => {
let (mjd_str, ut1_str) = (parts.get(cols.mjd)?, parts.get(cols.ut1_utc)?);
let mjd = mjd_str.parse::<Real>().ok()?;
let ut1 = ut1_str.parse::<Real>().ok()?;
(Some(mjd), Some(ut1))
}
};
match (mjd, ut1_utc) {
(Some(mjd_val), Some(ut1_val)) => Some(Ut1Row {
mjd: mjd_val,
ut1_minus_utc: ut1_val,
}),
_ => None,
}
}
fn parse_lines<'a>(
lines: impl Iterator<Item = &'a str>,
format: Ut1Format,
separator: Separator,
) -> Result<Vec<Ut1Row>, DtErr> {
let mut rows = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.len() > Self::MAX_LINE_LEN
{
continue;
}
if let Some(row) = Self::try_parse_row(trimmed, format, separator) {
rows.push(row);
}
}
if rows.is_empty() {
return Err(an_err!(DtErrKind::Incomplete, "no valid rows"));
}
rows.sort_by(|a, b| a.mjd.partial_cmp(&b.mjd).unwrap_or(Ordering::Equal));
Ok(rows)
}
pub fn data_from_str(
s: &str,
format: Ut1Format,
separator: Separator,
) -> Result<Vec<Ut1Row>, DtErr> {
Self::parse_lines(s.lines(), format, separator)
}
pub fn data_from_bytes(
bytes: &[u8],
format: Ut1Format,
separator: Separator,
) -> Result<Vec<Ut1Row>, DtErr> {
let s = core::str::from_utf8(bytes).unwrap_or("");
Self::data_from_str(s, format, separator)
}
pub fn from_str(s: &str, format: Ut1Format, separator: Separator) -> Result<Self, DtErr> {
let rows = Self::data_from_str(s, format, separator)?;
Ok(Self { rows })
}
pub fn from_bytes(
bytes: &[u8],
format: Ut1Format,
separator: Separator,
) -> Result<Self, DtErr> {
let rows = Self::data_from_bytes(bytes, format, separator)?;
Ok(Self { rows })
}
fn ut1_minus_utc_exact(&self, mjd_days: i64, mjd_frac: u128) -> Option<Real> {
let frac_days = f!(mjd_frac) / f!(ATTOS_PER_DAY);
let mjd_f = f!(mjd_days) + frac_days;
self.ut1_minus_utc(mjd_f)
}
pub fn ut1_minus_utc(&self, mjd: Real) -> Option<Real> {
if self.rows.is_empty() {
return None;
}
let idx = match self
.rows
.binary_search_by(|probe| probe.mjd.partial_cmp(&mjd).unwrap_or(Ordering::Equal))
{
Ok(i) => i,
Err(i) => {
if i == 0 {
return Some(self.rows[0].ut1_minus_utc);
}
if i >= self.rows.len() {
return Some(self.rows[self.rows.len() - 1].ut1_minus_utc);
}
i - 1
}
};
if idx + 1 < self.rows.len() {
let e0 = &self.rows[idx];
let e1 = &self.rows[idx + 1];
let t = (mjd - e0.mjd) / (e1.mjd - e0.mjd);
Some(e0.ut1_minus_utc + t * (e1.ut1_minus_utc - e0.ut1_minus_utc))
} else {
Some(self.rows[idx].ut1_minus_utc)
}
}
}
#[cfg(feature = "std")]
impl Ut1Data {
pub fn data_from_reader<R: std::io::BufRead>(
mut reader: R,
format: Ut1Format,
separator: Separator,
) -> Result<Vec<Ut1Row>, DtErr> {
let mut line_buf = String::with_capacity(256);
let mut rows = Vec::new();
loop {
line_buf.clear();
let bytes_read = match reader.read_line(&mut line_buf) {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
return Err(an_err!(DtErrKind::IOErr, "read line: {}", e));
}
};
if bytes_read > Self::MAX_LINE_LEN {
continue;
}
let trimmed = line_buf.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(row) = Self::try_parse_row(trimmed, format, separator) {
rows.push(row);
}
}
if rows.is_empty() {
return Err(an_err!(DtErrKind::Incomplete, "no valid rows"));
}
rows.sort_by(|a, b| a.mjd.partial_cmp(&b.mjd).unwrap_or(Ordering::Equal));
Ok(rows)
}
pub fn data_from_text_file<P: AsRef<std::path::Path>>(
path: P,
format: Ut1Format,
separator: Separator,
) -> Result<Vec<Ut1Row>, DtErr> {
use std::fs::File;
use std::io::BufReader;
let path = path.as_ref();
let file = File::open(path)
.map_err(|e| an_err!(DtErrKind::IOErr, "open file: '{}': {}", path.display(), e))?;
let reader = BufReader::new(file);
Self::data_from_reader(reader, format, separator)
}
pub fn from_text_file<P: AsRef<std::path::Path>>(
path: P,
format: Ut1Format,
separator: Separator,
) -> Result<Self, DtErr> {
let rows = Self::data_from_text_file(path, format, separator)?;
Ok(Self { rows })
}
}
impl Dt {
pub fn to_ut1(&self, ut1_data: &Ut1Data) -> Result<Self, DtErr> {
let (mjd_days, mjd_frac) = self.to_mjd();
let dut1 = ut1_data
.ut1_minus_utc_exact(mjd_days, mjd_frac)
.ok_or_else(|| {
let mjd_f = f!(mjd_days) + f!(mjd_frac) / SEC_PER_DAY_F;
an_err!(DtErrKind::OutOfRange, "mjd: {mjd_f}")
})?;
Ok(self.add(Dt::from_sec_f(dut1)))
}
pub fn from_ut1(ut1: Self, ut1_data: &Ut1Data) -> Result<Self, DtErr> {
if ut1_data.rows.is_empty() {
return Err(an_err!(DtErrKind::InternalErr, "contains no data"));
}
let mut utc_guess = ut1.clone();
for _ in 0..8 {
let (mjd_days, mjd_frac) = utc_guess.to_mjd();
let dut1 = ut1_data
.ut1_minus_utc_exact(mjd_days, mjd_frac)
.ok_or_else(|| {
let mjd_f = f!(mjd_days) + f!(mjd_frac) / SEC_PER_DAY_F;
an_err!(DtErrKind::OutOfRange, "mjd: {mjd_f}")
})?;
utc_guess = ut1.sub(Dt::from_sec_f(dut1));
}
Ok(utc_guess)
}
}