use std::collections::HashMap;
use nodedb_types::value::Value;
use crate::index::sorted_def::{WindowDef, WindowKind};
use super::keys::{FIELD_LEN, complement, score_prefix};
const MS_PER_DAY: u64 = 86_400_000;
pub(super) fn windowed_score_key(
index_name: &str,
score: &[u8; FIELD_LEN],
ts: &[u8; FIELD_LEN],
pk: &[u8],
) -> Vec<u8> {
let mut key = score_prefix(index_name);
key.extend_from_slice(score);
key.extend_from_slice(ts);
key.extend_from_slice(pk);
key
}
pub(super) fn extract_ts_from_value(columns: &HashMap<String, Value>, column: &str) -> Option<u64> {
match columns.get(column)? {
Value::Integer(n) => u64::try_from(*n).ok(),
Value::Float(f) if *f >= 0.0 && f.is_finite() => Some(*f as u64),
_ => None,
}
}
pub(super) fn encode_ts(ts_ms: u64, desc: bool) -> [u8; FIELD_LEN] {
let bytes = ts_ms.to_be_bytes();
if desc { complement(bytes) } else { bytes }
}
pub(super) fn decode_ts(bytes: &[u8; FIELD_LEN], desc: bool) -> u64 {
let bytes = if desc { complement(*bytes) } else { *bytes };
u64::from_be_bytes(bytes)
}
pub(super) fn window_start(window: &WindowDef, now_ms: u64) -> Option<u64> {
match window.kind {
WindowKind::None => None,
WindowKind::Daily => Some(start_of_day(now_ms)),
WindowKind::Weekly => Some(start_of_week(now_ms)),
WindowKind::Monthly => Some(start_of_month(now_ms)),
WindowKind::Custom => {
if now_ms >= window.start_ms && now_ms < window.end_ms {
Some(window.start_ms)
} else {
Some(u64::MAX)
}
}
}
}
fn start_of_day(now_ms: u64) -> u64 {
(now_ms / MS_PER_DAY) * MS_PER_DAY
}
fn start_of_week(now_ms: u64) -> u64 {
let days = now_ms / MS_PER_DAY;
let since_monday = (days + 3) % 7;
(days - since_monday) * MS_PER_DAY
}
fn start_of_month(now_ms: u64) -> u64 {
let days = (now_ms / MS_PER_DAY) as i64;
let (year, month) = civil_year_month(days);
days_from_civil(year, month, 1) as u64 * MS_PER_DAY
}
fn civil_year_month(days: i64) -> (i64, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
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 }) as u32;
let year = yoe + era * 400 + i64::from(month <= 2);
(year, month)
}
fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let year = if month <= 2 { year - 1 } else { year };
let era = year.div_euclid(400);
let yoe = year.rem_euclid(400);
let month = i64::from(month);
let mp = if month > 2 { month - 3 } else { month + 9 };
let doy = (153 * mp + 2) / 5 + i64::from(day) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
#[cfg(test)]
mod tests {
use super::*;
fn window(kind: WindowKind, start_ms: u64, end_ms: u64) -> WindowDef {
WindowDef {
kind,
timestamp_column: "ts".into(),
timestamp_desc: false,
start_ms,
end_ms,
}
}
const NOW: u64 = 1_714_663_800_000;
#[test]
fn calendar_windows_start_at_utc_boundaries() {
assert_eq!(
window_start(&window(WindowKind::Daily, 0, 0), NOW),
Some(1_714_608_000_000)
);
assert_eq!(
window_start(&window(WindowKind::Weekly, 0, 0), NOW),
Some(1_714_348_800_000)
);
assert_eq!(
window_start(&window(WindowKind::Monthly, 0, 0), NOW),
Some(1_714_521_600_000)
);
assert_eq!(window_start(&WindowDef::none(), NOW), None);
}
#[test]
fn month_start_handles_january_and_leap_february() {
assert_eq!(start_of_month(1_705_320_000_000), 1_704_067_200_000);
assert_eq!(start_of_month(1_709_208_000_000), 1_706_745_600_000);
}
#[test]
fn custom_window_is_empty_outside_its_range() {
let custom = window(WindowKind::Custom, 1_000, 2_000);
assert_eq!(window_start(&custom, 1_500), Some(1_000));
assert_eq!(window_start(&custom, 2_000), Some(u64::MAX));
assert_eq!(window_start(&custom, 999), Some(u64::MAX));
}
#[test]
fn timestamps_round_trip_in_both_directions() {
for desc in [false, true] {
assert_eq!(decode_ts(&encode_ts(1_234, desc), desc), 1_234);
}
assert!(encode_ts(1, true) > encode_ts(2, true));
}
}