rank method

RankedResults rank(
  1. List<MatchResult> results
)

Rank a list of match results and assign conformal confidence.

Implementation

RankedResults rank(List<MatchResult> results) {
  final count = results.length;
  if (count == 0) {
    return const RankedResults(
      items: [],
      summary: RankingSummary(
        count: 0,
        stableCount: 0,
        tieGroupCount: 0,
        medianGap: 0,
      ),
    );
  }

  // Tag with original index, sort descending by score then match type
  final indexed = <(int, MatchResult)>[];
  for (var i = 0; i < results.length; i++) {
    indexed.add((i, results[i]));
  }
  indexed.sort((a, b) {
    final scoreCmp = b.$2.score.compareTo(a.$2.score);
    if (scoreCmp != 0) return scoreCmp;
    return b.$2.matchType.index.compareTo(a.$2.matchType.index);
  });

  // Compute gaps
  final gaps = <double>[];
  for (var i = 0; i < indexed.length - 1; i++) {
    gaps.add(
      (indexed[i].$2.score - indexed[i + 1].$2.score).clamp(
        0,
        double.infinity,
      ),
    );
  }

  final sortedGaps = List<double>.from(gaps)..sort();

  // Compute confidence per position
  final items = <RankedItem>[];
  var stableCount = 0;
  var tieGroupCount = 0;
  var inTieGroup = false;

  for (var rank = 0; rank < indexed.length; rank++) {
    final (origIdx, result) = indexed[rank];
    final gapToNext = rank < gaps.length ? gaps[rank] : 0.0;

    double confidence;
    if (sortedGaps.isEmpty) {
      confidence = 1.0;
    } else {
      final leqCount = sortedGaps
          .where((g) => g <= gapToNext + tieEpsilon * 0.5)
          .length;
      confidence = leqCount / sortedGaps.length;
    }

    final isTie = gaps.isNotEmpty && gapToNext < tieEpsilon;
    RankStability stability;
    if (isTie) {
      if (!inTieGroup) {
        tieGroupCount++;
        inTieGroup = true;
      }
      stability = RankStability.unstable;
    } else {
      inTieGroup = false;
      if (confidence >= stableThreshold) {
        stableCount++;
        stability = RankStability.stable;
      } else if (confidence >= marginalThreshold) {
        stability = RankStability.marginal;
      } else {
        stability = RankStability.unstable;
      }
    }

    items.add(
      RankedItem(
        originalIndex: origIdx,
        result: result,
        rankConfidence: RankConfidence(
          confidence: confidence,
          gapToNext: gapToNext,
          stability: stability,
        ),
      ),
    );
  }

  // Median gap
  double medianGap;
  if (sortedGaps.isEmpty) {
    medianGap = 0;
  } else {
    final mid = sortedGaps.length ~/ 2;
    if (sortedGaps.length.isEven) {
      medianGap = (sortedGaps[mid - 1] + sortedGaps[mid]) / 2;
    } else {
      medianGap = sortedGaps[mid];
    }
  }

  return RankedResults(
    items: items,
    summary: RankingSummary(
      count: count,
      stableCount: stableCount,
      tieGroupCount: tieGroupCount,
      medianGap: medianGap,
    ),
  );
}