#![allow(clippy::unwrap_used, clippy::expect_used)]
use timeglyph::scan;
use timeglyph::{DateStyle, PosixNs, RenderZone, TzSemantics};
#[test]
fn extracts_only_long_numeric_runs() {
let nums = scan::scan_numbers("created=1577836800 id=42 v2 13390845530064940!");
assert_eq!(nums, vec!["1577836800", "13390845530064940"]);
assert!(scan::scan_numbers("only short 42 and 2020 here").is_empty());
}
#[test]
fn captures_fractional_float_as_one_token() {
let nums = scan::scan_numbers("ZMESSAGEDATE=606940977.71577 row");
assert_eq!(nums, vec!["606940977.71577"], "{nums:?}");
}
#[test]
fn trailing_dot_and_dotted_shorts_are_not_floats() {
assert_eq!(
scan::scan_numbers("the value is 1577836800. done"),
vec!["1577836800"]
);
assert!(scan::scan_numbers("ip 192.168.11.100 v1.2.3").is_empty());
}
#[test]
fn readings_for_fractional_float_yields_cocoa_float() {
let r = scan::readings_for("606940977.71577", 5, &RenderZone::Utc);
assert!(
r.iter().any(
|x| x.format_id == "cocoa_float" && x.rendered.starts_with("2020-03-26T18:42:57.7")
),
"{r:?}"
);
}
#[test]
fn inspect_text_surfaces_cocoa_float_for_a_fractional_value() {
let hits = scan::inspect_text("msg 606940977.71577 sent", 5, &RenderZone::Utc);
let hit = hits
.iter()
.find(|h| h.number == "606940977.71577")
.expect("fractional value scanned as one token");
assert!(
hit.readings.iter().any(|r| r.format_id == "cocoa_float"),
"{:?}",
hit.readings
);
}
#[test]
fn readings_are_ranked_in_window_datetimes_utc() {
let r = scan::readings_for("1577836800", 5, &RenderZone::Utc);
assert!(!r.is_empty());
assert!(
r[0].format_id.contains("unix")
&& r[0].rendered.contains("2020-01-01")
&& r[0].rendered.ends_with('Z')
&& !r[0].local,
"{r:?}"
);
}
#[test]
fn display_zone_shifts_utc_anchored_readings() {
let r = scan::readings_for("1577836800", 5, &RenderZone::parse("-05:00").unwrap());
let unix = r.iter().find(|x| x.format_id.contains("unix")).unwrap();
assert!(
unix.rendered.contains("2019-12-31") && unix.rendered.contains("-05:00") && !unix.local,
"{unix:?}"
);
}
#[test]
fn render_in_zone_respects_tz_semantics() {
let inst = PosixNs(1_592_222_400_000_000_000); let native = inst.render(&RenderZone::Utc).unwrap();
let east = RenderZone::parse("-05:00").unwrap();
let (utc, _) = scan::render_in_zone(
TzSemantics::Utc,
inst,
&native,
&RenderZone::Utc,
DateStyle::Iso8601,
);
let (shifted, shifted_local) =
scan::render_in_zone(TzSemantics::Utc, inst, &native, &east, DateStyle::Iso8601);
assert!(utc.ends_with('Z'));
assert!(
shifted.contains("2020-06-15T07:00:00") && shifted.contains("-05:00") && !shifted_local,
"{shifted}"
);
let (a, a_local) = scan::render_in_zone(
TzSemantics::LocalNaive,
inst,
&native,
&RenderZone::Utc,
DateStyle::Iso8601,
);
let (b, _) = scan::render_in_zone(
TzSemantics::LocalNaive,
inst,
&native,
&east,
DateStyle::Iso8601,
);
assert!(a_local, "local-naive must be flagged local");
assert_eq!(a, b, "local-naive must not be shifted by the display zone");
assert!(
a.contains("2020-06-15T12:00:00") && !a.contains('Z') && !a.contains("-05:00"),
"{a}"
);
}
#[test]
fn local_naive_honors_display_style_without_a_fabricated_offset() {
let inst = PosixNs(1_592_222_400_000_000_000); let native = inst.render(&RenderZone::Utc).unwrap();
let (rfc, is_local) = scan::render_in_zone(
TzSemantics::LocalNaive,
inst,
&native,
&RenderZone::Utc,
DateStyle::Rfc2822,
);
assert!(is_local, "still flagged local");
assert_eq!(rfc, "Mon, 15 Jun 2020 12:00:00", "RFC style, no offset");
assert!(!rfc.contains("+0000") && !rfc.contains('Z'), "{rfc}");
let (us, _) = scan::render_in_zone(
TzSemantics::LocalNaive,
inst,
&native,
&RenderZone::Utc,
DateStyle::UsStyle,
);
assert_eq!(us, "06/15/2020 12:00:00 PM", "US style, no zone abbrev");
let (utc_rfc, _) = scan::render_in_zone(
TzSemantics::Utc,
inst,
&native,
&RenderZone::Utc,
DateStyle::Rfc2822,
);
assert!(utc_rfc.contains("+0000"), "UTC RFC keeps offset: {utc_rfc}");
}
#[test]
fn hex_scan_gates_on_in_window_like_the_numeric_path() {
let hits = scan::inspect_text("v 0x0100000000000000 w", 50, &RenderZone::Utc);
let leaked: Vec<&str> = hits
.iter()
.flat_map(|h| &h.readings)
.filter(|r| r.rendered.starts_with("1970") || r.rendered.starts_with("1601"))
.map(|r| r.format_id.as_str())
.collect();
assert!(
leaked.is_empty(),
"out-of-window hex readings leaked into a normal scan: {leaked:?}"
);
let all = scan::inspect_text_opts(
"v 0x0100000000000000 w",
50,
scan::MIN_DIGITS,
true,
&RenderZone::Utc,
DateStyle::Iso8601,
);
assert!(
all.iter()
.flat_map(|h| &h.readings)
.any(|r| r.rendered.starts_with("1970") || r.rendered.starts_with("1601")),
"include_all must keep out-of-window readings"
);
}
#[test]
fn local_naive_out_of_range_reports_the_placeholder() {
let (s, is_local) = scan::render_in_zone(
TzSemantics::LocalNaive,
PosixNs(i128::MAX),
"native",
&RenderZone::Utc,
DateStyle::Iso8601,
);
assert!(is_local);
assert_eq!(s, "<out of civil range>");
}
#[test]
fn render_in_zone_falls_back_to_native_when_out_of_range() {
let (rendered, local) = scan::render_in_zone(
TzSemantics::Utc,
PosixNs(i128::MAX),
"9999-12-31T23:59:59Z",
&RenderZone::Utc,
DateStyle::Iso8601,
);
assert_eq!(rendered, "9999-12-31T23:59:59Z");
assert!(!local);
}
#[test]
fn inspect_text_keeps_only_numbers_with_a_confident_reading() {
let hits = scan::inspect_text(
"the cookie value is 13390845530064940 (chrome)",
5,
&RenderZone::Utc,
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].number, "13390845530064940");
assert!(hits[0]
.readings
.iter()
.any(|r| r.format_id.contains("webkit")));
assert!(scan::inspect_text("00000000000000000000", 5, &RenderZone::Utc).is_empty());
}
#[test]
fn string_datetimes_are_decoded() {
let r = scan::readings_for_string("2025-05-04T15:18:50Z", &RenderZone::Utc);
assert!(r.iter().any(|x| x.format_id.contains("iso8601")), "{r:?}");
}
#[test]
fn inspect_text_catches_embedded_datetime_strings() {
let hits = scan::inspect_text("modified: 2025-05-04T15:18:50Z (ok)", 5, &RenderZone::Utc);
assert!(
hits.iter().any(|h| h.number == "2025-05-04T15:18:50Z"
&& h.readings.iter().any(|r| r.format_id.contains("iso8601"))),
"{hits:?}"
);
}
#[test]
fn inspect_text_decodes_0x_prefixed_hex_token() {
let hits = scan::inspect_text("field = 0xa45a597a here", 8, &RenderZone::Utc);
let hit = hits
.iter()
.find(|h| h.number == "0xa45a597a")
.unwrap_or_else(|| panic!("0x token not decoded: {hits:?}"));
assert!(
hit.readings.iter().any(|r| r.format_id.contains("unix")),
"{hit:?}"
);
}
#[test]
fn inspect_text_decodes_bare_hex_with_letters() {
let hits = scan::inspect_text("val 0060947C58B2D501 x", 5, &RenderZone::Utc);
assert!(
hits.iter().any(|h| h.number == "0060947C58B2D501"),
"expected a hex reading: {hits:?}"
);
}
#[test]
fn inspect_text_decimal_run_still_decodes() {
let hits = scan::inspect_text("created=1577836800 done", 5, &RenderZone::Utc);
assert!(
hits.iter()
.any(|h| h.number == "1577836800"
&& h.readings.iter().any(|r| r.format_id.contains("unix"))),
"{hits:?}"
);
}
#[test]
fn inspect_text_short_hex_word_does_not_explode() {
let hits = scan::inspect_text("the cafe is nice", 5, &RenderZone::Utc);
assert!(
!hits.iter().any(|h| h.number == "cafe"),
"short hex word must not decode: {hits:?}"
);
}
#[test]
fn column_of_timestamps_gets_neighbour_monotonicity_component() {
let text = "ts1=1577836800 ts2=1577923200 ts3=1578009600";
let hits = scan::inspect_text(text, 5, &RenderZone::Utc);
assert_eq!(hits.len(), 3, "expected 3 numeric hits: {hits:?}");
for hit in &hits {
let top = hit.readings.first().expect("at least one reading");
assert!(
top.components
.iter()
.any(|(name, _)| *name == "neighbour_monotonicity"),
"reading for {} missing neighbour_monotonicity component; components: {:?}",
hit.number,
top.components
);
}
}
#[test]
fn single_integer_value_is_unchanged_without_neighbours() {
let lone = "created=1577836800 done";
let hits_inspect = scan::inspect_text(lone, 5, &RenderZone::Utc);
let nr = hits_inspect
.iter()
.find(|h| h.number == "1577836800")
.expect("integer must be found");
let readings_direct = scan::readings_for("1577836800", 5, &RenderZone::Utc);
assert_eq!(
nr.readings.len(),
readings_direct.len(),
"reading count must match"
);
for (a, b) in nr.readings.iter().zip(readings_direct.iter()) {
assert_eq!(
a.format_id, b.format_id,
"format_id mismatch for lone integer"
);
assert!(
(a.score - b.score).abs() < f64::EPSILON,
"score must be identical for lone integer (no context injected): \
inspect={} direct={}",
a.score,
b.score
);
assert_eq!(
a.components, b.components,
"components must be identical for lone integer (no neighbour_monotonicity)"
);
}
}