use std::{
collections::BTreeMap,
env,
fs::{self, File},
io::{BufRead, BufReader},
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use regex::Regex;
use std::fmt::Write;
#[derive(Debug)]
struct Entry {
ints: Vec<i64>,
floats: Vec<String>, }
fn file_format(key: &str) -> Option<(usize, usize)> {
let idx: u32 = key.trim_start_matches("ELP").parse().ok()?;
match idx {
1..=3 => Some((4, 7)),
10..=21 => Some((11, 3)),
4..=9 | 22..=36 => Some((5, 3)),
_ => None,
}
}
fn parse_line(line: &str, n_ints: usize, n_floats: usize, token_re: &Regex) -> Result<Entry> {
let tokens: Vec<&str> = token_re.find_iter(line).map(|m| m.as_str()).collect();
if tokens.len() != n_ints + n_floats {
anyhow::bail!(
"Unexpected column count {} (wanted {})",
tokens.len(),
n_ints + n_floats
);
}
let ints = tokens[..n_ints]
.iter()
.map(|t| t.parse::<i64>().map_err(|e| anyhow::anyhow!(e)))
.collect::<Result<Vec<_>>>()?;
let floats = tokens[n_ints..].iter().map(|t| t.to_string()).collect();
Ok(Entry { ints, floats })
}
fn parse_file(path: &Path, n_ints: usize, n_floats: usize) -> Result<Vec<Entry>> {
let file = File::open(path).with_context(|| format!("open {path:?}"))?;
let reader = BufReader::new(file);
let line_re = Regex::new(r"^\s*[-+]?\d").unwrap();
let token_re = Regex::new(r"[-+]?\d+\.\d+|[-+]?\d+").unwrap();
reader
.lines()
.map_while(Result::ok)
.filter(|l| line_re.is_match(l))
.map(|l| parse_line(&l, n_ints, n_floats, &token_re))
.collect()
}
fn parse_all_elps(dir: &Path) -> Result<BTreeMap<String, Vec<Entry>>> {
let mut map = BTreeMap::new();
let mut paths: Vec<_> = fs::read_dir(dir)
.with_context(|| format!("read-dir {dir:?}"))?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|s| s.starts_with("ELP"))
.unwrap_or(false)
})
.collect();
paths.sort();
for path in paths {
let key = path.file_stem().unwrap().to_string_lossy().to_uppercase();
if let Some((n_ints, n_floats)) = file_format(&key) {
let entries =
parse_file(&path, n_ints, n_floats).with_context(|| format!("Parsing {key}"))?;
map.insert(key, entries);
} else {
println!("cargo:warning=Skipping {key}: unknown format");
}
}
Ok(map)
}
fn generate_rust(data: &BTreeMap<String, Vec<Entry>>) -> Result<String> {
let mut out = String::new();
writeln!(
out,
"// ---------------------------------------------------\n"
)?;
writeln!(
out,
"// **AUTOGENERATED** by build.rs – DO NOT EDIT BY HAND\n"
)?;
writeln!(
out,
"// ---------------------------------------------------\n"
)?;
writeln!(out, "use crate::calculus::elp2000::elp_structs::*;\n")?;
for (name, entries) in data {
let idx: u32 = name.trim_start_matches("ELP").parse().unwrap();
let typ = match idx {
1..=3 => "MainProblem",
4..=9 | 22..=36 => "EarthPert",
10..=21 => "PlanetPert",
_ => continue,
};
writeln!(out, "pub static {name}: &[{typ}] = &[")?;
for e in entries {
match typ {
"MainProblem" => {
let ilu = e
.ints
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");
let a = &e.floats[0];
let b = e.floats[1..].join(", ");
writeln!(out, " {typ} {{ ilu: [{ilu}], a: {a}, b: [{b}] }},")?;
}
"EarthPert" => {
let iz = format!("{}{}", e.ints[0], ".0"); let ilu = e.ints[1..5]
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");
let (o, a, p) = (&e.floats[0], &e.floats[1], &e.floats[2]);
writeln!(
out,
" {typ} {{ iz: {iz}, ilu: [{ilu}], o: {o}, a: {a}, p: {p} }},"
)?;
}
"PlanetPert" => {
let ipla = e
.ints
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");
let (theta, o, p) = (&e.floats[0], &e.floats[1], &e.floats[2]);
writeln!(
out,
" {typ} {{ ipla: [{ipla}], theta: {theta}, o: {o}, p: {p} }},"
)?;
}
_ => unreachable!(),
}
}
writeln!(out, "];\n")?;
}
Ok(out)
}
fn ensure_dataset(dir: &Path) -> Result<()> {
use reqwest::blocking::Client;
fs::create_dir_all(dir)?;
let base = "https://cdsarc.cds.unistra.fr/ftp/VI/79/";
let client = Client::builder()
.user_agent("ELP build script (rust)")
.build()?;
for n in 1..=36 {
let name = format!("ELP{n}");
let path = dir.join(&name);
if path.exists() {
continue;
}
let url = format!("{base}/{name}");
println!("cargo:info=Downloading {url}");
let bytes = client
.get(&url)
.send()
.with_context(|| format!("GET {url}"))?
.error_for_status()?
.bytes()?;
fs::write(&path, &bytes).with_context(|| format!("write {path:?}"))?;
}
Ok(())
}
#[allow(dead_code)]
pub fn run(data_dir: &Path) -> Result<()> {
ensure_dataset(data_dir)?;
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", data_dir.display());
let parsed = parse_all_elps(data_dir)?;
let code = generate_rust(&parsed)?;
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::write(out_dir.join("elp_data.rs"), code.as_bytes())?;
println!("cargo:info=elp_data.rs generated");
Ok(())
}
pub fn run_regen(data_dir: &Path, gen_dir: &Path) -> Result<()> {
ensure_dataset(data_dir)?;
let parsed = parse_all_elps(data_dir)?;
let code = generate_rust(&parsed)?;
fs::create_dir_all(gen_dir)?;
fs::write(gen_dir.join("elp_data.rs"), code.as_bytes())?;
Ok(())
}