#[allow(clippy::cast_possible_wrap)]
#[must_use]
pub fn latency_ns(exchange_ms_of_day: i32, event_date: i32, received_at_ns: u64) -> i64 {
let exchange_epoch_ns = exchange_epoch_ns(exchange_ms_of_day, event_date);
received_at_ns as i64 - exchange_epoch_ns
}
#[allow(clippy::cast_sign_loss)]
fn exchange_epoch_ns(ms_of_day: i32, date_yyyymmdd: i32) -> i64 {
let year = date_yyyymmdd / 10_000;
let month = ((date_yyyymmdd % 10_000) / 100) as u32;
let day = (date_yyyymmdd % 100) as u32;
let days = civil_to_epoch_days(year, month, day);
let midnight_utc_ms = days * 86_400_000;
let approx_utc_ms = midnight_utc_ms + i64::from(ms_of_day) + 5 * 3_600 * 1_000; let offset_ms = eastern_offset_ms(approx_utc_ms as u64);
let exchange_epoch_ms = midnight_utc_ms + i64::from(ms_of_day) - offset_ms;
exchange_epoch_ms * 1_000_000
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn civil_to_epoch_days(year: i32, month: u32, day: u32) -> i64 {
let y = if month <= 2 {
i64::from(year) - 1
} else {
i64::from(year)
};
let m = if month <= 2 {
i64::from(month) + 9
} else {
i64::from(month) - 3
};
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let doy = (153 * m as u64 + 2) / 5 + u64::from(day) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe as i64 - 719_468
}
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
fn eastern_offset_ms(epoch_ms: u64) -> i64 {
let epoch_secs = epoch_ms as i64 / 1_000;
let days_since_epoch = epoch_secs / 86_400;
let z = days_since_epoch + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let year = yoe as i32 + (era * 400) as i32;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { year + 1 } else { year };
let dst_start_utc = march_second_sunday_utc(year);
let dst_end_utc = november_first_sunday_utc(year);
let epoch_ms_i64 = epoch_ms as i64;
if epoch_ms_i64 >= dst_start_utc && epoch_ms_i64 < dst_end_utc {
-4 * 3_600 * 1_000 } else {
-5 * 3_600 * 1_000 }
}
fn march_second_sunday_utc(year: i32) -> i64 {
let mar1 = civil_to_epoch_days(year, 3, 1);
let dow = ((mar1 + 3) % 7 + 7) % 7; let days_to_first_sunday = (6 - dow + 7) % 7;
let second_sunday = mar1 + days_to_first_sunday + 7;
second_sunday * 86_400_000 + 7 * 3_600 * 1_000
}
fn november_first_sunday_utc(year: i32) -> i64 {
let nov1 = civil_to_epoch_days(year, 11, 1);
let dow = ((nov1 + 3) % 7 + 7) % 7;
let days_to_first_sunday = (6 - dow + 7) % 7;
let first_sunday = nov1 + days_to_first_sunday;
first_sunday * 86_400_000 + 6 * 3_600 * 1_000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latency_basic_est() {
let days = civil_to_epoch_days(2024, 1, 15);
let midnight_utc_ns = days * 86_400_000 * 1_000_000;
let utc_offset_ns = (14 * 3600 + 30 * 60) as i64 * 1_000_000_000;
let received_at_ns = (midnight_utc_ns + utc_offset_ns) as u64;
let lat = latency_ns(34_200_000, 20240115, received_at_ns);
assert!(
lat.abs() < 1_000_000, "expected ~0 latency, got {lat} ns"
);
}
#[test]
fn latency_basic_edt() {
let days = civil_to_epoch_days(2024, 6, 15);
let midnight_utc_ns = days * 86_400_000 * 1_000_000;
let utc_offset_ns = (13 * 3600 + 30 * 60) as i64 * 1_000_000_000;
let received_at_ns = (midnight_utc_ns + utc_offset_ns) as u64;
let lat = latency_ns(34_200_000, 20240615, received_at_ns);
assert!(lat.abs() < 1_000_000, "expected ~0 latency, got {lat} ns");
}
#[test]
fn latency_positive_when_received_later() {
let days = civil_to_epoch_days(2024, 1, 15);
let midnight_utc_ns = days * 86_400_000 * 1_000_000;
let utc_offset_ns = (14 * 3600 + 30 * 60) as i64 * 1_000_000_000;
let received_at_ns = (midnight_utc_ns + utc_offset_ns) as u64 + 100_000_000;
let lat = latency_ns(34_200_000, 20240115, received_at_ns);
assert!(
(lat - 100_000_000).abs() < 1_000_000,
"expected ~100ms latency, got {lat} ns"
);
}
#[test]
fn latency_negative_when_clock_skewed() {
let days = civil_to_epoch_days(2024, 1, 15);
let midnight_utc_ns = days * 86_400_000 * 1_000_000;
let utc_offset_ns = (14 * 3600 + 30 * 60) as i64 * 1_000_000_000;
let received_at_ns = (midnight_utc_ns + utc_offset_ns) as u64 - 50_000_000;
let lat = latency_ns(34_200_000, 20240115, received_at_ns);
assert!(
(lat + 50_000_000).abs() < 1_000_000,
"expected ~-50ms latency, got {lat} ns"
);
}
}