use super::types::{DerivedMetrics, UsageBucket};
pub fn compute_derived(bucket: &UsageBucket) -> DerivedMetrics {
let api_coverage_pct = safe_pct(bucket.api_coverage_message_count, bucket.message_count);
let api_tokens_per_assistant_msg =
safe_div(bucket.api_tokens_total, bucket.assistant_message_count);
let content_tokens_per_user_msg =
safe_div(bucket.content_tokens_est_total, bucket.user_message_count);
let tool_calls_per_1k_api_tokens = if bucket.api_tokens_total > 0 {
Some(bucket.tool_call_count as f64 / (bucket.api_tokens_total as f64 / 1000.0))
} else {
None
};
let tool_calls_per_1k_content_tokens = if bucket.content_tokens_est_total > 0 {
Some(bucket.tool_call_count as f64 / (bucket.content_tokens_est_total as f64 / 1000.0))
} else {
None
};
let plan_message_pct = if bucket.message_count > 0 {
Some((bucket.plan_message_count as f64 / bucket.message_count as f64) * 100.0)
} else {
None
};
let plan_token_share_content = safe_div(
bucket.plan_content_tokens_est_total,
bucket.content_tokens_est_total,
);
let plan_token_share_api = safe_div(bucket.plan_api_tokens_total, bucket.api_tokens_total);
DerivedMetrics {
api_coverage_pct,
api_tokens_per_assistant_msg,
content_tokens_per_user_msg,
tool_calls_per_1k_api_tokens,
tool_calls_per_1k_content_tokens,
plan_message_pct,
plan_token_share_content,
plan_token_share_api,
}
}
pub fn safe_pct(numerator: i64, denominator: i64) -> f64 {
if denominator == 0 {
0.0
} else {
let pct = (numerator as f64 / denominator as f64) * 100.0;
(pct * 100.0).round() / 100.0
}
}
pub fn safe_div(numerator: i64, denominator: i64) -> Option<f64> {
if denominator == 0 {
None
} else {
Some(numerator as f64 / denominator as f64)
}
}
pub fn safe_div_f64(numerator: f64, denominator: i64) -> Option<f64> {
if denominator == 0 {
None
} else {
Some(numerator / denominator as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safe_div_zero_denominator() {
assert_eq!(safe_div(100, 0), None);
}
#[test]
fn safe_div_normal() {
assert_eq!(safe_div(100, 50), Some(2.0));
}
#[test]
fn safe_div_f64_zero_denominator() {
assert_eq!(safe_div_f64(1.50, 0), None);
}
#[test]
fn safe_div_f64_normal() {
let result = safe_div_f64(3.0, 2);
assert_eq!(result, Some(1.5));
}
#[test]
fn safe_pct_zero_denominator() {
assert_eq!(safe_pct(50, 0), 0.0);
}
#[test]
fn safe_pct_normal() {
let pct = safe_pct(75, 100);
assert!((pct - 75.0).abs() < 0.01);
}
#[test]
fn safe_pct_rounding() {
let pct = safe_pct(1, 3);
assert!((pct - 33.33).abs() < 0.01);
}
#[test]
fn compute_derived_empty_bucket() {
let bucket = UsageBucket::default();
let d = compute_derived(&bucket);
assert_eq!(d.api_coverage_pct, 0.0);
assert_eq!(d.api_tokens_per_assistant_msg, None);
assert_eq!(d.content_tokens_per_user_msg, None);
assert_eq!(d.tool_calls_per_1k_api_tokens, None);
assert_eq!(d.tool_calls_per_1k_content_tokens, None);
assert_eq!(d.plan_message_pct, None);
assert_eq!(d.plan_token_share_content, None);
assert_eq!(d.plan_token_share_api, None);
}
#[test]
fn compute_derived_realistic_bucket() {
let bucket = UsageBucket {
message_count: 100,
user_message_count: 50,
assistant_message_count: 50,
tool_call_count: 10,
plan_message_count: 5,
plan_content_tokens_est_total: 2_500,
plan_api_tokens_total: 3_000,
api_coverage_message_count: 80,
content_tokens_est_total: 50_000,
api_tokens_total: 60_000,
estimated_cost_usd: 3.00,
..Default::default()
};
let d = compute_derived(&bucket);
assert!((d.api_coverage_pct - 80.0).abs() < 0.01);
assert_eq!(d.api_tokens_per_assistant_msg, Some(1200.0));
assert_eq!(d.content_tokens_per_user_msg, Some(1000.0));
assert!(d.tool_calls_per_1k_api_tokens.is_some());
assert!(d.tool_calls_per_1k_content_tokens.is_some());
assert!((d.plan_message_pct.unwrap() - 5.0).abs() < 0.01);
assert_eq!(d.plan_token_share_content, Some(0.05));
assert_eq!(d.plan_token_share_api, Some(0.05));
}
#[test]
fn no_nan_or_infinity() {
let bucket = UsageBucket {
message_count: 0,
api_tokens_total: 0,
content_tokens_est_total: 0,
..Default::default()
};
let d = compute_derived(&bucket);
assert!(!d.api_coverage_pct.is_nan());
assert!(!d.api_coverage_pct.is_infinite());
}
}