@@ -31,7 +31,7 @@ impl Epoch {
3131 /// # Warning
3232 /// The time scale of this Epoch will be set to TAI! This is to ensure that no additional computations will change the duration since it's stored in TAI.
3333 /// However, this also means that calling `to_duration()` on this Epoch will return the TAI duration and not the UT1 duration!
34- pub fn from_ut1_duration ( duration : Duration , provider : Ut1Provider ) -> Self {
34+ pub fn from_ut1_duration ( duration : Duration , provider : & Ut1Provider ) -> Self {
3535 let mut e = Self :: from_tai_duration ( duration) ;
3636 // Compute the TAI to UT1 offset at this time.
3737 // We have the time in TAI. But we were given UT1.
@@ -41,26 +41,44 @@ impl Epoch {
4141 e
4242 }
4343
44- /// Get the accumulated offset between this epoch and UT1, assuming that the provider includes all data.
45- pub fn ut1_offset ( & self , provider : Ut1Provider ) -> Option < Duration > {
46- for delta_tai_ut1 in provider. rev ( ) {
47- if self > & delta_tai_ut1. epoch {
48- return Some ( delta_tai_ut1. delta_tai_minus_ut1 ) ;
44+ /// Get the accumulated offset between this epoch and UT1.
45+ /// Assumes the provider's records are sorted by ascending epoch (enforced in `from_eop_data`).
46+ ///
47+ /// Arguments
48+ /// -----------------
49+ /// * `provider`: Borrowed UT1 data source.
50+ ///
51+ /// Return
52+ /// ----------
53+ /// * `Some(Duration)` for the last record with `record.epoch <= self`, otherwise `None`.
54+ pub fn ut1_offset ( & self , provider : & Ut1Provider ) -> Option < Duration > {
55+ let s = provider. as_slice ( ) ;
56+
57+ // Fast-path: very common case — query is after the latest record.
58+ if let Some ( last) = s. last ( ) {
59+ if * self >= last. epoch {
60+ return Some ( last. delta_tai_minus_ut1 ) ;
4961 }
5062 }
51- None
63+
64+ // Find the index of the first element with epoch > self (monotonic predicate)
65+ let idx = s. partition_point ( |r| r. epoch <= * self ) ;
66+
67+ // Candidate is the previous element if any exists
68+ let rec = s. get ( idx. checked_sub ( 1 ) ?) ?;
69+ Some ( rec. delta_tai_minus_ut1 )
5270 }
5371
5472 #[ must_use]
5573 /// Returns this time in a Duration past J1900 counted in UT1
56- pub fn to_ut1_duration ( & self , provider : Ut1Provider ) -> Duration {
74+ pub fn to_ut1_duration ( & self , provider : & Ut1Provider ) -> Duration {
5775 // TAI = UT1 + offset <=> UTC = TAI - offset
5876 self . to_tai_duration ( ) - self . ut1_offset ( provider) . unwrap_or ( Duration :: ZERO )
5977 }
6078
6179 #[ must_use]
6280 /// Returns this time in a Duration past J1900 counted in UT1
63- pub fn to_ut1 ( & self , provider : Ut1Provider ) -> Self {
81+ pub fn to_ut1 ( & self , provider : & Ut1Provider ) -> Self {
6482 Self :: from_tai_duration ( self . to_ut1_duration ( provider) )
6583 }
6684}
@@ -82,6 +100,19 @@ pub struct Ut1Provider {
82100}
83101
84102impl Ut1Provider {
103+ /// Read-only view of the underlying UT1 records.
104+ ///
105+ /// Arguments
106+ /// -----------------
107+ /// * None.
108+ ///
109+ /// Return
110+ /// ----------
111+ /// * A slice `&[DeltaTaiUt1]` over all records.
112+ pub fn as_slice ( & self ) -> & [ DeltaTaiUt1 ] {
113+ & self . data
114+ }
115+
85116 /// Builds a UT1 provided by downloading the data from <https://eop2-external.jpl.nasa.gov/eop2/latest_eop2.short> (short time scale UT1 data) and parsing it.
86117 pub fn download_short_from_jpl ( ) -> Result < Self , HifitimeError > {
87118 Self :: download_from_jpl ( "latest_eop2.short" )
@@ -136,60 +167,98 @@ impl Ut1Provider {
136167 Self :: from_eop_data ( contents)
137168 }
138169
139- /// Builds a UT1 provider from the provided EOP data
170+ /// Builds a UT1 provider from the provided EOP data.
171+ /// Single-pass, no per-line allocation:
172+ /// - Use `split(',')` and take exactly columns 0 and 3 (no `collect()`).
173+ /// - Track sortedness and only sort at the end if needed.
174+ /// - Trim CR/LF and ignore empty lines.
175+ ///
176+ /// Arguments
177+ /// -----------------
178+ /// * `contents`: The full EOP2 text payload from JPL.
179+ ///
180+ /// Return
181+ /// ----------
182+ /// * `Ok(Self)` with records sorted by ascending epoch.
183+ /// * `Err(HifitimeError)` on malformed lines.
184+ ///
185+ /// See also
186+ /// ------------
187+ /// * [`Ut1Provider::from_eop_file`] – File-based variant calling this parser.
140188 pub fn from_eop_data ( contents : String ) -> Result < Self , HifitimeError > {
141189 let mut me = Self :: default ( ) ;
142-
143- let mut ignore = true ;
144- for line in contents. lines ( ) {
145- if line == " EOP2=" {
146- // Data will start after this line
147- ignore = false ;
190+ // Heuristic to reduce Vec reallocations
191+ me. data . reserve ( contents. len ( ) / 48 ) ;
192+
193+ let mut in_data = false ;
194+ let mut prev_epoch: Option < Epoch > = None ;
195+ let mut already_sorted = true ;
196+
197+ for raw in contents. lines ( ) {
198+ // Header section control
199+ if !in_data {
200+ if raw == " EOP2=" || raw == "EOP2=" {
201+ in_data = true ;
202+ }
148203 continue ;
149- } else if line == " $END" {
150- // We've reached the end of the EOP data file.
204+ }
205+ if raw == " $END" || raw == "$END" {
151206 break ;
152207 }
153- if ignore {
208+ if raw . is_empty ( ) {
154209 continue ;
155210 }
156211
157- // We have data of interest!
158- let data: Vec < & str > = line. split ( ',' ) . collect ( ) ;
159- if data. len ( ) < 4 {
160- return Err ( HifitimeError :: Parse {
161- source : ParsingError :: UnknownFormat ,
162- details : "expected EOP line to contain 4 comma-separated columns" ,
163- } ) ;
164- }
212+ // Extract exactly columns 0 and 3 (others ignored)
213+ let mut cols = raw. split ( ',' ) ;
214+ let mjd_col = cols. next ( ) . ok_or_else ( || HifitimeError :: Parse {
215+ source : ParsingError :: UnknownFormat ,
216+ details : "missing MJD column (0)" ,
217+ } ) ?;
218+ let delta_col = cols. nth ( 2 ) . ok_or_else ( || HifitimeError :: Parse {
219+ source : ParsingError :: UnknownFormat ,
220+ details : "missing ΔUT1 column (3)" ,
221+ } ) ?;
165222
166- let mjd_tai_days : f64 = match lexical_core :: parse ( data [ 0 ] . trim ( ) . as_bytes ( ) ) {
167- Ok ( val ) => val ,
168- Err ( err ) => {
169- return Err ( HifitimeError :: Parse {
223+ // Parse numeric fields
224+ let mjd_tai_days : f64 =
225+ lexical_core :: parse ( mjd_col . trim ( ) . as_bytes ( ) ) . map_err ( |err| {
226+ HifitimeError :: Parse {
170227 source : ParsingError :: Lexical { err } ,
171- details : "when parsing MJD TAI days (zeroth column)" ,
172- } )
173- }
174- } ;
228+ details : "when parsing MJD TAI days (column 0)" ,
229+ }
230+ } ) ?;
175231
176- let delta_ut1_ms: f64 ;
177- match lexical_core:: parse ( data[ 3 ] . trim ( ) . as_bytes ( ) ) {
178- Ok ( val) => delta_ut1_ms = val,
179- Err ( err) => {
180- return Err ( HifitimeError :: Parse {
232+ let delta_ut1_ms: f64 =
233+ lexical_core:: parse ( delta_col. trim ( ) . as_bytes ( ) ) . map_err ( |err| {
234+ HifitimeError :: Parse {
181235 source : ParsingError :: Lexical { err } ,
182- details : "when parsing ΔUT1 in ms (last column)" ,
183- } )
236+ details : "when parsing ΔUT1 in ms (column 3)" ,
237+ }
238+ } ) ?;
239+
240+ let epoch = Epoch :: from_mjd_tai ( mjd_tai_days) ;
241+ if let Some ( prev) = prev_epoch {
242+ if epoch < prev {
243+ already_sorted = false ;
184244 }
185245 }
246+ prev_epoch = Some ( epoch) ;
186247
187248 me. data . push ( DeltaTaiUt1 {
188- epoch : Epoch :: from_mjd_tai ( mjd_tai_days ) ,
249+ epoch,
189250 delta_tai_minus_ut1 : delta_ut1_ms * Unit :: Millisecond ,
190251 } ) ;
191252 }
192253
254+ if !already_sorted {
255+ me. data . sort_unstable_by ( |a, b| {
256+ a. epoch
257+ . partial_cmp ( & b. epoch )
258+ . expect ( "Epoch must be orderable (no NaN)" )
259+ } ) ;
260+ }
261+
193262 Ok ( me)
194263 }
195264}
@@ -243,6 +312,15 @@ impl Index<usize> for Ut1Provider {
243312 }
244313}
245314
315+ impl < ' a > IntoIterator for & ' a Ut1Provider {
316+ type Item = & ' a DeltaTaiUt1 ;
317+ type IntoIter = std:: slice:: Iter < ' a , DeltaTaiUt1 > ;
318+
319+ fn into_iter ( self ) -> Self :: IntoIter {
320+ self . data . iter ( )
321+ }
322+ }
323+
246324#[ cfg( kani) ]
247325mod kani_harnesses {
248326 use super :: * ;
0 commit comments