1+ use crate :: ui:: utils:: ByteSpeed ;
2+ use std:: ops:: Add ;
13use std:: { fmt:: Display , time:: Instant } ;
24
3- use crate :: ui:: utils:: ByteSpeed ;
5+ #[ derive( Debug , Clone , PartialEq ) ]
6+ pub struct EstimatedTimeInfo {
7+ secs_left : f64 ,
8+ now : chrono:: DateTime < chrono:: Local > ,
9+ }
410
511#[ derive( Debug , Clone , PartialEq ) ]
612pub enum EstimatedTime {
7- Known ( f64 ) ,
13+ Known ( EstimatedTimeInfo ) ,
814 Unknown ,
915}
1016
@@ -14,9 +20,9 @@ pub struct ByteSeries {
1420 start : Instant ,
1521}
1622
17- impl From < f64 > for EstimatedTime {
18- fn from ( value : f64 ) -> Self {
19- if value. is_finite ( ) {
23+ impl From < EstimatedTimeInfo > for EstimatedTime {
24+ fn from ( value : EstimatedTimeInfo ) -> Self {
25+ if value. secs_left . is_finite ( ) {
2026 Self :: Known ( value)
2127 } else {
2228 Self :: Unknown
@@ -27,7 +33,30 @@ impl From<f64> for EstimatedTime {
2733impl Display for EstimatedTime {
2834 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
2935 match self {
30- EstimatedTime :: Known ( x) => write ! ( f, "{x:.1}s" ) ,
36+ EstimatedTime :: Known ( x) => {
37+ let rounded = x. secs_left . round ( ) ;
38+ let secs = rounded % 60.0 ;
39+ let mins = ( rounded / 60.0 ) % 60.0 ;
40+ let hours = rounded / 3600.0 ;
41+
42+ let completion_time = x. now . add ( chrono:: TimeDelta :: seconds ( rounded as i64 ) ) ;
43+
44+ // Show the whole date only if completion time is at least a day ahead of now
45+ let completion_time_format = if hours >= 24.0 {
46+ "%Y-%m-%d %H:%M:%S"
47+ } else {
48+ "%H:%M:%S"
49+ } ;
50+
51+ write ! (
52+ f,
53+ "{:0>2}:{:0>2}:{:0>2} (complete at {})" ,
54+ hours as u64 ,
55+ mins as u8 ,
56+ secs as u8 ,
57+ completion_time. format( completion_time_format)
58+ )
59+ }
3160 EstimatedTime :: Unknown => write ! ( f, "[unknown]" ) ,
3261 }
3362 }
@@ -66,7 +95,10 @@ impl ByteSeries {
6695 // than total bytes, due to the nature of block writing.
6796 let bytes_left = total_bytes. saturating_sub ( self . bytes_encountered ( ) ) ;
6897 let secs_left = bytes_left as f64 / speed;
69- EstimatedTime :: from ( secs_left)
98+ EstimatedTime :: from ( EstimatedTimeInfo {
99+ secs_left,
100+ now : chrono:: Local :: now ( ) ,
101+ } )
70102 }
71103
72104 pub fn start ( & self ) -> Instant {
@@ -138,9 +170,10 @@ impl ByteSeries {
138170mod tests {
139171 use std:: time:: { Duration , Instant } ;
140172
173+ use super :: EstimatedTime ;
174+ use super :: { ByteSeries , EstimatedTimeInfo } ;
141175 use approx:: assert_relative_eq;
142-
143- use super :: ByteSeries ;
176+ use chrono:: { Local , TimeZone , Utc } ;
144177 use test_case:: test_case;
145178
146179 fn example_2s ( ) -> ByteSeries {
@@ -175,4 +208,16 @@ mod tests {
175208 let actual = example_2s ( ) . interp ( t) ;
176209 assert_relative_eq ! ( actual, expected) ;
177210 }
211+
212+ #[ test_case( f64 :: INFINITY , "[unknown]" ; "non finite" ) ]
213+ #[ test_case( 39_562.0 , "10:59:22 (complete at 20:59:27)" ; "less than a day" ) ]
214+ #[ test_case( 86_400.0 , "24:00:00 (complete at 2025-10-22 10:00:05)" ; "exactly a day" ) ]
215+ #[ test_case( 133_800.0 , "37:10:00 (complete at 2025-10-22 23:10:05)" ; "more than a day" ) ]
216+ #[ test_case( 60.5 , "00:01:01 (complete at 10:01:06)" ; "round decimals up" ) ]
217+ #[ test_case( 59.4 , "00:00:59 (complete at 10:01:04)" ; "round decimals down" ) ]
218+ fn estimated_time_display ( secs_left : f64 , expected : & str ) {
219+ let now = Local . with_ymd_and_hms ( 2025 , 10 , 21 , 10 , 0 , 5 ) . unwrap ( ) ;
220+ let actual = EstimatedTime :: from ( EstimatedTimeInfo { secs_left, now } ) . to_string ( ) ;
221+ assert_eq ! ( expected, actual) ;
222+ }
178223}
0 commit comments