tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
use tiny_counter::EventStore;

#[test]
fn test_create_store_and_record() {
    let store = EventStore::new();

    // Record some events
    store.record("user_login");
    store.record("user_login");
    store.record("page_view");

    // Query the events
    let login_count = store.query("user_login").last_minutes(1).sum();
    let page_view_count = store.query("page_view").last_minutes(1).sum();

    assert_eq!(login_count, Some(2));
    assert_eq!(page_view_count, Some(1));
}

#[test]
fn test_all_time_units() {
    // Explicitly configure all 6 time units to test all query methods work
    let store = EventStore::builder()
        .track_minutes(60)
        .track_hours(72)
        .track_days(56)
        .track_weeks(52)
        .track_months(12)
        .track_years(4)
        .build()
        .unwrap();

    // Record events
    for _ in 0..5 {
        store.record("test_event");
    }

    // Query using all time units
    assert_eq!(store.query("test_event").last_minutes(1).sum(), Some(5));
    assert_eq!(store.query("test_event").last_hours(1).sum(), Some(5));
    assert_eq!(store.query("test_event").last_days(1).sum(), Some(5));
    assert_eq!(store.query("test_event").last_weeks(1).sum(), Some(5));
    assert_eq!(store.query("test_event").last_months(1).sum(), Some(5));
    assert_eq!(store.query("test_event").last_years(1).sum(), Some(5));
}

#[test]
fn test_all_aggregations() {
    let store = EventStore::new();

    // Record multiple counts
    store.record_count("event", 10);
    store.record_count("event", 20);
    store.record_count("event", 30);

    // Test sum
    let sum = store.query("event").last_days(1).sum();
    assert_eq!(sum, Some(60));

    // Test average
    let avg = store.query("event").last_days(1).average();
    assert_eq!(avg, Some(60.0));

    // Test average_nonzero
    let avg_nonzero = store.query("event").last_days(1).average_nonzero();
    assert_eq!(avg_nonzero, Some(60.0));

    // Test sum_nonzero
    let count = store.query("event").last_days(1).count_nonzero();
    assert_eq!(count, Some(1)); // Only 1 bucket (current day)
}

#[test]
fn test_record_count() {
    let store = EventStore::new();

    // Record different counts
    store.record_count("batch_event", 100);

    let count = store.query("batch_event").last_hours(1).sum();
    assert_eq!(count, Some(100));
}

#[test]
fn test_query_nonexistent_event() {
    let store = EventStore::new();

    let count = store.query("nonexistent").last_days(1).sum();
    assert_eq!(count, None);
}

#[test]
fn test_multiple_events() {
    let store = EventStore::new();

    // Record different events
    store.record("event_a");
    store.record("event_b");
    store.record("event_a");
    store.record("event_c");
    store.record("event_b");
    store.record("event_a");

    // Query each event
    assert_eq!(store.query("event_a").last_days(1).sum(), Some(3));
    assert_eq!(store.query("event_b").last_days(1).sum(), Some(2));
    assert_eq!(store.query("event_c").last_days(1).sum(), Some(1));
}

#[test]
fn test_multi_query() {
    let store = EventStore::new();

    store.record_count("event1", 10);
    store.record_count("event2", 20);
    store.record_count("event3", 30);

    let total = store
        .query_many(&["event1", "event2", "event3"])
        .last_days(1)
        .sum();

    assert_eq!(total, Some(60));
}

#[test]
fn test_ratio_query() {
    let store = EventStore::new();

    store.record_count("conversions", 25);
    store.record_count("impressions", 100);

    let ratio = store.query_ratio("conversions", "impressions").last_days(1);

    assert_eq!(ratio, Some(0.25));
}

#[test]
fn test_delta_query() {
    let store = EventStore::new();

    store.record_count("additions", 50);
    store.record_count("deletions", 30);

    let delta = store
        .query_delta("additions", "deletions")
        .last_days(1)
        .sum();

    assert_eq!(delta, 20);
}

#[test]
fn test_ever_query() {
    let store = EventStore::new();

    store.record_count("lifetime_event", 100);

    let count = store.query("lifetime_event").ever().sum();
    assert_eq!(count, Some(100));
}

#[test]
fn test_last_seen() {
    let store = EventStore::new();

    store.record("recent_event");

    let last_seen = store.query("recent_event").last_seen();
    assert!(last_seen.is_some());
}

#[test]
fn test_into_buckets() {
    let store = EventStore::new();

    store.record_count("event", 42);

    let buckets = store.query("event").last_days(7).into_buckets();

    // Should have 7 buckets
    assert_eq!(buckets.len(), 7);

    // First bucket should have count 42
    assert_eq!(buckets[0], 42);
}

// ============================================================================
// TESTS: Ported from attic/tests.rs
// ============================================================================

#[test]
fn test_query_sum() {
    let store = EventStore::new();

    // Record events now
    store.record("event-1");
    store.record("event-1");
    store.record("event-1");

    // Query should return sum of 3
    assert_eq!(store.query("event-1").last_days(7).sum(), Some(3));

    // Query with offset beyond events should return 0 or None
    let result = store.query("event-1").days(7..14).sum();
    assert!(result == Some(0) || result.is_none());
}

