use chrono::{Datelike, Days, NaiveDate};
use git_koi::history::{AccountSnapshot, ContributionDay, ContributionHistory, ContributionWeek};
use git_koi::koi::{KoiDevelopment, RecentStreak, derive_koi_development};
use git_koi::markings::calculate_streak_marks;
#[test]
fn streak_fins_build_linearly_to_full_at_thirteen_weeks() {
for (streak_week_count, expected_strength) in
[(1, 1.0 / 13.0), (6, 6.0 / 13.0), (13, 1.0), (20, 1.0)]
{
let koi_development = develop_with_streaks(
streak_week_count,
Some(RecentStreak::Current(build_weeks(streak_week_count))),
);
assert_close(
calculate_streak_marks(&koi_development).streak_fin_strength,
expected_strength,
);
}
}
#[test]
fn streak_fins_fade_to_nothing_six_and_a_half_weeks_after_a_streak_ends() {
for (days_since_end, expected_strength) in [
(0, 1.0),
(7, 0.59066),
(14, 0.34238),
(28, 0.10046),
(45, 0.00147),
(46, 0.0),
(365, 0.0),
] {
let koi_development = develop_after_streak_end(13, days_since_end);
let streak_fin_strength = calculate_streak_marks(&koi_development).streak_fin_strength;
assert!(
(streak_fin_strength - expected_strength).abs() < 0.00001,
"{days_since_end} days after: {streak_fin_strength}"
);
}
}
#[test]
fn a_short_streak_fades_from_the_strength_it_reached() {
let ended_strength = calculate_streak_marks(&develop_after_streak_end(4, 0));
let faded_strength = calculate_streak_marks(&develop_after_streak_end(4, 7));
assert_close(ended_strength.streak_fin_strength, 4.0 / 13.0);
assert!((faded_strength.streak_fin_strength - 4.0 / 13.0 * 0.59066).abs() < 0.00001);
}
#[test]
fn the_longest_streak_brings_colors_from_dull_to_vivid_then_glimmer() {
let marks_at = |longest_week_count| {
calculate_streak_marks(&develop_with_streaks(longest_week_count, None))
};
assert_close(marks_at(0).color_vibrancy, 0.0);
assert!(marks_at(1).color_vibrancy > 0.4 && marks_at(1).color_vibrancy < 0.5);
assert_close(marks_at(4).color_vibrancy, 1.0);
assert_close(marks_at(52).color_vibrancy, 2.0);
assert_close(marks_at(52).glimmer_strength, 0.0);
assert!(marks_at(104).glimmer_strength > 0.0 && marks_at(104).glimmer_strength < 1.0);
assert_close(marks_at(208).glimmer_strength, 1.0);
assert_eq!(marks_at(400), marks_at(208));
let mut previous_marks = marks_at(0);
for longest_week_count in 1..=260 {
let longest_marks = marks_at(longest_week_count);
assert!(longest_marks.color_vibrancy >= previous_marks.color_vibrancy);
assert!(longest_marks.glimmer_strength >= previous_marks.glimmer_strength);
previous_marks = longest_marks;
}
}
#[test]
fn quiet_weeks_fade_the_fins_without_lowering_the_longest_streak_marks() {
let first_monday: NaiveDate = "2025-09-01".parse().unwrap();
let mut daily_counts = vec![1; 13 * 7];
let active_marks =
calculate_streak_marks(&develop_from_daily_counts(first_monday, &daily_counts));
assert_close(active_marks.streak_fin_strength, 1.0);
let mut previous_marks = active_marks;
for _ in 0..10 {
daily_counts.extend([0; 7]);
let quiet_marks =
calculate_streak_marks(&develop_from_daily_counts(first_monday, &daily_counts));
assert!(quiet_marks.streak_fin_strength <= previous_marks.streak_fin_strength);
assert_eq!(quiet_marks.color_vibrancy, active_marks.color_vibrancy);
assert_eq!(quiet_marks.glimmer_strength, active_marks.glimmer_strength);
previous_marks = quiet_marks;
}
assert_eq!(previous_marks.streak_fin_strength, 0.0);
}
fn develop_with_streaks(
longest_week_count: usize,
recent_streak: Option<RecentStreak>,
) -> KoiDevelopment {
let mut koi_development = read_fixture_development();
koi_development.streaks.longest = build_weeks(longest_week_count);
koi_development.streaks.recent = recent_streak;
koi_development
}
fn develop_after_streak_end(streak_week_count: usize, days_since_end: u64) -> KoiDevelopment {
let mut koi_development = read_fixture_development();
let ended_on = koi_development.observed_on - Days::new(days_since_end);
koi_development.streaks.longest = build_weeks(streak_week_count);
koi_development.streaks.recent = Some(RecentStreak::Ended {
weeks: build_weeks(streak_week_count),
ended_on,
});
koi_development
}
fn develop_from_daily_counts(first_monday: NaiveDate, daily_counts: &[u32]) -> KoiDevelopment {
let mut account_snapshot: AccountSnapshot =
serde_json::from_str(include_str!("fixtures/account-with-history.json")).unwrap();
let contribution_days: Vec<_> = daily_counts
.iter()
.enumerate()
.map(|(day_offset, &contribution_count)| ContributionDay {
date: first_monday + Days::new(day_offset as u64),
contribution_count,
})
.collect();
account_snapshot.contribution_history = Some(ContributionHistory {
observed_on: contribution_days.last().unwrap().date,
days: contribution_days,
});
derive_koi_development(&account_snapshot).unwrap()
}
fn read_fixture_development() -> KoiDevelopment {
let account_snapshot =
serde_json::from_str(include_str!("fixtures/account-with-history.json")).unwrap();
derive_koi_development(&account_snapshot).unwrap()
}
fn build_weeks(week_count: usize) -> Vec<ContributionWeek> {
let first_monday: NaiveDate = "2024-01-01".parse().unwrap();
(0..week_count)
.map(|week_index| ContributionWeek {
iso_week: (first_monday + Days::new(7 * week_index as u64)).iso_week(),
active_day_count: 7,
observed_day_count: 7,
})
.collect()
}
fn assert_close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-12,
"{actual} is not {expected}"
);
}