use std::collections::HashMap;
use serde::Serialize;
use crate::cli::RankAction;
use crate::context::CommandContext;
use crate::error::Result;
use crate::ranking::borda::{aggregate_rankings, qv_budget, total_vote_cost};
use crate::ranking::ordering::load_all_orderings;
use crate::utils::truncate;
pub fn execute(ctx: &CommandContext, action: RankAction) -> Result<()> {
match action {
RankAction::Show {
milestone,
by_user,
json,
} => show(ctx, milestone, by_user, json),
}
}
fn resolve_milestone_for_rank(ctx: &CommandContext, input: Option<String>) -> Result<String> {
if let Some(ref ms) = input {
return ctx.resolve_milestone(ms);
}
let milestones = ctx.store.list_milestones()?;
for m in &milestones {
if m.is_active() {
return Ok(m.id.clone());
}
}
Err(crate::error::JjjError::Validation(
"No active milestone found. Create one with `jjj milestone new` or specify a milestone."
.into(),
))
}
fn open_problems_in_milestone(
ctx: &CommandContext,
milestone_id: &str,
) -> Result<(Vec<String>, HashMap<String, String>)> {
let milestone = ctx.store.load_milestone(milestone_id)?;
let all_problems = ctx.store.list_problems()?;
let mut ids = Vec::new();
let mut titles = HashMap::new();
for p in &all_problems {
if p.milestone_id.as_deref() == Some(milestone_id) && p.is_open() {
ids.push(p.id.clone());
titles.insert(p.id.clone(), p.title.clone());
}
}
for pid in &milestone.problem_ids {
if !titles.contains_key(pid) {
if let Ok(p) = ctx.store.load_problem(pid) {
if p.is_open() {
ids.push(p.id.clone());
titles.insert(p.id.clone(), p.title.clone());
}
}
}
}
Ok((ids, titles))
}
#[derive(Serialize)]
struct RankEntry {
rank: usize,
problem_id: String,
title: String,
score: f64,
voters: usize,
}
#[derive(Serialize)]
struct UserOrderingEntry {
rank: usize,
problem_id: String,
title: String,
votes: i32,
}
#[derive(Serialize)]
struct UserBreakdown {
budget: u32,
budget_used: u32,
ordering: Vec<UserOrderingEntry>,
}
fn show(ctx: &CommandContext, milestone: Option<String>, by_user: bool, json: bool) -> Result<()> {
let milestone_id = resolve_milestone_for_rank(ctx, milestone)?;
let ms = ctx.store.load_milestone(&milestone_id)?;
let (problem_ids, titles) = open_problems_in_milestone(ctx, &milestone_id)?;
let orderings = load_all_orderings(ctx.store.meta_path(), &milestone_id)?;
if orderings.is_empty() {
if json {
println!("[]");
} else {
println!(
"No rankings yet for milestone '{}'. Start with `jjj rank session`.",
ms.title,
);
}
return Ok(());
}
if by_user {
show_by_user(&orderings, &problem_ids, &titles, json)?;
} else {
let ranked = aggregate_rankings(&orderings, problem_ids.len());
let entries: Vec<RankEntry> = ranked
.iter()
.filter(|(id, _)| problem_ids.contains(id))
.enumerate()
.map(|(i, (id, agg))| {
let title = titles.get(id).cloned().unwrap_or_default();
RankEntry {
rank: i + 1,
problem_id: id.clone(),
title,
score: (agg.score * 10.0).round() / 10.0,
voters: agg.voter_count,
}
})
.collect();
if json {
println!("{}", serde_json::to_string_pretty(&entries)?);
} else {
println!("Rankings for milestone: {}\n", ms.title);
println!(
" {:<5} {:<45} {:>7} {:>7}",
"Rank", "Problem", "Score", "Voters"
);
println!(" {}", "-".repeat(66));
for e in &entries {
println!(
" {:<5} {:<45} {:>7.1} {:>7}",
e.rank,
truncate(&e.title, 44),
e.score,
e.voters,
);
}
}
}
Ok(())
}
fn show_by_user(
orderings: &HashMap<String, crate::ranking::ordering::UserOrdering>,
problem_ids: &[String],
titles: &HashMap<String, String>,
json: bool,
) -> Result<()> {
let mut users: Vec<&String> = orderings.keys().collect();
users.sort();
let budget = qv_budget(problem_ids.len());
if json {
let mut all_data: HashMap<&str, UserBreakdown> = HashMap::new();
for user in &users {
let ordering = &orderings[*user];
let budget_used = total_vote_cost(&ordering.votes);
let entries: Vec<UserOrderingEntry> = ordering
.order
.iter()
.filter(|id| problem_ids.contains(id))
.enumerate()
.map(|(i, id)| UserOrderingEntry {
rank: i + 1,
problem_id: id.clone(),
title: titles.get(id).cloned().unwrap_or_default(),
votes: ordering.votes.get(id).copied().unwrap_or(0),
})
.collect();
all_data.insert(
user.as_str(),
UserBreakdown {
budget,
budget_used,
ordering: entries,
},
);
}
println!("{}", serde_json::to_string_pretty(&all_data)?);
} else {
for user in &users {
let ordering = &orderings[*user];
let budget_used = total_vote_cost(&ordering.votes);
println!("\n--- {} ---", user);
println!(" QV budget: {}/{} used\n", budget_used, budget);
println!(" {:<5} {:<45} {:>5}", "Rank", "Problem", "Votes");
println!(" {}", "-".repeat(57));
for (i, id) in ordering
.order
.iter()
.filter(|id| problem_ids.contains(id))
.enumerate()
{
let title = titles.get(id).cloned().unwrap_or_default();
let votes = ordering.votes.get(id).copied().unwrap_or(0);
let votes_str = if votes > 0 {
format!("+{}", votes)
} else if votes < 0 {
format!("{}", votes)
} else {
String::new()
};
println!(
" {:<5} {:<45} {:>5}",
i + 1,
truncate(&title, 44),
votes_str,
);
}
}
}
Ok(())
}