use std::{
env,
fs::{self, File},
io::{BufRead, BufReader},
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use std::fmt::Write;
#[derive(Debug)]
#[allow(dead_code)]
struct EopRow {
mjd: f64,
xp: f64,
yp: f64,
dut1: f64,
dx: f64,
dy: f64,
is_bulletin_b: bool,
}
fn parse_line(line: &str) -> Option<EopRow> {
if line.len() < 15 {
return None;
}
let mjd: f64 = line.get(7..15)?.trim().parse().ok()?;
if mjd < 37665.0 {
return None;
}
let (xp, yp, dut1, dx, dy, is_b) = if line.len() >= 185 {
let b_xp: Option<f64> = line.get(134..144).and_then(|s| s.trim().parse().ok());
let b_yp: Option<f64> = line.get(144..154).and_then(|s| s.trim().parse().ok());
let b_dut1: Option<f64> = line.get(154..165).and_then(|s| s.trim().parse().ok());
let b_dx: Option<f64> = line.get(165..175).and_then(|s| s.trim().parse().ok());
let b_dy: Option<f64> = line.get(175..185).and_then(|s| s.trim().parse().ok());
if let (Some(xp), Some(yp), Some(dut1)) = (b_xp, b_yp, b_dut1) {
(xp, yp, dut1, b_dx.unwrap_or(0.0), b_dy.unwrap_or(0.0), true)
} else {
parse_bulletin_a(line)?
}
} else {
parse_bulletin_a(line)?
};
Some(EopRow {
mjd,
xp,
yp,
dut1,
dx,
dy,
is_bulletin_b: is_b,
})
}
fn parse_bulletin_a(line: &str) -> Option<(f64, f64, f64, f64, f64, bool)> {
let xp: f64 = line.get(18..27)?.trim().parse().ok()?;
let yp: f64 = line.get(37..46)?.trim().parse().ok()?;
let dut1: f64 = if line.len() >= 68 {
line.get(58..68)?.trim().parse().ok()?
} else {
return None;
};
let dx: f64 = if line.len() >= 106 {
line.get(97..106)
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0.0)
} else {
0.0
};
let dy: f64 = if line.len() >= 125 {
line.get(116..125)
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0.0)
} else {
0.0
};
Some((xp, yp, dut1, dx, dy, false))
}
fn parse_finals(path: &Path) -> Result<Vec<EopRow>> {
let file = File::open(path).with_context(|| format!("open {path:?}"))?;
let reader = BufReader::new(file);
let rows: Vec<EopRow> = reader
.lines()
.map_while(Result::ok)
.filter_map(|line| parse_line(&line))
.collect();
anyhow::ensure!(!rows.is_empty(), "No valid EOP rows parsed from {path:?}");
Ok(rows)
}
fn generate_rust(rows: &[EopRow]) -> Result<String> {
let mut out = String::with_capacity(rows.len() * 120);
writeln!(
out,
"// ---------------------------------------------------"
)?;
writeln!(
out,
"// **AUTOGENERATED** by build.rs – DO NOT EDIT BY HAND"
)?;
writeln!(
out,
"// ---------------------------------------------------"
)?;
writeln!(out)?;
writeln!(out, "/// A single daily EOP record from IERS finals2000A.")?;
writeln!(out, "///")?;
writeln!(
out,
"/// All angular quantities are in their native IERS units:"
)?;
writeln!(
out,
"/// pole coordinates in arcseconds, UT1−UTC in seconds,"
)?;
writeln!(out, "/// CIP offsets dX/dY in milliarcseconds.")?;
writeln!(out, "#[derive(Debug, Clone, Copy)]")?;
writeln!(out, "pub struct EopEntry {{")?;
writeln!(out, " /// Modified Julian Date (UTC).")?;
writeln!(out, " pub mjd: f64,")?;
writeln!(out, " /// Pole x-coordinate (arcseconds).")?;
writeln!(out, " pub xp: f64,")?;
writeln!(out, " /// Pole y-coordinate (arcseconds).")?;
writeln!(out, " pub yp: f64,")?;
writeln!(out, " /// UT1 − UTC (seconds).")?;
writeln!(out, " pub dut1: f64,")?;
writeln!(
out,
" /// Celestial pole offset dX wrt IAU 2000A (milliarcseconds)."
)?;
writeln!(out, " pub dx: f64,")?;
writeln!(
out,
" /// Celestial pole offset dY wrt IAU 2000A (milliarcseconds)."
)?;
writeln!(out, " pub dy: f64,")?;
writeln!(out, "}}")?;
writeln!(out)?;
writeln!(
out,
"/// Embedded EOP table from IERS finals2000A.all ({} entries).",
rows.len()
)?;
writeln!(
out,
"/// MJD range: {:.1} – {:.1}.",
rows.first().unwrap().mjd,
rows.last().unwrap().mjd,
)?;
writeln!(out, "pub static EOP_TABLE: &[EopEntry] = &[")?;
for r in rows {
writeln!(
out,
" EopEntry {{ mjd: {:.2}, xp: {:.6}, yp: {:.6}, dut1: {:.7}, dx: {:.3}, dy: {:.3} }},",
r.mjd, r.xp, r.yp, r.dut1, r.dx, r.dy,
)?;
}
writeln!(out, "];")?;
Ok(out)
}
const FINALS_FILENAME: &str = "finals2000A.all";
const IERS_URL: &str = "https://datacenter.iers.org/products/eop/rapid/standard/finals2000A.all";
const USNO_URL: &str = "https://maia.usno.navy.mil/ser7/finals2000A.all";
fn ensure_dataset(dir: &Path) -> Result<PathBuf> {
let target = dir.join(FINALS_FILENAME);
if target.exists() {
eprintln!("IERS EOP: reusing cached {}", target.display());
return Ok(target);
}
fs::create_dir_all(dir)?;
download_finals(&target)?;
Ok(target)
}
fn download_finals(dst: &Path) -> Result<()> {
use reqwest::blocking::Client;
let client = Client::builder()
.user_agent("siderust-build (rust)")
.build()?;
for url in &[IERS_URL, USNO_URL] {
eprintln!("IERS EOP: downloading from {url}");
match client.get(*url).send() {
Ok(resp) if resp.status().is_success() => {
let bytes = resp.bytes()?;
if bytes.len() < 1000 {
eprintln!(
"IERS EOP: response too small ({} bytes), skipping",
bytes.len()
);
continue;
}
fs::write(dst, &bytes).with_context(|| format!("write {dst:?}"))?;
eprintln!("IERS EOP: saved {} bytes to {:?}", bytes.len(), dst);
return Ok(());
}
Ok(resp) => {
eprintln!("IERS EOP: HTTP {} from {url}", resp.status());
}
Err(e) => {
eprintln!("IERS EOP: download failed from {url}: {e}");
}
}
}
anyhow::bail!(
"Could not download finals2000A.all from IERS or USNO. \
You can manually download it and place it at: {}",
dst.display()
);
}
#[allow(dead_code)]
pub fn run(data_dir: &Path) -> Result<()> {
let finals_path = ensure_dataset(data_dir)?;
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", data_dir.display());
let rows = parse_finals(&finals_path)?;
eprintln!(
"IERS EOP: parsed {} rows (MJD {:.1}–{:.1})",
rows.len(),
rows.first().unwrap().mjd,
rows.last().unwrap().mjd,
);
let code = generate_rust(&rows)?;
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::write(out_dir.join("iers_eop_data.rs"), code.as_bytes())?;
eprintln!("IERS EOP: iers_eop_data.rs generated");
Ok(())
}
pub fn run_regen(data_dir: &Path, gen_dir: &Path) -> Result<()> {
let finals_path = ensure_dataset(data_dir)?;
let rows = parse_finals(&finals_path)?;
eprintln!(
"IERS EOP: parsed {} rows (MJD {:.1}–{:.1})",
rows.len(),
rows.first().unwrap().mjd,
rows.last().unwrap().mjd,
);
let code = generate_rust(&rows)?;
fs::create_dir_all(gen_dir)?;
fs::write(gen_dir.join("iers_eop_data.rs"), code.as_bytes())?;
eprintln!("IERS EOP: iers_eop_data.rs regenerated in {:?}", gen_dir);
Ok(())
}