#[test]
fn test_query_average() {
    let store = EventStore::new();

    // Record 3 events now
    store.record("event-1");
    store.record("event-1");
    store.record("event-1");

    // Average over 7 days: 3 events / 7 days
    let avg = store.query("event-1").last_days(7).average();
    assert!(avg.is_some());
    let avg_val = avg.unwrap();
    assert!((avg_val - 0.42857142857142855).abs() < 0.001);

    // Average over 1 day: 3 events / 1 day = 3.0
    let avg2 = store.query("event-1").last_days(1).average();
    assert!(avg2.is_some());
    let avg2_val = avg2.unwrap();
    assert!((avg2_val - 3.0).abs() < 0.001);
}

#[test]
fn test_query_last_seen() {
    use chrono::Duration;

    let store = EventStore::new();

    // Record events in the past using record_count_ago
    store.record_count_ago("event-1", 1, Duration::days(10));
    store.record_count_ago("event-1", 1, Duration::days(20));

    // Last seen should be ~10 days ago (most recent)
    let last_seen = store.query("event-1").last_seen();
    assert!(last_seen.is_some());
    let days_ago = last_seen.unwrap().num_days();
    assert!((9..=11).contains(&days_ago)); // Allow for some time variance
}

#[test]
fn test_record_past_event_queries() {
    use chrono::{Datelike, Duration, Utc};

    // Explicitly configure all 6 time units to test past event queries across all scales
    let store = EventStore::builder()
        .track_minutes(60)
        .track_hours(72)
        .track_days(56)
        .track_weeks(52)
        .track_months(12)
        .track_years(4)
        .build()
        .unwrap();

    let event_id = "app_launch";
    let delta = Duration::days(1);
    let now = Utc::now();
    let then = now - delta;

    // Record event 1 day ago
    store.record_count_ago(event_id, 5, delta);

    // Helper functions for time comparison
    fn same_week(now: chrono::DateTime<Utc>, then: chrono::DateTime<Utc>) -> bool {
        let now_week_num = now.ordinal0() / 7;
        let then_week_num = then.ordinal0() / 7;
        now_week_num == then_week_num
    }

    fn same_month(now: chrono::DateTime<Utc>, then: chrono::DateTime<Utc>) -> bool {
        let now_num = now.ordinal0() / 28;
        let then_num = then.ordinal0() / 28;
        now_num == then_num
    }

    fn same_year(now: chrono::DateTime<Utc>, then: chrono::DateTime<Utc>) -> bool {
        now.year() == then.year()
    }

    // Event sum for all intervals
    // Events from 1 day ago should not appear in minute/hour queries
    assert_eq!(store.query(event_id).last_minutes(60).sum(), Some(0));
    assert_eq!(store.query(event_id).last_hours(24).sum(), Some(5));

    // Should appear in day queries
    assert_eq!(store.query(event_id).last_days(56).sum(), Some(5));

    // Week, month, year tests depend on time boundaries
    if same_year(now, then) {
        if same_week(now, then) {
            assert_eq!(store.query(event_id).last_weeks(1).sum(), Some(5));
        } else {
            // Use larger window to capture event in different week
            assert_eq!(store.query(event_id).last_weeks(2).sum(), Some(5));
        }

        if same_month(now, then) {
            assert_eq!(store.query(event_id).last_months(1).sum(), Some(5));
        } else {
            // Use larger window to capture event in different month
            assert_eq!(store.query(event_id).last_months(2).sum(), Some(5));
        }

        // Note: Event might be in current year or previous depending on timing
        let year_sum = store.query(event_id).last_years(1).sum();
        assert!(year_sum == Some(5) || year_sum == Some(0));
    }

    // Absolute timing tests - use larger windows to avoid boundary issues
    // Note: With approximate time units, these might not always catch the event
    let weeks_sum = store.query(event_id).last_weeks(2).sum();
    assert!(weeks_sum == Some(5) || weeks_sum == Some(0));

    let months_sum = store.query(event_id).last_months(2).sum();
    assert!(months_sum == Some(5) || months_sum == Some(0));

    let years_sum = store.query(event_id).last_years(2).sum();
    assert!(years_sum == Some(5) || years_sum == Some(0));

    // Last seen tests
    let last_seen = store.query(event_id).last_seen();
    assert!(last_seen.is_some());
    let duration_since = last_seen.unwrap();

    // Should be approximately 1 day ago
    assert!(duration_since.num_days() >= 0 && duration_since.num_days() <= 2);
}

#[test]
fn test_record_past_event_week_queries() {
    use chrono::{Duration, Utc};

    let store = EventStore::new();
    let event_id = "app_launch";
    let delta = Duration::weeks(1);
    let now = Utc::now();
    let _then = now - delta;

    // Record event 1 week ago
    store.record_count_ago(event_id, 5, delta);

    // Check last seen
    let last_seen = store.query(event_id).last_seen();
    assert!(last_seen.is_some());
    let duration_since = last_seen.unwrap();

    // Should be approximately 1 week (7 days) ago
    assert!(duration_since.num_days() >= 6 && duration_since.num_days() <= 8);
}