#[cfg(feature = "python")]
use pyo3::prelude::*;
use std::{fs::File, io::Read, path::Path};
use core::ops::Index;
use crate::{
leap_seconds::{LeapSecond, LeapSecondProvider},
HifitimeError, ParsingError,
};
#[repr(C)]
#[cfg_attr(feature = "python", pyclass)]
#[derive(Clone, Debug, Default)]
pub struct LeapSecondsFile {
pub data: Vec<LeapSecond>,
iter_pos: usize,
}
impl LeapSecondsFile {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, HifitimeError> {
let mut f = match File::open(path) {
Ok(f) => f,
Err(e) => {
return Err(HifitimeError::Parse {
source: ParsingError::InOut { err: e.kind() },
details: "opening leap seconds file",
})
}
};
let mut contents = String::new();
if let Err(e) = f.read_to_string(&mut contents) {
return Err(HifitimeError::Parse {
source: ParsingError::InOut { err: e.kind() },
details: "reading leap seconds file",
});
}
Self::from_content(contents)
}
pub fn from_content(contents: String) -> Result<Self, HifitimeError> {
let mut me = Self::default();
for line in contents.lines() {
if let Some(first_char) = line.chars().next() {
if first_char == '#' {
continue;
} else {
let data: Vec<&str> = line.split_whitespace().collect();
if data.len() < 2 {
return Err(HifitimeError::Parse {
source: ParsingError::UnknownFormat,
details: "leap seconds file should have two columns exactly",
});
}
let timestamp_tai_s: u64 = match lexical_core::parse(data[0].as_bytes()) {
Ok(val) => val,
Err(_) => {
return Err(HifitimeError::Parse {
source: ParsingError::ValueError,
details: "first column value is not numeric",
})
}
};
let delta_at: u8 = match lexical_core::parse(data[1].as_bytes()) {
Ok(val) => val,
Err(_) => {
return Err(HifitimeError::Parse {
source: ParsingError::ValueError,
details: "second column value is not numeric",
})
}
};
me.data.push(LeapSecond {
timestamp_tai_s: (timestamp_tai_s as f64),
delta_at: (delta_at as f64),
announced_by_iers: true,
});
}
}
}
Ok(me)
}
#[cfg(feature = "lts")]
pub fn from_url(url: &str) -> Result<Self, HifitimeError> {
use ureq::get;
use ureq::Error;
match get(url).call() {
Ok(resp) => {
let Ok(response) = resp.into_body().read_to_string() else {
return Err(HifitimeError::Parse {
source: ParsingError::UnknownFormat,
details: "when reading contents",
});
};
Self::from_content(response)
}
Err(Error::StatusCode(code)) => Err(HifitimeError::Parse {
source: ParsingError::DownloadError { code },
details: "server returned an error",
}),
Err(_) => Err(HifitimeError::Parse {
source: ParsingError::UnknownFormat,
details: "could not parse response as leap second list",
}),
}
}
#[cfg(feature = "lts")]
pub fn from_iana() -> Result<Self, HifitimeError> {
Self::from_url("https://data.iana.org/time-zones/data/leap-seconds.list")
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl LeapSecondsFile {
#[new]
pub fn __new__(path: String) -> Result<Self, HifitimeError> {
Self::from_path(&path)
}
fn __repr__(&self) -> String {
format!("{self:?} @ {self:p}")
}
}
impl Iterator for LeapSecondsFile {
type Item = LeapSecond;
fn next(&mut self) -> Option<Self::Item> {
self.iter_pos += 1;
self.data.get(self.iter_pos - 1).copied()
}
}
impl DoubleEndedIterator for LeapSecondsFile {
fn next_back(&mut self) -> Option<Self::Item> {
if self.iter_pos == self.data.len() {
None
} else {
self.iter_pos += 1;
self.data.get(self.data.len() - self.iter_pos).copied()
}
}
}
impl Index<usize> for LeapSecondsFile {
type Output = LeapSecond;
fn index(&self, index: usize) -> &Self::Output {
self.data.index(index)
}
}
impl LeapSecondProvider for LeapSecondsFile {}
#[test]
fn leap_second_fetch() {
use crate::leap_seconds::LatestLeapSeconds;
use std::env;
use std::path::PathBuf;
let latest_leap_seconds = LatestLeapSeconds::default();
let path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.join("data")
.join("leap-seconds.list");
let leap_seconds = LeapSecondsFile::from_path(path.to_str().unwrap()).unwrap();
assert_eq!(
leap_seconds[0],
LeapSecond::new(2_272_060_800.0, 10.0, true),
);
assert_eq!(
leap_seconds[27],
LeapSecond::new(3_692_217_600.0, 37.0, true)
);
for (lsi, leap_second) in leap_seconds.enumerate() {
assert_eq!(leap_second, latest_leap_seconds[lsi + 14]);
}
}