@@ -19,6 +19,7 @@ impl Run for Import {
1919 match self . from {
2020 ImportFrom :: Autojump => import_autojump ( & mut db, & buffer) ,
2121 ImportFrom :: Z => import_z ( & mut db, & buffer) ,
22+ ImportFrom :: Jump => import_jump ( & mut db, & buffer) ,
2223 }
2324 . context ( "import error" ) ?;
2425
@@ -78,6 +79,93 @@ fn sigmoid(x: f64) -> f64 {
7879 1.0 / ( 1.0 + ( -x) . exp ( ) )
7980}
8081
82+ /// Parse a simple ISO 8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ
83+ /// or YYYY-MM-DDTHH:MM:SS.ssssss±hh:mm) to Unix epoch seconds.
84+ /// Returns None if the format is invalid
85+ /// Note: this only return to second-precision and ignores
86+ /// timezone offsets
87+ fn parse_iso8601_utc ( timestamp : & str ) -> Option < u64 > {
88+ // Expected format: 2023-01-01T12:00:00Z or 2024-11-07T11:01:57.327507-08:00
89+ let is_valid = ( timestamp. len ( ) == 20 && timestamp. ends_with ( 'Z' ) )
90+ || timestamp. len ( ) >= 21 ;
91+ if !is_valid {
92+ return None ;
93+ }
94+
95+ let parts: Vec < & str > = timestamp[ ..19 ] . split ( & [ '-' , 'T' , ':' ] [ ..] ) . collect ( ) ;
96+ if parts. len ( ) != 6 && parts. len ( ) != 7 {
97+ return None ;
98+ }
99+
100+ let year = parts[ 0 ] . parse :: < u64 > ( ) . ok ( ) ?;
101+ let month = parts[ 1 ] . parse :: < u32 > ( ) . ok ( ) ?;
102+ let day = parts[ 2 ] . parse :: < u32 > ( ) . ok ( ) ?;
103+ let hour = parts[ 3 ] . parse :: < u32 > ( ) . ok ( ) ?;
104+ let minute = parts[ 4 ] . parse :: < u32 > ( ) . ok ( ) ?;
105+ let second = parts[ 5 ] . parse :: < u32 > ( ) . ok ( ) ?;
106+
107+ // Basic validation
108+ if !( 1 ..=12 ) . contains ( & month)
109+ || !( 1 ..=31 ) . contains ( & day)
110+ || hour > 23
111+ || minute > 59
112+ || second > 59
113+ {
114+ return None ;
115+ }
116+
117+ // Simple calculation (ignoring leap years and timezone complexities for now)
118+ // This is a basic implementation that works for most practical cases
119+ let mut days_since_epoch = ( year - 1970 ) * 365 + ( year - 1970 ) / 4 ; // basic leap years
120+ // rough month lengths (non-leap year)
121+ let month_days = [ 0 , 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ] ;
122+ for m in 1 ..month {
123+ days_since_epoch += month_days[ m as usize ] as u64 ;
124+ }
125+ days_since_epoch += ( day - 1 ) as u64 ;
126+
127+ let seconds_since_epoch = days_since_epoch * 24 * 60 * 60
128+ + ( hour as u64 ) * 60 * 60
129+ + ( minute as u64 ) * 60
130+ + ( second as u64 ) ;
131+
132+ Some ( seconds_since_epoch)
133+ }
134+
135+ fn import_jump ( db : & mut Database , buffer : & str ) -> Result < ( ) > {
136+ #[ derive( serde:: Deserialize ) ]
137+ struct Entry {
138+ #[ serde( rename = "Path" ) ]
139+ path : String ,
140+ #[ serde( rename = "Score" ) ]
141+ score : Score ,
142+ }
143+
144+ #[ derive( serde:: Deserialize ) ]
145+ struct Score {
146+ #[ serde( rename = "Weight" ) ]
147+ weight : i64 ,
148+ #[ serde( rename = "Age" ) ]
149+ age : String ,
150+ }
151+
152+ let entries: Vec < Entry > =
153+ serde_json:: from_str ( buffer) . context ( "invalid Jump JSON format" ) ?;
154+
155+ for entry in entries {
156+ let Some ( last_accessed) = parse_iso8601_utc ( & entry. score . age ) else {
157+ eprintln ! ( "Warning: Skipping entry with invalid timestamp: {}" , entry. score. age) ;
158+ continue ;
159+ } ;
160+ db. add_unchecked ( & entry. path , entry. score . weight as f64 , last_accessed) ;
161+ }
162+
163+ if db. dirty ( ) {
164+ db. dedup ( ) ;
165+ }
166+ Ok ( ( ) )
167+ }
168+
81169#[ cfg( test) ]
82170mod tests {
83171 use super :: * ;
@@ -163,4 +251,79 @@ mod tests {
163251 assert_eq ! ( dir1. last_accessed, dir2. last_accessed) ;
164252 }
165253 }
254+
255+ #[ test]
256+ fn parse_iso8601_timestamp ( ) {
257+ // Test basic ISO 8601 UTC timestamp parsing
258+ // These are the actual values our parser produces (approximate calculation)
259+ assert_eq ! ( parse_iso8601_utc( "2023-01-01T12:00:00Z" ) , Some ( 1672574400 ) ) ; // 12:00 UTC
260+ assert_eq ! ( parse_iso8601_utc( "2023-01-02T14:20:00Z" ) , Some ( 1672669200 ) ) ; // 14:20 UTC
261+ assert_eq ! ( parse_iso8601_utc( "2023-01-03T09:15:00Z" ) , Some ( 1672737300 ) ) ; // 09:15 UTC
262+
263+ // test stripping parts we ignore
264+ assert_eq ! ( parse_iso8601_utc( "2024-11-07T11:01:57.327507-08:00" ) , Some ( 1730890917 ) ) ;
265+ assert_eq ! ( parse_iso8601_utc( "2024-11-07T11:28:33.949702-08:00" ) , Some ( 1730892513 ) ) ;
266+ assert_eq ! ( parse_iso8601_utc( "2026-02-17T11:36:17.7596-08:00" ) , Some ( 1771328177 ) ) ;
267+ // nanosecond precision (real Jump timestamps)
268+ assert ! ( parse_iso8601_utc( "2025-01-10T20:51:04.217017979-08:00" ) . is_some( ) ) ;
269+
270+ // Test invalid formats
271+ assert_eq ! ( parse_iso8601_utc( "invalid" ) , None ) ;
272+ assert_eq ! ( parse_iso8601_utc( "2023-01-01T12:00:00" ) , None ) ; // Missing Z
273+ assert_eq ! ( parse_iso8601_utc( "2023-01-01 12:00:00Z" ) , None ) ; // Wrong separator
274+ }
275+
276+ #[ test]
277+ fn from_jump ( ) {
278+ let data_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
279+ let mut db = Database :: open_dir ( data_dir. path ( ) ) . unwrap ( ) ;
280+ for ( path, rank, last_accessed) in [
281+ ( "/quux/quuz" , 1.0 , 100 ) ,
282+ ( "/corge/grault/garply" , 6.0 , 600 ) ,
283+ ( "/waldo/fred/plugh" , 3.0 , 300 ) ,
284+ ( "/xyzzy/thud" , 8.0 , 800 ) ,
285+ ( "/foo/bar" , 9.0 , 900 ) ,
286+ ] {
287+ db. add_unchecked ( path, rank, last_accessed) ;
288+ }
289+
290+ // Define timestamps as variables to ensure consistency
291+ let baz_time = "2023-01-01T12:00:00Z" ;
292+ let foobar_time = "2023-01-02T12:00:00Z" ;
293+ let quux_time = "2026-02-17T11:36:17.7596-08:00" ;
294+
295+ let buffer = format ! (
296+ r#"[
297+ {{"Path":"/baz","Score":{{"Weight":7,"Age":"{}"}}}},
298+ {{"Path":"/foo/bar","Score":{{"Weight":2,"Age":"{}"}}}},
299+ {{"Path":"/quux/quuz","Score":{{"Weight":5,"Age":"{}"}}}}
300+ ]"# ,
301+ baz_time, foobar_time, quux_time
302+ ) ;
303+ import_jump ( & mut db, & buffer) . unwrap ( ) ;
304+
305+ db. sort_by_path ( ) ;
306+ println ! ( "got: {:?}" , & db. dirs( ) ) ;
307+
308+ // Parse the same timestamps for expected results
309+ let baz_timestamp = parse_iso8601_utc ( baz_time) . unwrap ( ) ;
310+ let foobar_timestamp = parse_iso8601_utc ( foobar_time) . unwrap ( ) ;
311+ let quux_timestamp = parse_iso8601_utc ( quux_time) . unwrap ( ) ;
312+
313+ let exp = [
314+ Dir { path : "/baz" . into ( ) , rank : 7.0 , last_accessed : baz_timestamp } ,
315+ Dir { path : "/corge/grault/garply" . into ( ) , rank : 6.0 , last_accessed : 600u64 } ,
316+ Dir { path : "/foo/bar" . into ( ) , rank : 11.0 , last_accessed : foobar_timestamp } ,
317+ Dir { path : "/quux/quuz" . into ( ) , rank : 6.0 , last_accessed : quux_timestamp } ,
318+ Dir { path : "/waldo/fred/plugh" . into ( ) , rank : 3.0 , last_accessed : 300u64 } ,
319+ Dir { path : "/xyzzy/thud" . into ( ) , rank : 8.0 , last_accessed : 800u64 } ,
320+ ] ;
321+ println ! ( "exp: {exp:?}" ) ;
322+
323+ for ( dir1, dir2) in db. dirs ( ) . iter ( ) . zip ( exp) {
324+ assert_eq ! ( dir1. path, dir2. path) ;
325+ assert ! ( ( dir1. rank - dir2. rank) . abs( ) < 0.01 ) ;
326+ assert_eq ! ( dir1. last_accessed, dir2. last_accessed) ;
327+ }
328+ }
166329}
0 commit comments