Skip to content

Commit 7a31792

Browse files
authored
feat(fetchers): enhance HackerNewsFetcher with timestamp display (#90)
## What Enhance HackerNewsFetcher with timestamp display and additional tests. ## Why Closes #58 — The HN Firebase API returns a `time` field (Unix timestamp) that was not being deserialized or displayed. This adds time information to the structured output. ## How - Added `time` field to HNItem deserialization - Implemented `format_unix_timestamp()` for epoch-to-ISO8601 conversion (no external deps) - Display formatted time in item metadata - Added tests for: timestamp formatting, comment rendering, Ask HN text posts ## Risk - Low - Additive change only, no existing behavior modified ### Checklist - [x] Unit tests are passed - [x] Smoke tests are passed - [x] Specs are up to date and not in conflict
1 parent 9ce3234 commit 7a31792

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

crates/fetchkit/src/fetchers/hackernews.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ struct HNItem {
6363
url: Option<String>,
6464
by: Option<String>,
6565
score: Option<i64>,
66+
time: Option<u64>,
6667
descendants: Option<u64>,
6768
kids: Option<Vec<u64>>,
6869
}
@@ -190,6 +191,9 @@ fn format_hn_response(item: &HNItem, comments: &[(HNItem, Vec<HNItem>)]) -> Stri
190191
if let Some(score) = item.score {
191192
out.push_str(&format!("- **Score:** {}\n", score));
192193
}
194+
if let Some(time) = item.time {
195+
out.push_str(&format!("- **Time:** {}\n", format_unix_timestamp(time)));
196+
}
193197
if let Some(descendants) = item.descendants {
194198
out.push_str(&format!("- **Comments:** {}\n", descendants));
195199
}
@@ -228,6 +232,62 @@ fn format_hn_response(item: &HNItem, comments: &[(HNItem, Vec<HNItem>)]) -> Stri
228232
out
229233
}
230234

235+
/// Format a Unix timestamp as an ISO 8601 UTC date-time string
236+
fn format_unix_timestamp(ts: u64) -> String {
237+
let secs = ts % 60;
238+
let mins = (ts / 60) % 60;
239+
let hours = (ts / 3600) % 24;
240+
241+
// Days since epoch
242+
let mut days = (ts / 86400) as i64;
243+
244+
// Calculate year/month/day from days since epoch
245+
let mut year = 1970i64;
246+
loop {
247+
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
248+
if days < days_in_year {
249+
break;
250+
}
251+
days -= days_in_year;
252+
year += 1;
253+
}
254+
255+
let leap = is_leap_year(year);
256+
let days_in_months: [i64; 12] = [
257+
31,
258+
if leap { 29 } else { 28 },
259+
31,
260+
30,
261+
31,
262+
30,
263+
31,
264+
31,
265+
30,
266+
31,
267+
30,
268+
31,
269+
];
270+
271+
let mut month = 0;
272+
for (i, &dim) in days_in_months.iter().enumerate() {
273+
if days < dim {
274+
month = i + 1;
275+
break;
276+
}
277+
days -= dim;
278+
}
279+
let day = days + 1;
280+
281+
format!(
282+
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
283+
year, month, day, hours, mins, secs
284+
)
285+
}
286+
287+
fn is_leap_year(year: i64) -> bool {
288+
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
289+
}
290+
231291
fn format_comment(out: &mut String, comment: &HNItem, depth: usize) {
232292
let indent = "> ".repeat(depth);
233293
let by = comment.by.as_deref().unwrap_or("anonymous");
@@ -333,6 +393,7 @@ mod tests {
333393
url: Some("https://example.com".to_string()),
334394
by: Some("pg".to_string()),
335395
score: Some(100),
396+
time: Some(1704067200), // 2024-01-01T00:00:00Z
336397
descendants: Some(5),
337398
kids: None,
338399
};
@@ -342,6 +403,67 @@ mod tests {
342403
assert!(output.contains("# Show HN: My Project"));
343404
assert!(output.contains("**By:** pg"));
344405
assert!(output.contains("**Score:** 100"));
406+
assert!(output.contains("**Time:** 2024-01-01T00:00:00Z"));
345407
assert!(output.contains("https://example.com"));
346408
}
409+
410+
#[test]
411+
fn test_format_hn_response_with_comments() {
412+
let item = HNItem {
413+
id: 42,
414+
item_type: Some("story".to_string()),
415+
title: Some("Test Story".to_string()),
416+
text: None,
417+
url: None,
418+
by: Some("user1".to_string()),
419+
score: Some(50),
420+
time: None,
421+
descendants: Some(2),
422+
kids: None,
423+
};
424+
425+
let comment = HNItem {
426+
id: 43,
427+
item_type: Some("comment".to_string()),
428+
title: None,
429+
text: Some("Great post!".to_string()),
430+
url: None,
431+
by: Some("user2".to_string()),
432+
score: None,
433+
time: None,
434+
descendants: None,
435+
kids: None,
436+
};
437+
438+
let output = format_hn_response(&item, &[(comment, vec![])]);
439+
assert!(output.contains("## Comments"));
440+
assert!(output.contains("**user2**"));
441+
assert!(output.contains("Great post!"));
442+
}
443+
444+
#[test]
445+
fn test_format_hn_response_ask_hn() {
446+
let item = HNItem {
447+
id: 100,
448+
item_type: Some("story".to_string()),
449+
title: Some("Ask HN: Best Rust crates?".to_string()),
450+
text: Some("<p>Looking for recommendations.</p>".to_string()),
451+
url: None,
452+
by: Some("asker".to_string()),
453+
score: Some(25),
454+
time: None,
455+
descendants: Some(0),
456+
kids: None,
457+
};
458+
459+
let output = format_hn_response(&item, &[]);
460+
assert!(output.contains("Ask HN: Best Rust crates?"));
461+
assert!(output.contains("Looking for recommendations."));
462+
}
463+
464+
#[test]
465+
fn test_format_unix_timestamp() {
466+
assert_eq!(format_unix_timestamp(0), "1970-01-01T00:00:00Z");
467+
assert_eq!(format_unix_timestamp(1704067200), "2024-01-01T00:00:00Z");
468+
}
347469
}

0 commit comments

Comments
 (0)